@xrkseek/harness 0.5.14 → 0.5.19
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 +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/index.js +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/request-header-log.d.ts +10 -0
- package/node_modules/@xrkseek/core-agent-loop/dist/request-header-log.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/request-header-log.js +56 -7
- package/node_modules/@xrkseek/core-agent-loop/dist/request-header-log.js.map +1 -1
- package/node_modules/@xrkseek/core-session/dist/index.d.ts +1 -1
- package/node_modules/@xrkseek/core-session/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-session/dist/index.js +1 -1
- package/node_modules/@xrkseek/core-session/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/core-session/dist/request-header.d.ts +8 -1
- package/node_modules/@xrkseek/core-session/dist/request-header.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-session/dist/request-header.js +18 -9
- package/node_modules/@xrkseek/core-session/dist/request-header.js.map +1 -1
- package/node_modules/@xrkseek/core-session/dist/sqlite-store.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-session/dist/sqlite-store.js +33 -0
- package/node_modules/@xrkseek/core-session/dist/sqlite-store.js.map +1 -1
- package/node_modules/@xrkseek/preset-harness/dist/preset.d.ts +9 -0
- package/node_modules/@xrkseek/preset-harness/dist/preset.d.ts.map +1 -1
- package/node_modules/@xrkseek/preset-harness/dist/preset.js +3 -1
- package/node_modules/@xrkseek/preset-harness/dist/preset.js.map +1 -1
- package/node_modules/@xrkseek/preset-harness/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@xrkseek/preset-minimal/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@xrkseek/preset-server/dist/preset.d.ts.map +1 -1
- package/node_modules/@xrkseek/preset-server/dist/preset.js +2 -1
- package/node_modules/@xrkseek/preset-server/dist/preset.js.map +1 -1
- package/node_modules/@xrkseek/preset-server/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@xrkseek/protocol/dist/index.d.ts +1 -1
- package/node_modules/@xrkseek/protocol/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/protocol/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/protocol/dist/sidebar-previews.d.ts +16 -0
- package/node_modules/@xrkseek/protocol/dist/sidebar-previews.d.ts.map +1 -1
- package/node_modules/@xrkseek/protocol/dist/sidebar-previews.js.map +1 -1
- package/node_modules/@xrkseek/server-cron/dist/index.d.ts +1 -1
- package/node_modules/@xrkseek/server-cron/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-cron/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/server-cron/dist/tools.d.ts +13 -1
- package/node_modules/@xrkseek/server-cron/dist/tools.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-cron/dist/tools.js +35 -4
- package/node_modules/@xrkseek/server-cron/dist/tools.js.map +1 -1
- package/node_modules/@xrkseek/server-cron/dist/types.d.ts +5 -0
- package/node_modules/@xrkseek/server-cron/dist/types.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-cron/dist/types.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/history-paginate.d.ts +25 -0
- package/node_modules/@xrkseek/server-face/dist/adapt/history-paginate.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/history-paginate.js +132 -0
- package/node_modules/@xrkseek/server-face/dist/adapt/history-paginate.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/index.d.ts +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/index.js +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/index.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/tool-view.js +3 -4
- package/node_modules/@xrkseek/server-face/dist/adapt/tool-view.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/wire-ids.d.ts +5 -0
- package/node_modules/@xrkseek/server-face/dist/adapt/wire-ids.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/wire-ids.js +20 -2
- package/node_modules/@xrkseek/server-face/dist/adapt/wire-ids.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts +1 -1
- package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js +6 -9
- package/node_modules/@xrkseek/server-face/dist/agent-roster-store.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 +27 -9
- package/node_modules/@xrkseek/server-face/dist/handlers/session.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/index.d.ts +2 -1
- package/node_modules/@xrkseek/server-face/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/index.js +2 -1
- package/node_modules/@xrkseek/server-face/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/presence-dressing.d.ts +7 -0
- package/node_modules/@xrkseek/server-face/dist/presence-dressing.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/presence-dressing.js +46 -7
- package/node_modules/@xrkseek/server-face/dist/presence-dressing.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/projections/units/context-headers.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/projections/units/context-headers.js +6 -1
- package/node_modules/@xrkseek/server-face/dist/projections/units/context-headers.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 +2 -0
- package/node_modules/@xrkseek/server-face/dist/runtime.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-status.d.ts +16 -0
- package/node_modules/@xrkseek/server-face/dist/session-status.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-status.js +39 -4
- package/node_modules/@xrkseek/server-face/dist/session-status.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/settings-document.js +2 -2
- package/node_modules/@xrkseek/server-face/dist/settings-document.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/status-snapshot-cache.d.ts +23 -0
- package/node_modules/@xrkseek/server-face/dist/status-snapshot-cache.d.ts.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/status-snapshot-cache.js +95 -0
- package/node_modules/@xrkseek/server-face/dist/status-snapshot-cache.js.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/subagent-tools.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/subagent-tools.js +427 -370
- package/node_modules/@xrkseek/server-face/dist/subagent-tools.js.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/cron-http.d.ts +6 -8
- package/node_modules/@xrkseek/server-host/dist/cron-http.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/cron-http.js +104 -28
- package/node_modules/@xrkseek/server-host/dist/cron-http.js.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/index.d.ts +9 -0
- package/node_modules/@xrkseek/server-host/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/index.js +61 -3
- package/node_modules/@xrkseek/server-host/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/sidebar-face-bridge.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/sidebar-face-bridge.js +33 -8
- package/node_modules/@xrkseek/server-host/dist/sidebar-face-bridge.js.map +1 -1
- package/node_modules/@xrkseek/server-http/dist/dsh-compat/honest-http-catchall.js +1 -1
- package/node_modules/@xrkseek/server-http/dist/dsh-compat/honest-http-catchall.js.map +1 -1
- package/node_modules/@xrkseek/server-http/dist/sidebar/sidebar-face-bridge.d.ts +5 -1
- package/node_modules/@xrkseek/server-http/dist/sidebar/sidebar-face-bridge.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SUBAGENT_ROUTING_PROMPT_TEXT } from "@xrkseek/core-tools";
|
|
2
2
|
import { listPendingAdmits, readSessionEvents, } from "@xrkseek/core-session";
|
|
3
|
+
import { forgetRequestHeaderCache } from "@xrkseek/core-agent-loop";
|
|
3
4
|
import { dispatchFaceMethod } from "./dispatch.js";
|
|
4
5
|
import { formatChildOutcome, lastAssistantBodyText, lastModelRetryNotice, } from "./adapt/subagent-notice.js";
|
|
5
6
|
import { boundChildAnswer, SUBAGENT_ANSWER_INLINE_BYTES, } from "./adapt/subagent-answer-bound.js";
|
|
@@ -42,7 +43,7 @@ export function resolveSubagentQuota(runtime, sessionId) {
|
|
|
42
43
|
const profile = resolveAgentPresetProfile(presetId);
|
|
43
44
|
if (profile.subagents.mode === "off") {
|
|
44
45
|
const depth = subagentDepth(runtime, sessionId);
|
|
45
|
-
const active = countActiveChildren(runtime, sessionId);
|
|
46
|
+
const active = countActiveChildren(runtime, sessionId) + pendingSpawnCount(sessionId);
|
|
46
47
|
const delegated = runtime.subagents.listDelegated(sessionId).length;
|
|
47
48
|
return {
|
|
48
49
|
depth,
|
|
@@ -60,7 +61,7 @@ export function resolveSubagentQuota(runtime, sessionId) {
|
|
|
60
61
|
? Math.min(faceActive, profile.subagents.maxActiveChildren)
|
|
61
62
|
: faceActive;
|
|
62
63
|
const depth = subagentDepth(runtime, sessionId);
|
|
63
|
-
const active = countActiveChildren(runtime, sessionId);
|
|
64
|
+
const active = countActiveChildren(runtime, sessionId) + pendingSpawnCount(sessionId);
|
|
64
65
|
const delegated = runtime.subagents.listDelegated(sessionId).length;
|
|
65
66
|
return {
|
|
66
67
|
depth,
|
|
@@ -243,6 +244,25 @@ function countActiveChildren(runtime, parentSessionId) {
|
|
|
243
244
|
}
|
|
244
245
|
return n;
|
|
245
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* In-flight spawn reservations (sync). Two parallel `subagent` settles both
|
|
249
|
+
* read `countActiveChildren` before either child's drain latches — without
|
|
250
|
+
* this, both can pass `active < maxActive` and overshoot the cap.
|
|
251
|
+
*/
|
|
252
|
+
const pendingSpawnSlots = new Map();
|
|
253
|
+
function pendingSpawnCount(parentSessionId) {
|
|
254
|
+
return pendingSpawnSlots.get(parentSessionId) ?? 0;
|
|
255
|
+
}
|
|
256
|
+
function reserveSpawnSlot(parentSessionId) {
|
|
257
|
+
pendingSpawnSlots.set(parentSessionId, pendingSpawnCount(parentSessionId) + 1);
|
|
258
|
+
}
|
|
259
|
+
function releaseSpawnSlot(parentSessionId) {
|
|
260
|
+
const n = pendingSpawnCount(parentSessionId);
|
|
261
|
+
if (n <= 1)
|
|
262
|
+
pendingSpawnSlots.delete(parentSessionId);
|
|
263
|
+
else
|
|
264
|
+
pendingSpawnSlots.set(parentSessionId, n - 1);
|
|
265
|
+
}
|
|
246
266
|
/**
|
|
247
267
|
* Undo a half-made spawn. `session.create` / `session.fork` already made the
|
|
248
268
|
* child durable and registered its link, so any failure between there and a
|
|
@@ -266,6 +286,7 @@ async function discardUnstartedChild(runtime, childSessionId) {
|
|
|
266
286
|
catch {
|
|
267
287
|
/* store without delete / already gone */
|
|
268
288
|
}
|
|
289
|
+
forgetRequestHeaderCache(childSessionId);
|
|
269
290
|
await Promise.resolve(runtime.invalidateAgent?.(childSessionId)).catch(() => undefined);
|
|
270
291
|
}
|
|
271
292
|
/**
|
|
@@ -455,6 +476,10 @@ function createSubagentTool(options) {
|
|
|
455
476
|
},
|
|
456
477
|
required: ["prompt"],
|
|
457
478
|
},
|
|
479
|
+
// Without this, settle-batch treats every `subagent` call as an exclusive
|
|
480
|
+
// barrier — two tool calls in one turn run one-after-another, so Status
|
|
481
|
+
// shows "1 subagent" even when the model asked for parallel siblings.
|
|
482
|
+
isConcurrencySafe: () => true,
|
|
458
483
|
presentCall: (args) => ({
|
|
459
484
|
card: "generic",
|
|
460
485
|
title: String(args.description?.trim() || "Subagent"),
|
|
@@ -558,6 +583,14 @@ function createSubagentTool(options) {
|
|
|
558
583
|
isError: true,
|
|
559
584
|
};
|
|
560
585
|
}
|
|
586
|
+
reserveSpawnSlot(options.parentSessionId);
|
|
587
|
+
let externalSlotHeld = true;
|
|
588
|
+
const dropExternalSlot = () => {
|
|
589
|
+
if (!externalSlotHeld)
|
|
590
|
+
return;
|
|
591
|
+
externalSlotHeld = false;
|
|
592
|
+
releaseSpawnSlot(options.parentSessionId);
|
|
593
|
+
};
|
|
561
594
|
try {
|
|
562
595
|
const product = parseExternalAgentProduct(options.runtime.settingsNamespaces.view("external-agent").value);
|
|
563
596
|
if (background && supportsExternalContinuable(runtimeKind)) {
|
|
@@ -632,6 +665,8 @@ function createSubagentTool(options) {
|
|
|
632
665
|
? { spawnImpl: options.externalSpawn }
|
|
633
666
|
: {}),
|
|
634
667
|
});
|
|
668
|
+
// Child is busy in externalAgents; pending slot no longer needed.
|
|
669
|
+
dropExternalSlot();
|
|
635
670
|
return {
|
|
636
671
|
content: [
|
|
637
672
|
`Started background external subagent \`${childId}\` (${label} · ${runtimeKind}).`,
|
|
@@ -644,6 +679,9 @@ function createSubagentTool(options) {
|
|
|
644
679
|
].join("\n"),
|
|
645
680
|
};
|
|
646
681
|
}
|
|
682
|
+
// One-shot external has no Face child latch — do not hold the slot
|
|
683
|
+
// for the whole print turn (would starve parallel in-process spawns).
|
|
684
|
+
dropExternalSlot();
|
|
647
685
|
const result = await runExternalAgentTurn({
|
|
648
686
|
kind: runtimeKind,
|
|
649
687
|
cwd: externalCwd,
|
|
@@ -667,6 +705,9 @@ function createSubagentTool(options) {
|
|
|
667
705
|
: String(err);
|
|
668
706
|
return { content: `subagent external: ${msg}`, isError: true };
|
|
669
707
|
}
|
|
708
|
+
finally {
|
|
709
|
+
dropExternalSlot();
|
|
710
|
+
}
|
|
670
711
|
}
|
|
671
712
|
const depth = subagentDepth(options.runtime, options.parentSessionId);
|
|
672
713
|
const { maxDepth, maxActive: maxActiveChildren, active } = caps();
|
|
@@ -682,426 +723,442 @@ function createSubagentTool(options) {
|
|
|
682
723
|
isError: true,
|
|
683
724
|
};
|
|
684
725
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
726
|
+
reserveSpawnSlot(options.parentSessionId);
|
|
727
|
+
let spawnSlotHeld = true;
|
|
728
|
+
const dropSpawnSlot = () => {
|
|
729
|
+
if (!spawnSlotHeld)
|
|
730
|
+
return;
|
|
731
|
+
spawnSlotHeld = false;
|
|
732
|
+
releaseSpawnSlot(options.parentSessionId);
|
|
733
|
+
};
|
|
734
|
+
try {
|
|
735
|
+
const background = a.run_in_background === true;
|
|
736
|
+
const inherit = a.inherit_context === true;
|
|
737
|
+
const label = String(a.description ?? "").trim() ||
|
|
738
|
+
String(a.task_name ?? "").trim() ||
|
|
739
|
+
(background ? "subagent" : "subagent-task");
|
|
740
|
+
const linkMode = background ? "continuable" : "one-shot";
|
|
741
|
+
const parentCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
|
|
742
|
+
const taskTitle = String(a.task_name ?? "").trim() || label;
|
|
743
|
+
const taskIdHint = String(a.task_id ?? "").trim() || undefined;
|
|
744
|
+
// Before anything is allocated: reusing a task_id another child owns
|
|
745
|
+
// must not leave a worktree or a session behind.
|
|
746
|
+
const taskIdError = taskIdConflict(options.runtime, taskIdHint);
|
|
747
|
+
if (taskIdError) {
|
|
748
|
+
return { content: taskIdError, isError: true };
|
|
706
749
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (managedLease) {
|
|
714
|
-
isolated = {
|
|
715
|
-
path: managedLease.path,
|
|
716
|
-
branch: managedLease.branch,
|
|
717
|
-
repoRoot: managedLease.repoRoot,
|
|
718
|
-
baseCommit: managedLease.baseCommit,
|
|
719
|
-
};
|
|
750
|
+
let isolated = null;
|
|
751
|
+
let managedLease = null;
|
|
752
|
+
let worktreeSkip = "";
|
|
753
|
+
if (a.worktree === true) {
|
|
754
|
+
if (worktreeSkippedForRemote(options.runtime.remoteExecution)) {
|
|
755
|
+
worktreeSkip = "worktree skipped: not a local terminal";
|
|
720
756
|
}
|
|
721
757
|
else {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
758
|
+
managedLease =
|
|
759
|
+
options.runtime.managedWorktrees.allocate({
|
|
760
|
+
parentCwd,
|
|
761
|
+
parentSessionId: options.parentSessionId,
|
|
762
|
+
}) ?? null;
|
|
763
|
+
if (managedLease) {
|
|
764
|
+
isolated = {
|
|
765
|
+
path: managedLease.path,
|
|
766
|
+
branch: managedLease.branch,
|
|
767
|
+
repoRoot: managedLease.repoRoot,
|
|
768
|
+
baseCommit: managedLease.baseCommit,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
// Fall back to one-shot create when the registry path fails.
|
|
773
|
+
isolated = createSubagentWorktree(parentCwd);
|
|
774
|
+
if (!isolated) {
|
|
775
|
+
worktreeSkip =
|
|
776
|
+
"worktree skipped: not a git repository or worktree add failed";
|
|
777
|
+
}
|
|
727
778
|
}
|
|
728
779
|
}
|
|
729
780
|
}
|
|
730
|
-
|
|
731
|
-
let taskBody = prompt;
|
|
732
|
-
if (outputSchema) {
|
|
733
|
-
taskBody = appendOutputContract(taskBody, outputSchema);
|
|
734
|
-
}
|
|
735
|
-
taskBody = isolated
|
|
736
|
-
? `${taskBody}\n\n${worktreeContextNote(isolated)}`
|
|
737
|
-
: taskBody;
|
|
738
|
-
const openTask = (childSessionId) => {
|
|
739
|
-
const task = options.runtime.agentTeamTasks.open({
|
|
740
|
-
parentSessionId: options.parentSessionId,
|
|
741
|
-
title: taskTitle,
|
|
742
|
-
childSessionId,
|
|
743
|
-
...(spawnRole && spawnRole !== "default" ? { role: spawnRole } : {}),
|
|
744
|
-
...(taskIdHint ? { taskId: taskIdHint } : {}),
|
|
745
|
-
});
|
|
781
|
+
let taskBody = prompt;
|
|
746
782
|
if (outputSchema) {
|
|
747
|
-
|
|
783
|
+
taskBody = appendOutputContract(taskBody, outputSchema);
|
|
748
784
|
}
|
|
749
|
-
|
|
750
|
-
|
|
785
|
+
taskBody = isolated
|
|
786
|
+
? `${taskBody}\n\n${worktreeContextNote(isolated)}`
|
|
787
|
+
: taskBody;
|
|
788
|
+
const openTask = (childSessionId) => {
|
|
789
|
+
const task = options.runtime.agentTeamTasks.open({
|
|
790
|
+
parentSessionId: options.parentSessionId,
|
|
791
|
+
title: taskTitle,
|
|
751
792
|
childSessionId,
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
options.runtime.agentTeamTasks.bindWorktree(task.id, {
|
|
755
|
-
path: managedLease.path,
|
|
756
|
-
branch: managedLease.branch,
|
|
757
|
-
id: managedLease.id,
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
else if (isolated) {
|
|
761
|
-
options.runtime.agentTeamTasks.bindWorktree(task.id, {
|
|
762
|
-
path: isolated.path,
|
|
763
|
-
branch: isolated.branch,
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
return task;
|
|
767
|
-
};
|
|
768
|
-
const reclaimIsolated = (keep) => {
|
|
769
|
-
if (worktreeSkip)
|
|
770
|
-
return worktreeSkip;
|
|
771
|
-
if (!isolated)
|
|
772
|
-
return "";
|
|
773
|
-
if (keep) {
|
|
774
|
-
return `[worktree] ${isolated.path}\nbranch ${isolated.branch}\nleft in place (child still running)`;
|
|
775
|
-
}
|
|
776
|
-
if (managedLease) {
|
|
777
|
-
return formatWorktreeResult(options.runtime.managedWorktrees.reclaim(managedLease.id) ??
|
|
778
|
-
finalizeSubagentWorktree(isolated));
|
|
779
|
-
}
|
|
780
|
-
return formatWorktreeResult(finalizeSubagentWorktree(isolated));
|
|
781
|
-
};
|
|
782
|
-
const dropIsolated = () => {
|
|
783
|
-
if (!isolated)
|
|
784
|
-
return;
|
|
785
|
-
if (managedLease) {
|
|
786
|
-
options.runtime.managedWorktrees.reclaim(managedLease.id);
|
|
787
|
-
}
|
|
788
|
-
else {
|
|
789
|
-
finalizeSubagentWorktree(isolated);
|
|
790
|
-
}
|
|
791
|
-
};
|
|
792
|
-
/**
|
|
793
|
-
* Codex `spawn_guard.rs` (RAII): a half-made spawn is torn down on
|
|
794
|
-
* EVERY exit path, including an unexpected throw. Explicit
|
|
795
|
-
* `discardUnstartedChild` calls cover the known failure branches; this
|
|
796
|
-
* flag + finally is the backstop for anything they miss.
|
|
797
|
-
*/
|
|
798
|
-
let started = false;
|
|
799
|
-
const discardIfUnstarted = async () => {
|
|
800
|
-
if (started || !childId)
|
|
801
|
-
return;
|
|
802
|
-
started = true;
|
|
803
|
-
dropIsolated();
|
|
804
|
-
await discardUnstartedChild(options.runtime, childId);
|
|
805
|
-
};
|
|
806
|
-
let childId;
|
|
807
|
-
/** Fork seed size — bounds the root-evidence slice below. */
|
|
808
|
-
let seedCut = 0;
|
|
809
|
-
if (inherit) {
|
|
810
|
-
// A transport reject (not an RpcResult error) here would strand the
|
|
811
|
-
// worktree lease allocated above; catch keeps the cleanup total.
|
|
812
|
-
let forked;
|
|
813
|
-
try {
|
|
814
|
-
forked = await dispatchFaceMethod(options.runtime, "session.fork", `tool-sa-fork-${Date.now()}`, {
|
|
815
|
-
sessionId: options.parentSessionId,
|
|
816
|
-
linkMode,
|
|
817
|
-
label,
|
|
818
|
-
...(spawnRole && spawnRole !== "default"
|
|
819
|
-
? { role: spawnRole }
|
|
820
|
-
: {}),
|
|
793
|
+
...(spawnRole && spawnRole !== "default" ? { role: spawnRole } : {}),
|
|
794
|
+
...(taskIdHint ? { taskId: taskIdHint } : {}),
|
|
821
795
|
});
|
|
822
|
-
|
|
823
|
-
|
|
796
|
+
if (outputSchema) {
|
|
797
|
+
options.runtime.agentTeamTasks.setOutputSchema(task.id, outputSchema);
|
|
798
|
+
}
|
|
799
|
+
if (managedLease) {
|
|
800
|
+
options.runtime.managedWorktrees.bind(managedLease.id, {
|
|
801
|
+
childSessionId,
|
|
802
|
+
teamTaskId: task.id,
|
|
803
|
+
});
|
|
804
|
+
options.runtime.agentTeamTasks.bindWorktree(task.id, {
|
|
805
|
+
path: managedLease.path,
|
|
806
|
+
branch: managedLease.branch,
|
|
807
|
+
id: managedLease.id,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
else if (isolated) {
|
|
811
|
+
options.runtime.agentTeamTasks.bindWorktree(task.id, {
|
|
812
|
+
path: isolated.path,
|
|
813
|
+
branch: isolated.branch,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
return task;
|
|
817
|
+
};
|
|
818
|
+
const reclaimIsolated = (keep) => {
|
|
819
|
+
if (worktreeSkip)
|
|
820
|
+
return worktreeSkip;
|
|
821
|
+
if (!isolated)
|
|
822
|
+
return "";
|
|
823
|
+
if (keep) {
|
|
824
|
+
return `[worktree] ${isolated.path}\nbranch ${isolated.branch}\nleft in place (child still running)`;
|
|
825
|
+
}
|
|
826
|
+
if (managedLease) {
|
|
827
|
+
return formatWorktreeResult(options.runtime.managedWorktrees.reclaim(managedLease.id) ??
|
|
828
|
+
finalizeSubagentWorktree(isolated));
|
|
829
|
+
}
|
|
830
|
+
return formatWorktreeResult(finalizeSubagentWorktree(isolated));
|
|
831
|
+
};
|
|
832
|
+
const dropIsolated = () => {
|
|
833
|
+
if (!isolated)
|
|
834
|
+
return;
|
|
835
|
+
if (managedLease) {
|
|
836
|
+
options.runtime.managedWorktrees.reclaim(managedLease.id);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
finalizeSubagentWorktree(isolated);
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
/**
|
|
843
|
+
* Codex `spawn_guard.rs` (RAII): a half-made spawn is torn down on
|
|
844
|
+
* EVERY exit path, including an unexpected throw. Explicit
|
|
845
|
+
* `discardUnstartedChild` calls cover the known failure branches; this
|
|
846
|
+
* flag + finally is the backstop for anything they miss.
|
|
847
|
+
*/
|
|
848
|
+
let started = false;
|
|
849
|
+
const discardIfUnstarted = async () => {
|
|
850
|
+
if (started || !childId)
|
|
851
|
+
return;
|
|
852
|
+
started = true;
|
|
824
853
|
dropIsolated();
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
854
|
+
await discardUnstartedChild(options.runtime, childId);
|
|
855
|
+
};
|
|
856
|
+
let childId;
|
|
857
|
+
/** Fork seed size — bounds the root-evidence slice below. */
|
|
858
|
+
let seedCut = 0;
|
|
859
|
+
if (inherit) {
|
|
860
|
+
// A transport reject (not an RpcResult error) here would strand the
|
|
861
|
+
// worktree lease allocated above; catch keeps the cleanup total.
|
|
862
|
+
let forked;
|
|
863
|
+
try {
|
|
864
|
+
forked = await dispatchFaceMethod(options.runtime, "session.fork", `tool-sa-fork-${Date.now()}`, {
|
|
865
|
+
sessionId: options.parentSessionId,
|
|
866
|
+
linkMode,
|
|
835
867
|
label,
|
|
836
|
-
mode: linkMode,
|
|
837
|
-
...memberSpawn,
|
|
838
868
|
...(spawnRole && spawnRole !== "default"
|
|
839
869
|
? { role: spawnRole }
|
|
840
870
|
: {}),
|
|
841
871
|
});
|
|
842
|
-
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
dropIsolated();
|
|
875
|
+
return {
|
|
876
|
+
content: "subagent seed failed: fork request errored",
|
|
877
|
+
isError: true,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
if (!forked.result.ok) {
|
|
881
|
+
// No completed turn yet — fall back to a fresh child (DSH fork omits seed).
|
|
882
|
+
if (forked.result.error.code === "fork-unavailable") {
|
|
883
|
+
const created = await dispatchFaceMethod(options.runtime, "session.create", `tool-sa-${Date.now()}`, {
|
|
884
|
+
parentSessionId: options.parentSessionId,
|
|
885
|
+
label,
|
|
886
|
+
mode: linkMode,
|
|
887
|
+
...memberSpawn,
|
|
888
|
+
...(spawnRole && spawnRole !== "default"
|
|
889
|
+
? { role: spawnRole }
|
|
890
|
+
: {}),
|
|
891
|
+
});
|
|
892
|
+
if (!created.result.ok) {
|
|
893
|
+
dropIsolated();
|
|
894
|
+
return {
|
|
895
|
+
content: `subagent create failed: ${created.result.error.message}`,
|
|
896
|
+
isError: true,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
childId = String(created.result.value.sessionId);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
843
902
|
dropIsolated();
|
|
844
903
|
return {
|
|
845
|
-
content: `subagent
|
|
904
|
+
content: `subagent seed failed: ${forked.result.error.message}`,
|
|
846
905
|
isError: true,
|
|
847
906
|
};
|
|
848
907
|
}
|
|
849
|
-
childId = String(created.result.value.sessionId);
|
|
850
908
|
}
|
|
851
909
|
else {
|
|
910
|
+
childId = String(forked.result.value.sessionId);
|
|
911
|
+
// Durable seed boundary: every answer read of this child must
|
|
912
|
+
// slice from here, or the parent's last reply reads as its answer.
|
|
913
|
+
seedCut = Number(forked.result.value.eventCount ?? 0);
|
|
914
|
+
options.runtime.subagents.setSeedEventCount(childId, seedCut);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
const created = await dispatchFaceMethod(options.runtime, "session.create", `tool-sa-${Date.now()}`, {
|
|
919
|
+
parentSessionId: options.parentSessionId,
|
|
920
|
+
label,
|
|
921
|
+
mode: linkMode,
|
|
922
|
+
...memberSpawn,
|
|
923
|
+
...(spawnRole && spawnRole !== "default"
|
|
924
|
+
? { role: spawnRole }
|
|
925
|
+
: {}),
|
|
926
|
+
});
|
|
927
|
+
if (!created.result.ok) {
|
|
852
928
|
dropIsolated();
|
|
853
929
|
return {
|
|
854
|
-
content: `subagent
|
|
930
|
+
content: `subagent create failed: ${created.result.error.message}`,
|
|
855
931
|
isError: true,
|
|
856
932
|
};
|
|
857
933
|
}
|
|
934
|
+
childId = String(created.result.value.sessionId);
|
|
858
935
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
936
|
+
const modelPin = await applySubagentModelOverride(options.runtime, {
|
|
937
|
+
parentSessionId: options.parentSessionId,
|
|
938
|
+
childSessionId: childId,
|
|
939
|
+
...(typeof a.provider === "string" ? { provider: a.provider } : {}),
|
|
940
|
+
...(typeof a.model === "string" ? { model: a.model } : {}),
|
|
941
|
+
...(typeof a.reasoning_effort === "string"
|
|
942
|
+
? { reasoningEffort: a.reasoning_effort }
|
|
943
|
+
: {}),
|
|
944
|
+
});
|
|
945
|
+
if (!modelPin.ok) {
|
|
946
|
+
await discardIfUnstarted();
|
|
947
|
+
return { content: modelPin.message, isError: true };
|
|
865
948
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
949
|
+
if (isolated) {
|
|
950
|
+
options.runtime.sessionCwds.set(childId, isolated.path);
|
|
951
|
+
// Worktree bind failure must not leak the child: swallow and let the
|
|
952
|
+
// prompt below decide the spawn's fate (or the catch backstop).
|
|
953
|
+
await Promise.resolve(options.runtime.invalidateAgent?.(childId)).catch(() => undefined);
|
|
954
|
+
}
|
|
955
|
+
const childPrompt = applySubagentSpawnPreamble({
|
|
956
|
+
prompt: taskBody,
|
|
869
957
|
parentSessionId: options.parentSessionId,
|
|
870
|
-
|
|
958
|
+
childSessionId: childId,
|
|
871
959
|
mode: linkMode,
|
|
872
|
-
|
|
873
|
-
...(spawnRole
|
|
874
|
-
|
|
960
|
+
label,
|
|
961
|
+
...(spawnRole ? { role: spawnRole } : {}),
|
|
962
|
+
inheritContext: inherit,
|
|
963
|
+
cwd: isolated?.path ?? parentCwd,
|
|
964
|
+
isolatedWorktree: isolated !== null,
|
|
965
|
+
...(rosterMember
|
|
966
|
+
? { memberId: rosterMember.id, inject: rosterMember.inject }
|
|
875
967
|
: {}),
|
|
968
|
+
// The human's own asks: a child that only sees the parent's paraphrase
|
|
969
|
+
// drifts from what was actually authorized (Codex
|
|
970
|
+
// `control/user_authorization.rs`). A forked child already carries
|
|
971
|
+
// these in its seeded transcript, so only add them beyond the cut.
|
|
972
|
+
...(() => {
|
|
973
|
+
const block = rootUserAuthorizationBlock({
|
|
974
|
+
parentEvents: readSessionEvents(options.runtime.store, options.parentSessionId),
|
|
975
|
+
...(seedCut > 0 ? { sinceEventCount: seedCut } : {}),
|
|
976
|
+
});
|
|
977
|
+
return block ? { userAuthorization: block } : {};
|
|
978
|
+
})(),
|
|
876
979
|
});
|
|
877
|
-
|
|
878
|
-
|
|
980
|
+
const task = openTask(childId);
|
|
981
|
+
const worktreeLine = reclaimIsolated;
|
|
982
|
+
// A transport-level reject (not an RpcResult error) must not leak the
|
|
983
|
+
// child: the catch backstop tears the half-made spawn down.
|
|
984
|
+
let prompted;
|
|
985
|
+
try {
|
|
986
|
+
prompted = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-p-${childId}`, {
|
|
987
|
+
sessionId: childId,
|
|
988
|
+
mode: "queue",
|
|
989
|
+
content: [{ type: "text", text: childPrompt }],
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
await discardIfUnstarted();
|
|
994
|
+
const extra = worktreeLine(false);
|
|
995
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
879
996
|
return {
|
|
880
|
-
content: `subagent
|
|
997
|
+
content: [`subagent prompt failed: ${message}`, extra]
|
|
998
|
+
.filter(Boolean)
|
|
999
|
+
.join("\n"),
|
|
881
1000
|
isError: true,
|
|
882
1001
|
};
|
|
883
1002
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1003
|
+
if (!prompted.result.ok) {
|
|
1004
|
+
await discardIfUnstarted();
|
|
1005
|
+
const extra = worktreeLine(false);
|
|
1006
|
+
return {
|
|
1007
|
+
content: [`subagent prompt failed: ${prompted.result.error.message}`, extra]
|
|
1008
|
+
.filter(Boolean)
|
|
1009
|
+
.join("\n"),
|
|
1010
|
+
isError: true,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
// Spawn accepted — the child is no longer a half-made shell.
|
|
1014
|
+
started = true;
|
|
1015
|
+
// Drain (or busy latch) now counts in active; drop the pending slot so
|
|
1016
|
+
// a sibling parallel `subagent` is not double-charged during wait.
|
|
1017
|
+
dropSpawnSlot();
|
|
1018
|
+
if (background) {
|
|
1019
|
+
return {
|
|
1020
|
+
content: [
|
|
1021
|
+
`Started background subagent \`${childId}\` (${label}).`,
|
|
1022
|
+
`task ${task.id}` +
|
|
1023
|
+
(spawnRole && spawnRole !== "default" ? ` · role ${spawnRole}` : ""),
|
|
1024
|
+
inherit
|
|
1025
|
+
? "Seeded with this session's completed turns (open turn excluded)."
|
|
1026
|
+
: undefined,
|
|
1027
|
+
outputSchema
|
|
1028
|
+
? "OUTPUT CONTRACT attached; result will be schema-checked on idle."
|
|
1029
|
+
: undefined,
|
|
1030
|
+
childResumeHint(childId),
|
|
1031
|
+
"Use followup_task to wake a new task, send_message (delivery queue|steer) to continue, wait_agent for results, interrupt_agent (takeover true to pause for human) to stop, analytics for quota.",
|
|
1032
|
+
"Keep working; do not busy-poll.",
|
|
1033
|
+
worktreeLine(true) || undefined,
|
|
1034
|
+
]
|
|
1035
|
+
.filter(Boolean)
|
|
1036
|
+
.join("\n"),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
await waitDrainIdle(options.runtime, childId, signal, foregroundWaitMs);
|
|
1041
|
+
}
|
|
1042
|
+
catch (err) {
|
|
1043
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1044
|
+
const timedOut = /timed out waiting/i.test(message);
|
|
1045
|
+
// Salvage before cancelling: `session.cancel` drops the chain, so a
|
|
1046
|
+
// child that had already written its answer would bill the parent for
|
|
1047
|
+
// nothing. A child that produced nothing is stuck, so it still gets
|
|
1048
|
+
// cancelled instead of left dangling.
|
|
1049
|
+
const owned = childOwnedEvents(options.runtime, childId);
|
|
1050
|
+
const salvaged = lastAssistantBodyText(owned);
|
|
1051
|
+
const retrying = timedOut
|
|
1052
|
+
? lastModelRetryNotice(owned)
|
|
1053
|
+
: undefined;
|
|
1054
|
+
if (!salvaged) {
|
|
1055
|
+
await dispatchFaceMethod(options.runtime, "session.cancel", `tool-sa-c-${childId}`,
|
|
1056
|
+
// `parent`, not the default `user`: a parent that gave up waiting
|
|
1057
|
+
// is not a human hitting Stop. The old default stamped
|
|
1058
|
+
// turn/end `reason: user`, so every timed-out child read as "stopped
|
|
1059
|
+
// by the user" in the child log and in the completion notice.
|
|
1060
|
+
{ sessionId: childId, cause: { kind: "parent" } }).catch(() => undefined);
|
|
1061
|
+
}
|
|
1062
|
+
options.runtime.agentTeamTasks.complete(task.id, {
|
|
1063
|
+
ok: false,
|
|
1064
|
+
preview: message,
|
|
926
1065
|
});
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
.
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
try {
|
|
987
|
-
await waitDrainIdle(options.runtime, childId, signal, foregroundWaitMs);
|
|
988
|
-
}
|
|
989
|
-
catch (err) {
|
|
990
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
991
|
-
const timedOut = /timed out waiting/i.test(message);
|
|
992
|
-
// Salvage before cancelling: `session.cancel` drops the chain, so a
|
|
993
|
-
// child that had already written its answer would bill the parent for
|
|
994
|
-
// nothing. A child that produced nothing is stuck, so it still gets
|
|
995
|
-
// cancelled instead of left dangling.
|
|
996
|
-
const owned = childOwnedEvents(options.runtime, childId);
|
|
997
|
-
const salvaged = lastAssistantBodyText(owned);
|
|
998
|
-
const retrying = timedOut
|
|
999
|
-
? lastModelRetryNotice(owned)
|
|
1000
|
-
: undefined;
|
|
1001
|
-
if (!salvaged) {
|
|
1002
|
-
await dispatchFaceMethod(options.runtime, "session.cancel", `tool-sa-c-${childId}`,
|
|
1003
|
-
// `parent`, not the default `user`: a parent that gave up waiting
|
|
1004
|
-
// is not a human hitting Stop. The old default stamped
|
|
1005
|
-
// turn/end `reason: user`, so every timed-out child read as "stopped
|
|
1006
|
-
// by the user" in the child log and in the completion notice.
|
|
1007
|
-
{ sessionId: childId, cause: { kind: "parent" } }).catch(() => undefined);
|
|
1066
|
+
const extra = worktreeLine(false);
|
|
1067
|
+
return {
|
|
1068
|
+
content: [
|
|
1069
|
+
salvaged
|
|
1070
|
+
? `${message}, but the child finished work before the wait expired — its last answer is salvaged below.`
|
|
1071
|
+
: `subagent failed: ${message}`,
|
|
1072
|
+
salvaged || undefined,
|
|
1073
|
+
timedOut ? childResumeHint(childId) : undefined,
|
|
1074
|
+
retrying,
|
|
1075
|
+
extra,
|
|
1076
|
+
]
|
|
1077
|
+
.filter(Boolean)
|
|
1078
|
+
.join("\n"),
|
|
1079
|
+
isError: true,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
const all = readSessionEvents(options.runtime.store, childId);
|
|
1083
|
+
// Never return seeded parent assistant text as the child's result.
|
|
1084
|
+
const link = options.runtime.subagents.getByChild(childId);
|
|
1085
|
+
const readOwned = () => subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId));
|
|
1086
|
+
let owned = subagentOwnedEvents(link, all);
|
|
1087
|
+
let text = lastAssistantBodyText(owned);
|
|
1088
|
+
let schemaValid;
|
|
1089
|
+
let schemaErrors;
|
|
1090
|
+
if (outputSchema) {
|
|
1091
|
+
// An empty body cannot satisfy the contract either: a silent skip
|
|
1092
|
+
// would hand the parent `ok: true` with an unvalidated (missing)
|
|
1093
|
+
// answer, so treat "no text" as one more schema failure.
|
|
1094
|
+
let checked = text
|
|
1095
|
+
? validateOutputAgainstSchema(text, outputSchema)
|
|
1096
|
+
: { valid: false, errors: ["child produced no assistant text"] };
|
|
1097
|
+
if (!checked.valid) {
|
|
1098
|
+
const retry = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-schema-${childId}`, {
|
|
1099
|
+
sessionId: childId,
|
|
1100
|
+
mode: "queue",
|
|
1101
|
+
content: [
|
|
1102
|
+
{
|
|
1103
|
+
type: "text",
|
|
1104
|
+
text: buildOutputSchemaRetryMessage(checked.errors),
|
|
1105
|
+
},
|
|
1106
|
+
],
|
|
1107
|
+
});
|
|
1108
|
+
if (retry.result.ok) {
|
|
1109
|
+
try {
|
|
1110
|
+
await waitDrainIdle(options.runtime, childId, signal);
|
|
1111
|
+
}
|
|
1112
|
+
catch {
|
|
1113
|
+
/* keep first answer */
|
|
1114
|
+
}
|
|
1115
|
+
owned = readOwned();
|
|
1116
|
+
text = lastAssistantBodyText(owned) || text;
|
|
1117
|
+
checked = text
|
|
1118
|
+
? validateOutputAgainstSchema(text, outputSchema)
|
|
1119
|
+
: { valid: false, errors: ["child produced no assistant text"] };
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
schemaValid = checked.valid;
|
|
1123
|
+
schemaErrors = checked.errors;
|
|
1008
1124
|
}
|
|
1009
1125
|
options.runtime.agentTeamTasks.complete(task.id, {
|
|
1010
|
-
|
|
1011
|
-
|
|
1126
|
+
// An unvalidated answer is not a successful delegation: the task
|
|
1127
|
+
// board must not read `completed` when the contract was skipped.
|
|
1128
|
+
ok: schemaValid !== false,
|
|
1129
|
+
...(text ? { preview: text } : {}),
|
|
1130
|
+
...(schemaValid !== undefined ? { schemaValid } : {}),
|
|
1131
|
+
...(schemaErrors && schemaErrors.length > 0
|
|
1132
|
+
? { schemaErrors }
|
|
1133
|
+
: {}),
|
|
1012
1134
|
});
|
|
1013
1135
|
const extra = worktreeLine(false);
|
|
1136
|
+
if (!text) {
|
|
1137
|
+
return {
|
|
1138
|
+
content: [`(subagent ${childId} finished with no assistant text)`, extra]
|
|
1139
|
+
.filter(Boolean)
|
|
1140
|
+
.join("\n"),
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
const schemaLine = schemaValid === undefined
|
|
1144
|
+
? undefined
|
|
1145
|
+
: schemaValid
|
|
1146
|
+
? "schema_valid=true"
|
|
1147
|
+
: `schema_valid=false\n${(schemaErrors ?? []).map((e) => `- ${e}`).join("\n")}`;
|
|
1014
1148
|
return {
|
|
1015
1149
|
content: [
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
: `subagent failed: ${message}`,
|
|
1019
|
-
salvaged || undefined,
|
|
1020
|
-
timedOut ? childResumeHint(childId) : undefined,
|
|
1021
|
-
retrying,
|
|
1150
|
+
boundChildAnswer(options.parentSessionId, childId, text),
|
|
1151
|
+
schemaLine,
|
|
1022
1152
|
extra,
|
|
1023
1153
|
]
|
|
1024
1154
|
.filter(Boolean)
|
|
1025
1155
|
.join("\n"),
|
|
1026
|
-
isError: true,
|
|
1156
|
+
...(schemaValid === false ? { isError: true } : {}),
|
|
1027
1157
|
};
|
|
1028
1158
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const link = options.runtime.subagents.getByChild(childId);
|
|
1032
|
-
const readOwned = () => subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId));
|
|
1033
|
-
let owned = subagentOwnedEvents(link, all);
|
|
1034
|
-
let text = lastAssistantBodyText(owned);
|
|
1035
|
-
let schemaValid;
|
|
1036
|
-
let schemaErrors;
|
|
1037
|
-
if (outputSchema) {
|
|
1038
|
-
// An empty body cannot satisfy the contract either: a silent skip
|
|
1039
|
-
// would hand the parent `ok: true` with an unvalidated (missing)
|
|
1040
|
-
// answer, so treat "no text" as one more schema failure.
|
|
1041
|
-
let checked = text
|
|
1042
|
-
? validateOutputAgainstSchema(text, outputSchema)
|
|
1043
|
-
: { valid: false, errors: ["child produced no assistant text"] };
|
|
1044
|
-
if (!checked.valid) {
|
|
1045
|
-
const retry = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-schema-${childId}`, {
|
|
1046
|
-
sessionId: childId,
|
|
1047
|
-
mode: "queue",
|
|
1048
|
-
content: [
|
|
1049
|
-
{
|
|
1050
|
-
type: "text",
|
|
1051
|
-
text: buildOutputSchemaRetryMessage(checked.errors),
|
|
1052
|
-
},
|
|
1053
|
-
],
|
|
1054
|
-
});
|
|
1055
|
-
if (retry.result.ok) {
|
|
1056
|
-
try {
|
|
1057
|
-
await waitDrainIdle(options.runtime, childId, signal);
|
|
1058
|
-
}
|
|
1059
|
-
catch {
|
|
1060
|
-
/* keep first answer */
|
|
1061
|
-
}
|
|
1062
|
-
owned = readOwned();
|
|
1063
|
-
text = lastAssistantBodyText(owned) || text;
|
|
1064
|
-
checked = text
|
|
1065
|
-
? validateOutputAgainstSchema(text, outputSchema)
|
|
1066
|
-
: { valid: false, errors: ["child produced no assistant text"] };
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
schemaValid = checked.valid;
|
|
1070
|
-
schemaErrors = checked.errors;
|
|
1071
|
-
}
|
|
1072
|
-
options.runtime.agentTeamTasks.complete(task.id, {
|
|
1073
|
-
// An unvalidated answer is not a successful delegation: the task
|
|
1074
|
-
// board must not read `completed` when the contract was skipped.
|
|
1075
|
-
ok: schemaValid !== false,
|
|
1076
|
-
...(text ? { preview: text } : {}),
|
|
1077
|
-
...(schemaValid !== undefined ? { schemaValid } : {}),
|
|
1078
|
-
...(schemaErrors && schemaErrors.length > 0
|
|
1079
|
-
? { schemaErrors }
|
|
1080
|
-
: {}),
|
|
1081
|
-
});
|
|
1082
|
-
const extra = worktreeLine(false);
|
|
1083
|
-
if (!text) {
|
|
1084
|
-
return {
|
|
1085
|
-
content: [`(subagent ${childId} finished with no assistant text)`, extra]
|
|
1086
|
-
.filter(Boolean)
|
|
1087
|
-
.join("\n"),
|
|
1088
|
-
};
|
|
1159
|
+
finally {
|
|
1160
|
+
dropSpawnSlot();
|
|
1089
1161
|
}
|
|
1090
|
-
const schemaLine = schemaValid === undefined
|
|
1091
|
-
? undefined
|
|
1092
|
-
: schemaValid
|
|
1093
|
-
? "schema_valid=true"
|
|
1094
|
-
: `schema_valid=false\n${(schemaErrors ?? []).map((e) => `- ${e}`).join("\n")}`;
|
|
1095
|
-
return {
|
|
1096
|
-
content: [
|
|
1097
|
-
boundChildAnswer(options.parentSessionId, childId, text),
|
|
1098
|
-
schemaLine,
|
|
1099
|
-
extra,
|
|
1100
|
-
]
|
|
1101
|
-
.filter(Boolean)
|
|
1102
|
-
.join("\n"),
|
|
1103
|
-
...(schemaValid === false ? { isError: true } : {}),
|
|
1104
|
-
};
|
|
1105
1162
|
},
|
|
1106
1163
|
};
|
|
1107
1164
|
}
|