mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-30 22:11:45 +00:00
fix: title subagent sessions by their task (#1479)
A subagent's `subagent_update.title` was its type (`general-purpose`), or `name (type)`, so a client showing the title couldn't tell siblings apart; the task was only in `description`. The RFD makes the title the parent's display label and the description the child's role or purpose. The title is now the agent's name when it has one, else the task's short description, else its type. The description is the type, unless the type is already the title.
This commit is contained in:
@@ -25,11 +25,14 @@ export type NativeSubagent = {
|
||||
* form. It is absent when the adapter has no prompt.
|
||||
*/
|
||||
prompt?: string;
|
||||
/** The RFD form: the parent's label for the child, `subagent_update.title`. */
|
||||
/**
|
||||
* The RFD form: the parent's label for the child, `subagent_update.title`:
|
||||
* its name, else its task.
|
||||
*/
|
||||
title?: string;
|
||||
/** The RFD form: the child's role, its `subagent_type`, for the title. */
|
||||
/** The RFD form: the child's role, its `subagent_type`. */
|
||||
role?: string;
|
||||
/** The RFD form: the child's purpose, `subagent_update.description`. */
|
||||
/** The RFD form: `subagent_update.description`, the child's role. */
|
||||
description?: string;
|
||||
/**
|
||||
* The RFD form: the work state the client holds. The child is reusable, so
|
||||
@@ -1868,26 +1871,22 @@ export function rfdEndState(state: SubagentState, end: TaskEnd | undefined): Wor
|
||||
}
|
||||
|
||||
/**
|
||||
* The RFD form: the parent's labels for a child. The title is the agent's
|
||||
* name, with its role after it when both are known, else the role. The
|
||||
* description is the task's short description, never the prompt.
|
||||
* The RFD form: the parent's labels for a child. The title is what tells
|
||||
* siblings apart: the agent's name when it has one, else the task's short
|
||||
* description (never the prompt), else its role. The description is the
|
||||
* role, its `subagent_type`, unless the role is already the title.
|
||||
*/
|
||||
export function rfdLabels(
|
||||
name: unknown,
|
||||
role: unknown,
|
||||
description: unknown,
|
||||
): { title?: string; role?: string; description?: string } {
|
||||
const agentName = nonBlankString(name);
|
||||
const agentRole = nonBlankString(role);
|
||||
const title =
|
||||
agentName && agentRole && agentName !== agentRole
|
||||
? `${agentName} (${agentRole})`
|
||||
: (agentName ?? agentRole);
|
||||
const text = nonBlankString(description);
|
||||
const title = nonBlankString(name) ?? nonBlankString(description) ?? agentRole;
|
||||
return {
|
||||
...(title ? { title } : {}),
|
||||
...(agentRole ? { role: agentRole } : {}),
|
||||
...(text ? { description: text } : {}),
|
||||
...(agentRole && agentRole !== title ? { description: agentRole } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7747,8 +7747,8 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
update: {
|
||||
sessionUpdate: "subagent_update",
|
||||
sessionId: "agent-42",
|
||||
title: "Explore",
|
||||
description: "Investigate",
|
||||
title: "Investigate",
|
||||
description: "Explore",
|
||||
capabilities: { cancel: {} },
|
||||
state: { state: "running" },
|
||||
},
|
||||
@@ -8302,8 +8302,8 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
update: {
|
||||
sessionUpdate: "subagent_update",
|
||||
sessionId: "agent-7",
|
||||
title: "echoer (Explore)",
|
||||
description: "Echo a word",
|
||||
title: "echoer",
|
||||
description: "Explore",
|
||||
capabilities: { cancel: {} },
|
||||
state: { state: "running" },
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{"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":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"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":"<session>","update":{"capabilities":{"cancel":{}},"description":"general-purpose","sessionId":"a2","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Background review"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"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"}}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{"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":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"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":"<session>","update":{"capabilities":{"cancel":{}},"description":"general-purpose","sessionId":"agent_late","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Check the logs"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"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"}}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{"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":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"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":"<session>","update":{"capabilities":{"cancel":{}},"description":"general-purpose","sessionId":"agent_n","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Fix the build"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"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"}}}
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
{"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":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"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":"<session>","update":{"capabilities":{"cancel":{}},"description":"Plan","sessionId":"agent_outer","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Plan the work"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"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":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
|
||||
{"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_outer","update":{"capabilities":{"cancel":{}},"description":"Explore","sessionId":"agent_inner","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Read the spec"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"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"}}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{"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":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"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":"<session>","update":{"capabilities":{"cancel":{}},"description":"Explore","sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore code"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"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"}}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{"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":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"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":"<session>","update":{"capabilities":{"cancel":{}},"description":"Explore","sessionId":"agent_t","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Summarize"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"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"}}}
|
||||
|
||||
@@ -1087,8 +1087,8 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
update: {
|
||||
sessionUpdate: "subagent_update",
|
||||
sessionId: "worker-1",
|
||||
title: "builder (Explore)",
|
||||
description: "Fix the build",
|
||||
title: "builder",
|
||||
description: "Explore",
|
||||
capabilities: { cancel: {} },
|
||||
state: { state: "running" },
|
||||
},
|
||||
@@ -1297,10 +1297,10 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
expect(published).toEqual([]);
|
||||
});
|
||||
|
||||
it("titles a child without a name by its role", async () => {
|
||||
it("titles a child without a name by its task, and describes it by its role", async () => {
|
||||
const { published } = await runningWorker();
|
||||
|
||||
expect(published[0]?.update).toMatchObject({ title: "Explore", description: "Fix the build" });
|
||||
expect(published[0]?.update).toMatchObject({ title: "Fix the build", description: "Explore" });
|
||||
});
|
||||
|
||||
it("keeps the session of an idle child, which a resume runs again", async () => {
|
||||
@@ -1569,8 +1569,8 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
update: {
|
||||
sessionUpdate: "subagent_update",
|
||||
sessionId: "echoer-1",
|
||||
title: "echoer (Explore)",
|
||||
description: "Echo a word",
|
||||
title: "echoer",
|
||||
description: "Explore",
|
||||
capabilities: { cancel: {} },
|
||||
state: { state: "running" },
|
||||
},
|
||||
@@ -1623,8 +1623,8 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
expect(published[0]?.update).toEqual({
|
||||
sessionUpdate: "subagent_update",
|
||||
sessionId: "coordinator-1",
|
||||
title: "coordinator (general-purpose)",
|
||||
description: "Coordinate",
|
||||
title: "coordinator",
|
||||
description: "general-purpose",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1825,7 +1825,7 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the role in the title when a late frame names the child", async () => {
|
||||
it("titles the child by the name a late frame gives it, and keeps its role", async () => {
|
||||
const published: AcpSessionNotification[] = [];
|
||||
const runtime = new NativeSubagentRuntime(
|
||||
"rfd",
|
||||
@@ -1842,7 +1842,7 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
);
|
||||
await runtime.route(launch("launch-1", { name: "builder" }), async () => {});
|
||||
|
||||
expect(published[0]?.update).toMatchObject({ title: "builder (Explore)" });
|
||||
expect(published[0]?.update).toMatchObject({ title: "builder", description: "Explore" });
|
||||
});
|
||||
|
||||
it("reports nothing when the parent is cancelled, until the SDK ends the child", async () => {
|
||||
|
||||
Reference in New Issue
Block a user