Skip to content

feat(desktop): acp tool summaries (experiment)#1574

Draft
tellaho wants to merge 5 commits into
mainfrom
tho/acp-tool-summaries
Draft

feat(desktop): acp tool summaries (experiment)#1574
tellaho wants to merge 5 commits into
mainfrom
tho/acp-tool-summaries

Conversation

@tellaho

@tellaho tellaho commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Category: improvement
User Impact: Users can opt into friendlier agent activity rows that describe tool intent while the production default stays unchanged.
Problem: Tool activity rows currently expose implementation details like raw commands and file paths, which makes busy agent turns harder to scan. Because this touches production agent activity and adds an extra fast summary call, it needs a safe default-off rollout path.
Solution: Buzz now emits and consumes per-tool friendly ACP titles, but the visible summaries and background summary generation are both gated behind the acpToolSummaries preview experiment. When the experiment is off, compact rows keep the old raw labels and spawned agents receive BUZZ_AGENT_NO_TOOL_SUMMARY=1; when it is on, friendly labels paint while failures and user opt-outs still take precedence.

File changes

crates/buzz-agent/src/agent.rs
Adds deterministic friendly fallback titles and an async fast-pass summary update for individual tool calls. Keeps exact tool identity in toolName and sends summary updates without status so they cannot clobber tool state.

crates/buzz-agent/src/config.rs
Adds BUZZ_AGENT_NO_TOOL_SUMMARY and BUZZ_AGENT_TOOL_SUMMARY_MODEL configuration for disabling or redirecting the fast summary pass.

crates/buzz-agent/src/llm.rs
Carries the tool-summary config through LLM test/default setup so summary behavior can be isolated in tests.

crates/buzz-agent/tests/golden_transcripts.rs
Adds transcript coverage for pending friendly titles, title-only async summary updates, and the kill switch path.

crates/buzz-agent/tests/regressions.rs
Disables summary generation in request-count-sensitive regression harnesses so existing LLM sequencing remains deterministic.

desktop/scripts/check-file-sizes.mjs
Updates documented size-budget exceptions for files that gained focused gate/runtime logic.

desktop/src-tauri/src/commands/agent_settings.rs
Registers the desktop experiment mirror command so the frontend can persist preview toggle state for Rust spawn code.

desktop/src-tauri/src/lib.rs
Exposes the experiment mirror command to the Tauri invoke layer.

desktop/src-tauri/src/managed_agents/experiments.rs
Adds the desktop-side experiment mirror store, atomic writes, fail-safe parsing, and tests for missing/malformed state.

desktop/src-tauri/src/managed_agents/mod.rs
Exports the experiment helpers and the acpToolSummaries experiment id for managed-agent spawn code.

desktop/src-tauri/src/managed_agents/runtime.rs
Applies the spawn-time cost gate: experiment off forces BUZZ_AGENT_NO_TOOL_SUMMARY=1, experiment on leaves user opt-outs untouched.

desktop/src-tauri/src/managed_agents/runtime/tests.rs
Adds spawn-env tests covering default off, enabled behavior, and user-provided opt-out precedence.

desktop/src/app/AppShell.tsx
Mirrors preview experiment state to Rust on app boot and whenever the toggle state changes.

desktop/src/features/agents/ui/AgentSessionToolItem/CompactToolSummaryRow.render.test.mjs
Adds render-level coverage proving visible row labels change only when summaries are enabled and failures still win.

desktop/src/features/agents/ui/AgentSessionToolItem/CompactToolSummaryRow.tsx
Renders friendly summary titles ahead of action descriptors when enabled, while preserving exact receipts on hover/expand.

desktop/src/features/agents/ui/AgentSessionToolItem/ToolItem.tsx
Threads the preview flag into compact tool summary building for the main activity row surface.

desktop/src/features/agents/ui/agentSessionToolSummary.test.mjs
Adds unit coverage for summary-title off/on behavior and failure precedence at the summary-builder choke point.

desktop/src/features/agents/ui/agentSessionToolSummary.ts
Stores summaryTitle separately from raw labels and makes friendly title usage explicit through a default-off option.

desktop/src/features/agents/ui/agentSessionTranscript.test.mjs
Adds transcript ingestion tests for ACP summary updates, out-of-order updates, and fallback behavior.

desktop/src/features/agents/ui/agentSessionTranscript.ts
Ingests ACP tool_call_update summary titles into transcript state without mutating status, args, result, or timestamps.

desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
Adds helper logic for recognizing Buzz ACP tool-summary update metadata.

desktop/src/features/agents/ui/agentSessionTranscriptPresentation.test.mjs
Adds presentation-level coverage for headline gating behavior.

desktop/src/features/agents/ui/agentSessionTranscriptPresentation.ts
Threads summary-title gating into activity headline generation so compact feed headlines match row behavior.

desktop/src/features/agents/ui/agentSessionTypes.ts
Extends transcript tool state with separate summary-title data.

desktop/src/features/channels/ui/BotActivityBar.tsx
Reads the preview flag for composer activity headlines so the live activity bar matches the tool-row experiment state.

desktop/src/shared/api/tauri.ts
Adds setDesktopExperiments for mirroring preview toggle state to Rust.

desktop/src/shared/features/index.ts
Exports the new experiment mirror hook from the shared feature module.

desktop/src/shared/features/useDesktopExperimentsMirror.ts
Adds the React hook that observes preview feature overrides and mirrors them to the Tauri command.

preview-features.json
Adds the default-off acpToolSummaries preview experiment.

Reproduction Steps

  1. Build and run the desktop app from this branch.
  2. Leave Friendly Tool Summaries disabled in Settings → Experiments and run an agent turn with shell/file-read tools.
  3. Confirm compact activity rows keep the previous raw labels, such as Ran git status and Read crates/buzz-agent/src/agent.rs.
  4. Start or restart an agent with the experiment disabled and confirm the spawned environment includes BUZZ_AGENT_NO_TOOL_SUMMARY=1.
  5. Enable Friendly Tool Summaries, restart the agent, and run the same style of tool calls.
  6. Confirm compact rows now show intent-level labels such as checking repository state, while expanding or hovering still exposes the exact command/path.
  7. Trigger a failed shell tool and confirm the failure label still wins over any friendly phrase.

Screenshots/Demos

Screenshots from Marge's full UI pass against a dist rebuilt from gate commit 96a83a1bf. The only change between each pair is the acpToolSummaries preview flag.

1. Activity panel — tool rows (ToolItem / CompactToolSummaryRow)

OFF — raw compact labels:
image

ON — friendly intent summaries:
image

2. Composer activity headline (BotActivityBar / getActivityHeadline)

OFF — raw headline (Ran command · git status):
image

ON — friendly headline (checking repository state · git status):
image

Validation

  • pnpm test 1937/1937
  • desktop cargo test --lib 978 pass
  • cargo test -p buzz-agent
  • pnpm exec tsc --noEmit
  • biome check .
  • cargo fmt

Rollout note: cut production builds from 96a83a1bf or newer. A stale dist/ built before the gate does not contain acpToolSummaries and can make off/on testing misleading.

Coordination: buzz://message?channel=8905fd94-5537-4062-8d45-1d79eff51a74&thread=6508f23bd67de9b91443714c5be913e0cae949c5c98dee393a5822620edd15e1

@tellaho tellaho changed the title feat(desktop): gate acp tool summaries behind experiment feat(desktop): acp tool summaries (experiment) Jul 7, 2026
@tellaho tellaho marked this pull request as ready for review July 7, 2026 09:10
@tellaho tellaho enabled auto-merge (squash) July 7, 2026 09:10
@tellaho tellaho marked this pull request as draft July 7, 2026 15:33
auto-merge was automatically disabled July 7, 2026 15:33

Pull request was converted to draft

npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 5 commits July 7, 2026 14:19
Port the Goose two-stage tool-title pattern for individual tool calls.
emit_pending now sends a deterministic friendly title (server: tool
plus a detail excerpt from path/command/query-style args, 60-char
first-line cap, multibyte-safe) alongside an explicit toolName so the
desktop never loses tool identity. For runnable tools a fire-and-forget
fast pass asks the LLM (64 output tokens, 300-char args excerpt, one
retry) for a friendlier title and publishes it as a title-only
tool_call_update tagged _meta.buzz.toolSummary — deliberately without
a status field so it can never clobber pending/executing/terminal
state. Cancel-aware via the session watch channel; silent on failure.

Kill switch BUZZ_AGENT_NO_TOOL_SUMMARY=1 disables the fast pass; model
override via BUZZ_AGENT_TOOL_SUMMARY_MODEL. The regression harness
sets the kill switch since those tests queue exact LLM response
sequences and count requests; golden_transcripts.rs carries the
explicit summary-path coverage (pending title + toolName, title-only
async update, kill switch). No chain summaries in this slice.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Ingest the Buzz ACP fast-pass tool summaries: tool_call_update events
tagged _meta.buzz.toolSummary route to applyToolSummaryTitle, which
stores summaryTitle as a separate field — never mutating status, args,
result, or timestamps — and drops out-of-order summaries that arrive
before their tool_call. buildCompactToolSummary prefers summaryTitle
as the row label; failure labels always win (failed rows ignore the
friendly phrasing), the classifier stays as fallback for older/raw
events without the marker, and exact command/path receipts remain
available on expand. File-size exception for agentSessionTranscript.ts
bumped via its documented ledger; split still queued.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Marge's review caught that the friendly summaries never reached the
screen for the tools that matter most: getCompactToolActionLabel took
the structured action-descriptor branch first and never consulted the
label where summaryTitle lived, so shell/file-read rows kept rendering
raw 'Ran <command>' descriptors after the friendly title landed.

buildCompactToolSummary now exposes summaryTitle (null when the row
failed) and failed alongside label, and CompactToolSummaryRow renders
the friendly phrase as the visible row label ahead of the action
descriptor. The exact command/path receipt stays on the hover title
and on expand. Failed rows bypass the descriptor branch entirely so
the failure label is painted instead of the descriptor repainting
'Ran <command>' over it — this also fixes the previously-dead failure
precedence for action-bearing tools that Marge flagged as a minor.

New render-layer tests (renderToStaticMarkup through the real
buildCompactToolSummary pipeline) assert the user-visible HTML: the
friendly title paints for shell and file-read rows, failure beats the
friendly phrase, and rows without a summary keep today's descriptor
label.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Two-layer prod-safe gate for the acpToolSummaries experiment, default OFF:

UI gate: buildCompactToolSummary now takes summaryTitleEnabled (default
off) and only consults the agent-provided friendly phrase when the
experiment is on. ToolItem and BotActivityBar/getActivityHeadline read
useFeatureEnabled('acpToolSummaries') and thread the flag through.
Failure-precedence behavior is unchanged: failed rows always paint the
failure label regardless of the gate.

Runtime/cost gate: the frontend mirrors preview-experiment overrides to
experiments.json via the new set_desktop_experiments command (AppShell
mirrors on boot and on every toggle); spawn_agent_child sets
BUZZ_AGENT_NO_TOOL_SUMMARY=1 when the experiment is off so disabled
agents never spend the async LLM summary call. All spawn paths (start,
create, app-startup restore) funnel through spawn_agent_child, and
missing/unknown mirror state resolves to off — restore-time respawns
before the webview loads stay on the safe side. Experiment-on leaves
the env untouched, so a user-provided kill switch still wins.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…ents

The acpToolSummaries experiment is enforced at agent spawn time
(BUZZ_AGENT_NO_TOOL_SUMMARY in the child env), so flipping the toggle
repaints the UI but leaves running agents on their spawn-time behavior —
the 'enabled but still raw labels' confusion.

Toggling such an experiment now opens a shared AlertDialog. Confirm
applies the toggle, awaits an explicit set_desktop_experiments mirror
write (so respawns read the NEW env, not a racy passive-effect write),
then restarts the local agents that were running at confirmation time.
Cancel leaves the setting unchanged; stopped agents stay stopped.
Partial restart failures surface a failed-agent toast without rolling
the experiment back; a failed mirror write aborts the restart but keeps
the toggle applied (best-effort mirror convention).

Orchestration lives in a pure lib module with node tests covering
toggle→mirror→restart ordering, mirror-failure abort, local-running-only
selection, partial-failure collection, and outcome messaging.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/acp-tool-summaries branch from f38d8ff to de96c07 Compare July 7, 2026 21:27

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve. Read the full 32-file diff against merge-base and traced both the Rust and TS paths. CI note first: the two red checks (Desktop Smoke E2E (1) and its Desktop roll-up) were an apt-mirror infra flake (packages.microsoft.com ... Clearsigned file isn't valid while installing Playwright browser deps) — I reran the failed jobs and everything is green now, including the new golden transcripts and spawn-env tests.

What I verified:

  • Two-stage title design is sound. Stage 1 (emit_pending) is deterministic and immediate; stage 2 (spawn_tool_summary) publishes a title-only tool_call_update tagged _meta.buzz.toolSummary with deliberately no status field, and the golden transcript test asserts that (update.get("status").is_none()). On the ingest side, extractBuzzToolSummaryTitle routes tagged updates to applyToolSummaryTitle, which only sets summaryTitle — it never touches upsertTool's status merge, so a late summary can't resurrect or clobber a terminal row. Out-of-order summaries are dropped, which is the right call for a cosmetic title.
  • The gate is genuinely fail-safe. resolve_experiment_enabled is enabled only on explicit true; missing file, malformed JSON, absent key, and non-boolean values all resolve off (tested). Spawn gate writes BUZZ_AGENT_NO_TOOL_SUMMARY=1 after user env so the off-state is authoritative, and the on-state leaves a user opt-out intact (tested both ways in runtime/tests.rs).
  • Failure precedence holds at both layers. The builder nulls summaryTitle on failure, and CompactToolSummaryRow independently bypasses both the summary span and the action descriptor when failed — so even a future builder regression can't paint a friendly phrase over an error. Render test covers it.
  • The restart-confirm ordering is correct and well-reasoned: toggle → awaited explicit mirror write → snapshot respawn, precisely because the passive useDesktopExperimentsMirror effect is unordered relative to the respawn. Partial-failure messaging doesn't roll back the toggle, matching the stated best-effort convention.

Non-blocking observations:

  1. Stage 1 is ungated by design — worth keeping in mind at rollout. Even with the experiment off, the pending tool_call now carries the friendly deterministic title (with exact identity preserved in toolName). The desktop UI is unaffected (labels come from the classifier/status path, confirmed by the OFF screenshots), but any other ACP client that renders title verbatim will see the new phrasing regardless of the experiment. The PR body says "emits and consumes" but the emission half isn't behind the flag — intentional as far as I can tell, just not obvious from the rollout note.
  2. Summary cost defaults to the session model. With the experiment on, each tool call spends up to 2 extra LLM calls (retry) on the session's effective model unless BUZZ_AGENT_TOOL_SUMMARY_MODEL is set. Before this graduates beyond preview, consider defaulting the summary pass to a designated cheap/fast model rather than opt-in via env.
  3. check-file-sizes.mjs overrides grew again (runtime.rs 2213→2236, tauri.ts 1380→1391, agentSessionTranscript.ts 1167→1192). The entries are documented with queued splits per repo convention, but the queue is getting long — the transcript split in particular looks overdue.

Nice test discipline across the stack: wire-level golden transcripts, pure-logic node tests for the restart flow, and render-level coverage at the row. Ship it.

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the full diff (not just hunks) at de96c07 across three lenses — security/reliability, design/performance, functionality/testing. The gate design is solid and fail-safe on every axis I checked, and the wire contract (_meta.buzz.toolSummary) is genuinely harness-agnostic on the consumption side, which is the right seam. One blocking issue on the generation side plus two things to clean up before this graduates from the preview experiment. Details inline.

The blocker is localized to spawn_tool_summary so it should be a quick turnaround, not a redesign.

// summarization for real (runnable) tool calls only — fast-failed
// and built-in calls keep their deterministic title.
if self.cfg.tool_summary_enabled {
spawn_tool_summary(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 MUST FIX — unbounded detached summary fan-out

This fires a detached tokio::spawn for every runnable tool call before execute_parallel, so it bypasses the Semaphore::new(max_parallel_tools) bound at line 391 that gates actual tool execution. Each summary task runs a for attempt in 0..2 retry (line 690) → up to 2 provider requests each. With max_tool_calls at 64 (config.rs:557), a single busy turn can launch up to ~128 detached summary LLM requests, unbounded against max_parallel_tools, competing with the main turn for provider rate/cost budget.

Cancellation is checked between attempts and before emit, but not during the in-flight .await on llm.summarize — so once a request is issued it keeps running post-cancel until the shared ~240s LLM timeout (config.rs:719 / llm.rs:54).

Suggest routing summary jobs through a bounded semaphore/worker pool (≤ max_parallel_tools, or a lower summary-specific cap), and wrapping the .await in a select! on the cancel receiver plus a short summary-specific timeout so in-flight requests abort. Keep the retry inside that bound.

s
}
};
let user_prompt = format!("Tool: {}\nArguments: {args_json}", call.name);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 SHOULD FIX (before GA) — tool argument values sent to the summary LLM

The fast-pass prompt embeds call.arguments (truncated to 300 chars). That raw JSON can contain a file path like ~/.ssh/id_rsa, a shell command, or a URL with an embedded key — so a fragment of a secret can reach the summary model.

This is 🟡 not 🔴 because the summary model is the same provider already receiving the full tool call in the main conversation, and the feature is default-off behind the preview experiment. But before GA I'd send only the tool name + top-level argument keys (not values) to the summary model — that's enough for a descriptive phrase ("reading config file", "querying database") with zero value leakage — or redact a key blocklist (password, key, secret, token, auth, credential).

.map_err(|error| format!("failed to resolve app data dir: {error}"))?;
std::fs::create_dir_all(&dir)
.map_err(|error| format!("failed to create app data dir: {error}"))?;
Ok(dir.join("experiments.json"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 SHOULD FIX (before GA) — Rust experiment mirror drops the frontend override version

The frontend keys its override storage by manifest version — OVERRIDES_KEY = buzz-feature-overrides-v${manifest.version} at store.ts:12 — so a manifest bump orphans old overrides and resets the experiment to off. But this Rust mirror writes a fixed, unversioned experiments.json. On a future manifest bump, the app-startup restore path can read a stale true from the mirror and spawn agents with summaries ON while the UI now shows OFF, until the webview remirrors.

It self-heals once the webview loads and the failure mode is "on when expected off" (a cost surprise, not unsafe), hence 🟡. Suggest versioning the mirror payload or filename (e.g. experiments-v${manifest.version}.json) and ignoring mismatches, so it tracks the frontend's version semantics.

* Preferred as the row label; the classifier label remains the
* fallback and failure labels always win.
*/
summaryTitle?: string | null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟢 CONSIDER (nit) — document the two-layer failure-suppression contract

Minor: the transcript stores summaryTitle unconditionally, even on failed rows — suppression of the friendly title on failure happens downstream in the render/headline layer via the !failed guard. Both current consumers (CompactToolSummaryRow, getActivityHeadline) honor it and are tested, so this is correct today. A one-line comment on the summaryTitle field pointing at the render-layer !failed guard would keep a future consumer from reading it directly and showing a friendly phrase on a failed row.

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the full diff (not just hunks) at de96c07 across three lenses — security/reliability, design/performance, functionality/testing. The gate design is solid and fail-safe on every axis I checked, and the wire contract (_meta.buzz.toolSummary) is genuinely harness-agnostic on the consumption side, which is the right seam. One blocking issue on the generation side plus two things to clean up before this graduates from the preview experiment. Details inline.

The blocker is localized to spawn_tool_summary so it should be a quick turnaround, not a redesign.

@wpfleger96 wpfleger96 dismissed their stale review July 8, 2026 13:38

Duplicate of review #4654643765 (accidental double-post) — dismissing; see the identical review directly above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants