@xrkseek/harness 0.5.9 → 0.5.11
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 +4 -3
- package/node_modules/@xrkseek/core-agent-loop/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-agent-loop/dist/index.js +25 -60
- 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 +16 -3
- 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 +24 -8
- package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.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 +27 -1
- package/node_modules/@xrkseek/core-session/dist/sqlite-store.js.map +1 -1
- package/node_modules/@xrkseek/core-tools/dist/index.d.ts +1 -1
- package/node_modules/@xrkseek/core-tools/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-tools/dist/index.js +1 -1
- package/node_modules/@xrkseek/core-tools/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.d.ts +9 -0
- package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.d.ts.map +1 -1
- package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.js +45 -12
- package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.js.map +1 -1
- package/node_modules/@xrkseek/exec-fs/dist/index.d.ts +9 -1
- package/node_modules/@xrkseek/exec-fs/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-fs/dist/index.js +21 -14
- package/node_modules/@xrkseek/exec-fs/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/exec-fs/dist/paths.d.ts +11 -0
- package/node_modules/@xrkseek/exec-fs/dist/paths.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-fs/dist/paths.js +40 -0
- package/node_modules/@xrkseek/exec-fs/dist/paths.js.map +1 -1
- package/node_modules/@xrkseek/exec-memory/dist/index.d.ts +2 -2
- package/node_modules/@xrkseek/exec-memory/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-memory/dist/index.js +2 -2
- package/node_modules/@xrkseek/exec-memory/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/exec-memory/dist/store.d.ts +18 -0
- package/node_modules/@xrkseek/exec-memory/dist/store.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-memory/dist/store.js +108 -7
- package/node_modules/@xrkseek/exec-memory/dist/store.js.map +1 -1
- package/node_modules/@xrkseek/exec-memory/dist/tools.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-memory/dist/tools.js +17 -1
- package/node_modules/@xrkseek/exec-memory/dist/tools.js.map +1 -1
- package/node_modules/@xrkseek/exec-memory/dist/write-path.d.ts +2 -0
- package/node_modules/@xrkseek/exec-memory/dist/write-path.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-memory/dist/write-path.js +28 -3
- package/node_modules/@xrkseek/exec-memory/dist/write-path.js.map +1 -1
- package/node_modules/@xrkseek/exec-sandbox/dist/resolve.d.ts +7 -4
- package/node_modules/@xrkseek/exec-sandbox/dist/resolve.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-sandbox/dist/resolve.js +4 -0
- package/node_modules/@xrkseek/exec-sandbox/dist/resolve.js.map +1 -1
- package/node_modules/@xrkseek/exec-sandbox/dist/service.d.ts +7 -0
- package/node_modules/@xrkseek/exec-sandbox/dist/service.d.ts.map +1 -1
- package/node_modules/@xrkseek/exec-sandbox/dist/service.js +18 -8
- package/node_modules/@xrkseek/exec-sandbox/dist/service.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/preset.d.ts +16 -0
- package/node_modules/@xrkseek/preset-harness/dist/preset.d.ts.map +1 -1
- package/node_modules/@xrkseek/preset-harness/dist/preset.js +13 -2
- 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 +4 -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/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/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/model-route.d.ts +5 -0
- package/node_modules/@xrkseek/server-face/dist/adapt/model-route.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/model-route.js +16 -0
- package/node_modules/@xrkseek/server-face/dist/adapt/model-route.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/subagent-notice.d.ts +32 -0
- package/node_modules/@xrkseek/server-face/dist/adapt/subagent-notice.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/adapt/subagent-notice.js +87 -0
- package/node_modules/@xrkseek/server-face/dist/adapt/subagent-notice.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-roster-store.d.ts +102 -0
- package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js +495 -0
- package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.d.ts +12 -0
- package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.d.ts.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.js +141 -0
- package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.js.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/agent-team-roles.d.ts +46 -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 +150 -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/capture-member-playbook.d.ts +6 -0
- package/node_modules/@xrkseek/server-face/dist/capture-member-playbook.d.ts.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/capture-member-playbook.js +43 -0
- package/node_modules/@xrkseek/server-face/dist/capture-member-playbook.js.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/context.d.ts +21 -4
- package/node_modules/@xrkseek/server-face/dist/context.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/dispatch.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/dispatch.js +19 -0
- package/node_modules/@xrkseek/server-face/dist/dispatch.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/fork-cut.d.ts +3 -2
- package/node_modules/@xrkseek/server-face/dist/fork-cut.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/fork-cut.js +58 -20
- package/node_modules/@xrkseek/server-face/dist/fork-cut.js.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 +88 -18
- 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 +20 -0
- package/node_modules/@xrkseek/server-face/dist/handlers/subagent.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/handlers/team.d.ts +11 -0
- package/node_modules/@xrkseek/server-face/dist/handlers/team.d.ts.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/handlers/team.js +268 -0
- package/node_modules/@xrkseek/server-face/dist/handlers/team.js.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/index.d.ts +6 -2
- package/node_modules/@xrkseek/server-face/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/index.js +5 -1
- package/node_modules/@xrkseek/server-face/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/model-catalog.d.ts +17 -1
- package/node_modules/@xrkseek/server-face/dist/model-catalog.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/model-catalog.js +46 -1
- package/node_modules/@xrkseek/server-face/dist/model-catalog.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/presence-tools.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/presence-tools.js +15 -6
- package/node_modules/@xrkseek/server-face/dist/presence-tools.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/presets-catalog.d.ts +16 -0
- package/node_modules/@xrkseek/server-face/dist/presets-catalog.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/presets-catalog.js +5 -0
- package/node_modules/@xrkseek/server-face/dist/presets-catalog.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/projections/units/turn-outline.d.ts +8 -2
- package/node_modules/@xrkseek/server-face/dist/projections/units/turn-outline.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/projections/units/turn-outline.js +14 -12
- package/node_modules/@xrkseek/server-face/dist/projections/units/turn-outline.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 +96 -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-model-store.d.ts +2 -0
- package/node_modules/@xrkseek/server-face/dist/session-model-store.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-model-store.js +14 -0
- package/node_modules/@xrkseek/server-face/dist/session-model-store.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-status.d.ts +16 -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 +47 -6
- package/node_modules/@xrkseek/server-face/dist/session-status.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/session-thread-publish.d.ts +17 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-publish.d.ts.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-publish.js +34 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-publish.js.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-store.d.ts +38 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-store.d.ts.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-store.js +207 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-store.js.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-tools.d.ts +12 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-tools.d.ts.map +1 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-tools.js +223 -0
- package/node_modules/@xrkseek/server-face/dist/session-thread-tools.js.map +1 -0
- 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-document.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/settings-document.js +10 -11
- package/node_modules/@xrkseek/server-face/dist/settings-document.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 +17 -10
- package/node_modules/@xrkseek/server-face/dist/settings-schemas.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/subagent-registry.d.ts +46 -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 +111 -8
- package/node_modules/@xrkseek/server-face/dist/subagent-registry.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/subagent-tools.d.ts +21 -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 +422 -85
- package/node_modules/@xrkseek/server-face/dist/subagent-tools.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/types.d.ts +9 -0
- package/node_modules/@xrkseek/server-face/dist/types.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/wire/paths.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/wire/paths.js +2 -0
- package/node_modules/@xrkseek/server-face/dist/wire/paths.js.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/workspace-face.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-face/dist/workspace-face.js +7 -0
- package/node_modules/@xrkseek/server-face/dist/workspace-face.js.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/agent-cache.d.ts +5 -1
- package/node_modules/@xrkseek/server-host/dist/agent-cache.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/agent-cache.js +29 -3
- package/node_modules/@xrkseek/server-host/dist/agent-cache.js.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/index.d.ts +21 -0
- package/node_modules/@xrkseek/server-host/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-host/dist/index.js +108 -21
- package/node_modules/@xrkseek/server-host/dist/index.js.map +1 -1
- package/node_modules/@xrkseek/server-http/dist/static.d.ts.map +1 -1
- package/node_modules/@xrkseek/server-http/dist/static.js +45 -5
- package/node_modules/@xrkseek/server-http/dist/static.js.map +1 -1
- package/node_modules/@xrkseek/workspace/dist/ecosystem-instructions.d.ts +8 -0
- package/node_modules/@xrkseek/workspace/dist/ecosystem-instructions.d.ts.map +1 -1
- package/node_modules/@xrkseek/workspace/dist/ecosystem-instructions.js +6 -2
- package/node_modules/@xrkseek/workspace/dist/ecosystem-instructions.js.map +1 -1
- package/node_modules/@xrkseek/workspace/dist/index.d.ts +16 -0
- package/node_modules/@xrkseek/workspace/dist/index.d.ts.map +1 -1
- package/node_modules/@xrkseek/workspace/dist/index.js +17 -8
- package/node_modules/@xrkseek/workspace/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { SUBAGENT_ROUTING_PROMPT_TEXT } from "@xrkseek/core-tools";
|
|
2
2
|
import { listPendingAdmits, readSessionEvents, } from "@xrkseek/core-session";
|
|
3
3
|
import { dispatchFaceMethod } from "./dispatch.js";
|
|
4
|
-
import { lastAssistantBodyText } from "./adapt/subagent-notice.js";
|
|
4
|
+
import { formatChildOutcome, lastAssistantBodyText, lastModelRetryNotice, } 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";
|
|
11
|
+
import { canvasWorkspaceIdForSession } from "./canvas-tools.js";
|
|
12
|
+
import { formatRosterCatalog } from "./agent-roster-store.js";
|
|
10
13
|
import { ExternalAgentError, parseExternalAgentKind, parseExternalAgentProduct, runExternalAgentTurn, supportsExternalContinuable, startExternalContinuable, isChildSessionActive, } from "./external-agent-runtime.js";
|
|
11
14
|
import { createSubagentWorktree, finalizeSubagentWorktree, formatWorktreeResult, worktreeContextNote, worktreeSkippedForRemote, } from "./subagent-worktree.js";
|
|
12
|
-
import { resolveSessionModelSelection } from "./model-catalog.js";
|
|
15
|
+
import { resolveSessionModelSelection, resolveSubagentModelSetting, } from "./model-catalog.js";
|
|
13
16
|
import { selectSessionModel } from "./select-session-model.js";
|
|
14
17
|
import { appendOutputContract, buildOutputSchemaRetryMessage, coerceOutputSchema, validateOutputAgainstSchema, } from "./agent-team-output.js";
|
|
15
|
-
import {
|
|
18
|
+
import { applySubagentSpawnPreamble, parseAgentTeamSpawnRole, rootUserAuthorizationBlock, } from "./agent-team-roles.js";
|
|
16
19
|
import { isAgentTeamRole, } from "./agent-team-graph.js";
|
|
17
20
|
import { bindRalphTool } from "./ralph-tool.js";
|
|
18
21
|
const FOREGROUND_WAIT_MS = 10 * 60 * 1000;
|
|
@@ -106,19 +109,45 @@ async function waitDrainIdle(runtime, sessionId, signal, timeoutMs = FOREGROUND_
|
|
|
106
109
|
// for its child would otherwise hang the tool batch forever. The join
|
|
107
110
|
// stays attached in the background — the caller cancels the child right
|
|
108
111
|
// after this rejects, which is what actually drains it.
|
|
109
|
-
|
|
112
|
+
//
|
|
113
|
+
// The race must also carry the timeout: latch.run() joins until the
|
|
114
|
+
// child's chain *actually* settles, so a child stuck on a tool that
|
|
115
|
+
// ignores abort pins the parent's turn for as long as it hangs. The
|
|
116
|
+
// timeout used to apply only to the polling fallback below — i.e. never
|
|
117
|
+
// on the Host path, which always has run().
|
|
118
|
+
await raceDeadline(abortable(run(sessionId), signal), timeoutMs, sessionId);
|
|
110
119
|
return;
|
|
111
120
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
// Same deadline semantics as the hub.run branch above — one throw site, one
|
|
122
|
+
// message, so a caller (or a log grep) cannot tell the two paths apart.
|
|
123
|
+
await raceDeadline((async () => {
|
|
124
|
+
while (runtime.drain.isActive(sessionId)) {
|
|
125
|
+
if (signal?.aborted) {
|
|
126
|
+
throw new DOMException("aborted", "AbortError");
|
|
127
|
+
}
|
|
128
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
119
129
|
}
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
})(), timeoutMs, sessionId);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Reject once `timeoutMs` elapses, naming the session so the failure reads
|
|
134
|
+
* like the polling branch's. The wrapped promise keeps running — the caller
|
|
135
|
+
* cancels the child right after, which is what actually drains it.
|
|
136
|
+
*/
|
|
137
|
+
function raceDeadline(promise, timeoutMs, sessionId) {
|
|
138
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
139
|
+
return promise;
|
|
140
|
+
let timer;
|
|
141
|
+
const timedOut = new Promise((_, reject) => {
|
|
142
|
+
timer = setTimeout(() => reject(new Error(`subagent timed out waiting for session ${sessionId}`)), timeoutMs);
|
|
143
|
+
});
|
|
144
|
+
return Promise.race([
|
|
145
|
+
promise.finally(() => {
|
|
146
|
+
if (timer !== undefined)
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
}),
|
|
149
|
+
timedOut,
|
|
150
|
+
]);
|
|
122
151
|
}
|
|
123
152
|
/** Reject as soon as `signal` aborts; the wrapped promise keeps running. */
|
|
124
153
|
function abortable(promise, signal) {
|
|
@@ -146,6 +175,27 @@ function abortError(reason) {
|
|
|
146
175
|
err.name = "AbortError";
|
|
147
176
|
return err;
|
|
148
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* How a parent gets work back out of a child that outlived the foreground
|
|
180
|
+
* wait. One place on purpose: background spawn, timeout salvage and failure
|
|
181
|
+
* replies must not drift apart, or a parent learns the recovery verbs only
|
|
182
|
+
* on the happy path.
|
|
183
|
+
*/
|
|
184
|
+
export function childResumeHint(childId) {
|
|
185
|
+
return (`Child session \`${childId}\` is not gone: \`wait_agent\` keeps waiting for it, ` +
|
|
186
|
+
`\`session_read\` replays its history, \`session_trace\` shows its lineage, ` +
|
|
187
|
+
`\`list_agents\` its state, and \`followup_task\` wakes a new turn on it. Read it ` +
|
|
188
|
+
`before spawning a replacement so the new task does not repeat work it already did.`);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The slice of a child's log the parent owns: seeded parent text never counts.
|
|
192
|
+
* One read, two projections (`lastAssistantBodyText` for the salvaged answer,
|
|
193
|
+
* `lastModelRetryNotice` for what its model request was doing).
|
|
194
|
+
*/
|
|
195
|
+
export function childOwnedEvents(runtime, childId) {
|
|
196
|
+
const link = runtime.subagents.getByChild(childId);
|
|
197
|
+
return subagentOwnedEvents(link, readSessionEvents(runtime.store, childId));
|
|
198
|
+
}
|
|
149
199
|
function formatSubagentAbortReason(reason) {
|
|
150
200
|
if (reason === undefined || reason === null)
|
|
151
201
|
return "aborted";
|
|
@@ -193,29 +243,88 @@ function countActiveChildren(runtime, parentSessionId) {
|
|
|
193
243
|
}
|
|
194
244
|
return n;
|
|
195
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Undo a half-made spawn. `session.create` / `session.fork` already made the
|
|
248
|
+
* child durable and registered its link, so any failure between there and a
|
|
249
|
+
* successful `session.prompt` would otherwise leave an empty session in the
|
|
250
|
+
* sidebar and a permanent entry in the catalog / quota counts.
|
|
251
|
+
*/
|
|
252
|
+
async function discardUnstartedChild(runtime, childSessionId) {
|
|
253
|
+
runtime.subagents.detach(childSessionId);
|
|
254
|
+
runtime.agentTeams.removeNode(childSessionId);
|
|
255
|
+
runtime.agentTeamTasks.forgetByChild(childSessionId);
|
|
256
|
+
runtime.sessionCwds.delete(childSessionId);
|
|
257
|
+
try {
|
|
258
|
+
await dispatchFaceMethod(runtime, "session.cancel", `tool-sa-discard-${childSessionId}`, { sessionId: childSessionId, cascade: true });
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* best effort: the log is dropped either way */
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
runtime.store.delete?.(childSessionId);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
/* store without delete / already gone */
|
|
268
|
+
}
|
|
269
|
+
await Promise.resolve(runtime.invalidateAgent?.(childSessionId)).catch(() => undefined);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Reject a caller-supplied `task_id` another child already owns. Checked
|
|
273
|
+
* *before* the child exists: rebinding the card would misdirect that child's
|
|
274
|
+
* later idle notice (`completeByChild` keys on childSessionId).
|
|
275
|
+
*/
|
|
276
|
+
function taskIdConflict(runtime, taskIdHint) {
|
|
277
|
+
if (!taskIdHint)
|
|
278
|
+
return undefined;
|
|
279
|
+
const boundChild = runtime.agentTeamTasks.get(taskIdHint)?.childSessionId;
|
|
280
|
+
return boundChild
|
|
281
|
+
? `subagent: task_id ${taskIdHint} is already bound to child ${boundChild}; use a different task_id`
|
|
282
|
+
: undefined;
|
|
283
|
+
}
|
|
196
284
|
/**
|
|
197
285
|
* Optional LLM target for a freshly created child.
|
|
198
|
-
*
|
|
286
|
+
*
|
|
287
|
+
* Precedence: the call's own args → the fleet-wide `agent-loop.subagentModel`
|
|
288
|
+
* setting → the parent's own route. `model` alone inherits the parent's current
|
|
289
|
+
* provider; `provider` alone is rejected.
|
|
199
290
|
*/
|
|
200
291
|
async function applySubagentModelOverride(runtime, args) {
|
|
201
292
|
const providerRaw = args.provider?.trim() ?? "";
|
|
202
293
|
const modelRaw = args.model?.trim() ?? "";
|
|
203
294
|
const effortRaw = args.reasoningEffort?.trim() ?? "";
|
|
204
|
-
|
|
295
|
+
// Refuse loudly rather than silently: an explicit model on an external
|
|
296
|
+
// child would otherwise read as honored when the subprocess ignores it.
|
|
297
|
+
if (args.skipExternal && (providerRaw || modelRaw || effortRaw)) {
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
message: "subagent: model / provider / reasoning_effort do not apply to an external runtime " +
|
|
301
|
+
"(acp / app-server / claude-code) — the child CLI picks its own model. " +
|
|
302
|
+
"Drop the argument, or use runtime=in-process.",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// No per-call pin: the setting owns the route, and an empty setting leaves
|
|
306
|
+
// the child on the parent's selection that `session.create` copied.
|
|
307
|
+
const fleet = providerRaw || modelRaw || effortRaw
|
|
308
|
+
? undefined
|
|
309
|
+
: resolveSubagentModelSetting(runtime);
|
|
310
|
+
if (!providerRaw && !modelRaw && !effortRaw && !fleet)
|
|
205
311
|
return { ok: true };
|
|
206
|
-
|
|
312
|
+
const model = modelRaw || fleet?.model || "";
|
|
313
|
+
const effort = effortRaw || fleet?.reasoningEffort || "";
|
|
314
|
+
if (!model) {
|
|
207
315
|
return {
|
|
208
316
|
ok: false,
|
|
209
317
|
message: "subagent: model is required when overriding provider or reasoning_effort",
|
|
210
318
|
};
|
|
211
319
|
}
|
|
212
320
|
const provider = providerRaw
|
|
321
|
+
|| fleet?.provider
|
|
213
322
|
|| resolveSessionModelSelection(runtime, args.parentSessionId).provider;
|
|
214
323
|
const selected = await selectSessionModel(runtime, {
|
|
215
324
|
sessionId: args.childSessionId,
|
|
216
325
|
provider,
|
|
217
|
-
model
|
|
218
|
-
...(
|
|
326
|
+
model,
|
|
327
|
+
...(effort ? { reasoningEffort: effort } : {}),
|
|
219
328
|
});
|
|
220
329
|
if (!selected.ok) {
|
|
221
330
|
return {
|
|
@@ -226,27 +335,54 @@ async function applySubagentModelOverride(runtime, args) {
|
|
|
226
335
|
return { ok: true };
|
|
227
336
|
}
|
|
228
337
|
function createSubagentTool(options) {
|
|
229
|
-
|
|
230
|
-
|
|
338
|
+
/**
|
|
339
|
+
* Ceilings for one call. Resolved live (not frozen at bind time) so the
|
|
340
|
+
* numbers `analytics` / `list_agents` report are the numbers spawn
|
|
341
|
+
* enforces — a Settings change takes effect on the next call instead of
|
|
342
|
+
* at the next agent rebuild. The bind options are a test-only override.
|
|
343
|
+
*/
|
|
344
|
+
const caps = () => {
|
|
345
|
+
const quota = resolveSubagentQuota(options.runtime, options.parentSessionId);
|
|
346
|
+
return {
|
|
347
|
+
maxDepth: options.maxDepth ?? quota.maxDepth,
|
|
348
|
+
maxActive: options.maxActiveChildren ?? quota.maxActive,
|
|
349
|
+
active: quota.active,
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
const foregroundWaitMs = options.foregroundWaitMs ?? FOREGROUND_WAIT_MS;
|
|
353
|
+
const description = "Delegate a self-contained task to a teammate subagent (separate session, same workspace as this session unless worktree:true). " +
|
|
354
|
+
"Use for focused independent work — research, a scoped implementation, analysis, or read-only review — " +
|
|
355
|
+
"so it does not consume this conversation's context. " +
|
|
356
|
+
"By default the child cannot see this chat — give a complete standalone prompt (paths, goals, constraints, persona). " +
|
|
357
|
+
"Do not tell the child to read AGENTS.md to discover who it is; Face prepends parent/child session ids, mode, role, and cwd. " +
|
|
358
|
+
"Set inherit_context true to seed the child with this session's completed turns only " +
|
|
359
|
+
"(the current in-flight turn is excluded). " +
|
|
360
|
+
"By default waits for the result (one-shot, no later human messages on that child). " +
|
|
361
|
+
"That foreground wait is bounded (10 min by default): on expiry the child is NOT discarded — " +
|
|
362
|
+
"whatever answer it already wrote is salvaged into this result and the child id stays readable " +
|
|
363
|
+
"(session_read / followup_task), so prefer run_in_background for genuinely long work instead of retrying blind. " +
|
|
364
|
+
"Set run_in_background true for a continuable child (chat companion / long task) and continue via followup_task / send_message. " +
|
|
365
|
+
"Prefer Agent Team member_id (catalog on this tool and team_list) over restating playbook or tools; " +
|
|
366
|
+
"optional role (worker|researcher|reviewer|lead) is only the fallback when no member fits; " +
|
|
367
|
+
"optional output_schema appends an OUTPUT CONTRACT and validates the final JSON; " +
|
|
368
|
+
"optional task_id / task_name register the work on the Agent Teams task board. " +
|
|
369
|
+
"Optional provider / model / reasoning_effort pin the child's LLM (model alone keeps the parent provider). " +
|
|
370
|
+
"Optional runtime: omit or in-process (default Face child); acp / app-server / claude-code spawn an external subprocess. " +
|
|
371
|
+
"acp / app-server support run_in_background + followup_task / send_message / wait_agent / interrupt_agent on the same list surface; claude-code remains one-shot print. " +
|
|
372
|
+
"The child's answer is returned inline in this tool result when short. " +
|
|
373
|
+
"Only when the UTF-8 body exceeds ~12KB does Face spill it under ~/.xrk/spill/tool-outputs/ " +
|
|
374
|
+
"and replace the body with a path + head/tail preview — then read_file that path. " +
|
|
375
|
+
"An empty or missing tool-outputs directory means nothing spilled; the full answer is already in this result (or the session event log), not on disk.";
|
|
231
376
|
return {
|
|
232
377
|
name: "subagent",
|
|
233
|
-
description
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"optional output_schema appends an OUTPUT CONTRACT and validates the final JSON; " +
|
|
242
|
-
"optional task_id / task_name register the work on the Agent Teams task board. " +
|
|
243
|
-
"Optional provider / model / reasoning_effort pin the child's LLM (model alone keeps the parent provider). " +
|
|
244
|
-
"Optional runtime: omit or in-process (default Face child); acp / app-server / claude-code spawn an external subprocess. " +
|
|
245
|
-
"acp / app-server support run_in_background + followup_task / send_message / wait_agent / interrupt_agent on the same list surface; claude-code remains one-shot print. " +
|
|
246
|
-
"The child's answer is returned inline in this tool result when short. " +
|
|
247
|
-
"Only when the UTF-8 body exceeds ~12KB does Face spill it under ~/.xrk/spill/tool-outputs/ " +
|
|
248
|
-
"and replace the body with a path + head/tail preview — then read_file that path. " +
|
|
249
|
-
"An empty or missing tool-outputs directory means nothing spilled; the full answer is already in this result (or the session event log), not on disk.",
|
|
378
|
+
description,
|
|
379
|
+
dynamicSchema: () => {
|
|
380
|
+
const ws = canvasWorkspaceIdForSession(options.runtime, options.parentSessionId);
|
|
381
|
+
const catalog = formatRosterCatalog(options.runtime.agentRoster.listVisible(ws));
|
|
382
|
+
return {
|
|
383
|
+
description: `${description}\n\nPrefer member_id from this Agent Team catalog:\n${catalog}`,
|
|
384
|
+
};
|
|
385
|
+
},
|
|
250
386
|
parameters: {
|
|
251
387
|
type: "object",
|
|
252
388
|
properties: {
|
|
@@ -256,12 +392,19 @@ function createSubagentTool(options) {
|
|
|
256
392
|
},
|
|
257
393
|
prompt: {
|
|
258
394
|
type: "string",
|
|
259
|
-
description: "Task for the subagent.
|
|
395
|
+
description: "Task for the subagent. Put the actual job here (paths, goals, constraints, any persona). " +
|
|
396
|
+
"When inherit_context is false (default), the child does not see this conversation. " +
|
|
397
|
+
"When inherit_context is true, build on the seeded completed turns. " +
|
|
398
|
+
"Face already injects parent/child session ids — do not send the child to AGENTS.md to learn its role.",
|
|
260
399
|
},
|
|
261
400
|
inherit_context: {
|
|
262
401
|
type: "boolean",
|
|
263
402
|
description: "If true, seed the child with this session's completed-turn prefix (open turn excluded). Default false. Ignored for external runtimes.",
|
|
264
403
|
},
|
|
404
|
+
member_id: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "Agent Team member id. Face applies that member's tools, inject, and playbook — the same three surfaces the parent already edits. Prefer this over restating those in prompt.",
|
|
407
|
+
},
|
|
265
408
|
run_in_background: {
|
|
266
409
|
type: "boolean",
|
|
267
410
|
description: "If true, return the child session id immediately (continuable). Default false (wait for the child's final answer). Supported for in-process and external acp / app-server (not claude-code).",
|
|
@@ -329,10 +472,11 @@ function createSubagentTool(options) {
|
|
|
329
472
|
isError: true,
|
|
330
473
|
};
|
|
331
474
|
}
|
|
332
|
-
const
|
|
333
|
-
if (!
|
|
475
|
+
const promptBare = String(a.prompt ?? "").trim();
|
|
476
|
+
if (!promptBare) {
|
|
334
477
|
return { content: "subagent: empty prompt", isError: true };
|
|
335
478
|
}
|
|
479
|
+
let prompt = promptBare;
|
|
336
480
|
const roleRaw = a.role ?? a.agent_type;
|
|
337
481
|
let spawnRole;
|
|
338
482
|
if (roleRaw !== undefined && String(roleRaw).trim()) {
|
|
@@ -344,6 +488,23 @@ function createSubagentTool(options) {
|
|
|
344
488
|
};
|
|
345
489
|
}
|
|
346
490
|
}
|
|
491
|
+
const memberId = String(a.member_id ?? "").trim();
|
|
492
|
+
let rosterMember;
|
|
493
|
+
if (memberId) {
|
|
494
|
+
const workspaceId = canvasWorkspaceIdForSession(options.runtime, options.parentSessionId);
|
|
495
|
+
rosterMember = options.runtime.agentRoster.get(workspaceId, memberId);
|
|
496
|
+
if (!rosterMember) {
|
|
497
|
+
return {
|
|
498
|
+
content: `subagent: unknown member_id "${memberId}" (team_list first)`,
|
|
499
|
+
isError: true,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
prompt = `${rosterMember.playbook.trim()}\n\nTASK:\n${promptBare}`;
|
|
503
|
+
if (spawnRole === undefined && rosterMember.role !== "default") {
|
|
504
|
+
spawnRole = rosterMember.role;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const memberSpawn = memberId ? { memberId } : {};
|
|
347
508
|
const schemaCoerce = coerceOutputSchema(a.output_schema);
|
|
348
509
|
if (schemaCoerce.error) {
|
|
349
510
|
return {
|
|
@@ -361,20 +522,35 @@ function createSubagentTool(options) {
|
|
|
361
522
|
}
|
|
362
523
|
if (runtimeKind !== "in-process") {
|
|
363
524
|
const background = a.run_in_background === true;
|
|
525
|
+
// One cwd for every external branch. `resolveSessionCwd` already
|
|
526
|
+
// falls back to the workspace root, so the child runs where the
|
|
527
|
+
// parent runs instead of always at the root (which silently broke
|
|
528
|
+
// any session that had moved into another workspace).
|
|
529
|
+
const externalCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
|
|
364
530
|
if (background && !supportsExternalContinuable(runtimeKind)) {
|
|
365
531
|
return {
|
|
366
532
|
content: "subagent: run_in_background is not supported for claude-code (print); use acp or app-server",
|
|
367
533
|
isError: true,
|
|
368
534
|
};
|
|
369
535
|
}
|
|
536
|
+
if (a.worktree === true) {
|
|
537
|
+
// Worktree handling lives in the in-process branch below; silently
|
|
538
|
+
// dropping the flag would have the child edit the parent's own
|
|
539
|
+
// checkout while the caller believed it was isolated.
|
|
540
|
+
return {
|
|
541
|
+
content: "subagent: worktree is only supported for runtime=in-process " +
|
|
542
|
+
"(an external runtime owns its own working directory).",
|
|
543
|
+
isError: true,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
370
546
|
const depth = subagentDepth(options.runtime, options.parentSessionId);
|
|
547
|
+
const { maxDepth, maxActive: maxActiveChildren, active } = caps();
|
|
371
548
|
if (depth >= maxDepth) {
|
|
372
549
|
return {
|
|
373
550
|
content: `subagent: max depth ${maxDepth} reached (current depth ${depth})`,
|
|
374
551
|
isError: true,
|
|
375
552
|
};
|
|
376
553
|
}
|
|
377
|
-
const active = countActiveChildren(options.runtime, options.parentSessionId);
|
|
378
554
|
if (active >= maxActiveChildren) {
|
|
379
555
|
return {
|
|
380
556
|
content: `subagent: max active children ${maxActiveChildren} reached (active ${active})`,
|
|
@@ -391,6 +567,10 @@ function createSubagentTool(options) {
|
|
|
391
567
|
parentSessionId: options.parentSessionId,
|
|
392
568
|
label,
|
|
393
569
|
mode: "continuable",
|
|
570
|
+
...memberSpawn,
|
|
571
|
+
...(spawnRole && spawnRole !== "default"
|
|
572
|
+
? { role: spawnRole }
|
|
573
|
+
: {}),
|
|
394
574
|
});
|
|
395
575
|
if (!created.result.ok) {
|
|
396
576
|
return {
|
|
@@ -407,11 +587,22 @@ function createSubagentTool(options) {
|
|
|
407
587
|
...(typeof a.reasoning_effort === "string"
|
|
408
588
|
? { reasoningEffort: a.reasoning_effort }
|
|
409
589
|
: {}),
|
|
590
|
+
// An ACP / app-server / claude-code child runs on ITS OWN CLI
|
|
591
|
+
// config; a Face-side pin cannot reach it. Recording one anyway
|
|
592
|
+
// made `/status` and `list_agents` report a route the subprocess
|
|
593
|
+
// never used, so the record is skipped rather than faked.
|
|
594
|
+
skipExternal: true,
|
|
410
595
|
});
|
|
411
596
|
if (!modelPin.ok) {
|
|
597
|
+
await discardUnstartedChild(options.runtime, childId);
|
|
412
598
|
return { content: modelPin.message, isError: true };
|
|
413
599
|
}
|
|
414
600
|
const taskTitle = String(a.task_name ?? "").trim() || label;
|
|
601
|
+
const taskIdConflictMsg = taskIdConflict(options.runtime, String(a.task_id ?? "").trim() || undefined);
|
|
602
|
+
if (taskIdConflictMsg) {
|
|
603
|
+
await discardUnstartedChild(options.runtime, childId);
|
|
604
|
+
return { content: taskIdConflictMsg, isError: true };
|
|
605
|
+
}
|
|
415
606
|
const task = options.runtime.agentTeamTasks.open({
|
|
416
607
|
parentSessionId: options.parentSessionId,
|
|
417
608
|
title: taskTitle,
|
|
@@ -430,7 +621,7 @@ function createSubagentTool(options) {
|
|
|
430
621
|
runtime: options.runtime,
|
|
431
622
|
faceSessionId: childId,
|
|
432
623
|
kind: runtimeKind,
|
|
433
|
-
cwd:
|
|
624
|
+
cwd: externalCwd,
|
|
434
625
|
prompt,
|
|
435
626
|
background: true,
|
|
436
627
|
...(signal ? { signal } : {}),
|
|
@@ -454,7 +645,7 @@ function createSubagentTool(options) {
|
|
|
454
645
|
}
|
|
455
646
|
const result = await runExternalAgentTurn({
|
|
456
647
|
kind: runtimeKind,
|
|
457
|
-
cwd:
|
|
648
|
+
cwd: externalCwd,
|
|
458
649
|
prompt,
|
|
459
650
|
...(signal ? { signal } : {}),
|
|
460
651
|
...(options.externalEnv ? { env: options.externalEnv } : {}),
|
|
@@ -477,13 +668,13 @@ function createSubagentTool(options) {
|
|
|
477
668
|
}
|
|
478
669
|
}
|
|
479
670
|
const depth = subagentDepth(options.runtime, options.parentSessionId);
|
|
671
|
+
const { maxDepth, maxActive: maxActiveChildren, active } = caps();
|
|
480
672
|
if (depth >= maxDepth) {
|
|
481
673
|
return {
|
|
482
674
|
content: `subagent: max depth ${maxDepth} reached (current depth ${depth})`,
|
|
483
675
|
isError: true,
|
|
484
676
|
};
|
|
485
677
|
}
|
|
486
|
-
const active = countActiveChildren(options.runtime, options.parentSessionId);
|
|
487
678
|
if (active >= maxActiveChildren) {
|
|
488
679
|
return {
|
|
489
680
|
content: `subagent: max active children ${maxActiveChildren} reached (active ${active})`,
|
|
@@ -497,6 +688,14 @@ function createSubagentTool(options) {
|
|
|
497
688
|
(background ? "subagent" : "subagent-task");
|
|
498
689
|
const linkMode = background ? "continuable" : "one-shot";
|
|
499
690
|
const parentCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
|
|
691
|
+
const taskTitle = String(a.task_name ?? "").trim() || label;
|
|
692
|
+
const taskIdHint = String(a.task_id ?? "").trim() || undefined;
|
|
693
|
+
// Before anything is allocated: reusing a task_id another child owns
|
|
694
|
+
// must not leave a worktree or a session behind.
|
|
695
|
+
const taskIdError = taskIdConflict(options.runtime, taskIdHint);
|
|
696
|
+
if (taskIdError) {
|
|
697
|
+
return { content: taskIdError, isError: true };
|
|
698
|
+
}
|
|
500
699
|
let isolated = null;
|
|
501
700
|
let managedLease = null;
|
|
502
701
|
let worktreeSkip = "";
|
|
@@ -528,15 +727,13 @@ function createSubagentTool(options) {
|
|
|
528
727
|
}
|
|
529
728
|
}
|
|
530
729
|
}
|
|
531
|
-
let
|
|
730
|
+
let taskBody = prompt;
|
|
532
731
|
if (outputSchema) {
|
|
533
|
-
|
|
732
|
+
taskBody = appendOutputContract(taskBody, outputSchema);
|
|
534
733
|
}
|
|
535
|
-
|
|
536
|
-
? `${
|
|
537
|
-
:
|
|
538
|
-
const taskTitle = String(a.task_name ?? "").trim() || label;
|
|
539
|
-
const taskIdHint = String(a.task_id ?? "").trim() || undefined;
|
|
734
|
+
taskBody = isolated
|
|
735
|
+
? `${taskBody}\n\n${worktreeContextNote(isolated)}`
|
|
736
|
+
: taskBody;
|
|
540
737
|
const openTask = (childSessionId) => {
|
|
541
738
|
const task = options.runtime.agentTeamTasks.open({
|
|
542
739
|
parentSessionId: options.parentSessionId,
|
|
@@ -591,14 +788,44 @@ function createSubagentTool(options) {
|
|
|
591
788
|
finalizeSubagentWorktree(isolated);
|
|
592
789
|
}
|
|
593
790
|
};
|
|
791
|
+
/**
|
|
792
|
+
* Codex `spawn_guard.rs` (RAII): a half-made spawn is torn down on
|
|
793
|
+
* EVERY exit path, including an unexpected throw. Explicit
|
|
794
|
+
* `discardUnstartedChild` calls cover the known failure branches; this
|
|
795
|
+
* flag + finally is the backstop for anything they miss.
|
|
796
|
+
*/
|
|
797
|
+
let started = false;
|
|
798
|
+
const discardIfUnstarted = async () => {
|
|
799
|
+
if (started || !childId)
|
|
800
|
+
return;
|
|
801
|
+
started = true;
|
|
802
|
+
dropIsolated();
|
|
803
|
+
await discardUnstartedChild(options.runtime, childId);
|
|
804
|
+
};
|
|
594
805
|
let childId;
|
|
595
|
-
|
|
806
|
+
/** Fork seed size — bounds the root-evidence slice below. */
|
|
807
|
+
let seedCut = 0;
|
|
596
808
|
if (inherit) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
809
|
+
// A transport reject (not an RpcResult error) here would strand the
|
|
810
|
+
// worktree lease allocated above; catch keeps the cleanup total.
|
|
811
|
+
let forked;
|
|
812
|
+
try {
|
|
813
|
+
forked = await dispatchFaceMethod(options.runtime, "session.fork", `tool-sa-fork-${Date.now()}`, {
|
|
814
|
+
sessionId: options.parentSessionId,
|
|
815
|
+
linkMode,
|
|
816
|
+
label,
|
|
817
|
+
...(spawnRole && spawnRole !== "default"
|
|
818
|
+
? { role: spawnRole }
|
|
819
|
+
: {}),
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
dropIsolated();
|
|
824
|
+
return {
|
|
825
|
+
content: "subagent seed failed: fork request errored",
|
|
826
|
+
isError: true,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
602
829
|
if (!forked.result.ok) {
|
|
603
830
|
// No completed turn yet — fall back to a fresh child (DSH fork omits seed).
|
|
604
831
|
if (forked.result.error.code === "fork-unavailable") {
|
|
@@ -606,6 +833,10 @@ function createSubagentTool(options) {
|
|
|
606
833
|
parentSessionId: options.parentSessionId,
|
|
607
834
|
label,
|
|
608
835
|
mode: linkMode,
|
|
836
|
+
...memberSpawn,
|
|
837
|
+
...(spawnRole && spawnRole !== "default"
|
|
838
|
+
? { role: spawnRole }
|
|
839
|
+
: {}),
|
|
609
840
|
});
|
|
610
841
|
if (!created.result.ok) {
|
|
611
842
|
dropIsolated();
|
|
@@ -626,7 +857,10 @@ function createSubagentTool(options) {
|
|
|
626
857
|
}
|
|
627
858
|
else {
|
|
628
859
|
childId = String(forked.result.value.sessionId);
|
|
629
|
-
|
|
860
|
+
// Durable seed boundary: every answer read of this child must
|
|
861
|
+
// slice from here, or the parent's last reply reads as its answer.
|
|
862
|
+
seedCut = Number(forked.result.value.eventCount ?? 0);
|
|
863
|
+
options.runtime.subagents.setSeedEventCount(childId, seedCut);
|
|
630
864
|
}
|
|
631
865
|
}
|
|
632
866
|
else {
|
|
@@ -634,6 +868,10 @@ function createSubagentTool(options) {
|
|
|
634
868
|
parentSessionId: options.parentSessionId,
|
|
635
869
|
label,
|
|
636
870
|
mode: linkMode,
|
|
871
|
+
...memberSpawn,
|
|
872
|
+
...(spawnRole && spawnRole !== "default"
|
|
873
|
+
? { role: spawnRole }
|
|
874
|
+
: {}),
|
|
637
875
|
});
|
|
638
876
|
if (!created.result.ok) {
|
|
639
877
|
dropIsolated();
|
|
@@ -654,21 +892,65 @@ function createSubagentTool(options) {
|
|
|
654
892
|
: {}),
|
|
655
893
|
});
|
|
656
894
|
if (!modelPin.ok) {
|
|
657
|
-
|
|
895
|
+
await discardIfUnstarted();
|
|
658
896
|
return { content: modelPin.message, isError: true };
|
|
659
897
|
}
|
|
660
898
|
if (isolated) {
|
|
661
899
|
options.runtime.sessionCwds.set(childId, isolated.path);
|
|
662
|
-
|
|
900
|
+
// Worktree bind failure must not leak the child: swallow and let the
|
|
901
|
+
// prompt below decide the spawn's fate (or the catch backstop).
|
|
902
|
+
await Promise.resolve(options.runtime.invalidateAgent?.(childId)).catch(() => undefined);
|
|
663
903
|
}
|
|
904
|
+
const childPrompt = applySubagentSpawnPreamble({
|
|
905
|
+
prompt: taskBody,
|
|
906
|
+
parentSessionId: options.parentSessionId,
|
|
907
|
+
childSessionId: childId,
|
|
908
|
+
mode: linkMode,
|
|
909
|
+
label,
|
|
910
|
+
...(spawnRole ? { role: spawnRole } : {}),
|
|
911
|
+
inheritContext: inherit,
|
|
912
|
+
cwd: isolated?.path ?? parentCwd,
|
|
913
|
+
isolatedWorktree: isolated !== null,
|
|
914
|
+
...(rosterMember
|
|
915
|
+
? { memberId: rosterMember.id, inject: rosterMember.inject }
|
|
916
|
+
: {}),
|
|
917
|
+
// The human's own asks: a child that only sees the parent's paraphrase
|
|
918
|
+
// drifts from what was actually authorized (Codex
|
|
919
|
+
// `control/user_authorization.rs`). A forked child already carries
|
|
920
|
+
// these in its seeded transcript, so only add them beyond the cut.
|
|
921
|
+
...(() => {
|
|
922
|
+
const block = rootUserAuthorizationBlock({
|
|
923
|
+
parentEvents: readSessionEvents(options.runtime.store, options.parentSessionId),
|
|
924
|
+
...(seedCut > 0 ? { sinceEventCount: seedCut } : {}),
|
|
925
|
+
});
|
|
926
|
+
return block ? { userAuthorization: block } : {};
|
|
927
|
+
})(),
|
|
928
|
+
});
|
|
664
929
|
const task = openTask(childId);
|
|
665
930
|
const worktreeLine = reclaimIsolated;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
931
|
+
// A transport-level reject (not an RpcResult error) must not leak the
|
|
932
|
+
// child: the catch backstop tears the half-made spawn down.
|
|
933
|
+
let prompted;
|
|
934
|
+
try {
|
|
935
|
+
prompted = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-p-${childId}`, {
|
|
936
|
+
sessionId: childId,
|
|
937
|
+
mode: "queue",
|
|
938
|
+
content: [{ type: "text", text: childPrompt }],
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
catch (err) {
|
|
942
|
+
await discardIfUnstarted();
|
|
943
|
+
const extra = worktreeLine(false);
|
|
944
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
945
|
+
return {
|
|
946
|
+
content: [`subagent prompt failed: ${message}`, extra]
|
|
947
|
+
.filter(Boolean)
|
|
948
|
+
.join("\n"),
|
|
949
|
+
isError: true,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
671
952
|
if (!prompted.result.ok) {
|
|
953
|
+
await discardIfUnstarted();
|
|
672
954
|
const extra = worktreeLine(false);
|
|
673
955
|
return {
|
|
674
956
|
content: [`subagent prompt failed: ${prompted.result.error.message}`, extra]
|
|
@@ -677,6 +959,8 @@ function createSubagentTool(options) {
|
|
|
677
959
|
isError: true,
|
|
678
960
|
};
|
|
679
961
|
}
|
|
962
|
+
// Spawn accepted — the child is no longer a half-made shell.
|
|
963
|
+
started = true;
|
|
680
964
|
if (background) {
|
|
681
965
|
return {
|
|
682
966
|
content: [
|
|
@@ -689,7 +973,8 @@ function createSubagentTool(options) {
|
|
|
689
973
|
outputSchema
|
|
690
974
|
? "OUTPUT CONTRACT attached; result will be schema-checked on idle."
|
|
691
975
|
: undefined,
|
|
692
|
-
|
|
976
|
+
childResumeHint(childId),
|
|
977
|
+
"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.",
|
|
693
978
|
"Keep working; do not busy-poll.",
|
|
694
979
|
worktreeLine(true) || undefined,
|
|
695
980
|
]
|
|
@@ -698,29 +983,63 @@ function createSubagentTool(options) {
|
|
|
698
983
|
};
|
|
699
984
|
}
|
|
700
985
|
try {
|
|
701
|
-
await waitDrainIdle(options.runtime, childId, signal);
|
|
986
|
+
await waitDrainIdle(options.runtime, childId, signal, foregroundWaitMs);
|
|
702
987
|
}
|
|
703
988
|
catch (err) {
|
|
704
989
|
const message = err instanceof Error ? err.message : String(err);
|
|
705
|
-
|
|
990
|
+
const timedOut = /timed out waiting/i.test(message);
|
|
991
|
+
// Salvage before cancelling: `session.cancel` drops the chain, so a
|
|
992
|
+
// child that had already written its answer would bill the parent for
|
|
993
|
+
// nothing. A child that produced nothing is stuck, so it still gets
|
|
994
|
+
// cancelled instead of left dangling.
|
|
995
|
+
const owned = childOwnedEvents(options.runtime, childId);
|
|
996
|
+
const salvaged = lastAssistantBodyText(owned);
|
|
997
|
+
const retrying = timedOut
|
|
998
|
+
? lastModelRetryNotice(owned)
|
|
999
|
+
: undefined;
|
|
1000
|
+
if (!salvaged) {
|
|
1001
|
+
await dispatchFaceMethod(options.runtime, "session.cancel", `tool-sa-c-${childId}`,
|
|
1002
|
+
// `parent`, not the default `user`: a parent that gave up waiting
|
|
1003
|
+
// is not a human hitting Stop. The old default stamped
|
|
1004
|
+
// turn/end `reason: user`, so every timed-out child read as "stopped
|
|
1005
|
+
// by the user" in the child log and in the completion notice.
|
|
1006
|
+
{ sessionId: childId, cause: { kind: "parent" } }).catch(() => undefined);
|
|
1007
|
+
}
|
|
706
1008
|
options.runtime.agentTeamTasks.complete(task.id, {
|
|
707
1009
|
ok: false,
|
|
708
1010
|
preview: message,
|
|
709
1011
|
});
|
|
710
1012
|
const extra = worktreeLine(false);
|
|
711
1013
|
return {
|
|
712
|
-
content: [
|
|
1014
|
+
content: [
|
|
1015
|
+
salvaged
|
|
1016
|
+
? `${message}, but the child finished work before the wait expired — its last answer is salvaged below.`
|
|
1017
|
+
: `subagent failed: ${message}`,
|
|
1018
|
+
salvaged || undefined,
|
|
1019
|
+
timedOut ? childResumeHint(childId) : undefined,
|
|
1020
|
+
retrying,
|
|
1021
|
+
extra,
|
|
1022
|
+
]
|
|
1023
|
+
.filter(Boolean)
|
|
1024
|
+
.join("\n"),
|
|
713
1025
|
isError: true,
|
|
714
1026
|
};
|
|
715
1027
|
}
|
|
716
1028
|
const all = readSessionEvents(options.runtime.store, childId);
|
|
717
1029
|
// Never return seeded parent assistant text as the child's result.
|
|
718
|
-
|
|
1030
|
+
const link = options.runtime.subagents.getByChild(childId);
|
|
1031
|
+
const readOwned = () => subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId));
|
|
1032
|
+
let owned = subagentOwnedEvents(link, all);
|
|
719
1033
|
let text = lastAssistantBodyText(owned);
|
|
720
1034
|
let schemaValid;
|
|
721
1035
|
let schemaErrors;
|
|
722
|
-
if (outputSchema
|
|
723
|
-
|
|
1036
|
+
if (outputSchema) {
|
|
1037
|
+
// An empty body cannot satisfy the contract either: a silent skip
|
|
1038
|
+
// would hand the parent `ok: true` with an unvalidated (missing)
|
|
1039
|
+
// answer, so treat "no text" as one more schema failure.
|
|
1040
|
+
let checked = text
|
|
1041
|
+
? validateOutputAgainstSchema(text, outputSchema)
|
|
1042
|
+
: { valid: false, errors: ["child produced no assistant text"] };
|
|
724
1043
|
if (!checked.valid) {
|
|
725
1044
|
const retry = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-schema-${childId}`, {
|
|
726
1045
|
sessionId: childId,
|
|
@@ -739,18 +1058,20 @@ function createSubagentTool(options) {
|
|
|
739
1058
|
catch {
|
|
740
1059
|
/* keep first answer */
|
|
741
1060
|
}
|
|
742
|
-
owned =
|
|
743
|
-
? readSessionEvents(options.runtime.store, childId).slice(seedEventCount)
|
|
744
|
-
: readSessionEvents(options.runtime.store, childId);
|
|
1061
|
+
owned = readOwned();
|
|
745
1062
|
text = lastAssistantBodyText(owned) || text;
|
|
746
|
-
checked =
|
|
1063
|
+
checked = text
|
|
1064
|
+
? validateOutputAgainstSchema(text, outputSchema)
|
|
1065
|
+
: { valid: false, errors: ["child produced no assistant text"] };
|
|
747
1066
|
}
|
|
748
1067
|
}
|
|
749
1068
|
schemaValid = checked.valid;
|
|
750
1069
|
schemaErrors = checked.errors;
|
|
751
1070
|
}
|
|
752
1071
|
options.runtime.agentTeamTasks.complete(task.id, {
|
|
753
|
-
|
|
1072
|
+
// An unvalidated answer is not a successful delegation: the task
|
|
1073
|
+
// board must not read `completed` when the contract was skipped.
|
|
1074
|
+
ok: schemaValid !== false,
|
|
754
1075
|
...(text ? { preview: text } : {}),
|
|
755
1076
|
...(schemaValid !== undefined ? { schemaValid } : {}),
|
|
756
1077
|
...(schemaErrors && schemaErrors.length > 0
|
|
@@ -786,7 +1107,8 @@ function createSubagentTool(options) {
|
|
|
786
1107
|
function createListAgentsTool(options) {
|
|
787
1108
|
return {
|
|
788
1109
|
name: "list_agents",
|
|
789
|
-
description: "List direct child subagents of this session (id, label, mode, running, inbox queue/steer
|
|
1110
|
+
description: "List direct child subagents of this session (id, label, mode, running, model, inbox queue/steer, " +
|
|
1111
|
+
"and how each child's last turn actually ended — finished vs aborted by parent/user vs error, with idle age). " +
|
|
790
1112
|
"Prefaces with depth/active quota. Prefer `analytics` for a fuller quota snapshot.",
|
|
791
1113
|
parameters: { type: "object", properties: {} },
|
|
792
1114
|
isConcurrencySafe: () => true,
|
|
@@ -819,7 +1141,14 @@ function createListAgentsTool(options) {
|
|
|
819
1141
|
const inbox = id !== "?"
|
|
820
1142
|
? childInboxCounts(options.runtime, id)
|
|
821
1143
|
: { queued: 0, steering: 0 };
|
|
822
|
-
|
|
1144
|
+
const route = e.model ? `\t${e.model}` : "";
|
|
1145
|
+
// `idle` alone reads the same for a child that finished and one the
|
|
1146
|
+
// parent's wait budget cut off; the outcome line is what separates
|
|
1147
|
+
// "delivered" from "killed".
|
|
1148
|
+
const outcome = id === "?" || e.kind === "diagnostic"
|
|
1149
|
+
? ""
|
|
1150
|
+
: `\t${formatChildOutcome(readSessionEvents(options.runtime.store, id))}`;
|
|
1151
|
+
return `${id}\t${label}\t${mode}\t${activity}${route}${outcome}\tq=${inbox.queued}\tsteer=${inbox.steering}`;
|
|
823
1152
|
});
|
|
824
1153
|
return { content: [header, ...lines].join("\n") };
|
|
825
1154
|
},
|
|
@@ -873,7 +1202,8 @@ function createSendMessageTool(options) {
|
|
|
873
1202
|
isError: true,
|
|
874
1203
|
};
|
|
875
1204
|
}
|
|
876
|
-
|
|
1205
|
+
// Task-board resume is owned by `subagent.prompt` (the SoT for every
|
|
1206
|
+
// delivery route); calling it again here double-bumped the revision.
|
|
877
1207
|
return { content: `sent to ${childId} (${delivery})` };
|
|
878
1208
|
},
|
|
879
1209
|
};
|
|
@@ -1032,7 +1362,6 @@ function createTeamGraphTool(options) {
|
|
|
1032
1362
|
results.push(`${peer.id}\tfail\t${prompted.result.error.message}`);
|
|
1033
1363
|
continue;
|
|
1034
1364
|
}
|
|
1035
|
-
options.runtime.agentTeamTasks.markResumed(peer.id);
|
|
1036
1365
|
results.push(`${peer.id}\tok\t${delivery}`);
|
|
1037
1366
|
}
|
|
1038
1367
|
return {
|
|
@@ -1110,7 +1439,6 @@ function createFollowupTaskTool(options) {
|
|
|
1110
1439
|
isError: true,
|
|
1111
1440
|
};
|
|
1112
1441
|
}
|
|
1113
|
-
options.runtime.agentTeamTasks.markResumed(childId);
|
|
1114
1442
|
return { content: `followup_task sent to ${childId} (steer)` };
|
|
1115
1443
|
},
|
|
1116
1444
|
};
|
|
@@ -1217,10 +1545,11 @@ function createWaitAgentTool(options) {
|
|
|
1217
1545
|
: `wait_agent completed (${timeoutMs}ms budget)`,
|
|
1218
1546
|
];
|
|
1219
1547
|
for (const childId of targets) {
|
|
1548
|
+
const link = options.runtime.subagents.getByChild(childId);
|
|
1220
1549
|
const running = isChildSessionActive(options.runtime, childId);
|
|
1221
1550
|
const text = running
|
|
1222
1551
|
? undefined
|
|
1223
|
-
: lastAssistantBodyText(readSessionEvents(options.runtime.store, childId));
|
|
1552
|
+
: lastAssistantBodyText(subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId)));
|
|
1224
1553
|
const inbox = childInboxCounts(options.runtime, childId);
|
|
1225
1554
|
lines.push([
|
|
1226
1555
|
childId,
|
|
@@ -1245,7 +1574,8 @@ function createAnalyticsTool(options) {
|
|
|
1245
1574
|
return {
|
|
1246
1575
|
name: "analytics",
|
|
1247
1576
|
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
|
|
1577
|
+
"Shows depth/active caps, free slots, and each child's running state, model route, " +
|
|
1578
|
+
"plus inbox queue/steer counts.",
|
|
1249
1579
|
parameters: { type: "object", properties: {} },
|
|
1250
1580
|
isConcurrencySafe: () => true,
|
|
1251
1581
|
async execute() {
|
|
@@ -1278,7 +1608,14 @@ function createAnalyticsTool(options) {
|
|
|
1278
1608
|
? "running"
|
|
1279
1609
|
: "idle";
|
|
1280
1610
|
const inbox = childInboxCounts(options.runtime, link.childSessionId);
|
|
1281
|
-
|
|
1611
|
+
const externalKind = options.runtime.externalAgents.kind(link.childSessionId);
|
|
1612
|
+
const route = externalKind
|
|
1613
|
+
? `external:${externalKind}`
|
|
1614
|
+
: (() => {
|
|
1615
|
+
const m = resolveSessionModelSelection(options.runtime, link.childSessionId);
|
|
1616
|
+
return `${m.provider}/${m.model}`;
|
|
1617
|
+
})();
|
|
1618
|
+
lines.push(` ${link.childSessionId}\t${link.label ?? "subagent"}\t${link.mode}\t${activity}\t${route}\tq=${inbox.queued}\tsteer=${inbox.steering}`);
|
|
1282
1619
|
}
|
|
1283
1620
|
}
|
|
1284
1621
|
return { content: lines.join("\n") };
|