Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2210e2e
feat(chat): order-driven tool-chain grouping with deterministic summary
tellaho May 3, 2026
31f4a4a
feat(chat): clean labeled inputs in expanded tool cards
tellaho May 3, 2026
b3ba8a8
feat(chat): stacked-rail tool-chain layout with status bullets
tellaho May 4, 2026
5583976
feat(acp): persist server-side tool-call summaries
tellaho May 4, 2026
f087fa6
fix(acp): persist tool-call metadata to the row that owns the tool call
tellaho May 4, 2026
72ee0e5
fix(acp): retry once on transient fast-model failures and surface per…
tellaho May 4, 2026
a8c5588
fix(chat): late tool_call_update lands on the message that owns the t…
tellaho May 4, 2026
4bd0253
feat(chat): refine expanded tool card with controllable context and c…
tellaho May 4, 2026
a0e7cab
feat(goose2): polish tool chain card layout and spine
tellaho May 4, 2026
9d50e13
feat(goose2): refine tool chain spine and summary spacing
tellaho May 4, 2026
3297e66
feat(goose2): reset tool chain children on collapse and simplify erro…
tellaho May 4, 2026
9fb033a
feat(goose2): add left caret to single tool calls and hide trailing c…
tellaho May 4, 2026
cba5fec
chore(goose2): increase vertical spacing around tool chain cards
tellaho May 4, 2026
a591d44
feat(goose2): collapse tool chain cards by default during replay
tellaho May 4, 2026
afb4b35
feat(goose2): auto-collapse tool chain cards when they finish in real…
tellaho May 4, 2026
0e0dfd3
fix(goose2): keep tool call container full width inside chains
tellaho May 4, 2026
b27569e
test(goose2): expand collapsed chain card before asserting internal s…
tellaho May 4, 2026
d31d899
chore(goose2): adapt tool-chain branch to new artifact link API
tellaho May 4, 2026
094c640
refactor(goose2): drop fitWidth, plainText, and cap structured fallback
tellaho May 4, 2026
c37cb27
feat(goose2): smart de-dupe and title hoisting for tool results
tellaho May 4, 2026
66bbe73
fix(tool-chain): vertically center chevron with header label
tellaho May 4, 2026
ee4a194
fix(acp): detect tool chains across interleaved request/response events
tellaho May 5, 2026
cfd0e35
fix(goose2): use exact tool_call_id when attributing live tool responses
tellaho May 5, 2026
1df132b
chore(goose2): translate tools.openNamed in es/chat.json
tellaho May 5, 2026
0ada962
fix(goose2): localize tool input summary labels via i18n keys
tellaho May 5, 2026
07ab163
chore: merge origin/main into tho/tool-chain-grouping
tellaho May 5, 2026
7f9cc11
fix(goose2): preserve streaming pointer for late tool responses
matt2e May 5, 2026
fed4d78
Merge remote-tracking branch 'origin/main' into tho/tool-chain-grouping
tellaho May 5, 2026
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
856 changes: 818 additions & 38 deletions crates/goose/src/acp/server.rs

Large diffs are not rendered by default.

122 changes: 122 additions & 0 deletions crates/goose/src/conversation/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,60 @@ impl ToolRequest {
.and_then(|v| v.as_bool())
.unwrap_or(false)
}

/// Returns the persisted LLM-generated title for this tool call, if any.
/// Set asynchronously by [`crate::acp::server`] after `provider.complete_fast`
/// resolves; survives session reload via SQLite. Falls back to `None` for
/// older sessions that predate persistence — callers should use a deterministic
/// title in that case.
pub fn persisted_title(&self) -> Option<&str> {
self.tool_meta
.as_ref()
.and_then(|v| v.get(TOOL_META_TITLE_KEY))
.and_then(|v| v.as_str())
}

/// Returns the persisted per-chain summary anchored on this tool request,
/// if any. Only the FIRST tool request in a chain (a run of consecutive
/// tool blocks within one assistant message) carries this. See
/// [`crate::acp::server`] for how chains are detected and summarized.
pub fn persisted_chain_summary(&self) -> Option<PersistedChainSummary> {
let obj = self
.tool_meta
.as_ref()
.and_then(|v| v.get(TOOL_META_CHAIN_SUMMARY_KEY))?;
let summary = obj.get("summary").and_then(|v| v.as_str())?.to_string();
let count = obj.get("count").and_then(|v| v.as_u64())?;
if count == 0 {
return None;
}
Some(PersistedChainSummary {
summary,
count: count as usize,
})
}
}

/// A chain summary persisted on the first tool request of a chain.
#[derive(Debug, Clone, PartialEq)]
pub struct PersistedChainSummary {
pub summary: String,
pub count: usize,
}

/// Marker key under `ToolRequest.tool_meta` indicating the tool was already
/// executed externally; the agent loop must skip redispatch.
pub const TOOL_META_EXTERNAL_DISPATCH_KEY: &str = "goose.external_dispatch";

/// Key under `ToolRequest.tool_meta` storing the LLM-generated short title
/// for this tool call. Used to make the title survive session reload.
pub const TOOL_META_TITLE_KEY: &str = "goose.toolSummary.title";

/// Key under `ToolRequest.tool_meta` storing the LLM-generated chain summary
/// for the chain that starts at this tool request. Shape: `{ "summary": String,
/// "count": u64 }`. Only attached to the FIRST tool request in a chain.
pub const TOOL_META_CHAIN_SUMMARY_KEY: &str = "goose.toolChain.summary";

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(ToSchema)]
Expand Down Expand Up @@ -1635,4 +1683,78 @@ mod tests {
}
}
}

fn make_tool_request(meta: Option<serde_json::Value>) -> super::ToolRequest {
super::ToolRequest {
id: "id-1".to_string(),
tool_call: Ok(CallToolRequestParams::new("test_tool")),
metadata: None,
tool_meta: meta,
}
}

#[test]
fn persisted_title_returns_none_when_meta_missing() {
let req = make_tool_request(None);
assert_eq!(req.persisted_title(), None);
}

#[test]
fn persisted_title_returns_value_when_present() {
let meta = serde_json::json!({
super::TOOL_META_TITLE_KEY: "reading project configuration",
});
let req = make_tool_request(Some(meta));
assert_eq!(req.persisted_title(), Some("reading project configuration"));
}

#[test]
fn persisted_title_returns_none_for_non_string_value() {
let meta = serde_json::json!({ super::TOOL_META_TITLE_KEY: 42 });
let req = make_tool_request(Some(meta));
assert_eq!(req.persisted_title(), None);
}

#[test]
fn persisted_title_does_not_collide_with_external_dispatch() {
let meta = serde_json::json!({
super::TOOL_META_EXTERNAL_DISPATCH_KEY: true,
super::TOOL_META_TITLE_KEY: "running commands",
});
let req = make_tool_request(Some(meta));
assert!(req.is_externally_dispatched());
assert_eq!(req.persisted_title(), Some("running commands"));
}

#[test]
fn persisted_chain_summary_round_trips() {
let meta = serde_json::json!({
super::TOOL_META_CHAIN_SUMMARY_KEY: {
"summary": "applied dark mode polish",
"count": 4,
},
});
let req = make_tool_request(Some(meta));
let summary = req.persisted_chain_summary().expect("summary present");
assert_eq!(summary.summary, "applied dark mode polish");
assert_eq!(summary.count, 4);
}

#[test]
fn persisted_chain_summary_returns_none_for_missing_or_zero_count() {
let req = make_tool_request(None);
assert!(req.persisted_chain_summary().is_none());

let meta_zero = serde_json::json!({
super::TOOL_META_CHAIN_SUMMARY_KEY: { "summary": "x", "count": 0 },
});
let req_zero = make_tool_request(Some(meta_zero));
assert!(req_zero.persisted_chain_summary().is_none());

let meta_no_summary = serde_json::json!({
super::TOOL_META_CHAIN_SUMMARY_KEY: { "count": 3 },
});
let req_no_summary = make_tool_request(Some(meta_no_summary));
assert!(req_no_summary.persisted_chain_summary().is_none());
}
}
Loading
Loading