mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
060ce8de25 |
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "near-intents",
|
||||
"display_name": "Near Intents",
|
||||
"kind": "tool",
|
||||
"version": "0.1.0",
|
||||
"wit_version": "0.3.0",
|
||||
"description": "Token resolution, balance queries, and reverse lookups for NEAR Intents (Defuse protocol)",
|
||||
"keywords": ["near", "intents", "defuse", "defi", "trading", "token"],
|
||||
"source": {
|
||||
"dir": "tools-src/near-intents",
|
||||
"capabilities": "near-intents-tool.capabilities.json",
|
||||
"crate_name": "near-intents-tool"
|
||||
},
|
||||
"artifacts": {},
|
||||
"tags": ["trading"]
|
||||
}
|
||||
@@ -131,6 +131,18 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(extension_manager) = self.deps.extension_manager.as_ref() {
|
||||
match extension_manager.llm_extension_state_summary().await {
|
||||
Ok(Some(summary)) => {
|
||||
reasoning = reasoning.with_extension_state_summary(summary);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
tracing::debug!("Could not load extension state summary: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prompt) = system_prompt {
|
||||
reasoning = reasoning.with_system_prompt(prompt);
|
||||
}
|
||||
|
||||
@@ -1440,7 +1440,6 @@ fn handle_text_response(
|
||||
/// This is a simplified version of the full dispatcher loop:
|
||||
/// - Max 3-5 iterations (configurable)
|
||||
/// - Sequential tool execution (not parallel)
|
||||
/// - Uses the owner's live autonomous tool scope when lightweight tools are enabled
|
||||
/// - Auto-approval of non-Always tools
|
||||
/// - No hooks or approval dialogs
|
||||
async fn execute_lightweight_with_tools(
|
||||
|
||||
+2
-47
@@ -340,8 +340,8 @@ async fn create(
|
||||
prompt: prompt.to_string(),
|
||||
context_paths: Vec::new(),
|
||||
max_tokens: 4096,
|
||||
use_tools: true,
|
||||
max_tool_rounds: 3,
|
||||
use_tools: false,
|
||||
max_tool_rounds: 0,
|
||||
},
|
||||
guardrails: RoutineGuardrails {
|
||||
cooldown: std::time::Duration::from_secs(cooldown_secs),
|
||||
@@ -685,7 +685,6 @@ fn truncate(s: &str, max_chars: usize) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::agent::routine::RoutineAction;
|
||||
|
||||
#[test]
|
||||
fn format_relative_future() {
|
||||
@@ -744,48 +743,4 @@ mod tests {
|
||||
assert!(notify.on_failure); // safety: test-only assertion
|
||||
assert!(!notify.on_success); // safety: test-only assertion
|
||||
}
|
||||
|
||||
#[cfg(feature = "libsql")]
|
||||
#[tokio::test]
|
||||
async fn cli_create_defaults_lightweight_routines_to_tools_enabled() {
|
||||
let harness = crate::testing::TestHarnessBuilder::new().build().await;
|
||||
let db = harness.db.clone();
|
||||
|
||||
run_routines_command(
|
||||
RoutinesCommand::Create {
|
||||
name: "cli-digest".to_string(),
|
||||
schedule: "0 0 9 * * *".to_string(),
|
||||
prompt: "Prepare the morning digest.".to_string(),
|
||||
description: "CLI created routine".to_string(),
|
||||
timezone: Some("UTC".to_string()),
|
||||
cooldown: 300,
|
||||
notify_channel: None,
|
||||
},
|
||||
db.clone(),
|
||||
"user1",
|
||||
)
|
||||
.await
|
||||
.expect("create routine");
|
||||
|
||||
let routine = db
|
||||
.get_routine_by_name("user1", "cli-digest")
|
||||
.await
|
||||
.expect("get routine by name")
|
||||
.expect("cli-digest should exist");
|
||||
|
||||
match routine.action {
|
||||
RoutineAction::Lightweight {
|
||||
use_tools,
|
||||
max_tool_rounds,
|
||||
..
|
||||
} => {
|
||||
assert!(
|
||||
use_tools,
|
||||
"CLI-created lightweight routines should default to tools"
|
||||
);
|
||||
assert_eq!(max_tool_rounds, 3);
|
||||
}
|
||||
other => panic!("expected lightweight action, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1498,6 +1498,52 @@ impl ExtensionManager {
|
||||
Ok(extensions)
|
||||
}
|
||||
|
||||
/// Build a compact, deterministic extension snapshot for LLM prompt context.
|
||||
pub async fn llm_extension_state_summary(&self) -> Result<Option<String>, ExtensionError> {
|
||||
let mut extensions = self.list(None, false).await?;
|
||||
extensions.sort_by(|a, b| {
|
||||
llm_extension_sort_key(a.kind)
|
||||
.cmp(&llm_extension_sort_key(b.kind))
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
let mut channels = Vec::new();
|
||||
let mut tools = Vec::new();
|
||||
let mut servers = Vec::new();
|
||||
|
||||
for extension in extensions {
|
||||
let owner_bound = matches!(extension.kind, ExtensionKind::WasmChannel)
|
||||
&& self.has_wasm_channel_owner_binding(&extension.name).await;
|
||||
if !(extension.active || extension.authenticated || owner_bound) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let item = llm_extension_summary_item(&extension, owner_bound);
|
||||
match extension.kind {
|
||||
ExtensionKind::WasmChannel | ExtensionKind::ChannelRelay => channels.push(item),
|
||||
ExtensionKind::WasmTool => tools.push(item),
|
||||
ExtensionKind::McpServer => servers.push(item),
|
||||
}
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
if !channels.is_empty() {
|
||||
lines.push(format!("- Channels: {}", channels.join("; ")));
|
||||
}
|
||||
if !tools.is_empty() {
|
||||
lines.push(format!("- Tools: {}", tools.join("; ")));
|
||||
}
|
||||
if !servers.is_empty() {
|
||||
lines.push(format!("- MCP servers: {}", servers.join("; ")));
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(lines.join("\n")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an installed extension.
|
||||
pub async fn remove(&self, name: &str) -> Result<String, ExtensionError> {
|
||||
Self::validate_extension_name(name)?;
|
||||
@@ -5613,6 +5659,40 @@ fn combine_install_errors(
|
||||
}
|
||||
}
|
||||
|
||||
fn llm_extension_sort_key(kind: ExtensionKind) -> u8 {
|
||||
match kind {
|
||||
ExtensionKind::WasmChannel | ExtensionKind::ChannelRelay => 0,
|
||||
ExtensionKind::WasmTool => 1,
|
||||
ExtensionKind::McpServer => 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn llm_extension_summary_item(extension: &InstalledExtension, owner_bound: bool) -> String {
|
||||
let mut states = Vec::new();
|
||||
if extension.authenticated {
|
||||
states.push("authenticated".to_string());
|
||||
}
|
||||
if extension.active {
|
||||
states.push("active".to_string());
|
||||
} else if extension.authenticated {
|
||||
states.push("inactive".to_string());
|
||||
}
|
||||
if owner_bound {
|
||||
states.push("owner-bound".to_string());
|
||||
}
|
||||
if !extension.tools.is_empty() {
|
||||
let mut tool_names = extension.tools.clone();
|
||||
tool_names.sort();
|
||||
states.push(format!("tools: {}", tool_names.join(", ")));
|
||||
}
|
||||
|
||||
if states.is_empty() {
|
||||
extension.name.clone()
|
||||
} else {
|
||||
format!("{} ({})", extension.name, states.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Debug;
|
||||
@@ -6571,6 +6651,142 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "libsql")]
|
||||
#[tokio::test]
|
||||
async fn test_llm_extension_state_summary_reports_active_owner_bound_telegram()
|
||||
-> Result<(), String> {
|
||||
let dir = tempfile::tempdir().map_err(|err| format!("temp dir: {err}"))?;
|
||||
let channels_dir = dir.path().join("channels");
|
||||
std::fs::create_dir_all(&channels_dir).map_err(|err| format!("channels dir: {err}"))?;
|
||||
std::fs::write(channels_dir.join("telegram.wasm"), b"mock")
|
||||
.map_err(|err| format!("write wasm: {err}"))?;
|
||||
std::fs::write(
|
||||
channels_dir.join("telegram.capabilities.json"),
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"type": "channel",
|
||||
"name": "telegram",
|
||||
"setup": {
|
||||
"required_secrets": [
|
||||
{
|
||||
"name": "telegram_bot_token",
|
||||
"prompt": "Enter your Telegram Bot API token (from @BotFather)",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"channel": {
|
||||
"allowed_paths": ["/webhook/telegram"]
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"owner_id": null
|
||||
}
|
||||
}))
|
||||
.map_err(|err| format!("serialize capabilities: {err}"))?,
|
||||
)
|
||||
.map_err(|err| format!("write capabilities: {err}"))?;
|
||||
|
||||
let (db, _db_tmp) = crate::testing::test_db().await;
|
||||
let manager = {
|
||||
use crate::secrets::{InMemorySecretsStore, SecretsCrypto};
|
||||
use crate::testing::credentials::TEST_CRYPTO_KEY;
|
||||
use crate::tools::ToolRegistry;
|
||||
use crate::tools::mcp::process::McpProcessManager;
|
||||
use crate::tools::mcp::session::McpSessionManager;
|
||||
|
||||
let master_key = secrecy::SecretString::from(TEST_CRYPTO_KEY.to_string());
|
||||
let crypto = Arc::new(
|
||||
SecretsCrypto::new(master_key)
|
||||
.map_err(|err| format!("failed to construct test crypto: {err}"))?,
|
||||
);
|
||||
|
||||
ExtensionManager::new(
|
||||
Arc::new(McpSessionManager::new()),
|
||||
Arc::new(McpProcessManager::new()),
|
||||
Arc::new(InMemorySecretsStore::new(crypto)),
|
||||
Arc::new(ToolRegistry::new()),
|
||||
None,
|
||||
None,
|
||||
dir.path().join("tools"),
|
||||
channels_dir.clone(),
|
||||
None,
|
||||
"test".to_string(),
|
||||
Some(db),
|
||||
Vec::new(),
|
||||
)
|
||||
};
|
||||
|
||||
let channel_manager = Arc::new(ChannelManager::new());
|
||||
let runtime = Arc::new(
|
||||
WasmChannelRuntime::new(WasmChannelRuntimeConfig::for_testing())
|
||||
.map_err(|err| format!("runtime: {err}"))?,
|
||||
);
|
||||
let pairing_store = Arc::new(PairingStore::with_base_dir(
|
||||
dir.path().join("pairing-state"),
|
||||
));
|
||||
let router = Arc::new(WasmChannelRouter::new());
|
||||
manager
|
||||
.set_channel_runtime(
|
||||
Arc::clone(&channel_manager),
|
||||
Arc::clone(&runtime),
|
||||
Arc::clone(&pairing_store),
|
||||
Arc::clone(&router),
|
||||
std::collections::HashMap::new(),
|
||||
)
|
||||
.await;
|
||||
manager
|
||||
.set_test_wasm_channel_loader(Arc::new({
|
||||
let runtime = Arc::clone(&runtime);
|
||||
let pairing_store = Arc::clone(&pairing_store);
|
||||
move |name| {
|
||||
Ok(make_test_loaded_channel(
|
||||
Arc::clone(&runtime),
|
||||
name,
|
||||
Arc::clone(&pairing_store),
|
||||
))
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
manager
|
||||
.set_test_telegram_binding_resolver(Arc::new(|_token, existing_owner_id| {
|
||||
if existing_owner_id.is_some() {
|
||||
return Err(ExtensionError::Other(
|
||||
"owner binding should be derived during setup".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(TelegramBindingResult::Bound(TelegramBindingData {
|
||||
owner_id: 424242,
|
||||
bot_username: Some("test_hot_bot".to_string()),
|
||||
binding_state: TelegramOwnerBindingState::VerifiedNow,
|
||||
}))
|
||||
}))
|
||||
.await;
|
||||
|
||||
manager
|
||||
.configure(
|
||||
"telegram",
|
||||
&std::collections::HashMap::from([(
|
||||
"telegram_bot_token".to_string(),
|
||||
"123456789:ABCdefGhI".to_string(),
|
||||
)]),
|
||||
&std::collections::HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("configure succeeds: {err}"))?;
|
||||
|
||||
let summary = manager
|
||||
.llm_extension_state_summary()
|
||||
.await
|
||||
.map_err(|err| format!("summary: {err}"))?
|
||||
.ok_or_else(|| "expected extension summary".to_string())?;
|
||||
|
||||
require(
|
||||
summary.contains("- Channels: telegram (authenticated, active, owner-bound)"),
|
||||
format!("unexpected summary: {summary}"),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_telegram_hot_activation_returns_verification_challenge_before_binding()
|
||||
-> Result<(), String> {
|
||||
|
||||
+92
-12
@@ -353,6 +353,8 @@ pub struct Reasoning {
|
||||
workspace_system_prompt: Option<String>,
|
||||
/// Optional skill context block to inject into system prompt.
|
||||
skill_context: Option<String>,
|
||||
/// Optional snapshot of connected/active extensions for the current user.
|
||||
extension_state_summary: Option<String>,
|
||||
/// Channel name (e.g. "discord", "telegram") for formatting hints.
|
||||
channel: Option<String>,
|
||||
/// Model name for runtime context.
|
||||
@@ -371,6 +373,7 @@ impl Reasoning {
|
||||
llm,
|
||||
workspace_system_prompt: None,
|
||||
skill_context: None,
|
||||
extension_state_summary: None,
|
||||
channel: None,
|
||||
model_name: None,
|
||||
is_group_chat: false,
|
||||
@@ -400,6 +403,14 @@ impl Reasoning {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set extension runtime context to inject into the system prompt.
|
||||
pub fn with_extension_state_summary(mut self, summary: String) -> Self {
|
||||
if !summary.is_empty() {
|
||||
self.extension_state_summary = Some(summary);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the channel name for channel-specific formatting hints.
|
||||
pub fn with_channel(mut self, channel: impl Into<String>) -> Self {
|
||||
let ch = channel.into();
|
||||
@@ -932,21 +943,54 @@ Example:
|
||||
}
|
||||
|
||||
fn build_extensions_section_for_tools(&self, tools: &[ToolDefinition]) -> String {
|
||||
// Only include when the extension management tools are available
|
||||
let has_ext_tools = tools.iter().any(|t| t.name == "tool_search");
|
||||
if !has_ext_tools {
|
||||
let has_search = tools.iter().any(|t| t.name == "tool_search");
|
||||
let has_list = tools.iter().any(|t| t.name == "tool_list");
|
||||
let has_info = tools.iter().any(|t| t.name == "extension_info");
|
||||
|
||||
if self.extension_state_summary.is_none() && !has_search && !has_list && !has_info {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
"\n\n## Extensions\n\
|
||||
You can search, install, and activate extensions to add new capabilities:\n\
|
||||
- **Channels** (Telegram, Slack, Discord) — messaging integrations. \
|
||||
When users ask about connecting a messaging platform, search for it as a channel.\n\
|
||||
- **Tools** — sandboxed functions that extend your abilities.\n\
|
||||
- **MCP servers** — external API integrations via the Model Context Protocol.\n\n\
|
||||
Use `tool_search` to find extensions by name. Refer to them by their kind \
|
||||
(channel, tool, or server) — not as \"MCP server\" generically."
|
||||
.to_string()
|
||||
let mut blocks = Vec::new();
|
||||
if let Some(ref summary) = self.extension_state_summary {
|
||||
blocks.push(format!(
|
||||
"Current extension state for this user:\n{}",
|
||||
summary
|
||||
));
|
||||
}
|
||||
|
||||
if has_search || has_list || has_info {
|
||||
let mut guidance = String::from(
|
||||
"You can search, install, and activate extensions to add new capabilities:\n\
|
||||
- **Channels** (Telegram, Slack, Discord) — messaging integrations. \
|
||||
When users ask about connecting a messaging platform, search for it as a channel.\n\
|
||||
- **Tools** — sandboxed functions that extend your abilities.\n\
|
||||
- **MCP servers** — external API integrations via the Model Context Protocol.",
|
||||
);
|
||||
|
||||
if has_list {
|
||||
guidance.push_str(
|
||||
"\n\nBefore telling the user to connect, activate, or re-enable an extension, \
|
||||
inspect the current state with `tool_list`.",
|
||||
);
|
||||
}
|
||||
if has_info {
|
||||
guidance.push_str(
|
||||
"\nUse `extension_info` when you need deeper compatibility or runtime details \
|
||||
for an installed extension.",
|
||||
);
|
||||
}
|
||||
if has_search {
|
||||
guidance.push_str(
|
||||
"\nUse `tool_search` to find extensions by name. Refer to them by their kind \
|
||||
(channel, tool, or server) — not as \"MCP server\" generically.",
|
||||
);
|
||||
}
|
||||
|
||||
blocks.push(guidance);
|
||||
}
|
||||
|
||||
format!("\n\n## Extensions\n{}", blocks.join("\n\n"))
|
||||
}
|
||||
|
||||
fn build_channel_section(&self) -> String {
|
||||
@@ -2285,6 +2329,42 @@ That's my plan."#;
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_prompt_includes_extension_runtime_summary() {
|
||||
let reasoning = make_test_reasoning().with_extension_state_summary(
|
||||
"- Channels: telegram (authenticated, active, owner-bound)".to_string(),
|
||||
);
|
||||
|
||||
let prompt = reasoning.build_system_prompt_with_tools(&[]);
|
||||
assert!(
|
||||
prompt.contains("## Extensions"),
|
||||
"Prompt should contain an Extensions section when runtime state is present"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("telegram (authenticated, active, owner-bound)"),
|
||||
"Prompt should include the injected extension runtime summary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_prompt_extension_guidance_prefers_inspection_before_reconnect() {
|
||||
let reasoning = make_test_reasoning();
|
||||
let prompt = reasoning.build_system_prompt_with_tools(&make_tools(&[
|
||||
"tool_search",
|
||||
"tool_list",
|
||||
"extension_info",
|
||||
]));
|
||||
|
||||
assert!(
|
||||
prompt.contains("inspect the current state with `tool_list`"),
|
||||
"Prompt should direct the model to inspect installed/active state first"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("Use `extension_info` when you need deeper compatibility"),
|
||||
"Prompt should mention extension_info for deeper extension details"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- plan/evaluate bypass clean_response (Bug #564-2) ----
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -140,8 +140,7 @@ fn execution_properties() -> Value {
|
||||
},
|
||||
"use_tools": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Only applies to lightweight mode. New lightweight routines default this to true; when enabled, the routine can use the owner's live autonomous tool scope."
|
||||
"description": "Only applies to lightweight mode. When true, safe non-approval tools are available."
|
||||
},
|
||||
"max_tool_rounds": {
|
||||
"type": "integer",
|
||||
@@ -291,7 +290,7 @@ fn routine_request_discovery_schema() -> Value {
|
||||
fn lightweight_execution_variant() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"description": "Default lightweight execution. Applies when execution is omitted or execution.mode='lightweight'. New lightweight routines default to tools enabled unless execution.use_tools=false is set.",
|
||||
"description": "Default lightweight execution. Applies when execution is omitted or execution.mode='lightweight'.",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
@@ -305,8 +304,7 @@ fn lightweight_execution_variant() -> Value {
|
||||
},
|
||||
"use_tools": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Defaults to true for new lightweight routines. When enabled, the routine can use the owner's live autonomous tool scope."
|
||||
"description": "When true, safe non-approval tools are available."
|
||||
},
|
||||
"max_tool_rounds": {
|
||||
"type": "integer",
|
||||
@@ -337,7 +335,7 @@ fn full_job_execution_variant() -> Value {
|
||||
fn execution_discovery_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"description": "Optional execution settings. Omit this block for the default lightweight mode with tools enabled.",
|
||||
"description": "Optional execution settings. Omit this block for the default lightweight mode.",
|
||||
"properties": execution_properties(),
|
||||
"oneOf": [
|
||||
lightweight_execution_variant(),
|
||||
@@ -410,8 +408,7 @@ fn routine_create_tool_summary() -> ToolDiscoverySummary {
|
||||
"execution.mode='full_job' uses the owner's live autonomous tool scope and ignores use_tools, max_tool_rounds, and context_paths.".into(),
|
||||
],
|
||||
notes: vec![
|
||||
"Omitting execution defaults to lightweight mode with tools enabled.".into(),
|
||||
"Set execution.use_tools=false to keep a new lightweight routine text-only.".into(),
|
||||
"Omitting execution defaults to lightweight mode.".into(),
|
||||
"Omitting delivery.user falls back to the owner's last-seen notification target.".into(),
|
||||
"advanced.cooldown_secs defaults to 300.".into(),
|
||||
"Legacy flat aliases are still accepted for compatibility, but grouped fields are preferred.".into(),
|
||||
@@ -855,15 +852,11 @@ fn parse_execution_mode(value: Option<String>) -> Result<NormalizedExecutionMode
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_routine_execution(
|
||||
params: &Value,
|
||||
default_use_tools: bool,
|
||||
) -> Result<NormalizedExecutionRequest, ToolError> {
|
||||
fn parse_routine_execution(params: &Value) -> Result<NormalizedExecutionRequest, ToolError> {
|
||||
let mode = parse_execution_mode(string_field(params, "execution", "mode", &["action_type"]))?;
|
||||
let context_paths =
|
||||
string_array_field(params, "execution", "context_paths", &["context_paths"]);
|
||||
let use_tools =
|
||||
bool_field(params, "execution", "use_tools", &["use_tools"]).unwrap_or(default_use_tools);
|
||||
let use_tools = bool_field(params, "execution", "use_tools", &["use_tools"]).unwrap_or(false);
|
||||
let max_tool_rounds = u64_field(params, "execution", "max_tool_rounds", &["max_tool_rounds"])
|
||||
.unwrap_or(3)
|
||||
.clamp(1, crate::agent::routine::MAX_TOOL_ROUNDS_LIMIT as u64)
|
||||
@@ -895,7 +888,7 @@ fn parse_routine_create_request(
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let trigger = parse_routine_trigger(params)?;
|
||||
let execution = parse_routine_execution(params, true)?;
|
||||
let execution = parse_routine_execution(params)?;
|
||||
let delivery = parse_routine_delivery(params);
|
||||
let cooldown_secs =
|
||||
u64_field(params, "advanced", "cooldown_secs", &["cooldown_secs"]).unwrap_or(300);
|
||||
@@ -1870,56 +1863,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_lightweight_create_with_tools_enabled_by_default() {
|
||||
let params = serde_json::json!({
|
||||
"name": "manual-check",
|
||||
"prompt": "Inspect the repo for issues.",
|
||||
"request": {
|
||||
"kind": "manual"
|
||||
}
|
||||
});
|
||||
|
||||
let parsed = parse_routine_create_request(¶ms).expect("parse default lightweight");
|
||||
|
||||
assert!(
|
||||
matches!(parsed.execution.mode, NormalizedExecutionMode::Lightweight),
|
||||
"expected lightweight execution mode",
|
||||
);
|
||||
assert!(
|
||||
parsed.execution.use_tools,
|
||||
"new lightweight routines should default use_tools=true",
|
||||
);
|
||||
assert_eq!(parsed.execution.max_tool_rounds, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_lightweight_create_with_explicit_tools_disabled() {
|
||||
let params = serde_json::json!({
|
||||
"name": "manual-check",
|
||||
"prompt": "Inspect the repo for issues.",
|
||||
"request": {
|
||||
"kind": "manual"
|
||||
},
|
||||
"execution": {
|
||||
"use_tools": false
|
||||
}
|
||||
});
|
||||
|
||||
let parsed =
|
||||
parse_routine_create_request(¶ms).expect("parse lightweight with tools disabled");
|
||||
|
||||
assert!(
|
||||
matches!(parsed.execution.mode, NormalizedExecutionMode::Lightweight),
|
||||
"expected lightweight execution mode",
|
||||
);
|
||||
assert!(
|
||||
!parsed.execution.use_tools,
|
||||
"explicit use_tools=false should be preserved",
|
||||
);
|
||||
assert_eq!(parsed.execution.max_tool_rounds, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_context_paths_with_trim_drop_empty_and_stable_dedupe() {
|
||||
let params = serde_json::json!({
|
||||
@@ -2258,20 +2201,6 @@ mod tests {
|
||||
.any(|rule| rule.contains("request.kind='cron'")),
|
||||
"summary should explain cron requirement",
|
||||
);
|
||||
assert!(
|
||||
summary
|
||||
.notes
|
||||
.iter()
|
||||
.any(|note| note.contains("lightweight mode with tools enabled")),
|
||||
"summary should mention the new lightweight default",
|
||||
);
|
||||
assert!(
|
||||
summary
|
||||
.notes
|
||||
.iter()
|
||||
.any(|note| note.contains("execution.use_tools=false")),
|
||||
"summary should mention the text-only opt-out",
|
||||
);
|
||||
assert!(
|
||||
summary
|
||||
.notes
|
||||
|
||||
@@ -205,11 +205,11 @@ mod tests {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 5: routine_manual_create_defaults_to_tools_enabled
|
||||
// Test 5: routine_manual_create
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn routine_manual_create_defaults_to_tools_enabled() {
|
||||
async fn routine_manual_create() {
|
||||
let trace = LlmTrace::from_file(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/llm_traces/tools/routine_manual_create.json"
|
||||
@@ -235,51 +235,10 @@ mod tests {
|
||||
.expect("get_routine_by_name")
|
||||
.expect("manual-triage should exist");
|
||||
|
||||
assert!(matches!(routine.trigger, Trigger::Manual));
|
||||
assert!(
|
||||
matches!(&routine.action, RoutineAction::Lightweight { use_tools, .. } if *use_tools),
|
||||
"manual routine should default to lightweight with tools enabled: {:?}",
|
||||
routine.action
|
||||
);
|
||||
|
||||
rig.shutdown();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 6: routine_manual_create_explicit_no_tools
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn routine_manual_create_explicit_no_tools() {
|
||||
let trace = LlmTrace::from_file(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/llm_traces/tools/routine_manual_create_no_tools.json"
|
||||
))
|
||||
.expect("failed to load routine_manual_create_no_tools.json");
|
||||
|
||||
let rig = TestRigBuilder::new()
|
||||
.with_trace(trace.clone())
|
||||
.with_auto_approve_tools(true)
|
||||
.build()
|
||||
.await;
|
||||
|
||||
rig.send_message("Create a manual routine for quiet text-only bug triage")
|
||||
.await;
|
||||
let responses = rig.wait_for_responses(1, Duration::from_secs(15)).await;
|
||||
|
||||
rig.verify_trace_expects(&trace, &responses);
|
||||
|
||||
let routine = rig
|
||||
.database()
|
||||
.get_routine_by_name("test-user", "manual-triage-no-tools")
|
||||
.await
|
||||
.expect("get_routine_by_name")
|
||||
.expect("manual-triage-no-tools should exist");
|
||||
|
||||
assert!(matches!(routine.trigger, Trigger::Manual));
|
||||
assert!(
|
||||
matches!(&routine.action, RoutineAction::Lightweight { use_tools, .. } if !*use_tools),
|
||||
"manual routine should preserve explicit use_tools=false: {:?}",
|
||||
"manual routine should default to lightweight without tools: {:?}",
|
||||
routine.action
|
||||
);
|
||||
|
||||
@@ -287,7 +246,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 7: routine_history
|
||||
// Test 6: routine_history
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
@@ -324,7 +283,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 8: routine_system_event_emit
|
||||
// Test 7: routine_system_event_emit
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"model_name": "test-routine-manual-create-no-tools",
|
||||
"expects": {
|
||||
"tools_used": ["routine_create"],
|
||||
"all_tools_succeeded": true,
|
||||
"min_responses": 1
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"response": {
|
||||
"type": "tool_calls",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_rc_manual_2",
|
||||
"name": "routine_create",
|
||||
"arguments": {
|
||||
"name": "manual-triage-no-tools",
|
||||
"trigger_type": "manual",
|
||||
"prompt": "Summarize the latest bug reports when this routine is fired.",
|
||||
"execution": {
|
||||
"use_tools": false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"input_tokens": 90,
|
||||
"output_tokens": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"response": {
|
||||
"type": "text",
|
||||
"content": "Created the manual-triage-no-tools routine. It will only run when explicitly fired and stay text-only.",
|
||||
"input_tokens": 140,
|
||||
"output_tokens": 18
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "near-intents-tool"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Near Intents tools for token resolution, balance queries, and reverse lookups (WASM component)"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
wit-bindgen = "0.41.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
||||
[workspace]
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"wit_version": "0.3.0",
|
||||
"description": "Near Intents tools for token resolution, balance queries, and reverse lookups on the Defuse protocol.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["resolve_token", "reverse_resolve_token", "get_balance"],
|
||||
"description": "Which action to perform"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Token reference to resolve (for resolve_token)"
|
||||
},
|
||||
"list_all": {
|
||||
"type": "boolean",
|
||||
"description": "Return all tokens (for resolve_token)",
|
||||
"default": false
|
||||
},
|
||||
"asset_id": {
|
||||
"type": "string",
|
||||
"description": "Defuse asset ID (for reverse_resolve_token)"
|
||||
},
|
||||
"account_id": {
|
||||
"type": "string",
|
||||
"description": "NEAR wallet address (for get_balance)"
|
||||
},
|
||||
"token_ids": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Specific defuse asset IDs to query (for get_balance)"
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
},
|
||||
"capabilities": {
|
||||
"http": {
|
||||
"allowlist": [
|
||||
{
|
||||
"host": "1click.chaindefuser.com",
|
||||
"path_prefix": "/v0/tokens",
|
||||
"methods": ["GET"]
|
||||
},
|
||||
{
|
||||
"host": "rpc.mainnet.near.org",
|
||||
"path_prefix": "/",
|
||||
"methods": ["POST"]
|
||||
}
|
||||
],
|
||||
"rate_limit": {
|
||||
"requests_per_minute": 60
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["trading", "near", "defi", "intents"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user