a1143aa525
* fix(acp): handle claude-acp async_launched subagents end-to-end Claude Agent SDK's Task tool with run_in_background=true returns session/prompt with stop_reason=end_turn while subagents stay alive in the background, reporting `_meta.claudeCode.toolResponse.status = "async_launched"` instead of a terminal top-level status. kandev previously treated stop_reason as authoritative, cancelled the subagent card on prompt return, and fired an "agent finished without producing any output" notice — even though the dispatch did produce work and the subagent's child events would arrive seconds later. Backend: - `SubagentTaskPayload` gains `IsAsync` / `OutputFile` / `CanReadOutputFile` so the async-launched envelope is preserved. - `convertToolCallResultUpdate` recognizes async_launched as terminal for the Task tool itself (dispatch is the completion). - `cancelActiveToolCalls` preserves subagent_task tool calls through the prompt-return sweep so a late terminal tool_call_update from the SDK still lands correctly (covers the in-turn-complete case). Frontend: - `subagentMetaChips` emits a "background" chip when `is_async` or `status == "async_launched"` so the card is visually distinct. - `computeEmptyTurnNotice` suppresses the notice when any subagent task message exists for the turn (the dispatch IS output). Reproduced via `acpdbg prompt` against `claude-acp` 0.31.1; captured frames in acp-debug/claude-acp-prompt-20260602-110119.jsonl drive the backend test fixtures verbatim. * fix(acp): harden async_launched override against future SDK frames Address PR review feedback (greptile #1244, claude review): - Move the async_launched override inside the mutex lock so it can inspect the stored payload kind before flipping the status. - Drop the `status == ""` precondition. The earlier in_progress synthesis (Title/RawInput/Content/Locations) was implicitly trumping the override; a future SDK frame that combines async_launched with any of those fields would have left the card stuck in_progress forever. The override is now unconditional for async_launched subagent_task frames. - Gate the override on `payload.Kind() == ToolKindSubagentTask`. A hypothetical future claude-acp tool that reuses the `async_launched` literal in its own `_meta.claudeCode.toolResponse.status` would have been incorrectly marked complete by the prior unscoped check. Pinned by two new tests: - TestConvertToolCallResultUpdate_AsyncLaunched_OverridesInProgress - TestConvertToolCallResultUpdate_AsyncLaunched_NonSubagentNotTerminated --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { subagentMetaChips } from "./subagent-meta";
|
|
import type { SubagentTaskPayload } from "@/components/task/chat/types";
|
|
|
|
describe("subagentMetaChips", () => {
|
|
it("returns empty list for undefined payload", () => {
|
|
expect(subagentMetaChips(undefined)).toEqual([]);
|
|
});
|
|
|
|
it("returns empty list when no metric fields are present", () => {
|
|
const payload: SubagentTaskPayload = { description: "do work", subagent_type: "general" };
|
|
expect(subagentMetaChips(payload)).toEqual([]);
|
|
});
|
|
|
|
it("formats a full Claude payload", () => {
|
|
const payload: SubagentTaskPayload = {
|
|
subagent_type: "general-purpose",
|
|
agent_id: "agent_0123456789abcdef",
|
|
duration_ms: 2200,
|
|
total_tokens: 9987,
|
|
tool_use_count: 0,
|
|
status: "complete",
|
|
};
|
|
expect(subagentMetaChips(payload)).toEqual([
|
|
{ label: "duration", value: "2.2s" },
|
|
{ label: "tokens", value: "9,987 tokens" },
|
|
{ label: "tools", value: "0 tools" },
|
|
]);
|
|
});
|
|
|
|
it("formats an OpenCode payload", () => {
|
|
const payload: SubagentTaskPayload = {
|
|
subagent_type: "task",
|
|
model: "opencode/big-pickle",
|
|
child_session_id: "sess_abcdef0123456789",
|
|
};
|
|
expect(subagentMetaChips(payload)).toEqual([
|
|
{ label: "model", value: "opencode/big-pickle" },
|
|
{ label: "session", value: "sess_abcdef0…" },
|
|
]);
|
|
});
|
|
|
|
it("formats a Cursor payload (duration only)", () => {
|
|
const payload: SubagentTaskPayload = { subagent_type: "task", duration_ms: 850 };
|
|
expect(subagentMetaChips(payload)).toEqual([{ label: "duration", value: "850ms" }]);
|
|
});
|
|
|
|
it("singularizes a single tool use", () => {
|
|
expect(subagentMetaChips({ tool_use_count: 1 })).toEqual([{ label: "tools", value: "1 tool" }]);
|
|
});
|
|
|
|
it("skips zero/negative duration and zero tokens but keeps zero tools", () => {
|
|
const payload: SubagentTaskPayload = { duration_ms: 0, total_tokens: 0, tool_use_count: 0 };
|
|
expect(subagentMetaChips(payload)).toEqual([{ label: "tools", value: "0 tools" }]);
|
|
});
|
|
|
|
it("does not truncate short session ids", () => {
|
|
expect(subagentMetaChips({ child_session_id: "short" })).toEqual([
|
|
{ label: "session", value: "short" },
|
|
]);
|
|
});
|
|
|
|
// Pins the fix for the "main agent finished, subagent backgrounded" race:
|
|
// claude-acp's Task tool with run_in_background=true returns
|
|
// is_async=true + status=async_launched. The card must surface a "background"
|
|
// chip so users see this isn't a normal completion.
|
|
it("adds a background chip when is_async is true", () => {
|
|
const payload: SubagentTaskPayload = {
|
|
subagent_type: "general-purpose",
|
|
is_async: true,
|
|
status: "async_launched",
|
|
output_file: "/tmp/tasks/abc.output",
|
|
};
|
|
expect(subagentMetaChips(payload)).toEqual([{ label: "background", value: "background" }]);
|
|
});
|
|
|
|
it("adds a background chip when status is async_launched even without is_async", () => {
|
|
const payload: SubagentTaskPayload = { status: "async_launched" };
|
|
expect(subagentMetaChips(payload)).toEqual([{ label: "background", value: "background" }]);
|
|
});
|
|
});
|