@agentclientprotocol/claude-agent-acp 0.89.2-preview.8 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -8
- package/dist/acp-agent.d.ts +52 -3
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +507 -139
- package/dist/acp-subagents.d.ts +16 -6
- package/dist/acp-subagents.d.ts.map +1 -1
- package/dist/acp-subagents.js +8 -2
- package/dist/async-tasks.d.ts +4 -1
- package/dist/async-tasks.d.ts.map +1 -1
- package/dist/auth-status.d.ts +2 -2
- package/dist/auth-status.d.ts.map +1 -1
- package/dist/auth-status.js +18 -4
- package/dist/exit-plan.d.ts +17 -0
- package/dist/exit-plan.d.ts.map +1 -1
- package/dist/exit-plan.js +34 -0
- package/dist/native-subagents.d.ts +190 -11
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +419 -40
- package/dist/settings.d.ts +6 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +13 -0
- package/dist/subagent-history.d.ts +27 -2
- package/dist/subagent-history.d.ts.map +1 -1
- package/dist/subagent-history.js +38 -10
- package/dist/subagent-replay.d.ts +108 -0
- package/dist/subagent-replay.d.ts.map +1 -0
- package/dist/subagent-replay.js +261 -0
- package/dist/tool-calls/background.d.ts +9 -0
- package/dist/tool-calls/background.d.ts.map +1 -1
- package/dist/tool-calls/background.js +25 -0
- package/dist/tool-calls/renderer.d.ts +30 -9
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +48 -22
- package/dist/tool-calls/reporters/bash.d.ts +7 -0
- package/dist/tool-calls/reporters/bash.d.ts.map +1 -1
- package/dist/tool-calls/reporters/bash.js +16 -1
- package/dist/tool-calls/reporters/search.d.ts.map +1 -1
- package/dist/tool-calls/reporters/search.js +20 -2
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +45 -12
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +10 -1
- package/dist/v2/session-update.d.ts +11 -0
- package/dist/v2/session-update.d.ts.map +1 -1
- package/dist/v2/session-update.js +67 -0
- package/dist/v2/session.d.ts +3 -12
- package/dist/v2/session.d.ts.map +1 -1
- package/dist/v2/session.js +5 -18
- package/dist/v2/tool-call-meta.d.ts +44 -0
- package/dist/v2/tool-call-meta.d.ts.map +1 -0
- package/dist/v2/tool-call-meta.js +72 -0
- package/package.json +9 -9
package/dist/native-subagents.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RequestError } from "@agentclientprotocol/sdk";
|
|
1
2
|
import { AIR_SUBAGENT_KEY, airExtensionMeta } from "./air-extension.js";
|
|
2
3
|
const MAX_PENDING_PARENTS = 64;
|
|
3
4
|
const MAX_PENDING_UPDATES = 256;
|
|
@@ -14,6 +15,8 @@ export class NativeSubagentRuntime {
|
|
|
14
15
|
publish;
|
|
15
16
|
logger;
|
|
16
17
|
enabled;
|
|
18
|
+
/** The form of the child sessions; see {@link SubagentForm}. */
|
|
19
|
+
form;
|
|
17
20
|
children;
|
|
18
21
|
taskByToolUse;
|
|
19
22
|
parentByToolUse;
|
|
@@ -27,15 +30,19 @@ export class NativeSubagentRuntime {
|
|
|
27
30
|
*/
|
|
28
31
|
childByToolCall = new Map();
|
|
29
32
|
taskFinishPromises = new Map();
|
|
33
|
+
/** The RFD form: the session that made each SendMessage call. */
|
|
34
|
+
senderByToolUse = new Map();
|
|
30
35
|
generationByTaskId = new Map();
|
|
31
36
|
pending = new Map();
|
|
32
37
|
pendingCount = 0;
|
|
33
|
-
|
|
38
|
+
/** `form` is undefined for a client without native subagent sessions. */
|
|
39
|
+
constructor(form, rootSessionId, session, publish, logger) {
|
|
34
40
|
this.rootSessionId = rootSessionId;
|
|
35
41
|
this.session = session;
|
|
36
42
|
this.publish = publish;
|
|
37
43
|
this.logger = logger;
|
|
38
|
-
this.
|
|
44
|
+
this.form = form;
|
|
45
|
+
this.enabled = form !== undefined;
|
|
39
46
|
this.children = session.nativeSubagentsByTaskId ??= new Map();
|
|
40
47
|
this.taskByToolUse = session.nativeSubagentTaskIdByToolUseId ??= new Map();
|
|
41
48
|
this.parentByToolUse = session.nativeSubagentParentByToolUseId ??= new Map();
|
|
@@ -46,6 +53,12 @@ export class NativeSubagentRuntime {
|
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
async route(notification, deliver, forcedSessionId) {
|
|
56
|
+
const routed = await this.routeUpdate(notification, deliver, forcedSessionId);
|
|
57
|
+
if (routed && this.form === "rfd")
|
|
58
|
+
this.rememberSendMessage(routed);
|
|
59
|
+
return routed;
|
|
60
|
+
}
|
|
61
|
+
async routeUpdate(notification, deliver, forcedSessionId) {
|
|
49
62
|
const { update } = notification;
|
|
50
63
|
const claudeMeta = update._meta?.claudeCode;
|
|
51
64
|
const isControl = isNativeSubagentControlUpdate(update);
|
|
@@ -60,28 +73,43 @@ export class NativeSubagentRuntime {
|
|
|
60
73
|
if (identity) {
|
|
61
74
|
this.identityByToolUse.set(toolCallId, mergeSubagentIdentity(this.identityByToolUse.get(toolCallId), identity));
|
|
62
75
|
}
|
|
76
|
+
const parentChild = claudeMeta?.parentToolUseId
|
|
77
|
+
? this.childByParentToolUse.get(claudeMeta.parentToolUseId)
|
|
78
|
+
: undefined;
|
|
63
79
|
const parentSessionId = claudeMeta?.parentToolUseId
|
|
64
|
-
?
|
|
80
|
+
? parentChild?.sessionId
|
|
65
81
|
: this.rootSessionId;
|
|
66
|
-
|
|
82
|
+
// The RFD form: a call made inside a child that is not announced yet
|
|
83
|
+
// establishes no parent, because the RFD forbids announcing its child
|
|
84
|
+
// under the root or before that child. The update is buffered below
|
|
85
|
+
// under the child's tool use, and routed again, with the child as the
|
|
86
|
+
// parent, once the child is announced.
|
|
87
|
+
const parentPending = this.form === "rfd" && !!claudeMeta?.parentToolUseId && !parentChild?.announced;
|
|
67
88
|
const child = this.childByParentToolUse.get(toolCallId);
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await
|
|
89
|
+
if (!parentPending) {
|
|
90
|
+
this.parentByToolUse.set(toolCallId, parentSessionId ?? this.rootSessionId);
|
|
91
|
+
if (child && !child.announced) {
|
|
92
|
+
child.parentSessionId = parentSessionId ?? this.rootSessionId;
|
|
93
|
+
applySubagentIdentity(child, this.identityByToolUse.get(toolCallId));
|
|
94
|
+
await this.announce(child);
|
|
95
|
+
for (const pending of this.takePending(toolCallId))
|
|
96
|
+
await deliver(pending);
|
|
97
|
+
}
|
|
74
98
|
}
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
if (this.form === "air") {
|
|
100
|
+
if (!child && isFailedToolCallUpdate(update)) {
|
|
101
|
+
this.takePending(toolCallId);
|
|
102
|
+
const initial = this.controlByToolUse.get(toolCallId);
|
|
103
|
+
this.cleanupControl(toolCallId);
|
|
104
|
+
if (forcedSessionId) {
|
|
105
|
+
return { ...notification, sessionId: forcedSessionId };
|
|
106
|
+
}
|
|
107
|
+
return failedControlFallback(initial, notification, parentSessionId ?? this.rootSessionId);
|
|
81
108
|
}
|
|
82
|
-
return
|
|
109
|
+
return forcedSessionId ? { ...notification, sessionId: forcedSessionId } : null;
|
|
83
110
|
}
|
|
84
|
-
|
|
111
|
+
// The RFD form: the Agent/Task tool call is the parent's record of the
|
|
112
|
+
// delegation, so it stays a tool call of the session that made it.
|
|
85
113
|
}
|
|
86
114
|
const toolCallId = update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update"
|
|
87
115
|
? update.toolCallId
|
|
@@ -112,6 +140,13 @@ export class NativeSubagentRuntime {
|
|
|
112
140
|
async taskStarted(task, deliver) {
|
|
113
141
|
if (!this.enabled)
|
|
114
142
|
return;
|
|
143
|
+
const known = this.form === "rfd" ? this.children.get(task.taskId) : undefined;
|
|
144
|
+
if (known) {
|
|
145
|
+
// A child keeps its session for every delegation to it. The SDK starts
|
|
146
|
+
// a resumed agent again under the id of the call that resumed it.
|
|
147
|
+
await this.resumeChild(known, task.taskId, promptText(task.prompt), task.toolUseId ?? undefined, deliver);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
115
150
|
if (!task.subagentType) {
|
|
116
151
|
if (task.toolUseId) {
|
|
117
152
|
this.takePending(task.toolUseId);
|
|
@@ -135,6 +170,7 @@ export class NativeSubagentRuntime {
|
|
|
135
170
|
name: subagentDisplayName(identity?.name, identity?.description ?? task.description, identity?.subagentType ?? task.subagentType, task.taskId),
|
|
136
171
|
task: subagentDescription(identity?.prompt ?? task.prompt, identity?.description ?? task.description),
|
|
137
172
|
...promptField(promptText(task.prompt) ?? identity?.prompt),
|
|
173
|
+
...rfdLabels(identity?.name, identity?.subagentType ?? task.subagentType, identity?.description ?? task.description),
|
|
138
174
|
}, !!knownParentSessionId || !task.toolUseId, deliver);
|
|
139
175
|
}
|
|
140
176
|
/**
|
|
@@ -143,6 +179,10 @@ export class NativeSubagentRuntime {
|
|
|
143
179
|
* running `task_updated` patch or a SendMessage `resumedAgentId` is the
|
|
144
180
|
* signal. A child that did not finish is not changed. The `prompt` is the
|
|
145
181
|
* SendMessage text that resumed the child, when the adapter knows it.
|
|
182
|
+
*
|
|
183
|
+
* In the RFD form, the running patch only reports that the child runs: its
|
|
184
|
+
* prompt is a guess (the latest SendMessage to the agent), and the
|
|
185
|
+
* `task_started` or the SendMessage result carries the delegation.
|
|
146
186
|
*/
|
|
147
187
|
async taskResumed(taskId, deliver, prompt) {
|
|
148
188
|
if (!this.enabled)
|
|
@@ -153,6 +193,10 @@ export class NativeSubagentRuntime {
|
|
|
153
193
|
const finishing = this.taskFinishPromises.get(taskId) ?? previous.terminalPromise;
|
|
154
194
|
if (finishing)
|
|
155
195
|
await finishing.catch(() => { });
|
|
196
|
+
if (this.form === "rfd") {
|
|
197
|
+
await this.resumeChild(previous, taskId, undefined, undefined, deliver);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
156
200
|
if (this.children.get(taskId) !== previous || previous.terminalState === undefined)
|
|
157
201
|
return;
|
|
158
202
|
await this.openGeneration(taskId, previous, {
|
|
@@ -163,26 +207,97 @@ export class NativeSubagentRuntime {
|
|
|
163
207
|
...promptField(promptText(prompt)),
|
|
164
208
|
}, true, deliver);
|
|
165
209
|
}
|
|
166
|
-
|
|
210
|
+
/**
|
|
211
|
+
* The SDK reports in a SendMessage result that the call `toolUseId`, with
|
|
212
|
+
* the message `prompt`, resumed the agent `taskId`. AIR's draft treats it as
|
|
213
|
+
* any resume signal. In the RFD form, the call is the delegation, which the
|
|
214
|
+
* child takes unless its run already ended: the result can arrive after it.
|
|
215
|
+
*/
|
|
216
|
+
async sendMessageResumed(taskId, deliver, prompt, toolUseId) {
|
|
217
|
+
if (this.form !== "rfd")
|
|
218
|
+
return this.taskResumed(taskId, deliver, prompt);
|
|
219
|
+
const child = this.children.get(taskId);
|
|
220
|
+
if (!child)
|
|
221
|
+
return;
|
|
222
|
+
const finishing = this.taskFinishPromises.get(taskId);
|
|
223
|
+
if (finishing)
|
|
224
|
+
await finishing.catch(() => { });
|
|
225
|
+
if (this.isEndedDelegation(taskId, toolUseId))
|
|
226
|
+
return;
|
|
227
|
+
await this.resumeChild(child, taskId, promptText(prompt), toolUseId, deliver);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The RFD form: whether the run of the call `toolUseId` that delegated to
|
|
231
|
+
* the child `taskId` already ended. AIR's draft has no such runs.
|
|
232
|
+
*/
|
|
233
|
+
isEndedDelegation(taskId, toolUseId) {
|
|
234
|
+
if (this.form !== "rfd" || !toolUseId)
|
|
235
|
+
return false;
|
|
236
|
+
return this.children.get(taskId)?.endedRuns?.has(toolUseId) ?? false;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The RFD form: whether `toolUseId` is an earlier delegation of the child
|
|
240
|
+
* `taskId`, whose end does not end the current one. AIR's draft has no
|
|
241
|
+
* such delegations.
|
|
242
|
+
*/
|
|
243
|
+
isStaleEnd(taskId, toolUseId) {
|
|
244
|
+
if (this.form !== "rfd" || !toolUseId)
|
|
245
|
+
return false;
|
|
246
|
+
const child = this.children.get(taskId);
|
|
247
|
+
return (!!child &&
|
|
248
|
+
child.parentToolUseId !== undefined &&
|
|
249
|
+
toolUseId !== child.parentToolUseId &&
|
|
250
|
+
this.childByParentToolUse.get(toolUseId) === child);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Reports that the SDK ended the task's work. `end` has what the SDK says
|
|
254
|
+
* about it: its text for a failure, and its reason for an unusual end.
|
|
255
|
+
*/
|
|
256
|
+
async finishTask(taskId, status, deliver, toolUseId, end) {
|
|
167
257
|
if (!this.enabled)
|
|
168
258
|
return;
|
|
169
259
|
const state = nativeSubagentState(status);
|
|
170
260
|
const child = toolUseId ? this.childByParentToolUse.get(toolUseId) : this.children.get(taskId);
|
|
171
261
|
if (child && toolUseId && this.taskByToolUse.get(toolUseId) !== taskId)
|
|
172
262
|
return;
|
|
173
|
-
if (!state || !child
|
|
263
|
+
if (!state || !child)
|
|
174
264
|
return;
|
|
265
|
+
if (this.form === "rfd") {
|
|
266
|
+
if (this.isStaleEnd(taskId, toolUseId))
|
|
267
|
+
return;
|
|
268
|
+
if (!child.announced && !child.announcePromise) {
|
|
269
|
+
this.withdraw(taskId, child);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
175
273
|
const existing = this.taskFinishPromises.get(taskId);
|
|
176
|
-
if (existing)
|
|
177
|
-
|
|
274
|
+
if (existing) {
|
|
275
|
+
await existing;
|
|
276
|
+
if (this.form === "rfd")
|
|
277
|
+
await this.supplyError(child, state, end?.error);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (this.form === "air" ? child.terminalState !== undefined : child.workState === "idle") {
|
|
281
|
+
if (this.form === "rfd")
|
|
282
|
+
await this.supplyError(child, state, end?.error);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
178
285
|
const finish = Promise.resolve().then(async () => {
|
|
179
286
|
try {
|
|
180
|
-
await
|
|
287
|
+
await this.announce(child);
|
|
181
288
|
if (child.parentToolUseId) {
|
|
182
289
|
for (const pending of this.takePending(child.parentToolUseId))
|
|
183
290
|
await deliver(pending);
|
|
184
291
|
}
|
|
185
|
-
|
|
292
|
+
if (this.form === "rfd") {
|
|
293
|
+
await this.reportState(child, rfdEndState(state, end));
|
|
294
|
+
child.missingError = state === "failed" && !end?.error && !end?.unexplained;
|
|
295
|
+
if (child.parentToolUseId)
|
|
296
|
+
(child.endedRuns ??= new Set()).add(child.parentToolUseId);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
await finishNativeSubagent(this.session, taskId, state, this.publish);
|
|
300
|
+
}
|
|
186
301
|
}
|
|
187
302
|
finally {
|
|
188
303
|
if (child.parentToolUseId) {
|
|
@@ -199,12 +314,20 @@ export class NativeSubagentRuntime {
|
|
|
199
314
|
this.taskFinishPromises.delete(taskId);
|
|
200
315
|
}
|
|
201
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Ends every child that still runs, because the stream ended or the
|
|
319
|
+
* conversation was reset. AIR's draft reports `state` for each. In the RFD
|
|
320
|
+
* form the cause of each child's end is unknown, so it is idle with no stop
|
|
321
|
+
* reason: a child that a cancel stopped already reported it from the SDK's
|
|
322
|
+
* events, and a background child that outlived the cancel was not cancelled.
|
|
323
|
+
*/
|
|
202
324
|
async finishAll(state, deliver) {
|
|
203
325
|
const errors = [];
|
|
326
|
+
const end = this.form === "rfd" ? { unexplained: true } : undefined;
|
|
204
327
|
try {
|
|
205
328
|
for (const taskId of [...this.children.keys()].reverse()) {
|
|
206
329
|
try {
|
|
207
|
-
await this.finishTask(taskId, state, deliver);
|
|
330
|
+
await this.finishTask(taskId, state, deliver, undefined, end);
|
|
208
331
|
}
|
|
209
332
|
catch (error) {
|
|
210
333
|
errors.push(error);
|
|
@@ -223,15 +346,120 @@ export class NativeSubagentRuntime {
|
|
|
223
346
|
if (errors.length > 1)
|
|
224
347
|
throw new AggregateError(errors, "Failed to finish native subagents");
|
|
225
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* The parent session got `session/cancel`. AIR's draft ends every child as
|
|
351
|
+
* cancelled here. The RFD form reports nothing: acknowledging a cancel is no
|
|
352
|
+
* evidence that a child stopped. Claude Code's interrupt stops the
|
|
353
|
+
* foreground children that the turn waits on and, currently, background
|
|
354
|
+
* children too, and the SDK's task events report each as cancelled. A child
|
|
355
|
+
* that keeps running keeps its open requests
|
|
356
|
+
* (agentclientprotocol/agent-client-protocol#2308).
|
|
357
|
+
*/
|
|
358
|
+
async parentCancelled(deliver) {
|
|
359
|
+
if (this.form === "air")
|
|
360
|
+
await this.finishAll("cancelled", deliver);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Runs `request`, a permission or question request of the session
|
|
364
|
+
* `sessionId`. In the RFD form, a child waiting on it is `requires_action`
|
|
365
|
+
* until its last open request ends.
|
|
366
|
+
*/
|
|
367
|
+
async awaitingUser(sessionId, request) {
|
|
368
|
+
const child = this.form === "rfd" ? this.childOfSession(sessionId) : undefined;
|
|
369
|
+
if (!child?.announced)
|
|
370
|
+
return request();
|
|
371
|
+
child.openRequests = (child.openRequests ?? 0) + 1;
|
|
372
|
+
if (child.openRequests === 1 && child.workState === "running") {
|
|
373
|
+
await this.reportState(child, { state: "requires_action" }).catch((error) => this.logger.log(`Failed to report that subagent ${child.sessionId} waits: ${error}`));
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
return await request();
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
child.openRequests = (child.openRequests ?? 1) - 1;
|
|
380
|
+
if (child.openRequests === 0 && child.workState === "requires_action") {
|
|
381
|
+
await this.reportState(child, { state: "running" }).catch((error) => this.logger.log(`Failed to report that subagent ${child.sessionId} runs: ${error}`));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
226
385
|
discardPending(parentToolUseId) {
|
|
227
386
|
this.takePending(parentToolUseId);
|
|
228
387
|
}
|
|
229
388
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
389
|
+
* The RFD form: the child runs again, and keeps its session. `toolUseId` is
|
|
390
|
+
* the call that delegated, usually a SendMessage: the SDK runs the resumed
|
|
391
|
+
* work under its id, so it becomes the child's current delegation, whose
|
|
392
|
+
* end ends the run, and its prompt is the message with that id. A child
|
|
393
|
+
* that is already working only takes the delegation; a prompt already in
|
|
394
|
+
* its transcript is not sent again. A child that was never announced is
|
|
395
|
+
* not changed.
|
|
396
|
+
*/
|
|
397
|
+
async resumeChild(child, taskId, prompt, toolUseId, deliver) {
|
|
398
|
+
if (!child.announced)
|
|
399
|
+
return;
|
|
400
|
+
if (toolUseId && toolUseId !== child.parentToolUseId) {
|
|
401
|
+
this.taskByToolUse.set(toolUseId, taskId);
|
|
402
|
+
this.childByParentToolUse.set(toolUseId, child);
|
|
403
|
+
child.parentToolUseId = toolUseId;
|
|
404
|
+
}
|
|
405
|
+
if (!isWorking(child)) {
|
|
406
|
+
await this.reportState(child, {
|
|
407
|
+
state: child.openRequests ? "requires_action" : "running",
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
if (prompt && toolUseId) {
|
|
411
|
+
await publishPrompt(child, prompt, toolUseId, this.senderByToolUse.get(toolUseId), this.publish);
|
|
412
|
+
}
|
|
413
|
+
for (const pending of toolUseId ? this.takePending(toolUseId) : [])
|
|
414
|
+
await deliver(pending);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The RFD form: a child that ends before its parent is known stays
|
|
418
|
+
* unexposed, since the RFD forbids guessing its parent. Its buffered updates
|
|
419
|
+
* are dropped, and it is forgotten, so a late frame does not announce it.
|
|
420
|
+
* (Buffering until that frame would also meet the RFD, but the frame comes
|
|
421
|
+
* before the task in the SDK's order, so a child ending without it is rare.)
|
|
422
|
+
*/
|
|
423
|
+
withdraw(taskId, child) {
|
|
424
|
+
this.children.delete(taskId);
|
|
425
|
+
if (child.parentToolUseId) {
|
|
426
|
+
this.takePending(child.parentToolUseId);
|
|
427
|
+
this.cleanupControl(child.parentToolUseId);
|
|
428
|
+
this.childByParentToolUse.delete(child.parentToolUseId);
|
|
429
|
+
this.taskByToolUse.delete(child.parentToolUseId);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The RFD form: a later event of a failure can carry the SDK's error text
|
|
434
|
+
* that the reported idle snapshot lacks.
|
|
435
|
+
*/
|
|
436
|
+
async supplyError(child, state, error) {
|
|
437
|
+
if (state !== "failed" || !error || !child.missingError)
|
|
438
|
+
return;
|
|
439
|
+
await this.reportState(child, rfdEndState("failed", { error }));
|
|
440
|
+
}
|
|
441
|
+
/** Announces `child` in the client's form. A runtime without one never does. */
|
|
442
|
+
async announce(child) {
|
|
443
|
+
if (this.form)
|
|
444
|
+
await announceNativeSubagent(child, this.publish, this.form);
|
|
445
|
+
}
|
|
446
|
+
/** The RFD form: publishes the work state of `child` on its parent. */
|
|
447
|
+
async reportState(child, state) {
|
|
448
|
+
child.workState = state.state;
|
|
449
|
+
child.missingError = false;
|
|
450
|
+
await this.publish({
|
|
451
|
+
sessionId: child.parentSessionId,
|
|
452
|
+
update: { sessionUpdate: "subagent_update", sessionId: child.sessionId, state },
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Routes an update to the child session. It never goes to the root session:
|
|
457
|
+
* in AIR's draft, an update of a finished child is dropped.
|
|
232
458
|
*/
|
|
233
459
|
toChild(child, notification, toolCallId) {
|
|
234
|
-
|
|
460
|
+
// AIR's draft ends a child for good. In the RFD form an idle child is
|
|
461
|
+
// reusable, and its background work can still report.
|
|
462
|
+
if (this.form === "air" && (child.terminalState !== undefined || child.terminalPromise)) {
|
|
235
463
|
this.logger.log(`Session ${this.rootSessionId}: ignoring late update for terminal subagent ${child.sessionId}`);
|
|
236
464
|
return null;
|
|
237
465
|
}
|
|
@@ -245,10 +473,11 @@ export class NativeSubagentRuntime {
|
|
|
245
473
|
* tool call when the work started. An async task can outlive that child: a
|
|
246
474
|
* subagent can start a background command and end its turn. So the route
|
|
247
475
|
* still sends `async_task_spawned`, `async_task_progress` and
|
|
248
|
-
* `async_task_state_update` to the child generation after it finished.
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
476
|
+
* `async_task_state_update` to the child generation after it finished. In
|
|
477
|
+
* AIR's draft, the route drops every other update after the child finished;
|
|
478
|
+
* in the RFD form, an idle child gets them all. `undefined` means that the
|
|
479
|
+
* root session owns the tool call. `eagerSessionId` is the session where a
|
|
480
|
+
* permission request created the tool call before the stream routed it.
|
|
252
481
|
*/
|
|
253
482
|
routeOfToolCall(toolCallId, eagerSessionId) {
|
|
254
483
|
if (!this.enabled)
|
|
@@ -260,6 +489,25 @@ export class NativeSubagentRuntime {
|
|
|
260
489
|
? { ...notification, sessionId: owner.sessionId }
|
|
261
490
|
: this.toChild(owner, notification, undefined)));
|
|
262
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* The RFD form: remembers the session that made a SendMessage call, the
|
|
494
|
+
* sender of the prompt when the call resumes a child.
|
|
495
|
+
*/
|
|
496
|
+
rememberSendMessage(notification) {
|
|
497
|
+
const { update } = notification;
|
|
498
|
+
if (update.sessionUpdate !== "tool_call")
|
|
499
|
+
return;
|
|
500
|
+
const claudeMeta = update._meta?.claudeCode;
|
|
501
|
+
if (claudeMeta?.toolName !== "SendMessage")
|
|
502
|
+
return;
|
|
503
|
+
this.senderByToolUse.delete(update.toolCallId);
|
|
504
|
+
this.senderByToolUse.set(update.toolCallId, notification.sessionId);
|
|
505
|
+
if (this.senderByToolUse.size > MAX_CHILD_TOOL_CALLS) {
|
|
506
|
+
const oldest = this.senderByToolUse.keys().next().value;
|
|
507
|
+
if (oldest !== undefined)
|
|
508
|
+
this.senderByToolUse.delete(oldest);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
263
511
|
rememberToolCallOwner(toolCallId, child) {
|
|
264
512
|
this.childByToolCall.delete(toolCallId);
|
|
265
513
|
this.childByToolCall.set(toolCallId, child);
|
|
@@ -288,6 +536,7 @@ export class NativeSubagentRuntime {
|
|
|
288
536
|
this.controlByToolUse.clear();
|
|
289
537
|
this.childByParentToolUse.clear();
|
|
290
538
|
this.taskFinishPromises.clear();
|
|
539
|
+
this.senderByToolUse.clear();
|
|
291
540
|
this.generationByTaskId.clear();
|
|
292
541
|
this.pending.clear();
|
|
293
542
|
this.pendingCount = 0;
|
|
@@ -353,10 +602,20 @@ export class NativeSubagentRuntime {
|
|
|
353
602
|
}
|
|
354
603
|
if (!announce)
|
|
355
604
|
return;
|
|
356
|
-
await
|
|
605
|
+
await this.announce(child);
|
|
357
606
|
for (const pending of toolUseId ? this.takePending(toolUseId) : [])
|
|
358
607
|
await deliver(pending);
|
|
359
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* The ACP session id of a child: the SDK agent id, and in AIR's draft a
|
|
611
|
+
* generation suffix for each resume. The RFD needs it unique within the
|
|
612
|
+
* connection and never under two parents. Agent ids are random, and an
|
|
613
|
+
* agent keeps its parent, so only a fork could repeat one under another
|
|
614
|
+
* root. That holds today because the SDK's `forkSession` copies no
|
|
615
|
+
* `subagents/` history, so a fork cannot resume an agent of the original.
|
|
616
|
+
* If a fork ever can, the RFD requires remapping the copied ids, for
|
|
617
|
+
* example by deriving the child id from the root session id.
|
|
618
|
+
*/
|
|
360
619
|
nextChildSessionId(taskId, previous) {
|
|
361
620
|
if (!previous) {
|
|
362
621
|
this.generationByTaskId.set(taskId, 1);
|
|
@@ -367,12 +626,37 @@ export class NativeSubagentRuntime {
|
|
|
367
626
|
return `${taskId}:generation:${generation}`;
|
|
368
627
|
}
|
|
369
628
|
}
|
|
370
|
-
|
|
629
|
+
/**
|
|
630
|
+
* Announces the child on its parent, once, before any update of the child.
|
|
631
|
+
* The RFD form announces it as running and then sends the prompt as the
|
|
632
|
+
* first message of the child. The child gets no capabilities, so the client
|
|
633
|
+
* may not cancel or otherwise change it.
|
|
634
|
+
*/
|
|
635
|
+
export async function announceNativeSubagent(child, publish, form) {
|
|
371
636
|
if (child.announced)
|
|
372
637
|
return;
|
|
373
638
|
if (child.announcePromise)
|
|
374
639
|
return child.announcePromise;
|
|
375
640
|
const announce = Promise.resolve().then(async () => {
|
|
641
|
+
if (form === "rfd") {
|
|
642
|
+
child.workState = "running";
|
|
643
|
+
await publish({
|
|
644
|
+
sessionId: child.parentSessionId,
|
|
645
|
+
update: {
|
|
646
|
+
sessionUpdate: "subagent_update",
|
|
647
|
+
sessionId: child.sessionId,
|
|
648
|
+
...(child.title ? { title: child.title } : {}),
|
|
649
|
+
...(child.description ? { description: child.description } : {}),
|
|
650
|
+
state: { state: "running" },
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
child.announced = true;
|
|
654
|
+
// The immediate parent made the Agent/Task call that launched the child.
|
|
655
|
+
if (child.prompt) {
|
|
656
|
+
await publishPrompt(child, child.prompt, child.parentToolUseId, child.parentSessionId, publish);
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
376
660
|
await publish({
|
|
377
661
|
sessionId: child.parentSessionId,
|
|
378
662
|
update: {
|
|
@@ -402,7 +686,7 @@ export async function finishNativeSubagent(session, taskId, state, publish) {
|
|
|
402
686
|
if (child.terminalPromise)
|
|
403
687
|
return child.terminalPromise;
|
|
404
688
|
const finish = Promise.resolve().then(async () => {
|
|
405
|
-
await announceNativeSubagent(child, publish);
|
|
689
|
+
await announceNativeSubagent(child, publish, "air");
|
|
406
690
|
await publish({
|
|
407
691
|
sessionId: child.parentSessionId,
|
|
408
692
|
update: {
|
|
@@ -422,6 +706,85 @@ export async function finishNativeSubagent(session, taskId, state, publish) {
|
|
|
422
706
|
child.terminalPromise = undefined;
|
|
423
707
|
}
|
|
424
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* The RFD form: a message to the child, in the child's transcript (its
|
|
711
|
+
* incoming view). Its id is the id of the tool use that sent it, the
|
|
712
|
+
* Agent/Task call or a SendMessage, so a prompt whose id is already in the
|
|
713
|
+
* transcript is the same delegation and is not sent again. `sender` is the
|
|
714
|
+
* session that made that call, when known.
|
|
715
|
+
*/
|
|
716
|
+
async function publishPrompt(child, prompt, toolUseId, sender, publish) {
|
|
717
|
+
child.promptIds ??= new Set();
|
|
718
|
+
const messageId = toolUseId ?? `${child.sessionId}:prompt:${child.promptIds.size + 1}`;
|
|
719
|
+
if (child.promptIds.has(messageId))
|
|
720
|
+
return;
|
|
721
|
+
child.promptIds.add(messageId);
|
|
722
|
+
await publish({
|
|
723
|
+
sessionId: child.sessionId,
|
|
724
|
+
update: {
|
|
725
|
+
sessionUpdate: "session_message",
|
|
726
|
+
messageId,
|
|
727
|
+
...(sender ? { senderSessionId: sender } : {}),
|
|
728
|
+
recipientSessionId: child.sessionId,
|
|
729
|
+
content: [{ type: "text", text: prompt }],
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/** The RFD form: whether the child's foreground work is in progress. */
|
|
734
|
+
function isWorking(child) {
|
|
735
|
+
return child.workState === "running" || child.workState === "requires_action";
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* The RFD form: the work state at the end of a child's work.
|
|
739
|
+
*
|
|
740
|
+
* A failure is idle with the `error` stop reason and, when the SDK gives its
|
|
741
|
+
* text, the JSON-RPC `error`, as the RFD asks: a v1 child has no prompt
|
|
742
|
+
* response, so this is how a client learns that its work failed. The error is
|
|
743
|
+
* an internal error, as for a failed turn on the v2 surface.
|
|
744
|
+
*
|
|
745
|
+
* A task that a worker restart orphaned, or that was still running when the
|
|
746
|
+
* stream ended, is idle with no stop reason: its work ended, but the adapter
|
|
747
|
+
* did not see a cancellation or a failure, which the RFD forbids claiming.
|
|
748
|
+
*/
|
|
749
|
+
export function rfdEndState(state, end) {
|
|
750
|
+
if (end?.unexplained || end?.reason === "worker_restart")
|
|
751
|
+
return { state: "idle" };
|
|
752
|
+
switch (state) {
|
|
753
|
+
case "completed":
|
|
754
|
+
return { state: "idle", stopReason: "end_turn" };
|
|
755
|
+
case "cancelled":
|
|
756
|
+
return { state: "idle", stopReason: "cancelled" };
|
|
757
|
+
case "failed":
|
|
758
|
+
return {
|
|
759
|
+
state: "idle",
|
|
760
|
+
stopReason: "error",
|
|
761
|
+
...(end?.error
|
|
762
|
+
? { error: { code: RequestError.internalError().code, message: end.error } }
|
|
763
|
+
: {}),
|
|
764
|
+
};
|
|
765
|
+
default:
|
|
766
|
+
// "disconnected": the child can no longer be observed.
|
|
767
|
+
return { state: "unknown" };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* The RFD form: the parent's labels for a child. The title is the agent's
|
|
772
|
+
* name, with its role after it when both are known, else the role. The
|
|
773
|
+
* description is the task's short description, never the prompt.
|
|
774
|
+
*/
|
|
775
|
+
export function rfdLabels(name, role, description) {
|
|
776
|
+
const agentName = nonBlankString(name);
|
|
777
|
+
const agentRole = nonBlankString(role);
|
|
778
|
+
const title = agentName && agentRole && agentName !== agentRole
|
|
779
|
+
? `${agentName} (${agentRole})`
|
|
780
|
+
: (agentName ?? agentRole);
|
|
781
|
+
const text = nonBlankString(description);
|
|
782
|
+
return {
|
|
783
|
+
...(title ? { title } : {}),
|
|
784
|
+
...(agentRole ? { role: agentRole } : {}),
|
|
785
|
+
...(text ? { description: text } : {}),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
425
788
|
/**
|
|
426
789
|
* The agent id that a successful SendMessage result resumed. The SDK puts it
|
|
427
790
|
* in `tool_use_result.resumedAgentId` when a finished agent runs again.
|
|
@@ -438,22 +801,34 @@ export function resumedNativeSubagentId(toolUseResult) {
|
|
|
438
801
|
* carried the resume. Otherwise the latest SendMessage call to `agentId` counts.
|
|
439
802
|
*/
|
|
440
803
|
export function sendMessageResumePrompt(toolUses, agentId, resultToolUseIds = []) {
|
|
804
|
+
return sendMessageResume(toolUses, agentId, resultToolUseIds)?.text;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* The SendMessage call that resumed the agent `agentId`, and its text when it
|
|
808
|
+
* has one; see {@link sendMessageResumePrompt}. The call is known even
|
|
809
|
+
* without text, so a late result still names the delegation it reports.
|
|
810
|
+
*/
|
|
811
|
+
export function sendMessageResume(toolUses, agentId, resultToolUseIds = []) {
|
|
812
|
+
let withoutText;
|
|
441
813
|
for (const toolUseId of resultToolUseIds) {
|
|
442
814
|
const toolUse = toolUses[toolUseId];
|
|
443
815
|
if (toolUse?.name !== "SendMessage")
|
|
444
816
|
continue;
|
|
445
817
|
const text = promptText(toolUse.input?.message);
|
|
446
818
|
if (text)
|
|
447
|
-
return text;
|
|
819
|
+
return { toolUseId, text };
|
|
820
|
+
withoutText ??= toolUseId;
|
|
448
821
|
}
|
|
449
822
|
if (resultToolUseIds.length > 0)
|
|
450
|
-
return undefined;
|
|
451
|
-
for (const toolUse of Object.
|
|
823
|
+
return withoutText ? { toolUseId: withoutText } : undefined;
|
|
824
|
+
for (const [toolUseId, toolUse] of Object.entries(toolUses).reverse()) {
|
|
452
825
|
if (toolUse?.name !== "SendMessage")
|
|
453
826
|
continue;
|
|
454
827
|
const input = toolUse.input;
|
|
455
|
-
if (input?.to
|
|
456
|
-
|
|
828
|
+
if (input?.to !== agentId)
|
|
829
|
+
continue;
|
|
830
|
+
const text = promptText(input.message);
|
|
831
|
+
return text ? { toolUseId, text } : { toolUseId };
|
|
457
832
|
}
|
|
458
833
|
return undefined;
|
|
459
834
|
}
|
|
@@ -596,6 +971,10 @@ function applySubagentIdentity(child, identity) {
|
|
|
596
971
|
child.task = subagentDescription(identity.prompt, identity.description);
|
|
597
972
|
}
|
|
598
973
|
child.prompt ??= identity.prompt;
|
|
974
|
+
const labels = rfdLabels(identity.name, identity.subagentType ?? child.role, identity.description);
|
|
975
|
+
child.title = labels.title ?? child.title;
|
|
976
|
+
child.role = labels.role ?? child.role;
|
|
977
|
+
child.description = labels.description ?? child.description;
|
|
599
978
|
}
|
|
600
979
|
/** The prompt text unchanged, or `undefined` when it is not a non-blank string. */
|
|
601
980
|
function promptText(value) {
|
package/dist/settings.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare class SettingsManager {
|
|
|
19
19
|
private cwd;
|
|
20
20
|
private effective;
|
|
21
21
|
private managedDeniedModels;
|
|
22
|
+
private managedApiKeyHelper?;
|
|
22
23
|
private watchers;
|
|
23
24
|
private onChange?;
|
|
24
25
|
private logger;
|
|
@@ -53,6 +54,11 @@ export declare class SettingsManager {
|
|
|
53
54
|
* Returns the current merged settings
|
|
54
55
|
*/
|
|
55
56
|
getSettings(): Settings;
|
|
57
|
+
/**
|
|
58
|
+
* The `apiKeyHelper` the CLI runs: managed settings outrank the `--settings`
|
|
59
|
+
* override, which outranks the user/project/local files.
|
|
60
|
+
*/
|
|
61
|
+
getApiKeyHelper(settingsOverride?: Settings): string | undefined;
|
|
56
62
|
/**
|
|
57
63
|
* `deniedModels` entries from the managed tier, the only tier the CLI
|
|
58
64
|
* honors them from.
|