@xrkseek/harness 0.5.9 → 0.5.10
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/node_modules/@xrkseek/core-agent-loop/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/index.js +4 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/llm-retry.d.ts +0 -2
- package/node_modules/@xrkseek/core-agent-loop/dist/llm-retry.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/llm-retry.js +7 -3
- package/node_modules/@xrkseek/core-agent-loop/dist/llm-retry.js.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.d.ts +5 -0
- package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.js +1 -0
- package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.js.map +1 -1
- package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.js +9 -3
- package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.js.map +1 -1
- package/node_modules/@xrkseek/exec-fs/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-fs/dist/index.js +3 -1
- package/node_modules/@xrkseek/exec-fs/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/exec-web/dist/tools.d.ts +1 -1
- package/node_modules/@xrkseek/exec-web/dist/tools.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-web/dist/tools.js +10 -0
- package/node_modules/@xrkseek/exec-web/dist/tools.js.map +1 -1
- package/node_modules/@xrkseek/llm/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/llm/dist/index.js +8 -2
- package/node_modules/@xrkseek/llm/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/preset-harness/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@xrkseek/protocol/dist/session-events.d.ts +5 -0
- package/node_modules/@xrkseek/protocol/dist/session-events.d.ts.map +1 -1
- package/node_modules/@xrkseek/protocol/dist/session-events.js.map +1 -1
- package/node_modules/@xrkseek/protocol/dist/validate.d.ts.map +1 -1
- package/node_modules/@xrkseek/protocol/dist/validate.js +5 -0
- package/node_modules/@xrkseek/protocol/dist/validate.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/wire-event.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/wire-event.js +29 -2
- package/node_modules/@xrkseek/server-face/dist/adapt/wire-event.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/agent-team-roles.d.ts +40 -0
- package/node_modules/@xrkseek/server-face/dist/agent-team-roles.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/agent-team-roles.js +89 -1
- package/node_modules/@xrkseek/server-face/dist/agent-team-roles.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/agent-team-tasks.d.ts +5 -0
- package/node_modules/@xrkseek/server-face/dist/agent-team-tasks.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/agent-team-tasks.js +24 -0
- package/node_modules/@xrkseek/server-face/dist/agent-team-tasks.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/context.d.ts +17 -4
- package/node_modules/@xrkseek/server-face/dist/context.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/handlers/job.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/handlers/job.js +1 -0
- package/node_modules/@xrkseek/server-face/dist/handlers/job.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/handlers/session.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/handlers/session.js +46 -16
- package/node_modules/@xrkseek/server-face/dist/handlers/session.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/handlers/subagent.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/handlers/subagent.js +15 -0
- package/node_modules/@xrkseek/server-face/dist/handlers/subagent.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/index.d.ts +2 -2
- package/node_modules/@xrkseek/server-face/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/index.js +1 -1
- package/node_modules/@xrkseek/server-face/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/model-catalog.d.ts +6 -0
- package/node_modules/@xrkseek/server-face/dist/model-catalog.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/model-catalog.js +19 -0
- package/node_modules/@xrkseek/server-face/dist/model-catalog.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/ralph-tool.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/ralph-tool.js +15 -12
- package/node_modules/@xrkseek/server-face/dist/ralph-tool.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/runtime.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/runtime.js +77 -14
- package/node_modules/@xrkseek/server-face/dist/runtime.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-cwd.d.ts +12 -0
- package/node_modules/@xrkseek/server-face/dist/session-cwd.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-cwd.js +14 -0
- package/node_modules/@xrkseek/server-face/dist/session-cwd.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-status.d.ts +3 -1
- package/node_modules/@xrkseek/server-face/dist/session-status.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-status.js +28 -4
- package/node_modules/@xrkseek/server-face/dist/session-status.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/settings-agent-tools.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/settings-agent-tools.js +13 -1
- package/node_modules/@xrkseek/server-face/dist/settings-agent-tools.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/settings-schemas.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/settings-schemas.js +12 -0
- package/node_modules/@xrkseek/server-face/dist/settings-schemas.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/subagent-registry.d.ts +27 -0
- package/node_modules/@xrkseek/server-face/dist/subagent-registry.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/subagent-registry.js +69 -0
- package/node_modules/@xrkseek/server-face/dist/subagent-registry.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/subagent-tools.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/subagent-tools.js +281 -55
- package/node_modules/@xrkseek/server-face/dist/subagent-tools.js.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/index.js +4 -14
- package/node_modules/@xrkseek/server-host/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,15 +4,16 @@ import { dispatchFaceMethod } from "./dispatch.js";
|
|
|
4
4
|
import { lastAssistantBodyText } from "./adapt/subagent-notice.js";
|
|
5
5
|
import { boundChildAnswer, SUBAGENT_ANSWER_INLINE_BYTES, } from "./adapt/subagent-answer-bound.js";
|
|
6
6
|
export { boundChildAnswer, SUBAGENT_ANSWER_INLINE_BYTES };
|
|
7
|
+
import { subagentOwnedEvents } from "./subagent-registry.js";
|
|
7
8
|
import { DEFAULT_MAX_ACTIVE_CHILDREN, DEFAULT_MAX_DEPTH, resolveAgentPresetProfile, } from "./presets-catalog.js";
|
|
8
9
|
import { effectiveSessionAgentPreset } from "./session-agent-preset.js";
|
|
9
10
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
10
11
|
import { ExternalAgentError, parseExternalAgentKind, parseExternalAgentProduct, runExternalAgentTurn, supportsExternalContinuable, startExternalContinuable, isChildSessionActive, } from "./external-agent-runtime.js";
|
|
11
12
|
import { createSubagentWorktree, finalizeSubagentWorktree, formatWorktreeResult, worktreeContextNote, worktreeSkippedForRemote, } from "./subagent-worktree.js";
|
|
12
|
-
import { resolveSessionModelSelection } from "./model-catalog.js";
|
|
13
|
+
import { resolveSessionModelSelection, resolveSubagentModelSetting, } from "./model-catalog.js";
|
|
13
14
|
import { selectSessionModel } from "./select-session-model.js";
|
|
14
15
|
import { appendOutputContract, buildOutputSchemaRetryMessage, coerceOutputSchema, validateOutputAgainstSchema, } from "./agent-team-output.js";
|
|
15
|
-
import {
|
|
16
|
+
import { applySubagentSpawnPreamble, parseAgentTeamSpawnRole, rootUserAuthorizationBlock, } from "./agent-team-roles.js";
|
|
16
17
|
import { isAgentTeamRole, } from "./agent-team-graph.js";
|
|
17
18
|
import { bindRalphTool } from "./ralph-tool.js";
|
|
18
19
|
const FOREGROUND_WAIT_MS = 10 * 60 * 1000;
|
|
@@ -106,7 +107,13 @@ async function waitDrainIdle(runtime, sessionId, signal, timeoutMs = FOREGROUND_
|
|
|
106
107
|
// for its child would otherwise hang the tool batch forever. The join
|
|
107
108
|
// stays attached in the background — the caller cancels the child right
|
|
108
109
|
// after this rejects, which is what actually drains it.
|
|
109
|
-
|
|
110
|
+
//
|
|
111
|
+
// The race must also carry the timeout: latch.run() joins until the
|
|
112
|
+
// child's chain *actually* settles, so a child stuck on a tool that
|
|
113
|
+
// ignores abort pins the parent's turn for as long as it hangs. The
|
|
114
|
+
// timeout used to apply only to the polling fallback below — i.e. never
|
|
115
|
+
// on the Host path, which always has run().
|
|
116
|
+
await raceDeadline(abortable(run(sessionId), signal), timeoutMs, sessionId);
|
|
110
117
|
return;
|
|
111
118
|
}
|
|
112
119
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -120,6 +127,26 @@ async function waitDrainIdle(runtime, sessionId, signal, timeoutMs = FOREGROUND_
|
|
|
120
127
|
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
121
128
|
}
|
|
122
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Reject once `timeoutMs` elapses, naming the session so the failure reads
|
|
132
|
+
* like the polling branch's. The wrapped promise keeps running — the caller
|
|
133
|
+
* cancels the child right after, which is what actually drains it.
|
|
134
|
+
*/
|
|
135
|
+
function raceDeadline(promise, timeoutMs, sessionId) {
|
|
136
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
137
|
+
return promise;
|
|
138
|
+
let timer;
|
|
139
|
+
const timedOut = new Promise((_, reject) => {
|
|
140
|
+
timer = setTimeout(() => reject(new Error(`subagent timed out waiting for session ${sessionId}`)), timeoutMs);
|
|
141
|
+
});
|
|
142
|
+
return Promise.race([
|
|
143
|
+
promise.finally(() => {
|
|
144
|
+
if (timer !== undefined)
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}),
|
|
147
|
+
timedOut,
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
123
150
|
/** Reject as soon as `signal` aborts; the wrapped promise keeps running. */
|
|
124
151
|
function abortable(promise, signal) {
|
|
125
152
|
if (!signal)
|
|
@@ -193,29 +220,88 @@ function countActiveChildren(runtime, parentSessionId) {
|
|
|
193
220
|
}
|
|
194
221
|
return n;
|
|
195
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Undo a half-made spawn. `session.create` / `session.fork` already made the
|
|
225
|
+
* child durable and registered its link, so any failure between there and a
|
|
226
|
+
* successful `session.prompt` would otherwise leave an empty session in the
|
|
227
|
+
* sidebar and a permanent entry in the catalog / quota counts.
|
|
228
|
+
*/
|
|
229
|
+
async function discardUnstartedChild(runtime, childSessionId) {
|
|
230
|
+
runtime.subagents.detach(childSessionId);
|
|
231
|
+
runtime.agentTeams.removeNode(childSessionId);
|
|
232
|
+
runtime.agentTeamTasks.forgetByChild(childSessionId);
|
|
233
|
+
runtime.sessionCwds.delete(childSessionId);
|
|
234
|
+
try {
|
|
235
|
+
await dispatchFaceMethod(runtime, "session.cancel", `tool-sa-discard-${childSessionId}`, { sessionId: childSessionId, cascade: true });
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
/* best effort: the log is dropped either way */
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
runtime.store.delete?.(childSessionId);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
/* store without delete / already gone */
|
|
245
|
+
}
|
|
246
|
+
await Promise.resolve(runtime.invalidateAgent?.(childSessionId)).catch(() => undefined);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Reject a caller-supplied `task_id` another child already owns. Checked
|
|
250
|
+
* *before* the child exists: rebinding the card would misdirect that child's
|
|
251
|
+
* later idle notice (`completeByChild` keys on childSessionId).
|
|
252
|
+
*/
|
|
253
|
+
function taskIdConflict(runtime, taskIdHint) {
|
|
254
|
+
if (!taskIdHint)
|
|
255
|
+
return undefined;
|
|
256
|
+
const boundChild = runtime.agentTeamTasks.get(taskIdHint)?.childSessionId;
|
|
257
|
+
return boundChild
|
|
258
|
+
? `subagent: task_id ${taskIdHint} is already bound to child ${boundChild}; use a different task_id`
|
|
259
|
+
: undefined;
|
|
260
|
+
}
|
|
196
261
|
/**
|
|
197
262
|
* Optional LLM target for a freshly created child.
|
|
198
|
-
*
|
|
263
|
+
*
|
|
264
|
+
* Precedence: the call's own args → the fleet-wide `agent-loop.subagentModel`
|
|
265
|
+
* setting → the parent's own route. `model` alone inherits the parent's current
|
|
266
|
+
* provider; `provider` alone is rejected.
|
|
199
267
|
*/
|
|
200
268
|
async function applySubagentModelOverride(runtime, args) {
|
|
201
269
|
const providerRaw = args.provider?.trim() ?? "";
|
|
202
270
|
const modelRaw = args.model?.trim() ?? "";
|
|
203
271
|
const effortRaw = args.reasoningEffort?.trim() ?? "";
|
|
204
|
-
|
|
272
|
+
// Refuse loudly rather than silently: an explicit model on an external
|
|
273
|
+
// child would otherwise read as honored when the subprocess ignores it.
|
|
274
|
+
if (args.skipExternal && (providerRaw || modelRaw || effortRaw)) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
message: "subagent: model / provider / reasoning_effort do not apply to an external runtime " +
|
|
278
|
+
"(acp / app-server / claude-code) — the child CLI picks its own model. " +
|
|
279
|
+
"Drop the argument, or use runtime=in-process.",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// No per-call pin: the setting owns the route, and an empty setting leaves
|
|
283
|
+
// the child on the parent's selection that `session.create` copied.
|
|
284
|
+
const fleet = providerRaw || modelRaw || effortRaw
|
|
285
|
+
? undefined
|
|
286
|
+
: resolveSubagentModelSetting(runtime);
|
|
287
|
+
if (!providerRaw && !modelRaw && !effortRaw && !fleet)
|
|
205
288
|
return { ok: true };
|
|
206
|
-
|
|
289
|
+
const model = modelRaw || fleet?.model || "";
|
|
290
|
+
const effort = effortRaw || fleet?.reasoningEffort || "";
|
|
291
|
+
if (!model) {
|
|
207
292
|
return {
|
|
208
293
|
ok: false,
|
|
209
294
|
message: "subagent: model is required when overriding provider or reasoning_effort",
|
|
210
295
|
};
|
|
211
296
|
}
|
|
212
297
|
const provider = providerRaw
|
|
298
|
+
|| fleet?.provider
|
|
213
299
|
|| resolveSessionModelSelection(runtime, args.parentSessionId).provider;
|
|
214
300
|
const selected = await selectSessionModel(runtime, {
|
|
215
301
|
sessionId: args.childSessionId,
|
|
216
302
|
provider,
|
|
217
|
-
model
|
|
218
|
-
...(
|
|
303
|
+
model,
|
|
304
|
+
...(effort ? { reasoningEffort: effort } : {}),
|
|
219
305
|
});
|
|
220
306
|
if (!selected.ok) {
|
|
221
307
|
return {
|
|
@@ -226,17 +312,31 @@ async function applySubagentModelOverride(runtime, args) {
|
|
|
226
312
|
return { ok: true };
|
|
227
313
|
}
|
|
228
314
|
function createSubagentTool(options) {
|
|
229
|
-
|
|
230
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Ceilings for one call. Resolved live (not frozen at bind time) so the
|
|
317
|
+
* numbers `analytics` / `list_agents` report are the numbers spawn
|
|
318
|
+
* enforces — a Settings change takes effect on the next call instead of
|
|
319
|
+
* at the next agent rebuild. The bind options are a test-only override.
|
|
320
|
+
*/
|
|
321
|
+
const caps = () => {
|
|
322
|
+
const quota = resolveSubagentQuota(options.runtime, options.parentSessionId);
|
|
323
|
+
return {
|
|
324
|
+
maxDepth: options.maxDepth ?? quota.maxDepth,
|
|
325
|
+
maxActive: options.maxActiveChildren ?? quota.maxActive,
|
|
326
|
+
active: quota.active,
|
|
327
|
+
};
|
|
328
|
+
};
|
|
231
329
|
return {
|
|
232
330
|
name: "subagent",
|
|
233
|
-
description: "Delegate a self-contained task to a teammate subagent (separate session
|
|
331
|
+
description: "Delegate a self-contained task to a teammate subagent (separate session, same workspace as this session unless worktree:true). " +
|
|
234
332
|
"Use for focused independent work — research, a scoped implementation, analysis, or read-only review — " +
|
|
235
333
|
"so it does not consume this conversation's context. " +
|
|
236
|
-
"By default the child cannot see this chat — give a complete standalone prompt. " +
|
|
334
|
+
"By default the child cannot see this chat — give a complete standalone prompt (paths, goals, constraints, persona). " +
|
|
335
|
+
"Do not tell the child to read AGENTS.md to discover who it is; Face prepends parent/child session ids, mode, role, and cwd. " +
|
|
237
336
|
"Set inherit_context true to seed the child with this session's completed turns only " +
|
|
238
337
|
"(the current in-flight turn is excluded). " +
|
|
239
|
-
"By default waits for the result
|
|
338
|
+
"By default waits for the result (one-shot, no later human messages on that child). " +
|
|
339
|
+
"Set run_in_background true for a continuable child (chat companion / long task) and continue via followup_task / send_message. " +
|
|
240
340
|
"Optional role (worker|researcher|reviewer|lead) prefixes a role reminder; " +
|
|
241
341
|
"optional output_schema appends an OUTPUT CONTRACT and validates the final JSON; " +
|
|
242
342
|
"optional task_id / task_name register the work on the Agent Teams task board. " +
|
|
@@ -256,7 +356,10 @@ function createSubagentTool(options) {
|
|
|
256
356
|
},
|
|
257
357
|
prompt: {
|
|
258
358
|
type: "string",
|
|
259
|
-
description: "Task for the subagent.
|
|
359
|
+
description: "Task for the subagent. Put the actual job here (paths, goals, constraints, any persona). " +
|
|
360
|
+
"When inherit_context is false (default), the child does not see this conversation. " +
|
|
361
|
+
"When inherit_context is true, build on the seeded completed turns. " +
|
|
362
|
+
"Face already injects parent/child session ids — do not send the child to AGENTS.md to learn its role.",
|
|
260
363
|
},
|
|
261
364
|
inherit_context: {
|
|
262
365
|
type: "boolean",
|
|
@@ -361,20 +464,35 @@ function createSubagentTool(options) {
|
|
|
361
464
|
}
|
|
362
465
|
if (runtimeKind !== "in-process") {
|
|
363
466
|
const background = a.run_in_background === true;
|
|
467
|
+
// One cwd for every external branch. `resolveSessionCwd` already
|
|
468
|
+
// falls back to the workspace root, so the child runs where the
|
|
469
|
+
// parent runs instead of always at the root (which silently broke
|
|
470
|
+
// any session that had moved into another workspace).
|
|
471
|
+
const externalCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
|
|
364
472
|
if (background && !supportsExternalContinuable(runtimeKind)) {
|
|
365
473
|
return {
|
|
366
474
|
content: "subagent: run_in_background is not supported for claude-code (print); use acp or app-server",
|
|
367
475
|
isError: true,
|
|
368
476
|
};
|
|
369
477
|
}
|
|
478
|
+
if (a.worktree === true) {
|
|
479
|
+
// Worktree handling lives in the in-process branch below; silently
|
|
480
|
+
// dropping the flag would have the child edit the parent's own
|
|
481
|
+
// checkout while the caller believed it was isolated.
|
|
482
|
+
return {
|
|
483
|
+
content: "subagent: worktree is only supported for runtime=in-process " +
|
|
484
|
+
"(an external runtime owns its own working directory).",
|
|
485
|
+
isError: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
370
488
|
const depth = subagentDepth(options.runtime, options.parentSessionId);
|
|
489
|
+
const { maxDepth, maxActive: maxActiveChildren, active } = caps();
|
|
371
490
|
if (depth >= maxDepth) {
|
|
372
491
|
return {
|
|
373
492
|
content: `subagent: max depth ${maxDepth} reached (current depth ${depth})`,
|
|
374
493
|
isError: true,
|
|
375
494
|
};
|
|
376
495
|
}
|
|
377
|
-
const active = countActiveChildren(options.runtime, options.parentSessionId);
|
|
378
496
|
if (active >= maxActiveChildren) {
|
|
379
497
|
return {
|
|
380
498
|
content: `subagent: max active children ${maxActiveChildren} reached (active ${active})`,
|
|
@@ -407,11 +525,22 @@ function createSubagentTool(options) {
|
|
|
407
525
|
...(typeof a.reasoning_effort === "string"
|
|
408
526
|
? { reasoningEffort: a.reasoning_effort }
|
|
409
527
|
: {}),
|
|
528
|
+
// An ACP / app-server / claude-code child runs on ITS OWN CLI
|
|
529
|
+
// config; a Face-side pin cannot reach it. Recording one anyway
|
|
530
|
+
// made `/status` and `list_agents` report a route the subprocess
|
|
531
|
+
// never used, so the record is skipped rather than faked.
|
|
532
|
+
skipExternal: true,
|
|
410
533
|
});
|
|
411
534
|
if (!modelPin.ok) {
|
|
535
|
+
await discardUnstartedChild(options.runtime, childId);
|
|
412
536
|
return { content: modelPin.message, isError: true };
|
|
413
537
|
}
|
|
414
538
|
const taskTitle = String(a.task_name ?? "").trim() || label;
|
|
539
|
+
const taskIdConflictMsg = taskIdConflict(options.runtime, String(a.task_id ?? "").trim() || undefined);
|
|
540
|
+
if (taskIdConflictMsg) {
|
|
541
|
+
await discardUnstartedChild(options.runtime, childId);
|
|
542
|
+
return { content: taskIdConflictMsg, isError: true };
|
|
543
|
+
}
|
|
415
544
|
const task = options.runtime.agentTeamTasks.open({
|
|
416
545
|
parentSessionId: options.parentSessionId,
|
|
417
546
|
title: taskTitle,
|
|
@@ -430,7 +559,7 @@ function createSubagentTool(options) {
|
|
|
430
559
|
runtime: options.runtime,
|
|
431
560
|
faceSessionId: childId,
|
|
432
561
|
kind: runtimeKind,
|
|
433
|
-
cwd:
|
|
562
|
+
cwd: externalCwd,
|
|
434
563
|
prompt,
|
|
435
564
|
background: true,
|
|
436
565
|
...(signal ? { signal } : {}),
|
|
@@ -454,7 +583,7 @@ function createSubagentTool(options) {
|
|
|
454
583
|
}
|
|
455
584
|
const result = await runExternalAgentTurn({
|
|
456
585
|
kind: runtimeKind,
|
|
457
|
-
cwd:
|
|
586
|
+
cwd: externalCwd,
|
|
458
587
|
prompt,
|
|
459
588
|
...(signal ? { signal } : {}),
|
|
460
589
|
...(options.externalEnv ? { env: options.externalEnv } : {}),
|
|
@@ -477,13 +606,13 @@ function createSubagentTool(options) {
|
|
|
477
606
|
}
|
|
478
607
|
}
|
|
479
608
|
const depth = subagentDepth(options.runtime, options.parentSessionId);
|
|
609
|
+
const { maxDepth, maxActive: maxActiveChildren, active } = caps();
|
|
480
610
|
if (depth >= maxDepth) {
|
|
481
611
|
return {
|
|
482
612
|
content: `subagent: max depth ${maxDepth} reached (current depth ${depth})`,
|
|
483
613
|
isError: true,
|
|
484
614
|
};
|
|
485
615
|
}
|
|
486
|
-
const active = countActiveChildren(options.runtime, options.parentSessionId);
|
|
487
616
|
if (active >= maxActiveChildren) {
|
|
488
617
|
return {
|
|
489
618
|
content: `subagent: max active children ${maxActiveChildren} reached (active ${active})`,
|
|
@@ -497,6 +626,14 @@ function createSubagentTool(options) {
|
|
|
497
626
|
(background ? "subagent" : "subagent-task");
|
|
498
627
|
const linkMode = background ? "continuable" : "one-shot";
|
|
499
628
|
const parentCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
|
|
629
|
+
const taskTitle = String(a.task_name ?? "").trim() || label;
|
|
630
|
+
const taskIdHint = String(a.task_id ?? "").trim() || undefined;
|
|
631
|
+
// Before anything is allocated: reusing a task_id another child owns
|
|
632
|
+
// must not leave a worktree or a session behind.
|
|
633
|
+
const taskIdError = taskIdConflict(options.runtime, taskIdHint);
|
|
634
|
+
if (taskIdError) {
|
|
635
|
+
return { content: taskIdError, isError: true };
|
|
636
|
+
}
|
|
500
637
|
let isolated = null;
|
|
501
638
|
let managedLease = null;
|
|
502
639
|
let worktreeSkip = "";
|
|
@@ -528,15 +665,13 @@ function createSubagentTool(options) {
|
|
|
528
665
|
}
|
|
529
666
|
}
|
|
530
667
|
}
|
|
531
|
-
let
|
|
668
|
+
let taskBody = prompt;
|
|
532
669
|
if (outputSchema) {
|
|
533
|
-
|
|
670
|
+
taskBody = appendOutputContract(taskBody, outputSchema);
|
|
534
671
|
}
|
|
535
|
-
|
|
536
|
-
? `${
|
|
537
|
-
:
|
|
538
|
-
const taskTitle = String(a.task_name ?? "").trim() || label;
|
|
539
|
-
const taskIdHint = String(a.task_id ?? "").trim() || undefined;
|
|
672
|
+
taskBody = isolated
|
|
673
|
+
? `${taskBody}\n\n${worktreeContextNote(isolated)}`
|
|
674
|
+
: taskBody;
|
|
540
675
|
const openTask = (childSessionId) => {
|
|
541
676
|
const task = options.runtime.agentTeamTasks.open({
|
|
542
677
|
parentSessionId: options.parentSessionId,
|
|
@@ -591,14 +726,41 @@ function createSubagentTool(options) {
|
|
|
591
726
|
finalizeSubagentWorktree(isolated);
|
|
592
727
|
}
|
|
593
728
|
};
|
|
729
|
+
/**
|
|
730
|
+
* Codex `spawn_guard.rs` (RAII): a half-made spawn is torn down on
|
|
731
|
+
* EVERY exit path, including an unexpected throw. Explicit
|
|
732
|
+
* `discardUnstartedChild` calls cover the known failure branches; this
|
|
733
|
+
* flag + finally is the backstop for anything they miss.
|
|
734
|
+
*/
|
|
735
|
+
let started = false;
|
|
736
|
+
const discardIfUnstarted = async () => {
|
|
737
|
+
if (started || !childId)
|
|
738
|
+
return;
|
|
739
|
+
started = true;
|
|
740
|
+
dropIsolated();
|
|
741
|
+
await discardUnstartedChild(options.runtime, childId);
|
|
742
|
+
};
|
|
594
743
|
let childId;
|
|
595
|
-
|
|
744
|
+
/** Fork seed size — bounds the root-evidence slice below. */
|
|
745
|
+
let seedCut = 0;
|
|
596
746
|
if (inherit) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
747
|
+
// A transport reject (not an RpcResult error) here would strand the
|
|
748
|
+
// worktree lease allocated above; catch keeps the cleanup total.
|
|
749
|
+
let forked;
|
|
750
|
+
try {
|
|
751
|
+
forked = await dispatchFaceMethod(options.runtime, "session.fork", `tool-sa-fork-${Date.now()}`, {
|
|
752
|
+
sessionId: options.parentSessionId,
|
|
753
|
+
linkMode,
|
|
754
|
+
label,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
dropIsolated();
|
|
759
|
+
return {
|
|
760
|
+
content: "subagent seed failed: fork request errored",
|
|
761
|
+
isError: true,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
602
764
|
if (!forked.result.ok) {
|
|
603
765
|
// No completed turn yet — fall back to a fresh child (DSH fork omits seed).
|
|
604
766
|
if (forked.result.error.code === "fork-unavailable") {
|
|
@@ -626,7 +788,10 @@ function createSubagentTool(options) {
|
|
|
626
788
|
}
|
|
627
789
|
else {
|
|
628
790
|
childId = String(forked.result.value.sessionId);
|
|
629
|
-
|
|
791
|
+
// Durable seed boundary: every answer read of this child must
|
|
792
|
+
// slice from here, or the parent's last reply reads as its answer.
|
|
793
|
+
seedCut = Number(forked.result.value.eventCount ?? 0);
|
|
794
|
+
options.runtime.subagents.setSeedEventCount(childId, seedCut);
|
|
630
795
|
}
|
|
631
796
|
}
|
|
632
797
|
else {
|
|
@@ -654,21 +819,62 @@ function createSubagentTool(options) {
|
|
|
654
819
|
: {}),
|
|
655
820
|
});
|
|
656
821
|
if (!modelPin.ok) {
|
|
657
|
-
|
|
822
|
+
await discardIfUnstarted();
|
|
658
823
|
return { content: modelPin.message, isError: true };
|
|
659
824
|
}
|
|
660
825
|
if (isolated) {
|
|
661
826
|
options.runtime.sessionCwds.set(childId, isolated.path);
|
|
662
|
-
|
|
827
|
+
// Worktree bind failure must not leak the child: swallow and let the
|
|
828
|
+
// prompt below decide the spawn's fate (or the catch backstop).
|
|
829
|
+
await Promise.resolve(options.runtime.invalidateAgent?.(childId)).catch(() => undefined);
|
|
663
830
|
}
|
|
831
|
+
const childPrompt = applySubagentSpawnPreamble({
|
|
832
|
+
prompt: taskBody,
|
|
833
|
+
parentSessionId: options.parentSessionId,
|
|
834
|
+
childSessionId: childId,
|
|
835
|
+
mode: linkMode,
|
|
836
|
+
label,
|
|
837
|
+
...(spawnRole ? { role: spawnRole } : {}),
|
|
838
|
+
inheritContext: inherit,
|
|
839
|
+
cwd: isolated?.path ?? parentCwd,
|
|
840
|
+
isolatedWorktree: isolated !== null,
|
|
841
|
+
// The human's own asks: a child that only sees the parent's paraphrase
|
|
842
|
+
// drifts from what was actually authorized (Codex
|
|
843
|
+
// `control/user_authorization.rs`). A forked child already carries
|
|
844
|
+
// these in its seeded transcript, so only add them beyond the cut.
|
|
845
|
+
...(() => {
|
|
846
|
+
const block = rootUserAuthorizationBlock({
|
|
847
|
+
parentEvents: readSessionEvents(options.runtime.store, options.parentSessionId),
|
|
848
|
+
...(seedCut > 0 ? { sinceEventCount: seedCut } : {}),
|
|
849
|
+
});
|
|
850
|
+
return block ? { userAuthorization: block } : {};
|
|
851
|
+
})(),
|
|
852
|
+
});
|
|
664
853
|
const task = openTask(childId);
|
|
665
854
|
const worktreeLine = reclaimIsolated;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
855
|
+
// A transport-level reject (not an RpcResult error) must not leak the
|
|
856
|
+
// child: the catch backstop tears the half-made spawn down.
|
|
857
|
+
let prompted;
|
|
858
|
+
try {
|
|
859
|
+
prompted = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-p-${childId}`, {
|
|
860
|
+
sessionId: childId,
|
|
861
|
+
mode: "queue",
|
|
862
|
+
content: [{ type: "text", text: childPrompt }],
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
await discardIfUnstarted();
|
|
867
|
+
const extra = worktreeLine(false);
|
|
868
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
869
|
+
return {
|
|
870
|
+
content: [`subagent prompt failed: ${message}`, extra]
|
|
871
|
+
.filter(Boolean)
|
|
872
|
+
.join("\n"),
|
|
873
|
+
isError: true,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
671
876
|
if (!prompted.result.ok) {
|
|
877
|
+
await discardIfUnstarted();
|
|
672
878
|
const extra = worktreeLine(false);
|
|
673
879
|
return {
|
|
674
880
|
content: [`subagent prompt failed: ${prompted.result.error.message}`, extra]
|
|
@@ -677,6 +883,8 @@ function createSubagentTool(options) {
|
|
|
677
883
|
isError: true,
|
|
678
884
|
};
|
|
679
885
|
}
|
|
886
|
+
// Spawn accepted — the child is no longer a half-made shell.
|
|
887
|
+
started = true;
|
|
680
888
|
if (background) {
|
|
681
889
|
return {
|
|
682
890
|
content: [
|
|
@@ -715,12 +923,19 @@ function createSubagentTool(options) {
|
|
|
715
923
|
}
|
|
716
924
|
const all = readSessionEvents(options.runtime.store, childId);
|
|
717
925
|
// Never return seeded parent assistant text as the child's result.
|
|
718
|
-
|
|
926
|
+
const link = options.runtime.subagents.getByChild(childId);
|
|
927
|
+
const readOwned = () => subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId));
|
|
928
|
+
let owned = subagentOwnedEvents(link, all);
|
|
719
929
|
let text = lastAssistantBodyText(owned);
|
|
720
930
|
let schemaValid;
|
|
721
931
|
let schemaErrors;
|
|
722
|
-
if (outputSchema
|
|
723
|
-
|
|
932
|
+
if (outputSchema) {
|
|
933
|
+
// An empty body cannot satisfy the contract either: a silent skip
|
|
934
|
+
// would hand the parent `ok: true` with an unvalidated (missing)
|
|
935
|
+
// answer, so treat "no text" as one more schema failure.
|
|
936
|
+
let checked = text
|
|
937
|
+
? validateOutputAgainstSchema(text, outputSchema)
|
|
938
|
+
: { valid: false, errors: ["child produced no assistant text"] };
|
|
724
939
|
if (!checked.valid) {
|
|
725
940
|
const retry = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-schema-${childId}`, {
|
|
726
941
|
sessionId: childId,
|
|
@@ -739,18 +954,20 @@ function createSubagentTool(options) {
|
|
|
739
954
|
catch {
|
|
740
955
|
/* keep first answer */
|
|
741
956
|
}
|
|
742
|
-
owned =
|
|
743
|
-
? readSessionEvents(options.runtime.store, childId).slice(seedEventCount)
|
|
744
|
-
: readSessionEvents(options.runtime.store, childId);
|
|
957
|
+
owned = readOwned();
|
|
745
958
|
text = lastAssistantBodyText(owned) || text;
|
|
746
|
-
checked =
|
|
959
|
+
checked = text
|
|
960
|
+
? validateOutputAgainstSchema(text, outputSchema)
|
|
961
|
+
: { valid: false, errors: ["child produced no assistant text"] };
|
|
747
962
|
}
|
|
748
963
|
}
|
|
749
964
|
schemaValid = checked.valid;
|
|
750
965
|
schemaErrors = checked.errors;
|
|
751
966
|
}
|
|
752
967
|
options.runtime.agentTeamTasks.complete(task.id, {
|
|
753
|
-
|
|
968
|
+
// An unvalidated answer is not a successful delegation: the task
|
|
969
|
+
// board must not read `completed` when the contract was skipped.
|
|
970
|
+
ok: schemaValid !== false,
|
|
754
971
|
...(text ? { preview: text } : {}),
|
|
755
972
|
...(schemaValid !== undefined ? { schemaValid } : {}),
|
|
756
973
|
...(schemaErrors && schemaErrors.length > 0
|
|
@@ -786,7 +1003,7 @@ function createSubagentTool(options) {
|
|
|
786
1003
|
function createListAgentsTool(options) {
|
|
787
1004
|
return {
|
|
788
1005
|
name: "list_agents",
|
|
789
|
-
description: "List direct child subagents of this session (id, label, mode, running, inbox queue/steer). " +
|
|
1006
|
+
description: "List direct child subagents of this session (id, label, mode, running, model, inbox queue/steer). " +
|
|
790
1007
|
"Prefaces with depth/active quota. Prefer `analytics` for a fuller quota snapshot.",
|
|
791
1008
|
parameters: { type: "object", properties: {} },
|
|
792
1009
|
isConcurrencySafe: () => true,
|
|
@@ -819,7 +1036,8 @@ function createListAgentsTool(options) {
|
|
|
819
1036
|
const inbox = id !== "?"
|
|
820
1037
|
? childInboxCounts(options.runtime, id)
|
|
821
1038
|
: { queued: 0, steering: 0 };
|
|
822
|
-
|
|
1039
|
+
const route = e.model ? `\t${e.model}` : "";
|
|
1040
|
+
return `${id}\t${label}\t${mode}\t${activity}${route}\tq=${inbox.queued}\tsteer=${inbox.steering}`;
|
|
823
1041
|
});
|
|
824
1042
|
return { content: [header, ...lines].join("\n") };
|
|
825
1043
|
},
|
|
@@ -873,7 +1091,8 @@ function createSendMessageTool(options) {
|
|
|
873
1091
|
isError: true,
|
|
874
1092
|
};
|
|
875
1093
|
}
|
|
876
|
-
|
|
1094
|
+
// Task-board resume is owned by `subagent.prompt` (the SoT for every
|
|
1095
|
+
// delivery route); calling it again here double-bumped the revision.
|
|
877
1096
|
return { content: `sent to ${childId} (${delivery})` };
|
|
878
1097
|
},
|
|
879
1098
|
};
|
|
@@ -1032,7 +1251,6 @@ function createTeamGraphTool(options) {
|
|
|
1032
1251
|
results.push(`${peer.id}\tfail\t${prompted.result.error.message}`);
|
|
1033
1252
|
continue;
|
|
1034
1253
|
}
|
|
1035
|
-
options.runtime.agentTeamTasks.markResumed(peer.id);
|
|
1036
1254
|
results.push(`${peer.id}\tok\t${delivery}`);
|
|
1037
1255
|
}
|
|
1038
1256
|
return {
|
|
@@ -1110,7 +1328,6 @@ function createFollowupTaskTool(options) {
|
|
|
1110
1328
|
isError: true,
|
|
1111
1329
|
};
|
|
1112
1330
|
}
|
|
1113
|
-
options.runtime.agentTeamTasks.markResumed(childId);
|
|
1114
1331
|
return { content: `followup_task sent to ${childId} (steer)` };
|
|
1115
1332
|
},
|
|
1116
1333
|
};
|
|
@@ -1217,10 +1434,11 @@ function createWaitAgentTool(options) {
|
|
|
1217
1434
|
: `wait_agent completed (${timeoutMs}ms budget)`,
|
|
1218
1435
|
];
|
|
1219
1436
|
for (const childId of targets) {
|
|
1437
|
+
const link = options.runtime.subagents.getByChild(childId);
|
|
1220
1438
|
const running = isChildSessionActive(options.runtime, childId);
|
|
1221
1439
|
const text = running
|
|
1222
1440
|
? undefined
|
|
1223
|
-
: lastAssistantBodyText(readSessionEvents(options.runtime.store, childId));
|
|
1441
|
+
: lastAssistantBodyText(subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId)));
|
|
1224
1442
|
const inbox = childInboxCounts(options.runtime, childId);
|
|
1225
1443
|
lines.push([
|
|
1226
1444
|
childId,
|
|
@@ -1245,7 +1463,8 @@ function createAnalyticsTool(options) {
|
|
|
1245
1463
|
return {
|
|
1246
1464
|
name: "analytics",
|
|
1247
1465
|
description: "Local subagent quota and queue snapshot for this session (not cloud telemetry). " +
|
|
1248
|
-
"Shows depth/active caps, free slots, and each child's running state
|
|
1466
|
+
"Shows depth/active caps, free slots, and each child's running state, model route, " +
|
|
1467
|
+
"plus inbox queue/steer counts.",
|
|
1249
1468
|
parameters: { type: "object", properties: {} },
|
|
1250
1469
|
isConcurrencySafe: () => true,
|
|
1251
1470
|
async execute() {
|
|
@@ -1278,7 +1497,14 @@ function createAnalyticsTool(options) {
|
|
|
1278
1497
|
? "running"
|
|
1279
1498
|
: "idle";
|
|
1280
1499
|
const inbox = childInboxCounts(options.runtime, link.childSessionId);
|
|
1281
|
-
|
|
1500
|
+
const externalKind = options.runtime.externalAgents.kind(link.childSessionId);
|
|
1501
|
+
const route = externalKind
|
|
1502
|
+
? `external:${externalKind}`
|
|
1503
|
+
: (() => {
|
|
1504
|
+
const m = resolveSessionModelSelection(options.runtime, link.childSessionId);
|
|
1505
|
+
return `${m.provider}/${m.model}`;
|
|
1506
|
+
})();
|
|
1507
|
+
lines.push(` ${link.childSessionId}\t${link.label ?? "subagent"}\t${link.mode}\t${activity}\t${route}\tq=${inbox.queued}\tsteer=${inbox.steering}`);
|
|
1282
1508
|
}
|
|
1283
1509
|
}
|
|
1284
1510
|
return { content: lines.join("\n") };
|