Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 30 additions & 83 deletions crates/goose/src/acp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ pub struct GooseAcpAgent {
client_supports_acp_elicitation: OnceCell<bool>,
client_supports_goose_custom_notifications: OnceCell<bool>,
client_supports_recipe_param_requests: OnceCell<bool>,
client_supports_tool_call_label_enrichment: OnceCell<bool>,
client_requests_tool_call_label_enrichment: OnceCell<bool>,
use_login_shell_path: OnceCell<bool>,
client_cx: OnceCell<ConnectionTo<Client>>,
config_dir: std::path::PathBuf,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1125,6 +1132,10 @@ impl GooseAcpAgent {
session_id: &SessionId,
cx: &ConnectionTo<Client>,
) {
if !self.requests_tool_call_label_enrichment() {
return;
}

let tool_call_notifier = ToolCallNotifier::new(cx, session_id);
spawn_chain_summary_enrichment(
agent,
Expand All @@ -1144,14 +1155,13 @@ impl GooseAcpAgent {
agent: &Arc<Agent>,
cx: &ConnectionTo<Client>,
) -> 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(());
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Comment on lines +1455 to +1458

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Advertise or preserve tool-call label enrichment

When running the bundled ACP frontends, this new default treats the missing toolCallLabelEnrichment bit as an opt-out: the desktop initializer still sends only mcpHostCapabilities, customNotifications, and recipeParameterRequests in ui/desktop/src/acp/acpConnection.ts:145-152, and the text UI sends clientCapabilities: {} in ui/text/src/tui.tsx:795-799 and 1338-1342. As a result handle_tool_request returns before spawning title generation and spawn_ready_chain_summary also returns, so these clients stop generating/persisting the enriched tool titles and chain summaries that previously appeared on replay; either set the capability in the bundled clients or preserve the old behavior for clients that have not been updated.

Useful? React with 👍 / 👎.

@lifeizhou-ap lifeizhou-ap Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ui/desktop:
Currently we deliberately skipped the llm generated enriched notification, so with this PR, it does not change any ui/desktop behaviour. UI change will be a separate PR to remove the special skip logic after this PR is merged

TUI
We paused TUI dev already

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));
Expand Down Expand Up @@ -2345,15 +2351,12 @@ pub async fn run(builtins: Vec<String>) -> 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;
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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(
Expand All @@ -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));
}
}
116 changes: 100 additions & 16 deletions crates/goose/src/acp/server/load_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<Client>,
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);
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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() {
Expand Down
Loading
Loading