mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
feat: report RFD delegations as linked session messages (#1405)
## Summary For a client on the subagents RFD (every ACP v2 client, and v1 clients that declare `subagents`), session messages now replace the Agent/Task call that launches a child. The parent no longer sees that tool call. AIR's form is unchanged. - **Prompt.** The launch's prompt is a `session_message` in both transcripts: the parent's outgoing view and the child's incoming one. Both use the launch's tool use id, and each names the other with `recipientMessageId` / `senderMessageId` from agentclientprotocol/agent-client-protocol#2465. - **Answer.** A foreground child's answer, from the launch's result, is the child's `session_message` to the parent (`<launch id>:reply`). It uses the result as `AgentReporter` renders it, so the model-facing `agentId: …` line and `<usage>` block are gone. A failure or cancellation comes from the child's `idle` stop reason, which it already reported. A background launch's result gets no reply. - **Hand-back mode.** Claude Code (seen with 2.1.303) can have the child hand its report back with a `SubagentHandback` call; the Agent result then only says that the report "was delivered to you as a message". That call is reported as the child's message to the parent, in both transcripts and linked the same way, and the launch's reply is skipped for that run. A failed hand-back delivered nothing, so it stays a tool call of the child. - **Unexposed work.** A launch whose child is never exposed is shown as a tool call after all, with its final status (`unexposedControlFallback`, the former `failedControlFallback`). - **Hidden launches by id.** The adapter's own report that closes an unfinished call (for example at a forced cancel) carries no `toolName`, so the runtime tracks hidden launches by id to keep that report from reaching the client for a call it never saw. - **Replay** sends the same messages from the child's transcript: the launch's prompt, the reply, and hand-backs. A launch without Claude Code's metadata file keeps its tool call and the missing-history notice. Replay can't tell a child's runs apart, so any hand-back in its transcript suppresses its launch's reply. - **SendMessage** calls stay tool calls of their sender, and the child gets the incoming view without references, as before. - README's subagent section describes the new behavior. Do not merge before agentclientprotocol/agent-client-protocol#2465 settles the field names: a merge to `main` publishes a preview right away. Clients on the published schema ignore the two fields (no `deny_unknown_fields` in the Rust schema; the scenario validation with the SDK's zod schema passes). ## Testing - `npm run build`, `npm run check` and `npm run test:run` pass (57 files, 4 skipped as before). - The six v2 `subagent-*.jsonl` scenario snapshots changed: the launch's tool-call updates are gone, the parent gets the outgoing view, the child's view carries `senderMessageId`, and foreground results became `:reply` messages. AIR snapshots are unchanged. - New unit tests cover hidden launches, nested launches, replies, failed and background launches, unexposed launches with both final statuses, the close report without a tool name, and hand-back success and failure, live and on replay. - A real run: the built adapter driven over v2 stdio with Claude Code, one foreground subagent asked to answer PONG, then `session/resume` with replay in a fresh process. Live and replay both showed the linked prompt (parent → child), the linked hand-back "PONG" (child → parent), and no Agent tool call.
This commit is contained in:
19
README.md
19
README.md
@@ -47,9 +47,18 @@ Task subagent as its own child session, following the [subagents RFD](https://gi
|
||||
`end_turn` or `cancelled` when the SDK gives the reason. A failure is `idle` with the `error`
|
||||
stop reason and the SDK's error text as its `error`. The child keeps its session for every
|
||||
delegation to it.
|
||||
- The prompt of the Agent or Task call that launches the child, and the message of each
|
||||
SendMessage call that resumes it, is a `session_message` in the child's transcript. The tool
|
||||
call stays a tool call of the session that made it.
|
||||
- Session messages replace the Agent or Task call that launches the child, which its parent is
|
||||
not shown:
|
||||
- The launch's prompt is a `session_message` in both transcripts: the parent's outgoing view and
|
||||
the child's incoming one. Both use the launch's tool use id, and each names the other with the
|
||||
draft RFD's `recipientMessageId` and `senderMessageId`.
|
||||
- A foreground child's answer, from the launch's result, is the child's `session_message` to
|
||||
the parent. In Claude Code's hand-back mode, the child's `SubagentHandback` call is its
|
||||
message to the parent instead, in both transcripts and linked the same way.
|
||||
- A launch whose child is never exposed is shown as a tool call after all, with its final
|
||||
status.
|
||||
- The message of a SendMessage call that resumes the child is a `session_message` in the child's
|
||||
transcript. The SendMessage call stays a tool call of the session that made it.
|
||||
- `session/cancel` on the parent interrupts Claude Code, which stops the foreground children that
|
||||
the turn waits on. Background children keep running, as the RFD prefers; a client stops one with
|
||||
its own `cancel`. Each stopped child reports `idle` with `cancelled` from Claude Code's own
|
||||
@@ -65,8 +74,8 @@ Task subagent as its own child session, following the [subagents RFD](https://gi
|
||||
exits is `idle` with no stop reason.
|
||||
- A child whose parent is not yet known when it ends is not exposed, since the RFD forbids
|
||||
guessing its parent.
|
||||
- A replay restores each child under its live session id, from its own transcript: its prompt and
|
||||
each SendMessage message are `session_message`s, and its end is an `idle` before the response.
|
||||
- A replay restores each child under its live session id, from its own transcript, with the same
|
||||
session messages as live, and its end is an `idle` before the response.
|
||||
A launch without Claude Code's metadata file for the child stays an ordinary tool call of the
|
||||
parent, and a notice, for a client that takes notices, says that subagent history is missing.
|
||||
- After a load or resume, with or without replay, a SendMessage that resumes an agent from before
|
||||
|
||||
@@ -9602,6 +9602,7 @@ export class ClaudeAcpAgent {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (rfdReplay && (await rfdReplay.toolCallUpdate(notification.update, child))) continue;
|
||||
await this.client.sessionUpdate({ ...notification, sessionId: replayTargetSessionId });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RequestError, type SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { AcpSessionNotification, SubagentForm, SubagentState } from "./acp-subagents.js";
|
||||
import { AIR_SUBAGENT_KEY, airExtensionMeta } from "./air-extension.js";
|
||||
import { isFinalToolCallStatus } from "./tool-calls/renderer.js";
|
||||
|
||||
/** The work state of a child, as the RFD's `subagent_update.state` carries it. */
|
||||
export type WorkState = NonNullable<
|
||||
@@ -44,6 +45,8 @@ export type NativeSubagent = {
|
||||
promptIds?: Set<string>;
|
||||
/** The RFD form: the calls that delegated to the child, whose runs ended. */
|
||||
endedRuns?: Set<string>;
|
||||
/** The RFD form: the calls that delegated to the child, whose runs handed back a report. */
|
||||
handedBack?: Set<string>;
|
||||
announced?: boolean;
|
||||
terminalState?: SubagentState;
|
||||
/** Connection-local single-flight state; never serialized on the wire. */
|
||||
@@ -144,6 +147,21 @@ export class NativeSubagentRuntime {
|
||||
private readonly taskFinishPromises = new Map<string, Promise<void>>();
|
||||
/** The session that made each SendMessage call. */
|
||||
private readonly senderByToolUse = new Map<string, string>();
|
||||
/**
|
||||
* The RFD form: the Agent/Task calls that the client is not shown, until
|
||||
* their final update. Kept by id because the adapter's own report that
|
||||
* closes an unfinished call has no `toolName` to recognize it by.
|
||||
*/
|
||||
private readonly hiddenLaunches = new Map<string, { background: boolean }>();
|
||||
/**
|
||||
* The RFD form: the SubagentHandback calls of announced children that the
|
||||
* client is not shown yet, with their first report, the report from their
|
||||
* input, and the call that delegated the run.
|
||||
*/
|
||||
private readonly handbacks = new Map<
|
||||
string,
|
||||
{ initial: AcpSessionNotification; message?: string; delegationId?: string }
|
||||
>();
|
||||
private readonly generationByTaskId = new Map<string, number>();
|
||||
private readonly pending = new Map<string, AcpSessionNotification[]>();
|
||||
private pendingCount = 0;
|
||||
@@ -206,7 +224,7 @@ export class NativeSubagentRuntime {
|
||||
const { update } = notification;
|
||||
const claudeMeta = update._meta?.claudeCode as
|
||||
{ parentToolUseId?: string | null; toolName?: string } | undefined;
|
||||
const isControl = isNativeSubagentControlUpdate(update);
|
||||
const isControl = this.isControlUpdate(update);
|
||||
|
||||
if (!this.enabled) return notification;
|
||||
|
||||
@@ -253,7 +271,7 @@ export class NativeSubagentRuntime {
|
||||
if (forcedSessionId) {
|
||||
return { ...notification, sessionId: forcedSessionId };
|
||||
}
|
||||
return failedControlFallback(
|
||||
return unexposedControlFallback(
|
||||
initial,
|
||||
notification,
|
||||
parentSessionId ?? this.rootSessionId,
|
||||
@@ -261,8 +279,19 @@ export class NativeSubagentRuntime {
|
||||
}
|
||||
return forcedSessionId ? { ...notification, sessionId: forcedSessionId } : null;
|
||||
}
|
||||
// The RFD form: the Agent/Task tool call is the parent's record of the
|
||||
// delegation, so it stays a tool call of the session that made it.
|
||||
// The RFD form: the parent's session messages replace the Agent/Task
|
||||
// tool call, its prompt as the parent's outgoing message (see
|
||||
// `linkedSessionMessages`) and its result as the child's reply (see
|
||||
// `launchReply`). A call made inside a child that is not announced yet
|
||||
// is buffered below, as any update of that child.
|
||||
if (!parentPending) {
|
||||
return this.routeRfdLaunch(
|
||||
notification,
|
||||
child,
|
||||
parentSessionId ?? this.rootSessionId,
|
||||
forcedSessionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const toolCallId =
|
||||
@@ -281,6 +310,15 @@ export class NativeSubagentRuntime {
|
||||
}
|
||||
|
||||
const owner = toolCallId ? this.childByToolCall.get(toolCallId) : undefined;
|
||||
const announcedChild =
|
||||
owner ??
|
||||
(claudeMeta?.parentToolUseId
|
||||
? this.childByParentToolUse.get(claudeMeta.parentToolUseId)
|
||||
: undefined);
|
||||
if (this.form === "rfd" && announcedChild?.announced) {
|
||||
const handback = await this.routeRfdHandback(notification, announcedChild);
|
||||
if (handback !== undefined) return handback;
|
||||
}
|
||||
if (owner) return this.toChild(owner, notification, toolCallId);
|
||||
|
||||
if (this.enabled && claudeMeta?.parentToolUseId) {
|
||||
@@ -973,6 +1011,109 @@ export class NativeSubagentRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The RFD form: an update of the Agent/Task call that launches `child`,
|
||||
* which the parent's session messages replace. Only a launch whose child
|
||||
* was never exposed stays a tool call of the parent, with its final status,
|
||||
* since the RFD lets the Agent represent unexposed work through the parent.
|
||||
* A background launch completes before its child starts, so its completion
|
||||
* is not such a case.
|
||||
*/
|
||||
private async routeRfdLaunch(
|
||||
notification: AcpSessionNotification,
|
||||
child: NativeSubagent | undefined,
|
||||
parentSessionId: string,
|
||||
forcedSessionId: string | undefined,
|
||||
): Promise<AcpSessionNotification | null> {
|
||||
const { update } = notification;
|
||||
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
||||
return null;
|
||||
}
|
||||
const toolCallId = update.toolCallId;
|
||||
// A permission request already showed the call to the client, so it
|
||||
// keeps getting its updates.
|
||||
if (forcedSessionId) return { ...notification, sessionId: forcedSessionId };
|
||||
const launch = this.hiddenLaunches.get(toolCallId) ?? { background: false };
|
||||
if (isBackgroundLaunch(update.rawInput)) launch.background = true;
|
||||
if (!isFinalToolCallStatus(update.status)) {
|
||||
this.hiddenLaunches.set(toolCallId, launch);
|
||||
return null;
|
||||
}
|
||||
this.hiddenLaunches.delete(toolCallId);
|
||||
if (child?.announcePromise) await child.announcePromise;
|
||||
const background = launch.background;
|
||||
if (child?.announced) {
|
||||
const reply =
|
||||
update.status === "completed" && !background && !child.handedBack?.has(toolCallId)
|
||||
? launchReply(toolCallId, child.sessionId, child.parentSessionId, update)
|
||||
: undefined;
|
||||
if (reply) await this.publish(reply);
|
||||
return null;
|
||||
}
|
||||
if (background && update.status === "completed") return null;
|
||||
this.takePending(toolCallId);
|
||||
const initial = this.controlByToolUse.get(toolCallId);
|
||||
this.cleanupControl(toolCallId);
|
||||
return unexposedControlFallback(initial, notification, parentSessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The RFD form: an update of a SubagentHandback call of the announced
|
||||
* `child`, which its report replaces: the child's message to its parent,
|
||||
* see {@link linkedSessionMessages}. A failed call delivered nothing, so the
|
||||
* child is shown the call after all. `undefined` for any other update.
|
||||
*/
|
||||
private async routeRfdHandback(
|
||||
notification: AcpSessionNotification,
|
||||
child: NativeSubagent,
|
||||
): Promise<AcpSessionNotification | null | undefined> {
|
||||
const { update } = notification;
|
||||
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
||||
return undefined;
|
||||
}
|
||||
const claudeMeta = update._meta?.claudeCode as
|
||||
{ parentToolUseId?: string | null; toolName?: string } | undefined;
|
||||
let handback = this.handbacks.get(update.toolCallId);
|
||||
if (!handback) {
|
||||
if (claudeMeta?.toolName !== HANDBACK_TOOL) return undefined;
|
||||
handback = {
|
||||
initial: notification,
|
||||
delegationId: claudeMeta.parentToolUseId ?? child.parentToolUseId,
|
||||
};
|
||||
this.handbacks.set(update.toolCallId, handback);
|
||||
}
|
||||
handback.message = handbackMessage(update.rawInput) ?? handback.message;
|
||||
if (!isFinalToolCallStatus(update.status)) return null;
|
||||
this.handbacks.delete(update.toolCallId);
|
||||
if (update.status === "completed" && handback.message) {
|
||||
for (const view of linkedSessionMessages(
|
||||
update.toolCallId,
|
||||
child.sessionId,
|
||||
child.parentSessionId,
|
||||
handback.message,
|
||||
)) {
|
||||
await this.publish(view);
|
||||
}
|
||||
if (handback.delegationId) (child.handedBack ??= new Set()).add(handback.delegationId);
|
||||
return null;
|
||||
}
|
||||
return unexposedControlFallback(handback.initial, notification, child.sessionId);
|
||||
}
|
||||
|
||||
/** Whether `update` is of an Agent/Task call, by its tool name or, in the
|
||||
* RFD form, by the id of a call the client is not shown. */
|
||||
private isControlUpdate(
|
||||
update: AcpSessionNotification["update"],
|
||||
): update is Extract<
|
||||
AcpSessionNotification["update"],
|
||||
{ sessionUpdate: "tool_call" | "tool_call_update" }
|
||||
> {
|
||||
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
||||
return false;
|
||||
}
|
||||
return this.hiddenLaunches.has(update.toolCallId) || isNativeSubagentControlUpdate(update);
|
||||
}
|
||||
|
||||
private rememberToolCallOwner(toolCallId: string, child: NativeSubagent): void {
|
||||
this.childByToolCall.delete(toolCallId);
|
||||
this.childByToolCall.set(toolCallId, child);
|
||||
@@ -1019,6 +1160,8 @@ export class NativeSubagentRuntime {
|
||||
this.childByParentToolUse.clear();
|
||||
this.taskFinishPromises.clear();
|
||||
this.senderByToolUse.clear();
|
||||
this.hiddenLaunches.clear();
|
||||
this.handbacks.clear();
|
||||
this.generationByTaskId.clear();
|
||||
this.pending.clear();
|
||||
this.pendingCount = 0;
|
||||
@@ -1196,6 +1339,7 @@ export async function announceNativeSubagent(
|
||||
child.parentToolUseId,
|
||||
child.parentSessionId,
|
||||
publish,
|
||||
{ replacesLaunch: true },
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -1256,6 +1400,10 @@ export async function finishNativeSubagent(
|
||||
* Agent/Task call or a SendMessage, so a prompt whose id is already in the
|
||||
* transcript is the same delegation and is not sent again. `sender` is the
|
||||
* session that made that call, when known.
|
||||
*
|
||||
* A launch's prompt (`replacesLaunch`) also goes to the parent, see
|
||||
* {@link linkedSessionMessages}. A SendMessage call stays a tool call of the
|
||||
* sender, so its message has only the incoming view.
|
||||
*/
|
||||
async function publishPrompt(
|
||||
child: NativeSubagent,
|
||||
@@ -1263,11 +1411,18 @@ async function publishPrompt(
|
||||
toolUseId: string | undefined,
|
||||
sender: string | undefined,
|
||||
publish: Publish,
|
||||
{ replacesLaunch = false }: { replacesLaunch?: boolean } = {},
|
||||
): Promise<void> {
|
||||
child.promptIds ??= new Set();
|
||||
const messageId = toolUseId ?? `${child.sessionId}:prompt:${child.promptIds.size + 1}`;
|
||||
if (child.promptIds.has(messageId)) return;
|
||||
child.promptIds.add(messageId);
|
||||
if (replacesLaunch && sender !== undefined && toolUseId !== undefined) {
|
||||
for (const view of linkedSessionMessages(toolUseId, sender, child.sessionId, prompt)) {
|
||||
await publish(view);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await publish({
|
||||
sessionId: child.sessionId,
|
||||
update: {
|
||||
@@ -1280,6 +1435,114 @@ async function publishPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The RFD form: a message that a tool call sent, which the client is not
|
||||
* shown, as two session messages: the sender's outgoing view, then the
|
||||
* recipient's incoming one. That call is a launch's Agent/Task call, whose
|
||||
* prompt goes from the parent to the child, or a child's SubagentHandback
|
||||
* call, whose report goes from the child to the parent. Both views use the
|
||||
* call's tool use id, and each names the other with the draft RFD's
|
||||
* `recipientMessageId` and `senderMessageId`.
|
||||
*/
|
||||
export function linkedSessionMessages(
|
||||
toolUseId: string,
|
||||
senderSessionId: string,
|
||||
recipientSessionId: string,
|
||||
text: string,
|
||||
): AcpSessionNotification[] {
|
||||
const message = {
|
||||
sessionUpdate: "session_message" as const,
|
||||
messageId: toolUseId,
|
||||
senderSessionId,
|
||||
recipientSessionId,
|
||||
content: [{ type: "text" as const, text }],
|
||||
};
|
||||
return [
|
||||
{
|
||||
sessionId: senderSessionId,
|
||||
update: { ...message, recipientMessageId: toolUseId } as AcpSessionNotification["update"],
|
||||
},
|
||||
{
|
||||
sessionId: recipientSessionId,
|
||||
update: { ...message, senderMessageId: toolUseId } as AcpSessionNotification["update"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The tool with which a child hands its report to its parent, when Claude
|
||||
* Code runs in hand-back mode. The Agent/Task result then only says that the
|
||||
* report was delivered as a message, with `handback: "send"` in its
|
||||
* structured result.
|
||||
*/
|
||||
export const HANDBACK_TOOL = "SubagentHandback";
|
||||
|
||||
/** The report of a SubagentHandback call, from its input. */
|
||||
export function handbackMessage(input: unknown): string | undefined {
|
||||
const message = (input as { message?: unknown } | null | undefined)?.message;
|
||||
return typeof message === "string" && message.trim() ? message : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The RFD form: the answer of a foreground child, from the completed result
|
||||
* of the Agent/Task call `launchId` that launched it, as the child's message
|
||||
* in the parent's transcript (its incoming view). It replaces that call's
|
||||
* result, which the parent is not shown. The child's transcript has its
|
||||
* answer as its own agent message, not as a session message, so this view
|
||||
* links to no other. `undefined` for a result without text. Not used for a
|
||||
* run whose child handed back its report: the result has no report then.
|
||||
*/
|
||||
export function launchReply(
|
||||
launchId: string,
|
||||
childSessionId: string,
|
||||
parentSessionId: string,
|
||||
result: Extract<
|
||||
AcpSessionNotification["update"],
|
||||
{ sessionUpdate: "tool_call_update" | "tool_call" }
|
||||
>,
|
||||
): AcpSessionNotification | undefined {
|
||||
const content = replyContent(result);
|
||||
if (content.length === 0) return undefined;
|
||||
return {
|
||||
sessionId: parentSessionId,
|
||||
update: {
|
||||
sessionUpdate: "session_message",
|
||||
messageId: `${launchId}:reply`,
|
||||
senderSessionId: childSessionId,
|
||||
recipientSessionId: parentSessionId,
|
||||
content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The text of a completed launch's result, from the tool call's `content`:
|
||||
* `AgentReporter` removed the parts meant for the model there (the
|
||||
* `agentId: … (use SendMessage …)` line, the `<usage>` block and the hand-back
|
||||
* frame), which `rawOutput` still has.
|
||||
*/
|
||||
function replyContent(
|
||||
result: Extract<
|
||||
AcpSessionNotification["update"],
|
||||
{ sessionUpdate: "tool_call_update" | "tool_call" }
|
||||
>,
|
||||
): Array<{ type: "text"; text: string }> {
|
||||
return (result.content ?? []).flatMap((item) =>
|
||||
item.type === "content" && item.content.type === "text" && item.content.text.trim()
|
||||
? [{ type: "text" as const, text: item.content.text }]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether an Agent/Task call's input runs its child in the background. */
|
||||
function isBackgroundLaunch(rawInput: unknown): boolean {
|
||||
return (
|
||||
typeof rawInput === "object" &&
|
||||
rawInput !== null &&
|
||||
(rawInput as { run_in_background?: unknown }).run_in_background === true
|
||||
);
|
||||
}
|
||||
|
||||
/** The RFD form: whether the child's foreground work is in progress. */
|
||||
function isWorking(child: NativeSubagent): boolean {
|
||||
return child.workState === "running" || child.workState === "requires_action";
|
||||
@@ -1443,7 +1706,13 @@ function isFailedToolCallUpdate(update: AcpSessionNotification["update"]): boole
|
||||
);
|
||||
}
|
||||
|
||||
function failedControlFallback(
|
||||
/**
|
||||
* A tool call that the client was not shown, as one `tool_call` with its
|
||||
* final status, shown after all: an Agent/Task call whose child was never
|
||||
* exposed (AIR's draft shows only a failed one, the RFD form also a completed
|
||||
* one), or a failed SubagentHandback call.
|
||||
*/
|
||||
function unexposedControlFallback(
|
||||
initial: AcpSessionNotification | undefined,
|
||||
terminal: AcpSessionNotification,
|
||||
sessionId: string,
|
||||
@@ -1462,7 +1731,7 @@ function failedControlFallback(
|
||||
update: {
|
||||
...terminal.update,
|
||||
sessionUpdate: "tool_call",
|
||||
status: "failed",
|
||||
status: terminal.update.status ?? "failed",
|
||||
// The synthesized tool_call is this call's first report, so give it
|
||||
// the standard `name` the initial one would have carried.
|
||||
...(typeof claudeMeta?.toolName === "string" ? { name: claudeMeta.toolName } : {}),
|
||||
@@ -1483,7 +1752,7 @@ function failedControlFallback(
|
||||
...initial.update,
|
||||
...terminal.update,
|
||||
sessionUpdate: "tool_call",
|
||||
status: "failed",
|
||||
status: terminal.update.status ?? "failed",
|
||||
title:
|
||||
typeof terminal.update.title === "string" && terminal.update.title.trim().length > 0
|
||||
? terminal.update.title
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
* history to the child session. Every child's end goes out before the replay
|
||||
* ends, since no subagent from before the reload still runs.
|
||||
*
|
||||
* As live, the parent is not shown the Agent or Task call of a child that the
|
||||
* replay announces: the launch's prompt is the parent's outgoing message, and
|
||||
* a foreground child's answer is its reply to the parent. A child's
|
||||
* successful SubagentHandback call is its message to the parent instead.
|
||||
* Unlike live, the replay does not tell runs apart, so a child that handed
|
||||
* back any report gets no reply from its launch's result.
|
||||
*
|
||||
* A launch without metadata has no known id, so it is not announced: the RFD
|
||||
* forbids inventing one. Its Agent call stays an ordinary tool call of the
|
||||
* parent, and a notice says that subagent history is missing.
|
||||
@@ -15,7 +22,11 @@
|
||||
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { AcpSessionNotification, SubagentState } from "./acp-subagents.js";
|
||||
import {
|
||||
HANDBACK_TOOL,
|
||||
handbackMessage,
|
||||
isNativeSubagentControlTool,
|
||||
launchReply,
|
||||
linkedSessionMessages,
|
||||
nativeSubagentState,
|
||||
rfdEndState,
|
||||
rfdLabels,
|
||||
@@ -43,6 +54,8 @@ export type ReplayChild = {
|
||||
notified?: SubagentState;
|
||||
announced: boolean;
|
||||
promptSent: boolean;
|
||||
/** Whether it handed a report to its parent with a SubagentHandback call. */
|
||||
handedBack?: boolean;
|
||||
};
|
||||
|
||||
type SendMessageCall = {
|
||||
@@ -62,6 +75,11 @@ export class RfdSubagentReplay {
|
||||
private readonly omitted = new Set<string>();
|
||||
/** Children whose transcript could not be read: only their association is replayed. */
|
||||
private withoutHistory = 0;
|
||||
/** The SubagentHandback calls, with their report and whether their result failed. */
|
||||
private readonly handbacks = new Map<
|
||||
string,
|
||||
{ message: string; failed: boolean; sent: boolean }
|
||||
>();
|
||||
|
||||
constructor(
|
||||
private readonly history: SubagentHistory,
|
||||
@@ -78,6 +96,9 @@ export class RfdSubagentReplay {
|
||||
if (block.type === "tool_use" && typeof block.id === "string") {
|
||||
if (isNativeSubagentControlTool(block.name)) {
|
||||
this.registerLaunch(block.id, inputOf(block), ownerSessionId);
|
||||
} else if (block.name === HANDBACK_TOOL) {
|
||||
const message = handbackMessage(block.input);
|
||||
if (message) this.handbacks.set(block.id, { message, failed: false, sent: false });
|
||||
} else if (block.name === "SendMessage") {
|
||||
const input = inputOf(block);
|
||||
if (typeof input.to === "string" && typeof input.message === "string") {
|
||||
@@ -91,6 +112,8 @@ export class RfdSubagentReplay {
|
||||
}
|
||||
}
|
||||
} else if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
||||
const handback = this.handbacks.get(block.tool_use_id);
|
||||
if (handback && block.is_error === true) handback.failed = true;
|
||||
const child = this.children.get(block.tool_use_id);
|
||||
// A background launch answers at once; its result is not the end.
|
||||
if (child && !child.record.background) {
|
||||
@@ -167,6 +190,51 @@ export class RfdSubagentReplay {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a replayed update of a tool call, from the transcript of `owner`
|
||||
* when it is a child's. Returns whether the client is not shown it: an
|
||||
* update of the launch of a child that the replay shows, or of a successful
|
||||
* SubagentHandback call of `owner`. A foreground launch's completed result
|
||||
* goes out as the child's reply, and a hand-back as the child's message to
|
||||
* its parent.
|
||||
*/
|
||||
async toolCallUpdate(
|
||||
update: AcpSessionNotification["update"],
|
||||
owner: ReplayChild | undefined,
|
||||
): Promise<boolean> {
|
||||
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
||||
return false;
|
||||
}
|
||||
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
|
||||
if (owner && handback && !handback.failed) {
|
||||
if (!handback.sent) {
|
||||
handback.sent = true;
|
||||
owner.handedBack = true;
|
||||
for (const view of linkedSessionMessages(
|
||||
update.toolCallId,
|
||||
owner.sessionId,
|
||||
owner.parentSessionId,
|
||||
handback.message,
|
||||
)) {
|
||||
await this.publish(view);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const child = this.children.get(update.toolCallId);
|
||||
if (!child) return false;
|
||||
if (
|
||||
update.status === "completed" &&
|
||||
child.announced &&
|
||||
!child.record.background &&
|
||||
!child.handedBack
|
||||
) {
|
||||
const reply = launchReply(child.launchId, child.sessionId, child.parentSessionId, update);
|
||||
if (reply) await this.publish(reply);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a persisted task notification. Returns whether it was a child's:
|
||||
* its task id is the agent id.
|
||||
@@ -234,10 +302,17 @@ export class RfdSubagentReplay {
|
||||
this.byAgentId.set(record.agentId, child);
|
||||
}
|
||||
|
||||
/** The prompt of the launch, with the launch's id, as live. */
|
||||
/** The prompt of the launch, with the launch's id, to the parent and the child, as live. */
|
||||
private async sendPrompt(child: ReplayChild, text: string): Promise<void> {
|
||||
child.promptSent = true;
|
||||
await this.sendMessage(child, child.launchId, text, child.parentSessionId);
|
||||
for (const view of linkedSessionMessages(
|
||||
child.launchId,
|
||||
child.parentSessionId,
|
||||
child.sessionId,
|
||||
text,
|
||||
)) {
|
||||
await this.publish(view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3803,10 +3803,19 @@ describe("subagent transcript replay", () => {
|
||||
switch (update.sessionUpdate) {
|
||||
case "subagent_update":
|
||||
return [`${on} subagent_update ${u.sessionId} ${JSON.stringify(u.state)}`];
|
||||
case "session_message":
|
||||
return [
|
||||
`${on} session_message ${u.messageId} from=${u.senderSessionId ?? "-"} ${u.content[0].text}`,
|
||||
case "session_message": {
|
||||
const references = [
|
||||
...(u.senderMessageId ? [`senderMessageId=${u.senderMessageId}`] : []),
|
||||
...(u.recipientMessageId ? [`recipientMessageId=${u.recipientMessageId}`] : []),
|
||||
];
|
||||
return [
|
||||
[
|
||||
`${on} session_message ${u.messageId} from=${u.senderSessionId ?? "-"}`,
|
||||
...references,
|
||||
u.content[0].text,
|
||||
].join(" "),
|
||||
];
|
||||
}
|
||||
case "tool_call":
|
||||
return [`${on} tool_call ${u.toolCallId}`];
|
||||
case "notice":
|
||||
@@ -3877,11 +3886,12 @@ describe("subagent transcript replay", () => {
|
||||
expect(trace).toEqual([
|
||||
"root user_message_chunk Check it",
|
||||
"root agent_message_chunk I'll fork a worker.",
|
||||
"root tool_call toolu_fork",
|
||||
"root tool_call toolu_read",
|
||||
'root subagent_update fork1 {"state":"running"}',
|
||||
`fork1 session_message toolu_fork from=${sessionId} Inspect the parser`,
|
||||
`root session_message toolu_fork from=${sessionId} recipientMessageId=toolu_fork Inspect the parser`,
|
||||
`fork1 session_message toolu_fork from=${sessionId} senderMessageId=toolu_fork Inspect the parser`,
|
||||
"fork1 agent_message_chunk Parser is fine.",
|
||||
"root session_message toolu_fork:reply from=fork1 Parser is fine.",
|
||||
"root agent_message_chunk Done.",
|
||||
'root subagent_update fork1 {"state":"idle","stopReason":"end_turn"}',
|
||||
]);
|
||||
@@ -3959,20 +3969,25 @@ describe("subagent transcript replay", () => {
|
||||
|
||||
expect(trace).toEqual([
|
||||
"root user_message_chunk Explore it",
|
||||
"root tool_call toolu_agent",
|
||||
// The launch without metadata stays a tool call; the other one is
|
||||
// replaced by the parent's session messages.
|
||||
"root tool_call toolu_lost",
|
||||
// The child is announced under its agent id, right after its launch.
|
||||
'root subagent_update abc123 {"state":"running"}',
|
||||
"abc123 session_message toolu_agent from=" + sessionId + " Look around",
|
||||
`root session_message toolu_agent from=${sessionId} recipientMessageId=toolu_agent Look around`,
|
||||
`abc123 session_message toolu_agent from=${sessionId} senderMessageId=toolu_agent Look around`,
|
||||
"abc123 agent_message_chunk Child report.",
|
||||
"abc123 tool_call toolu_nested",
|
||||
// A nested child is announced on the child that launched it.
|
||||
'abc123 subagent_update def456 {"state":"running"}',
|
||||
"def456 session_message toolu_nested from=abc123 Read spec.md",
|
||||
"abc123 session_message toolu_nested from=abc123 recipientMessageId=toolu_nested Read spec.md",
|
||||
"def456 session_message toolu_nested from=abc123 senderMessageId=toolu_nested Read spec.md",
|
||||
"def456 agent_message_chunk X.",
|
||||
// The delivered message has the id, text, and sender of its SendMessage call.
|
||||
"abc123 session_message toolu_nested:reply from=def456 The spec says X.",
|
||||
// The delivered message has the id, text, and sender of its SendMessage
|
||||
// call, which stays a tool call of its sender.
|
||||
"abc123 session_message toolu_send from=" + sessionId + " Check the tests too",
|
||||
"abc123 agent_message_chunk Tests pass.",
|
||||
"root session_message toolu_agent:reply from=abc123 Found it.",
|
||||
"root tool_call toolu_send",
|
||||
"root agent_message_chunk Done.",
|
||||
// Every end comes before the response.
|
||||
@@ -3982,6 +3997,45 @@ describe("subagent transcript replay", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("replays a child's hand-back as its message to the parent, instead of the launch's result", async () => {
|
||||
const trace = await replayRfd(
|
||||
[
|
||||
user("u1", "Ask it"),
|
||||
assistant("a1", [
|
||||
toolUse("toolu_agent", "Agent", { subagent_type: "Explore", prompt: "Say PONG" }),
|
||||
]),
|
||||
// In hand-back mode the launch's result only says where the report went.
|
||||
user("u2", [toolResult("toolu_agent", "The report was delivered as a message.")]),
|
||||
assistant("a2", [{ type: "text", text: "It said PONG." }]),
|
||||
],
|
||||
{
|
||||
abc123: {
|
||||
meta: { agentType: "Explore", toolUseId: "toolu_agent", requestShape: "foreground" },
|
||||
records: [
|
||||
user("c1", "Say PONG"),
|
||||
assistant("c2", [toolUse("toolu_handback", "SubagentHandback", { message: "PONG" })]),
|
||||
user("c3", [toolResult("toolu_handback", "Report delivered to your caller.")]),
|
||||
// A failed hand-back delivered nothing, so it stays a tool call.
|
||||
assistant("c4", [toolUse("toolu_lost", "SubagentHandback", { message: "Again" })]),
|
||||
user("c5", [{ ...toolResult("toolu_lost", "Not delivered"), is_error: true }]),
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(trace).toEqual([
|
||||
"root user_message_chunk Ask it",
|
||||
'root subagent_update abc123 {"state":"running"}',
|
||||
`root session_message toolu_agent from=${sessionId} recipientMessageId=toolu_agent Say PONG`,
|
||||
`abc123 session_message toolu_agent from=${sessionId} senderMessageId=toolu_agent Say PONG`,
|
||||
"abc123 session_message toolu_handback from=abc123 recipientMessageId=toolu_handback PONG",
|
||||
"root session_message toolu_handback from=abc123 senderMessageId=toolu_handback PONG",
|
||||
"abc123 tool_call toolu_lost",
|
||||
"root agent_message_chunk It said PONG.",
|
||||
'root subagent_update abc123 {"state":"idle","stopReason":"end_turn"}',
|
||||
]);
|
||||
});
|
||||
|
||||
it("ends a child that the user stopped as cancelled, and one without a known end as idle", async () => {
|
||||
const trace = await replayRfd(
|
||||
[
|
||||
@@ -6402,7 +6456,7 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
expect(childEvents).toEqual(["tool_call pending agent-42", "subagent cancelled"]);
|
||||
});
|
||||
|
||||
it("keeps the Agent call's own heartbeat and result on the session that made the call (the subagents RFD)", async () => {
|
||||
it("replaces the Agent call with the parent's session messages, and keeps its heartbeat out of the child (the subagents RFD)", async () => {
|
||||
const updates: AcpSessionNotification[] = [];
|
||||
const agent = new ClaudeAcpAgent(
|
||||
{
|
||||
@@ -6462,16 +6516,36 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
const agentCall = updates.flatMap(({ sessionId, update }) =>
|
||||
(update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") &&
|
||||
update.toolCallId === "toolu_parent"
|
||||
? [{ sessionId, status: update.status }]
|
||||
: [],
|
||||
);
|
||||
expect(agentCall.map(({ status }) => status)).toEqual(
|
||||
expect.arrayContaining(["in_progress", "completed"]),
|
||||
);
|
||||
expect(new Set(agentCall.map(({ sessionId }) => sessionId))).toEqual(new Set(["test-session"]));
|
||||
// Neither the call nor its heartbeat reaches any session, the child's
|
||||
// included: its prompt and result are the parent's session messages.
|
||||
expect(
|
||||
updates.filter(
|
||||
({ update }) =>
|
||||
(update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") &&
|
||||
update.toolCallId.startsWith("toolu_parent"),
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
updates.flatMap(({ sessionId, update }) =>
|
||||
sessionId === "test-session" && update.sessionUpdate === "session_message" ? [update] : [],
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
sessionUpdate: "session_message",
|
||||
messageId: "toolu_parent",
|
||||
senderSessionId: "test-session",
|
||||
recipientSessionId: "agent-42",
|
||||
recipientMessageId: "toolu_parent",
|
||||
content: [{ type: "text", text: "Find the parser" }],
|
||||
},
|
||||
{
|
||||
sessionUpdate: "session_message",
|
||||
messageId: "toolu_parent:reply",
|
||||
senderSessionId: "agent-42",
|
||||
recipientSessionId: "test-session",
|
||||
content: [{ type: "text", text: "It is in x.ts." }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("routes a native child AskUserQuestion and its tool call to the child session", async () => {
|
||||
@@ -7121,6 +7195,17 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
state: { state: "running" },
|
||||
},
|
||||
},
|
||||
{
|
||||
sessionId: "test-session",
|
||||
update: {
|
||||
sessionUpdate: "session_message",
|
||||
messageId: "toolu_parent",
|
||||
senderSessionId: "test-session",
|
||||
recipientSessionId: "agent-42",
|
||||
recipientMessageId: "toolu_parent",
|
||||
content: [{ type: "text", text: "Go" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
sessionId: "agent-42",
|
||||
update: {
|
||||
@@ -7128,6 +7213,7 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
messageId: "toolu_parent",
|
||||
senderSessionId: "test-session",
|
||||
recipientSessionId: "agent-42",
|
||||
senderMessageId: "toolu_parent",
|
||||
content: [{ type: "text", text: "Go" }],
|
||||
},
|
||||
},
|
||||
@@ -7469,6 +7555,17 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
state: { state: "running" },
|
||||
},
|
||||
},
|
||||
{
|
||||
sessionId: "test-session",
|
||||
update: {
|
||||
sessionUpdate: "session_message",
|
||||
messageId: "toolu_parent",
|
||||
senderSessionId: "test-session",
|
||||
recipientSessionId: "agent-42",
|
||||
recipientMessageId: "toolu_parent",
|
||||
content: [{ type: "text", text: "Go" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
sessionId: "agent-42",
|
||||
update: {
|
||||
@@ -7476,6 +7573,7 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
messageId: "toolu_parent",
|
||||
senderSessionId: "test-session",
|
||||
recipientSessionId: "agent-42",
|
||||
senderMessageId: "toolu_parent",
|
||||
content: [{ type: "text", text: "Go" }],
|
||||
},
|
||||
},
|
||||
@@ -7832,7 +7930,7 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("ends the Agent call with Claude Code's result, and the stopped child's tool call in the child's session", async () => {
|
||||
it("reports the stop as the child's idle state, and closes its tool call in the child's session", async () => {
|
||||
const log: string[] = [];
|
||||
const updates: AcpSessionNotification[] = [];
|
||||
const agent = new ClaudeAcpAgent(
|
||||
@@ -7916,25 +8014,21 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
).resolves.toMatchObject({ stopReason: "cancelled" });
|
||||
await agent.sessions["test-session"]?.consumer;
|
||||
|
||||
// In the Agent call's own session, Claude Code's result; in the child's
|
||||
// session, the report that closes its command, after the child went idle.
|
||||
// The parent does not show the Agent call, so the child's idle state
|
||||
// reports the stop. In the child's session, the report that closes its
|
||||
// command comes after the child went idle.
|
||||
expect(log).toEqual([
|
||||
'agent-42 {"state":"running"}',
|
||||
'agent-42 {"state":"idle","stopReason":"cancelled"}',
|
||||
"tool @test-session toolu_agent failed",
|
||||
"tool @agent-42 toolu_child failed",
|
||||
]);
|
||||
const endOf = (toolCallId: string) =>
|
||||
updates.find(
|
||||
({ update }) =>
|
||||
update.sessionUpdate === "tool_call_update" &&
|
||||
update.toolCallId === toolCallId &&
|
||||
update.status === "failed",
|
||||
)?.update;
|
||||
expect(endOf("toolu_agent")).toMatchObject({
|
||||
_meta: { claudeCode: { nonExecutionKind: "user-rejected" } },
|
||||
});
|
||||
expect(JSON.stringify(endOf("toolu_child"))).toContain("The turn was cancelled");
|
||||
const endOfChildCommand = updates.find(
|
||||
({ update }) =>
|
||||
update.sessionUpdate === "tool_call_update" &&
|
||||
update.toolCallId === "toolu_child" &&
|
||||
update.status === "failed",
|
||||
)?.update;
|
||||
expect(JSON.stringify(endOfChildCommand)).toContain("The turn was cancelled");
|
||||
for (const payload of updates) {
|
||||
expect(validateRecorded({ kind: "sessionUpdate", payload }, new Set())).toEqual([]);
|
||||
}
|
||||
@@ -8044,9 +8138,9 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
await agent.sessions["test-session"]?.consumer;
|
||||
|
||||
// The command ends in the child's session, after the child reported idle.
|
||||
// The background launch's own result is not shown in the parent.
|
||||
expect(log).toEqual([
|
||||
'agent-42 {"state":"running"}',
|
||||
"tool @test-session toolu_agent completed",
|
||||
"prompt cancelled",
|
||||
'agent-42 {"state":"idle","stopReason":"cancelled"}',
|
||||
"tool @agent-42 toolu_child failed",
|
||||
|
||||
@@ -4,17 +4,11 @@
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_agent"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Background review"},"sessionUpdate":"tool_call_update","title":"Background review","toolCallId":"toolu_agent"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Background review","prompt":"Review the diff"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Background review","prompt":"Review the diff","subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Review the diff","type":"text"},"type":"content"}],"rawInput":{"description":"Background review","prompt":"Review the diff","run_in_background":true,"subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Async agent launched successfully.","type":"text"},"type":"content"}],"rawOutput":"Async agent launched successfully.\nagentId: a2 (use SendMessage to continue)","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_agent"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolResponse":{"isAsync":true,"status":"async_launched"}}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Background review","sessionId":"a2","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"general-purpose"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientSessionId":"a2","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientMessageId":"toolu_agent","recipientSessionId":"a2","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientSessionId":"a2","senderMessageId":"toolu_agent","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"Reviewi","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"ng now.","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"a2","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
|
||||
|
||||
@@ -4,17 +4,14 @@
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_late"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Check the logs"},"sessionUpdate":"tool_call_update","title":"Check the logs","toolCallId":"toolu_late"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Check the logs","prompt":"Check logs"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_late"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Check logs","type":"text"},"type":"content"}],"rawInput":{"description":"Check the logs","prompt":"Check logs","subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_late"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Check the logs","sessionId":"agent_late","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"general-purpose"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientSessionId":"agent_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientMessageId":"toolu_late","recipientSessionId":"agent_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientSessionId":"agent_late","senderMessageId":"toolu_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_late_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"locations":[{"line":1,"path":"/var/log/app.log"}],"rawInput":{"file_path":"/var/log/app.log"},"sessionUpdate":"tool_call_update","title":"Read /var/log/app.log","toolCallId":"toolu_late_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"The logs are clean.","type":"text"},"type":"content"}],"rawOutput":[{"text":"The logs are clean.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_late"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"The logs are clean.","type":"text"}],"messageId":"toolu_late:reply","recipientSessionId":"<session>","senderSessionId":"agent_late","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_late","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late","toolResponse":{"elapsedTimeSeconds":4}}},"sessionUpdate":"tool_call_update","status":"in_progress","toolCallId":"toolu_late_read"}}}
|
||||
|
||||
@@ -4,14 +4,11 @@
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_native"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Fix the build"},"sessionUpdate":"tool_call_update","title":"Fix the build","toolCallId":"toolu_native"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Fix the build","prompt":"Make the build pass"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Make the build pass","type":"text"},"type":"content"}],"rawInput":{"description":"Fix the build","prompt":"Make the build pass","subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Fix the build","sessionId":"agent_n","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"general-purpose"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientSessionId":"agent_n","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientMessageId":"toolu_native","recipientSessionId":"agent_n","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientSessionId":"agent_n","senderMessageId":"toolu_native","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"Running t","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"he build.","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[],"kind":"search","name":"Grep","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"grep","toolCallId":"toolu_native_grep"}}}
|
||||
@@ -33,7 +30,6 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"output":{"data":"bWFrZTogKioqIEVycm9yIDI="},"sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"exitStatus":{},"sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_native_bash"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\nThe build still fails.\n```","type":"text"},"type":"content"}],"rawOutput":[{"text":"The build still fails.","type":"text"}],"sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_native"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_n","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"error"}}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"error"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
|
||||
@@ -4,30 +4,24 @@
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_outer"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Plan the work"},"sessionUpdate":"tool_call_update","title":"Plan the work","toolCallId":"toolu_outer"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Plan the work","prompt":"Plan it"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_outer"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Plan it","type":"text"},"type":"content"}],"rawInput":{"description":"Plan the work","prompt":"Plan it","subagent_type":"Plan"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_outer"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Plan the work","sessionId":"agent_outer","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Plan"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientSessionId":"agent_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_inner"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"rawInput":{"description":"Read the spec"},"sessionUpdate":"tool_call_update","title":"Read the spec","toolCallId":"toolu_inner"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"rawInput":{"description":"Read the spec","prompt":"Read spec.md"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_inner"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"content":{"text":"Read spec.md","type":"text"},"type":"content"}],"rawInput":{"description":"Read the spec","prompt":"Read spec.md","subagent_type":"Explore"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_inner"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientMessageId":"toolu_outer","recipientSessionId":"agent_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientSessionId":"agent_outer","senderMessageId":"toolu_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"capabilities":{"cancel":{}},"description":"Read the spec","sessionId":"agent_inner","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientSessionId":"agent_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientMessageId":"toolu_inner","recipientSessionId":"agent_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientSessionId":"agent_inner","senderMessageId":"toolu_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_grandchild_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"locations":[{"line":1,"path":"/spec.md"}],"rawInput":{"file_path":"/spec.md"},"sessionUpdate":"tool_call_update","title":"Read /spec.md","toolCallId":"toolu_grandchild_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"content":{"text":"```\n1\tspec\n```","type":"text"},"type":"content"}],"rawOutput":"1\tspec","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_grandchild_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"content":{"text":"The spec says X.","type":"text"},"type":"content"}],"rawOutput":[{"text":"The spec says X.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_inner"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"The spec says X.","type":"text"}],"messageId":"toolu_inner:reply","recipientSessionId":"agent_outer","senderSessionId":"agent_inner","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionId":"agent_inner","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"content":{"text":"Plan:","type":"text"},"messageId":"msg_outer_text","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"content":{"text":" do X.","type":"text"},"messageId":"msg_outer_text","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Plan: do X.","type":"text"},"type":"content"}],"rawOutput":[{"text":"Plan: do X.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_outer"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Plan: do X.","type":"text"}],"messageId":"toolu_outer:reply","recipientSessionId":"<session>","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_outer","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
|
||||
@@ -4,19 +4,15 @@
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Task","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_task"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Explore code"},"sessionUpdate":"tool_call_update","title":"Explore code","toolCallId":"toolu_task"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Explore code","prompt":"Find the parser"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_task"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Find the parser","type":"text"},"type":"content"}],"rawInput":{"description":"Explore code","prompt":"Find the parser","subagent_type":"Explore"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_task"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Explore code","sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientSessionId":"agent_1","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientMessageId":"toolu_task","recipientSessionId":"agent_1","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientSessionId":"agent_1","senderMessageId":"toolu_task","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Looking for","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":" the parser.","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_sub_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"locations":[{"line":1,"path":"/x.ts"}],"rawInput":{"file_path":"/x.ts"},"sessionUpdate":"tool_call_update","title":"Read /x.ts","toolCallId":"toolu_sub_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolResponse":{"elapsedTimeSeconds":3,"subagentType":"Explore"}}},"sessionUpdate":"tool_call_update","status":"in_progress","toolCallId":"toolu_task"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"content":{"text":"```\n1\tparser\n```","type":"text"},"type":"content"}],"rawOutput":"1\tparser","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[{"terminalId":"toolu_sub_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_sub_bash"}}}
|
||||
@@ -48,8 +44,7 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/x.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/x.ts <cwd>/x.ts\n--- <cwd>/x.ts\n+++ <cwd>/x.ts\n@@ -1 +1 @@\n-a\n+b\n"},"type":"diff"}],"locations":[{"line":1,"path":"<cwd>/x.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Foun","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"d it.","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"The parser is in x.ts.","type":"text"},"type":"content"}],"rawOutput":[{"text":"The parser is in x.ts.\nagentId: a1 (use SendMessage to continue)","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_task"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolResponse":{"status":"completed"}}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_task"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"The parser is in x.ts.","type":"text"}],"messageId":"toolu_task:reply","recipientSessionId":"<session>","senderSessionId":"agent_1","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
|
||||
@@ -4,14 +4,11 @@
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Task","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_transcript"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Summarize"},"sessionUpdate":"tool_call_update","title":"Summarize","toolCallId":"toolu_transcript"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Summarize","prompt":"Summarize the repo"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_transcript"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Summarize the repo","type":"text"},"type":"content"}],"rawInput":{"description":"Summarize","prompt":"Summarize the repo","subagent_type":"Explore"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_transcript"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Summarize","sessionId":"agent_t","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientSessionId":"agent_t","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientMessageId":"toolu_transcript","recipientSessionId":"agent_t","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientSessionId":"agent_t","senderMessageId":"toolu_transcript","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"Look at t","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"he layout.","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"The repo has ","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_message_chunk"}}}
|
||||
@@ -19,7 +16,7 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":[],"kind":"search","locations":[],"name":"Glob","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Find","toolCallId":"toolu_transcript_glob"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"rawInput":{"pattern":"*"},"sessionUpdate":"tool_call_update","title":"Find `*`","toolCallId":"toolu_transcript_glob"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"content":[{"content":{"text":"a\nb","type":"text"},"type":"content"}],"rawOutput":"a\nb","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_transcript_glob"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Two packages.","type":"text"},"type":"content"}],"rawOutput":[{"text":"Two packages.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_transcript"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Two packages.","type":"text"}],"messageId":"toolu_transcript:reply","recipientSessionId":"<session>","senderSessionId":"agent_t","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_t","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
|
||||
@@ -1927,24 +1927,19 @@ describe("ACP v2 prompts", () => {
|
||||
|
||||
const { root, updates } = await cancelWhileRunning("toolu_child");
|
||||
|
||||
// The Agent call ends in the session that made it, with Claude Code's
|
||||
// result and the kind that it stamped. The subagent's command ends in the
|
||||
// child's session, whose own idle came first.
|
||||
// The parent does not show the Agent call: the child's idle reports the
|
||||
// stop. The subagent's command ends in the child's session, before the
|
||||
// root's idle.
|
||||
const idle = idleAt(root, updates);
|
||||
const agentEnd = lastStatusAt(updates, "toolu_agent");
|
||||
expect(updates[agentEnd]).toMatchObject({
|
||||
sessionId: root,
|
||||
update: {
|
||||
status: "cancelled",
|
||||
_meta: { claudeCode: { nonExecutionKind: "user-rejected" } },
|
||||
},
|
||||
expect(lastStatusAt(updates, "toolu_agent")).toBe(-1);
|
||||
expect(updates[idleAt("agent-1", updates)]).toMatchObject({
|
||||
update: { stopReason: "cancelled" },
|
||||
});
|
||||
const childEnd = lastStatusAt(updates, "toolu_child");
|
||||
expect(updates[childEnd]).toMatchObject({
|
||||
sessionId: "agent-1",
|
||||
update: { status: "cancelled" },
|
||||
});
|
||||
expect(agentEnd).toBeLessThan(idle);
|
||||
expect(childEnd).toBeLessThan(idle);
|
||||
});
|
||||
|
||||
@@ -2584,14 +2579,12 @@ describe("ACP v2 model cycles that Claude Code starts on its own", () => {
|
||||
|
||||
const { root, updates } = await cancelCycle("toolu_child");
|
||||
|
||||
// Claude Code's result ends the Agent call. The command ends in the
|
||||
// child's session, after the child's idle and before the cycle's.
|
||||
expect(updates[lastStatusAt(updates, "toolu_agent")]).toMatchObject({
|
||||
sessionId: root,
|
||||
update: {
|
||||
status: "cancelled",
|
||||
_meta: { claudeCode: { nonExecutionKind: "user-rejected" } },
|
||||
},
|
||||
// The parent does not show the Agent call: the child's idle reports the
|
||||
// stop. The command ends in the child's session, after the child's idle
|
||||
// and before the cycle's.
|
||||
expect(statusesOf(updates, "toolu_agent")).toEqual([]);
|
||||
expect(updates[lastIdleAt(updates, "agent-1")]).toMatchObject({
|
||||
update: { stopReason: "cancelled" },
|
||||
});
|
||||
const end = lastStatusAt(updates, "toolu_child");
|
||||
expect(updates[end]).toMatchObject({ sessionId: "agent-1", update: { status: "cancelled" } });
|
||||
|
||||
@@ -776,6 +776,27 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
_meta: { claudeCode: { parentToolUseId } },
|
||||
},
|
||||
}) as AcpSessionNotification;
|
||||
/** The final update of the Agent call `toolCallId`, showing `text` as the
|
||||
* reporter renders the result, under the SDK's raw output and its trailer. */
|
||||
const launchResult = (
|
||||
toolCallId: string,
|
||||
status: "completed" | "failed",
|
||||
text: string,
|
||||
parentToolUseId?: string,
|
||||
): AcpSessionNotification =>
|
||||
({
|
||||
sessionId: "root",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId,
|
||||
status,
|
||||
content: [{ type: "content", content: { type: "text", text } }],
|
||||
rawOutput: [{ type: "text", text: `${text}\nagentId: a1 (use SendMessage to continue)` }],
|
||||
_meta: {
|
||||
claudeCode: { toolName: "Agent", ...(parentToolUseId ? { parentToolUseId } : {}) },
|
||||
},
|
||||
},
|
||||
}) as AcpSessionNotification;
|
||||
/** A SendMessage call, of the root or of the child that `parentToolUseId` launched. */
|
||||
const sendMessage = (toolCallId: string, parentToolUseId?: string): AcpSessionNotification =>
|
||||
({
|
||||
@@ -821,17 +842,17 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
update.sessionUpdate === "subagent_update" && update.state ? [update.state] : [],
|
||||
);
|
||||
|
||||
it("keeps the Agent tool call as a tool call of the session that made it", async () => {
|
||||
it("hides the Agent tool call, which the parent's session messages replace", async () => {
|
||||
const { runtime, control } = await runningWorker();
|
||||
|
||||
expect(control).toMatchObject({ sessionId: "root", update: { toolCallId: "launch-1" } });
|
||||
// A nested launch is a tool call of the child that made it.
|
||||
expect(control).toBeNull();
|
||||
// So is a nested launch, in the child that made it.
|
||||
await expect(
|
||||
runtime.route(launch("launch-2", { description: "Dig" }, "launch-1"), async () => {}),
|
||||
).resolves.toMatchObject({ sessionId: "worker-1", update: { toolCallId: "launch-2" } });
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("announces the child with the parent's labels, then sends it the prompt", async () => {
|
||||
it("announces the child with the parent's labels, then sends the prompt to both transcripts", async () => {
|
||||
const { published } = await runningWorker({ name: "builder" });
|
||||
|
||||
expect(published).toEqual([
|
||||
@@ -846,6 +867,17 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
state: { state: "running" },
|
||||
},
|
||||
},
|
||||
{
|
||||
sessionId: "root",
|
||||
update: {
|
||||
sessionUpdate: "session_message",
|
||||
messageId: "launch-1",
|
||||
senderSessionId: "root",
|
||||
recipientSessionId: "worker-1",
|
||||
recipientMessageId: "launch-1",
|
||||
content: [{ type: "text", text: "Make it pass" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
sessionId: "worker-1",
|
||||
update: {
|
||||
@@ -853,12 +885,192 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
messageId: "launch-1",
|
||||
senderSessionId: "root",
|
||||
recipientSessionId: "worker-1",
|
||||
senderMessageId: "launch-1",
|
||||
content: [{ type: "text", text: "Make it pass" }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends a foreground child's answer to its parent as the child's reply", async () => {
|
||||
const { runtime, published } = await runningWorker();
|
||||
|
||||
await expect(
|
||||
runtime.route(launchResult("launch-1", "completed", "The build passes."), async () => {}),
|
||||
).resolves.toBeNull();
|
||||
|
||||
expect(published.at(-1)).toEqual({
|
||||
sessionId: "root",
|
||||
update: {
|
||||
sessionUpdate: "session_message",
|
||||
messageId: "launch-1:reply",
|
||||
senderSessionId: "worker-1",
|
||||
recipientSessionId: "root",
|
||||
content: [{ type: "text", text: "The build passes." }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("a child's SubagentHandback call", () => {
|
||||
/** An update of the child's hand-back call. Only its first update names the tool. */
|
||||
const handback = (
|
||||
status: "pending" | "completed" | "failed",
|
||||
rawInput?: Record<string, unknown>,
|
||||
): AcpSessionNotification =>
|
||||
({
|
||||
sessionId: "root",
|
||||
update: {
|
||||
sessionUpdate: status === "pending" ? "tool_call" : "tool_call_update",
|
||||
toolCallId: "handback-1",
|
||||
status,
|
||||
...(status === "pending" ? { title: "SubagentHandback" } : {}),
|
||||
...(rawInput ? { rawInput } : {}),
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
parentToolUseId: "launch-1",
|
||||
...(status === "pending" ? { toolName: "SubagentHandback" } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as AcpSessionNotification;
|
||||
const pointer = "This agent's report was delivered to you as a message.";
|
||||
|
||||
it("is the child's message to its parent, which replaces the launch's reply", async () => {
|
||||
const { runtime, published } = await runningWorker();
|
||||
const before = published.length;
|
||||
|
||||
// The streamed call starts without input; a refinement brings the report.
|
||||
const refinement = {
|
||||
sessionId: "root",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "handback-1",
|
||||
rawInput: { message: "PONG" },
|
||||
_meta: { claudeCode: { parentToolUseId: "launch-1" } },
|
||||
},
|
||||
} as AcpSessionNotification;
|
||||
for (const update of [
|
||||
handback("pending", {}),
|
||||
refinement,
|
||||
handback("completed"),
|
||||
launchResult("launch-1", "completed", pointer),
|
||||
]) {
|
||||
await expect(runtime.route(update, async () => {})).resolves.toBeNull();
|
||||
}
|
||||
|
||||
const message = {
|
||||
sessionUpdate: "session_message",
|
||||
messageId: "handback-1",
|
||||
senderSessionId: "worker-1",
|
||||
recipientSessionId: "root",
|
||||
content: [{ type: "text", text: "PONG" }],
|
||||
};
|
||||
expect(published.slice(before)).toEqual([
|
||||
{ sessionId: "worker-1", update: { ...message, recipientMessageId: "handback-1" } },
|
||||
{ sessionId: "root", update: { ...message, senderMessageId: "handback-1" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("stays a tool call of the child when it fails, and the launch's result is the reply", async () => {
|
||||
const { runtime, published } = await runningWorker();
|
||||
|
||||
await runtime.route(handback("pending", { message: "PONG" }), async () => {});
|
||||
await expect(runtime.route(handback("failed"), async () => {})).resolves.toMatchObject({
|
||||
sessionId: "worker-1",
|
||||
update: { sessionUpdate: "tool_call", toolCallId: "handback-1", status: "failed" },
|
||||
});
|
||||
await runtime.route(launchResult("launch-1", "completed", "PONG"), async () => {});
|
||||
|
||||
expect(published.at(-1)).toMatchObject({
|
||||
sessionId: "root",
|
||||
update: { messageId: "launch-1:reply", content: [{ type: "text", text: "PONG" }] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("reports no reply for a failed launch, whose child's idle state reports the failure", async () => {
|
||||
const { runtime, published } = await runningWorker();
|
||||
const before = published.length;
|
||||
|
||||
await expect(
|
||||
runtime.route(launchResult("launch-1", "failed", "The build still fails."), async () => {}),
|
||||
).resolves.toBeNull();
|
||||
|
||||
expect(published).toHaveLength(before);
|
||||
});
|
||||
|
||||
it("reports no reply for a background launch, which completes before its child works", async () => {
|
||||
const { runtime, published } = await runningWorker({ run_in_background: true });
|
||||
const before = published.length;
|
||||
|
||||
await expect(
|
||||
runtime.route(
|
||||
launchResult("launch-1", "completed", "Async agent launched successfully."),
|
||||
async () => {},
|
||||
),
|
||||
).resolves.toBeNull();
|
||||
|
||||
expect(published).toHaveLength(before);
|
||||
});
|
||||
|
||||
it("recognizes the adapter's report that closes a hidden launch, which has no tool name", async () => {
|
||||
const { runtime, published } = await runningWorker();
|
||||
// The report that closes a call the cancel left open, for a v2 client.
|
||||
const close = (toolCallId: string): AcpSessionNotification => ({
|
||||
sessionId: "root",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId,
|
||||
status: "cancelled" as "failed",
|
||||
content: [],
|
||||
},
|
||||
});
|
||||
const before = published.length;
|
||||
|
||||
// The child was exposed, so its idle state reports the stop.
|
||||
await expect(runtime.route(close("launch-1"), async () => {})).resolves.toBeNull();
|
||||
expect(published).toHaveLength(before);
|
||||
// A launch whose child never started is shown after all.
|
||||
await runtime.route(launch("launch-9", { description: "Dig" }), async () => {});
|
||||
await expect(runtime.route(close("launch-9"), async () => {})).resolves.toMatchObject({
|
||||
sessionId: "root",
|
||||
update: { sessionUpdate: "tool_call", toolCallId: "launch-9", status: "cancelled" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a launch whose child never started as a tool call with its final status", async () => {
|
||||
const published: AcpSessionNotification[] = [];
|
||||
const runtime = new NativeSubagentRuntime(
|
||||
"rfd",
|
||||
"root",
|
||||
{},
|
||||
async (notification) => {
|
||||
published.push(notification);
|
||||
},
|
||||
{ log: () => {} },
|
||||
);
|
||||
for (const [toolCallId, status] of [
|
||||
["launch-ok", "completed"],
|
||||
["launch-bad", "failed"],
|
||||
] as const) {
|
||||
await expect(
|
||||
runtime.route(launch(toolCallId, { description: "Dig" }), async () => {}),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
runtime.route(launchResult(toolCallId, status, "Done"), async () => {}),
|
||||
).resolves.toMatchObject({
|
||||
sessionId: "root",
|
||||
update: { sessionUpdate: "tool_call", toolCallId, status },
|
||||
});
|
||||
}
|
||||
// A background launch completes before its child starts, so it stays hidden.
|
||||
await runtime.route(launch("launch-bg", { run_in_background: true }), async () => {});
|
||||
await expect(
|
||||
runtime.route(launchResult("launch-bg", "completed", "Async agent launched"), async () => {}),
|
||||
).resolves.toBeNull();
|
||||
expect(published).toEqual([]);
|
||||
});
|
||||
|
||||
it("titles a child without a name by its role", async () => {
|
||||
const { published } = await runningWorker();
|
||||
|
||||
@@ -885,7 +1097,10 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
{ state: "running" },
|
||||
{ state: "idle", stopReason: "end_turn" },
|
||||
]);
|
||||
// The SendMessage call stays a tool call of its sender, so its message has
|
||||
// only the child's view, with no reference.
|
||||
expect(published.filter(({ update }) => update.sessionUpdate === "session_message")).toEqual([
|
||||
expect.objectContaining({ sessionId: "root" }),
|
||||
expect.objectContaining({ sessionId: "worker-1" }),
|
||||
{
|
||||
sessionId: "worker-1",
|
||||
@@ -938,8 +1153,10 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
expect(
|
||||
published
|
||||
.filter(({ update }) => update.sessionUpdate === "session_message")
|
||||
.map(({ update }) => (update as { messageId: string }).messageId),
|
||||
).toEqual(["launch-1", "send-1"]);
|
||||
.map(
|
||||
({ sessionId, update }) => `${sessionId} ${(update as { messageId: string }).messageId}`,
|
||||
),
|
||||
).toEqual(["root launch-1", "worker-1 launch-1", "worker-1 send-1"]);
|
||||
});
|
||||
|
||||
it("ignores a SendMessage result that arrives after the run it started ended", async () => {
|
||||
@@ -985,7 +1202,10 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
|
||||
expect(states(published).at(-1)).toEqual({ state: "running" });
|
||||
expect(
|
||||
published.filter(({ update }) => update.sessionUpdate === "session_message"),
|
||||
published.filter(
|
||||
({ sessionId, update }) =>
|
||||
sessionId === "worker-1" && update.sessionUpdate === "session_message",
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(runtime.isStaleEnd("worker-1", "launch-1")).toBe(false);
|
||||
});
|
||||
@@ -1245,10 +1465,17 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
// A resume signal does not give the unexposed child another call.
|
||||
await runtime.taskStarted({ taskId: "worker-2", toolUseId: "send-9" }, async () => {});
|
||||
await runtime.finishTask("worker-2", "completed", async () => {});
|
||||
// The frame arrives late: it is the tool call of worker-1 that made it.
|
||||
// The frame arrives late, and the launch stays hidden while it runs. Its
|
||||
// child was never exposed, so its result makes it a tool call of worker-1.
|
||||
await expect(
|
||||
runtime.route(launch("launch-2", { description: "Dig" }, "launch-1"), async () => {}),
|
||||
).resolves.toMatchObject({ sessionId: "worker-1" });
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
runtime.route(launchResult("launch-2", "completed", "Dug", "launch-1"), async () => {}),
|
||||
).resolves.toMatchObject({
|
||||
sessionId: "worker-1",
|
||||
update: { sessionUpdate: "tool_call", toolCallId: "launch-2", status: "completed" },
|
||||
});
|
||||
|
||||
expect(
|
||||
published.some(
|
||||
|
||||
Reference in New Issue
Block a user