From b0d7c100504836e92bd14efd310fba1e1741cd36 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 23 Jul 2026 11:24:41 +1000 Subject: [PATCH] apply tool call label capability --- crates/goose/src/acp/server.rs | 113 +++++------------ crates/goose/src/acp/server/load_session.rs | 116 +++++++++++++++--- .../src/acp/server/tool_calls/conversion.rs | 46 +++++-- crates/goose/src/config/base.rs | 1 - .../docs/guides/environment-variables.md | 1 - 5 files changed, 166 insertions(+), 111 deletions(-) diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index 0d5e660aeacf..34eaa0794409 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -201,7 +201,7 @@ pub struct GooseAcpAgent { client_supports_acp_elicitation: OnceCell, client_supports_goose_custom_notifications: OnceCell, client_supports_recipe_param_requests: OnceCell, - client_supports_tool_call_label_enrichment: OnceCell, + client_requests_tool_call_label_enrichment: OnceCell, use_login_shell_path: OnceCell, client_cx: OnceCell>, config_dir: std::path::PathBuf, @@ -575,6 +575,13 @@ impl GooseAcpAgent { .unwrap_or(false) } + fn requests_tool_call_label_enrichment(&self) -> bool { + self.client_requests_tool_call_label_enrichment + .get() + .copied() + .unwrap_or(false) + } + fn supports_acp_elicitation(&self) -> bool { self.client_supports_acp_elicitation .get() @@ -617,7 +624,7 @@ impl GooseAcpAgent { client_supports_acp_elicitation: OnceCell::new(), client_supports_goose_custom_notifications: OnceCell::new(), client_supports_recipe_param_requests: OnceCell::new(), - client_supports_tool_call_label_enrichment: OnceCell::new(), + client_requests_tool_call_label_enrichment: OnceCell::new(), use_login_shell_path: OnceCell::new(), client_cx: OnceCell::new(), config_dir: options.config_dir, @@ -1125,6 +1132,10 @@ impl GooseAcpAgent { session_id: &SessionId, cx: &ConnectionTo, ) { + if !self.requests_tool_call_label_enrichment() { + return; + } + let tool_call_notifier = ToolCallNotifier::new(cx, session_id); spawn_chain_summary_enrichment( agent, @@ -1144,14 +1155,13 @@ impl GooseAcpAgent { agent: &Arc, cx: &ConnectionTo, ) -> Result<(), agent_client_protocol::Error> { - let initial_tool_call = build_initial_tool_call(tool_request); + let client_requests_label_enrichment = self.requests_tool_call_label_enrichment(); + let initial_tool_call = + build_initial_tool_call(tool_request, client_requests_label_enrichment); let tool_call_notifier = ToolCallNotifier::new(cx, session_id); tool_call_notifier.send_initial(initial_tool_call)?; - if Config::global() - .get_goose_disable_tool_call_summary() - .unwrap_or(false) - { + if !client_requests_label_enrichment { return Ok(()); } @@ -1282,14 +1292,6 @@ fn extract_client_supports_recipe_param_requests( .unwrap_or(false) } -fn extract_client_supports_tool_call_label_enrichment( - goose_client_capabilities: Option<&GooseClientCapabilities>, -) -> bool { - goose_client_capabilities - .and_then(|goose| goose.tool_call_label_enrichment) - .unwrap_or(false) -} - fn outcome_to_confirmation(outcome: &RequestPermissionOutcome) -> PermissionConfirmation { PermissionConfirmation { principal_type: PrincipalType::Tool, @@ -1450,9 +1452,13 @@ impl GooseAcpAgent { let _ = self.client_supports_recipe_param_requests.set( extract_client_supports_recipe_param_requests(goose_client_capabilities.as_ref()), ); - let _ = self.client_supports_tool_call_label_enrichment.set( - extract_client_supports_tool_call_label_enrichment(goose_client_capabilities.as_ref()), - ); + let client_requests_tool_call_label_enrichment = goose_client_capabilities + .as_ref() + .and_then(|goose| goose.tool_call_label_enrichment) + .unwrap_or(false); + let _ = self + .client_requests_tool_call_label_enrichment + .set(client_requests_tool_call_label_enrichment); let _ = self .client_supports_acp_elicitation .set(elicitation::client_supports_form_elicitation(&args)); @@ -2345,15 +2351,12 @@ pub async fn run(builtins: Vec) -> Result<()> { #[cfg(test)] mod tests { use super::*; - use crate::acp::server::tool_calls::enrichment::tool_chain_summary; - use crate::conversation::message::ToolRequest; use crate::session::session_manager::SessionType; use agent_client_protocol::schema::v1::{ EnvVariable, HttpHeader, McpServer, McpServerHttp, McpServerSse, McpServerStdio, PermissionOptionId, ResourceLink, SelectedPermissionOutcome, }; use goose_providers::conversation::token_usage::Usage as TokenUsage; - use rmcp::model::CallToolRequestParams; use std::io::Write; use std::path::PathBuf; use tempfile::NamedTempFile; @@ -2449,62 +2452,6 @@ print(\"hello, world\") assert_eq!(result, expected,) } - #[test] - fn replay_attaches_chain_summary_meta_for_first_tool_request_with_persisted_summary() { - let tool_request = ToolRequest { - id: "req_first".to_string(), - tool_call: Ok(CallToolRequestParams::new("developer__shell")), - metadata: None, - tool_meta: Some(serde_json::json!({ - crate::conversation::message::TOOL_META_CHAIN_SUMMARY_KEY: { - "summary": "applied dark mode polish", - "count": 3, - }, - })), - }; - - let mut initial_tool_call = build_initial_tool_call(&tool_request); - let goose = initial_tool_call - .meta - .as_mut() - .and_then(|meta| meta.get_mut("goose")) - .and_then(serde_json::Value::as_object_mut) - .expect("valid initial tool call should contain goose metadata"); - let chain_summary = tool_request - .generated_chain_summary() - .expect("chain summary should be present"); - goose.extend([tool_chain_summary(&chain_summary)]); - - assert_eq!( - goose.get("toolCall"), - Some( - &serde_json::json!({ "toolName": "developer__shell", "extensionName": "developer" }) - ), - "replay must preserve identity meta alongside the chain summary", - ); - assert_eq!( - goose.get("toolChainSummary"), - Some(&serde_json::json!({ "summary": "applied dark mode polish", "count": 3 })), - "replay must attach toolChainSummary so the chain header renders on first paint", - ); - } - - #[test] - fn replay_does_not_attach_chain_summary_for_tool_requests_without_persisted_summary() { - let tool_request = ToolRequest { - id: "req_second".to_string(), - tool_call: Ok(CallToolRequestParams::new("developer__shell")), - metadata: None, - tool_meta: None, - }; - - let chain_summary = tool_request.generated_chain_summary(); - assert!( - chain_summary.is_none(), - "non-first tool requests must not carry chain summaries", - ); - } - #[test_case( RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(PermissionOptionId::from("allow_once".to_string()))), PermissionConfirmation { principal_type: PrincipalType::Tool, permission: Permission::AllowOnce }; @@ -2720,9 +2667,9 @@ print(\"hello, world\") InitializeRequest::new(agent_client_protocol::schema::ProtocolVersion::LATEST); let goose_client_capabilities = extract_client_capabilities_meta(&request).and_then(|meta| meta.goose); - assert!(!extract_client_supports_tool_call_label_enrichment( - goose_client_capabilities.as_ref() - )); + assert!(!goose_client_capabilities + .and_then(|goose| goose.tool_call_label_enrichment) + .unwrap_or(false)); let mut goose_meta = serde_json::Map::new(); goose_meta.insert( @@ -2738,8 +2685,8 @@ print(\"hello, world\") ); let goose_client_capabilities = extract_client_capabilities_meta(&request).and_then(|meta| meta.goose); - assert!(extract_client_supports_tool_call_label_enrichment( - goose_client_capabilities.as_ref() - )); + assert!(goose_client_capabilities + .and_then(|goose| goose.tool_call_label_enrichment) + .unwrap_or(false)); } } diff --git a/crates/goose/src/acp/server/load_session.rs b/crates/goose/src/acp/server/load_session.rs index db34735532cb..bc99f3ceb809 100644 --- a/crates/goose/src/acp/server/load_session.rs +++ b/crates/goose/src/acp/server/load_session.rs @@ -3,6 +3,7 @@ use super::tool_calls::conversion::{ }; use super::tool_calls::enrichment::tool_chain_summary; use super::*; +use agent_client_protocol::schema::v1::ToolCall; fn replay_message_meta(message: &Message) -> Meta { let mut meta = serde_json::Map::new(); @@ -69,10 +70,41 @@ fn send_replay_content_chunk( cx.send_notification(SessionNotification::new(session_id.clone(), update)) } +fn build_replayed_tool_call( + tool_request: &ToolRequest, + client_requests_tool_call_label_enrichment: bool, +) -> ToolCall { + let mut tool_call = + build_initial_tool_call(tool_request, client_requests_tool_call_label_enrichment); + + if !client_requests_tool_call_label_enrichment { + return tool_call; + } + + let Some(chain_summary) = tool_request.generated_chain_summary() else { + return tool_call; + }; + let goose_meta = tool_call + .meta + .get_or_insert_default() + .entry("goose".to_string()) + .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())); + if !goose_meta.is_object() { + *goose_meta = serde_json::Value::Object(serde_json::Map::new()); + } + goose_meta + .as_object_mut() + .expect("goose metadata was initialized as an object") + .extend([tool_chain_summary(&chain_summary)]); + + tool_call +} + fn replay_conversation_to_client( cx: &ConnectionTo, session: &Session, supports_goose_custom_notifications: bool, + client_requests_tool_call_label_enrichment: bool, ) -> Result<(), agent_client_protocol::Error> { let session_id = SessionId::new(session.id.clone()); let tool_call_notifier = ToolCallNotifier::new(cx, &session_id); @@ -112,21 +144,11 @@ fn replay_conversation_to_client( MessageContent::ToolRequest(tool_request) => { replay_tool_requests.insert(tool_request.id.clone(), tool_request.clone()); - let mut tool_call = build_initial_tool_call(tool_request); - let mut meta = tool_call.meta.take(); - if let Some(chain_summary) = tool_request.generated_chain_summary() { - let goose_meta = meta - .get_or_insert_default() - .entry("goose".to_string()) - .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())); - if !goose_meta.is_object() { - *goose_meta = serde_json::Value::Object(serde_json::Map::new()); - } - goose_meta - .as_object_mut() - .expect("goose metadata was initialized as an object") - .extend([tool_chain_summary(&chain_summary)]); - } + let mut tool_call = build_replayed_tool_call( + tool_request, + client_requests_tool_call_label_enrichment, + ); + let meta = tool_call.meta.take(); let tool_call = tool_call.meta(merge_replay_message_meta(meta, message)); tool_call_notifier.send_initial(tool_call)?; @@ -201,7 +223,12 @@ impl GooseAcpAgent { .prepare_session_for_activation(session, args.cwd.clone(), args.mcp_servers, true) .await?; - replay_conversation_to_client(cx, &session, self.supports_goose_custom_notifications())?; + replay_conversation_to_client( + cx, + &session, + self.supports_goose_custom_notifications(), + self.requests_tool_call_label_enrichment(), + )?; let (agent, extension_results) = self.prepare_acp_session_agent(cx, &session).await?; self.apply_session_recipe(&agent, &session).await?; self.register_acp_session(session_id_str.clone(), agent.clone()) @@ -238,6 +265,63 @@ impl GooseAcpAgent { #[cfg(test)] mod tests { use super::*; + use rmcp::model::CallToolRequestParams; + + fn persisted_enriched_tool_request() -> ToolRequest { + ToolRequest { + id: "req_first".to_string(), + tool_call: Ok(CallToolRequestParams::new("developer__shell")), + metadata: None, + tool_meta: Some(serde_json::json!({ + (crate::conversation::message::TOOL_META_TITLE_KEY): "applied dark mode polish", + (crate::conversation::message::TOOL_META_CHAIN_SUMMARY_KEY): { + "summary": "applied dark mode polish", + "count": 3, + }, + })), + } + } + + #[test] + fn replay_includes_persisted_enrichment_when_requested() { + let tool_call = build_replayed_tool_call(&persisted_enriched_tool_request(), true); + let goose = tool_call + .meta + .as_ref() + .and_then(|meta| meta.get("goose")) + .expect("valid initial tool call should contain goose metadata"); + + assert_eq!(tool_call.title, "applied dark mode polish"); + assert_eq!( + goose.get("toolCall"), + Some( + &serde_json::json!({ "toolName": "developer__shell", "extensionName": "developer" }) + ), + ); + assert_eq!( + goose.get("toolChainSummary"), + Some(&serde_json::json!({ "summary": "applied dark mode polish", "count": 3 })), + ); + } + + #[test] + fn replay_omits_persisted_enrichment_when_not_requested() { + let tool_call = build_replayed_tool_call(&persisted_enriched_tool_request(), false); + let goose = tool_call + .meta + .as_ref() + .and_then(|meta| meta.get("goose")) + .expect("valid initial tool call should contain goose metadata"); + + assert_eq!(tool_call.title, "developer: shell"); + assert_eq!( + goose.get("toolCall"), + Some( + &serde_json::json!({ "toolName": "developer__shell", "extensionName": "developer" }) + ), + ); + assert_eq!(goose.get("toolChainSummary"), None); + } #[test] fn merge_replay_message_meta_preserves_existing_goose_meta() { diff --git a/crates/goose/src/acp/server/tool_calls/conversion.rs b/crates/goose/src/acp/server/tool_calls/conversion.rs index 3abe07dd2ed9..ef3e0614be09 100644 --- a/crates/goose/src/acp/server/tool_calls/conversion.rs +++ b/crates/goose/src/acp/server/tool_calls/conversion.rs @@ -82,7 +82,10 @@ pub(crate) fn goose_tool_call_meta(tool_request: &ToolRequest) -> Option { Some(meta) } -pub(crate) fn build_initial_tool_call(tool_request: &ToolRequest) -> ToolCall { +pub(crate) fn build_initial_tool_call( + tool_request: &ToolRequest, + include_generated_title: bool, +) -> ToolCall { let tool_name = match &tool_request.tool_call { Ok(tool_call) => tool_call.name.to_string(), Err(_) => "error".to_string(), @@ -96,10 +99,14 @@ pub(crate) fn build_initial_tool_call(tool_request: &ToolRequest) -> ToolCall { let default_tool_call_title = default_tool_title(&tool_name, args_value.as_ref()); let goose_meta = goose_tool_call_meta(tool_request); - let initial_title = tool_request - .generated_title() - .map(|s| s.to_string()) - .unwrap_or(default_tool_call_title); + let initial_title = if include_generated_title { + tool_request + .generated_title() + .map(str::to_string) + .unwrap_or(default_tool_call_title) + } else { + default_tool_call_title + }; let mut tool_call = ToolCall::new(ToolCallId::new(tool_request.id.clone()), initial_title) .status(ToolCallStatus::Pending); @@ -368,7 +375,7 @@ mod tests { tool_meta: Some(serde_json::json!({"goose_extension": "developer"})), }; - let tool_call = build_initial_tool_call(&request); + let tool_call = build_initial_tool_call(&request, false); assert_eq!(tool_call.title, "edit · /src/main.rs"); assert_eq!(tool_call.status, ToolCallStatus::Pending); @@ -388,7 +395,7 @@ mod tests { } #[test] - fn uses_generated_title() { + fn uses_generated_title_when_enrichment_is_enabled() { let arguments = json_object(vec![("command", serde_json::json!("cargo test"))]); let request = ToolRequest { id: "req_1".to_string(), @@ -401,11 +408,30 @@ mod tests { })), }; - let tool_call = build_initial_tool_call(&request); + let tool_call = build_initial_tool_call(&request, true); assert_eq!(tool_call.title, "running focused tests"); } + #[test] + fn uses_default_title_when_enrichment_is_disabled() { + let arguments = json_object(vec![("command", serde_json::json!("cargo test"))]); + let request = ToolRequest { + id: "req_1".to_string(), + tool_call: Ok( + CallToolRequestParams::new("developer__shell").with_arguments(arguments) + ), + metadata: None, + tool_meta: Some(serde_json::json!({ + (TOOL_META_TITLE_KEY): "running focused tests", + })), + }; + + let tool_call = build_initial_tool_call(&request, false); + + assert_eq!(tool_call.title, "developer: shell · cargo test"); + } + #[test] fn handles_invalid_request() { let request = ToolRequest { @@ -415,7 +441,7 @@ mod tests { tool_meta: None, }; - let tool_call = build_initial_tool_call(&request); + let tool_call = build_initial_tool_call(&request, false); assert_eq!(tool_call.title, "error"); assert_eq!(tool_call.status, ToolCallStatus::Pending); @@ -437,7 +463,7 @@ mod tests { metadata: None, tool_meta: None, }; - let initial = build_initial_tool_call(&request); + let initial = build_initial_tool_call(&request, false); let permission = build_permission_tool_call_update( &request.id, diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index 9bfabbba551a..8a62b98ea15e 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -1138,7 +1138,6 @@ config_value!(GOOSE_PROMPT_EDITOR, Option); config_value!(GOOSE_PROMPT_EDITOR_ALWAYS, Option); config_value!(GOOSE_MAX_ACTIVE_AGENTS, usize); config_value!(GOOSE_DISABLE_SESSION_NAMING, bool); -config_value!(GOOSE_DISABLE_TOOL_CALL_SUMMARY, bool); impl Config { pub fn get_goose_context_limit(&self) -> Result, ConfigError> { diff --git a/documentation/docs/guides/environment-variables.md b/documentation/docs/guides/environment-variables.md index 8d5cd7a9d927..770d953756bd 100644 --- a/documentation/docs/guides/environment-variables.md +++ b/documentation/docs/guides/environment-variables.md @@ -160,7 +160,6 @@ These variables control how goose manages conversation sessions and context. | `GOOSE_MAX_BACKGROUND_TASKS` | Sets the maximum number of concurrent background [subagent](/docs/guides/context-engineering/subagents) tasks goose can run at once | Integer (e.g., 1, 5, 10) | 5 | | `CONTEXT_FILE_NAMES` | Specifies custom filenames for [hint/context files](/docs/guides/context-engineering/using-goosehints#custom-context-files) | JSON array of strings (e.g., `["CLAUDE.md", ".goosehints"]`) | `[".goosehints", "AGENTS.md"]` | | `GOOSE_DISABLE_SESSION_NAMING` | Disables automatic AI-generated session naming; avoids the background model call and keeps the default "CLI Session" (goose CLI) or "New Chat" (goose Desktop) | "1", "true" (case-insensitive) to enable | false | -| `GOOSE_DISABLE_TOOL_CALL_SUMMARY` | Disables the per-tool-call AI-generated summary title, keeping the fallback title instead. Saves one provider call per tool invocation. | "1", "true" (case-insensitive) to enable | false | | `GOOSE_PROMPT_EDITOR` | [External editor](/docs/guides/goose-cli-commands#external-editor-mode) to use for composing prompts instead of CLI input | Editor command (e.g., "vim", "code --wait") | Unset (uses CLI input) | | `GOOSE_CLI_THEME` | [Theme](/docs/guides/goose-cli-commands#themes) for CLI response markdown | "light", "dark", "ansi" | "ansi" | | `GOOSE_CLI_LIGHT_THEME` | Custom [bat theme](https://github.com/sharkdp/bat#adding-new-themes) for syntax highlighting when using light mode | bat theme name (e.g., "Solarized (light)", "OneHalfLight") | "GitHub" |