fix: preserve background Agent terminal lifecycle - #941
Conversation
Why: - Background Agent launch acknowledgements were treated as terminal tool results, while later task terminal signals only pruned internal state. What: - Keep async Agent/Task calls in progress and bridge trusted task notifications, task updates, and TaskOutput results to ACP terminal statuses. - Cover ordering races, duplicate signals, untrusted text, synchronous Agents, and background Bash regressions. Impact: - Only Agent/Task results identified as async launches use the deferred lifecycle; synchronous tools retain existing behavior. Refs: agentclientprotocol#865
|
@benbrandt Could you please review this PR when you have a chance? |
# Conflicts: # src/acp-agent.ts
|
Thanks for tackling this — #941 lines up almost exactly with an independently-developed fix I've been running as a patch on the built adapter ( 1. Read 2. Attach the completion result body, not just the status. The terminal update flips Happy to open a small follow-up PR against your branch with both, or you can fold them in — whichever you prefer. One caveat for maintainers: @benbrandt noted in #876 that v2 "removes the turn concept a bit," so if the terminal |
Summary
Background Agent/Task results with
status: "async_launched"acknowledge a launch; they do not mean the subagent has finished. The adapter currently converts that acknowledgement into an ACPcompletedtool call and later consumes task terminal events only for internal bookkeeping, so hosts cannot determine when the parent Agent card actually settles.This change keeps those parent calls
in_progressuntil Claude reports a real terminal state, while preserving the launch result content and raw output on the existing card.Changes
tool_use_resultand correlateagentIdwith the parenttool_use_id.completed->completedfailed,killed,stopped->failedtask_notificationtask_updatedorigin.kind: "task-notification"TaskOutputresults as a fallback_meta.claudeCode.taskStatuswhen the parent tool metadata is available.async_launchedbefore a partially populatedtask_startedtool_usetool_usewas dropped after cancellationScope
This PR intentionally addresses only the background Agent/Task parent-tool lifecycle described in #865. It does not expose the full
background_tasks_changedset and does not change background Bash/shell completion reporting, so it usesAddressesrather thanFixes.Addresses #865
Verification
Real Claude end-to-end smoke test
Ran the built adapter against the vendored Claude Code
2.1.220executable with@anthropic-ai/claude-agent-sdk0.3.220and an authenticated first-party OAuth session. The prompt required one realAgentcall withrun_in_background=true; the child used Bash to wait five seconds. Raw SDK messages and ACP session updates were recorded independently.Two consecutive runs passed. The second run produced this parent lifecycle (elapsed from process start):
async_launchedacknowledgementin_progresstask_updated:completedcompletedsession/promptreturnedThe Agent task id and parent tool-use id were correlated from the real SDK payloads. The parent emitted exactly one ACP terminal update. A separate background Bash task inside the Agent also emitted its own task lifecycle and did not settle the parent early. The first run independently showed the same ordering (
in_progressat 9,068 ms, Agent/ACP terminal at 38,541 ms, prompt return at 41,949 ms).Automated checks
npx vitest run src/tests/acp-agent.test.ts -t "background Agent tool lifecycle"- 18 passednpm run build- passednpm run lint -- --no-fix- passednpx tsc --noEmit- passednpx prettier --check src/acp-agent.ts src/tests/acp-agent.test.ts- passedgit diff --check- passednpm run test:run- 709 passed, 20 skipped, with the same 6 pre-existing Windows path-separator failures as the unmodified baseline (src/main.tsexpected,src\\main.tsreceived)Regression coverage
The new tests cover completed/failed/killed/stopped mappings, duplicate signals, trusted and untrusted task-notification messages, TaskOutput fallback, optional
isAsync, early-terminal races, permission-before-stream ordering, missing-cache stragglers, synchronous Agent cleanup, and unchanged foreground Agent/background Bash behavior.