@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +85 -34
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +49 -32
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +46 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +12 -2
- package/dist/types/task/index.d.ts +13 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +63 -51
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +38 -13
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +26 -66
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +38 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +226 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
package/src/task/executor.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
10
11
|
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
12
|
import type { Rule } from "../capability/rule";
|
|
12
13
|
import { ModelRegistry } from "../config/model-registry";
|
|
@@ -26,8 +27,9 @@ import type { MCPManager } from "../mcp/manager";
|
|
|
26
27
|
import type { MnemopiSessionState } from "../mnemopi/state";
|
|
27
28
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
28
29
|
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
|
30
|
+
import { AgentLifecycleManager } from "../registry/agent-lifecycle";
|
|
29
31
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
30
|
-
import { createAgentSession, discoverAuthStorage } from "../sdk";
|
|
32
|
+
import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "../sdk";
|
|
31
33
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
32
34
|
import type { ArtifactManager } from "../session/artifacts";
|
|
33
35
|
import type { AuthStorage } from "../session/auth-storage";
|
|
@@ -35,6 +37,7 @@ import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
|
35
37
|
import { SessionManager } from "../session/session-manager";
|
|
36
38
|
import { truncateTail } from "../session/streaming-output";
|
|
37
39
|
import type { ContextFileEntry } from "../tools";
|
|
40
|
+
import { isIrcEnabled } from "../tools/irc";
|
|
38
41
|
import { normalizeSchema } from "../tools/jtd-to-json-schema";
|
|
39
42
|
import {
|
|
40
43
|
buildOutputValidator,
|
|
@@ -63,6 +66,30 @@ import {
|
|
|
63
66
|
|
|
64
67
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
65
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Soft per-agent request budgets (assistant requests per run). When a subagent
|
|
71
|
+
* crosses its budget it receives ONE steering notice asking it to wrap up; at
|
|
72
|
+
* 1.5x the budget the run is aborted gracefully so partial output is salvaged.
|
|
73
|
+
* The `default` key applies to agents without an explicit entry and can be
|
|
74
|
+
* overridden via the `task.softRequestBudget` setting (0 disables the guard).
|
|
75
|
+
*/
|
|
76
|
+
export const SOFT_REQUEST_BUDGET: Record<string, number> = {
|
|
77
|
+
explore: 40,
|
|
78
|
+
quick_task: 40,
|
|
79
|
+
default: 90,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Steering notice injected once when a subagent crosses its soft request budget. */
|
|
83
|
+
export function buildBudgetNotice(requests: number): string {
|
|
84
|
+
return `[budget notice] You have used ${requests} requests in this run. Wrap up now: finish the current step and yield your final report.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Flatten whitespace and clip salvage text for the cancelled-child summary line. */
|
|
88
|
+
function formatSalvageSnippet(text: string, maxLength = 500): string {
|
|
89
|
+
const flattened = text.replace(/\s+/g, " ").trim();
|
|
90
|
+
return flattened.length > maxLength ? `${flattened.slice(0, maxLength - 1)}…` : flattened;
|
|
91
|
+
}
|
|
92
|
+
|
|
66
93
|
/** Agent event types to forward for progress tracking. */
|
|
67
94
|
const agentEventTypes = new Set<AgentEvent["type"]>([
|
|
68
95
|
"agent_start",
|
|
@@ -94,9 +121,13 @@ function normalizeModelPatterns(value: string | string[] | undefined): string[]
|
|
|
94
121
|
function renderIrcPeerRoster(selfId: string): string {
|
|
95
122
|
const peers = AgentRegistry.global()
|
|
96
123
|
.list()
|
|
97
|
-
.filter(ref => ref.id !== selfId &&
|
|
98
|
-
if (peers.length === 0) return "- (no other
|
|
99
|
-
|
|
124
|
+
.filter(ref => ref.id !== selfId && ref.status !== "aborted");
|
|
125
|
+
if (peers.length === 0) return "- (no other agents)";
|
|
126
|
+
const lines = peers.map(peer => `- \`${peer.id}\` — ${peer.displayName} (${peer.kind}, ${peer.status})`);
|
|
127
|
+
if (peers.some(peer => peer.status === "idle" || peer.status === "parked")) {
|
|
128
|
+
lines.push("Idle/parked peers are not gone: messaging them wakes (or revives) them.");
|
|
129
|
+
}
|
|
130
|
+
return lines.join("\n");
|
|
100
131
|
}
|
|
101
132
|
|
|
102
133
|
function withAbortTimeout<T>(promise: Promise<T>, timeoutMs: number, signal?: AbortSignal): Promise<T> {
|
|
@@ -152,6 +183,7 @@ export interface ExecutorOptions {
|
|
|
152
183
|
agent: AgentDefinition;
|
|
153
184
|
task: string;
|
|
154
185
|
assignment?: string;
|
|
186
|
+
/** Shared background from the task call (`task.batch`), rendered into the subagent's system prompt. */
|
|
155
187
|
context?: string;
|
|
156
188
|
/**
|
|
157
189
|
* The session's active overall plan, handed off so subagents spawned during
|
|
@@ -162,6 +194,7 @@ export interface ExecutorOptions {
|
|
|
162
194
|
description?: string;
|
|
163
195
|
index: number;
|
|
164
196
|
id: string;
|
|
197
|
+
parentToolCallId?: string;
|
|
165
198
|
modelOverride?: string | string[];
|
|
166
199
|
/**
|
|
167
200
|
* Active model selector of the parent session, used as an auth-aware fallback
|
|
@@ -185,8 +218,6 @@ export interface ExecutorOptions {
|
|
|
185
218
|
sessionFile?: string | null;
|
|
186
219
|
persistArtifacts?: boolean;
|
|
187
220
|
artifactsDir?: string;
|
|
188
|
-
/** Path to parent conversation context file */
|
|
189
|
-
contextFile?: string;
|
|
190
221
|
eventBus?: EventBus;
|
|
191
222
|
contextFiles?: ContextFileEntry[];
|
|
192
223
|
skills?: Skill[];
|
|
@@ -610,28 +641,67 @@ export function createSubagentSettings(
|
|
|
610
641
|
});
|
|
611
642
|
}
|
|
612
643
|
|
|
644
|
+
type AbortReason = "signal" | "terminate" | "timeout" | "budget";
|
|
645
|
+
|
|
646
|
+
/** Inputs for the run monitor driving one subagent assignment. */
|
|
647
|
+
interface RunMonitorArgs {
|
|
648
|
+
index: number;
|
|
649
|
+
id: string;
|
|
650
|
+
agent: AgentDefinition;
|
|
651
|
+
task: string;
|
|
652
|
+
assignment?: string;
|
|
653
|
+
description?: string;
|
|
654
|
+
modelOverride?: string | string[];
|
|
655
|
+
signal?: AbortSignal;
|
|
656
|
+
onProgress?: (progress: AgentProgress) => void;
|
|
657
|
+
eventBus?: EventBus;
|
|
658
|
+
parentToolCallId?: string;
|
|
659
|
+
sessionFile?: string;
|
|
660
|
+
/** Soft assistant-request budget; 0 disables the guard. */
|
|
661
|
+
softRequestBudget: number;
|
|
662
|
+
/** Wall-clock cap in ms; 0 disables the timer. */
|
|
663
|
+
maxRuntimeMs: number;
|
|
664
|
+
}
|
|
665
|
+
|
|
613
666
|
/**
|
|
614
|
-
*
|
|
667
|
+
* The run-monitoring core of {@link runSubprocess}: progress tracking, event
|
|
668
|
+
* processing, abort/budget machinery, usage accumulation, and output capture
|
|
669
|
+
* for one assignment run.
|
|
615
670
|
*/
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
671
|
+
interface SubagentRunMonitor {
|
|
672
|
+
readonly progress: AgentProgress;
|
|
673
|
+
/** Fires when the run was asked to stop (caller signal, timeout, budget, terminate). */
|
|
674
|
+
readonly abortSignal: AbortSignal;
|
|
675
|
+
readonly accumulatedUsage: Usage;
|
|
676
|
+
hasUsage(): boolean;
|
|
677
|
+
yieldCalled(): boolean;
|
|
678
|
+
runtimeLimitExceeded(): boolean;
|
|
679
|
+
/** True when the abort carries a precise external reason (signal / wall-clock / budget). */
|
|
680
|
+
hasExplicitAbortReason(): boolean;
|
|
681
|
+
/** Whether the (attempted) abort counts as a cancelled run rather than an internal failure. */
|
|
682
|
+
isAbortedRun(): boolean;
|
|
683
|
+
requestAbort(reason: AbortReason): void;
|
|
684
|
+
resolveSignalAbortReason(): string;
|
|
685
|
+
resolveAbortReasonText(): string;
|
|
686
|
+
setActiveSession(session: AgentSession | null): void;
|
|
687
|
+
/** Return and clear the active session reference. */
|
|
688
|
+
takeActiveSession(): AgentSession | null;
|
|
689
|
+
/** Subscribe the monitor to a session's events. Returns the unsubscribe function. */
|
|
690
|
+
attach(session: AgentSession): () => void;
|
|
691
|
+
/** Best-effort capture of the last assistant text for cancelled-run salvage. */
|
|
692
|
+
captureSalvage(session: AgentSession): void;
|
|
693
|
+
lastAssistantSalvageText(): string | undefined;
|
|
694
|
+
/** Final raw output: end-of-run assistant text when available, else accumulated chunks. */
|
|
695
|
+
rawOutput(): string;
|
|
696
|
+
scheduleProgress(flush?: boolean): void;
|
|
697
|
+
/** Stop processing events and clear listeners/timers. Call once the run settled. */
|
|
698
|
+
finish(): void;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
702
|
+
const { index, id, agent, task, assignment, signal, onProgress, softRequestBudget, maxRuntimeMs } = args;
|
|
632
703
|
const startTime = Date.now();
|
|
633
704
|
|
|
634
|
-
// Initialize progress
|
|
635
705
|
const progress: AgentProgress = {
|
|
636
706
|
index,
|
|
637
707
|
id,
|
|
@@ -640,109 +710,23 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
640
710
|
status: "running",
|
|
641
711
|
task,
|
|
642
712
|
assignment,
|
|
643
|
-
description:
|
|
713
|
+
description: args.description,
|
|
644
714
|
lastIntent: undefined,
|
|
645
715
|
recentTools: [],
|
|
646
716
|
recentOutput: [],
|
|
647
717
|
toolCount: 0,
|
|
718
|
+
requests: 0,
|
|
648
719
|
tokens: 0,
|
|
649
720
|
cost: 0,
|
|
650
721
|
durationMs: 0,
|
|
651
|
-
modelOverride,
|
|
722
|
+
modelOverride: args.modelOverride,
|
|
652
723
|
};
|
|
653
724
|
|
|
654
|
-
// Check if already aborted
|
|
655
|
-
if (signal?.aborted) {
|
|
656
|
-
return {
|
|
657
|
-
index,
|
|
658
|
-
id,
|
|
659
|
-
agent: agent.name,
|
|
660
|
-
agentSource: agent.source,
|
|
661
|
-
task,
|
|
662
|
-
assignment,
|
|
663
|
-
description: options.description,
|
|
664
|
-
exitCode: 1,
|
|
665
|
-
output: "",
|
|
666
|
-
stderr: "Cancelled before start",
|
|
667
|
-
truncated: false,
|
|
668
|
-
durationMs: 0,
|
|
669
|
-
tokens: 0,
|
|
670
|
-
modelOverride,
|
|
671
|
-
error: "Cancelled before start",
|
|
672
|
-
aborted: true,
|
|
673
|
-
abortReason: "Cancelled before start",
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Set up artifact paths and write input file upfront if artifacts dir provided
|
|
678
|
-
let subtaskSessionFile: string | undefined;
|
|
679
|
-
if (options.artifactsDir) {
|
|
680
|
-
subtaskSessionFile = path.join(options.artifactsDir, `${id}.jsonl`);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const settings = options.settings ?? Settings.isolated();
|
|
684
|
-
const subagentSettings = createSubagentSettings(
|
|
685
|
-
settings,
|
|
686
|
-
agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
|
|
687
|
-
);
|
|
688
|
-
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
689
|
-
const maxRuntimeMs = Math.max(
|
|
690
|
-
0,
|
|
691
|
-
Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
|
|
692
|
-
);
|
|
693
|
-
const parentDepth = options.taskDepth ?? 0;
|
|
694
|
-
const childDepth = parentDepth + 1;
|
|
695
|
-
const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
|
|
696
|
-
|
|
697
|
-
// Add tools if specified
|
|
698
|
-
let toolNames: string[] | undefined;
|
|
699
|
-
if (agent.tools && agent.tools.length > 0) {
|
|
700
|
-
toolNames = agent.tools;
|
|
701
|
-
// Auto-include task tool if spawns defined but task not in tools
|
|
702
|
-
if (agent.spawns !== undefined && !toolNames.includes("task") && !atMaxDepth) {
|
|
703
|
-
toolNames = [...toolNames, "task"];
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (atMaxDepth && toolNames?.includes("task")) {
|
|
708
|
-
toolNames = toolNames.filter(name => name !== "task");
|
|
709
|
-
}
|
|
710
|
-
// IRC is always available; the COOP prompt section advertises it, so a restricted
|
|
711
|
-
// whitelist must still carry `irc` for the subagent to actually use it.
|
|
712
|
-
if (toolNames && !toolNames.includes("irc")) {
|
|
713
|
-
toolNames = [...toolNames, "irc"];
|
|
714
|
-
}
|
|
715
|
-
if (toolNames?.includes("exec")) {
|
|
716
|
-
const allowEvalPy = settings.get("eval.py") ?? true;
|
|
717
|
-
const allowEvalJs = settings.get("eval.js") ?? true;
|
|
718
|
-
const expanded = toolNames.filter(name => name !== "exec");
|
|
719
|
-
if (allowEvalPy || allowEvalJs) expanded.push("eval");
|
|
720
|
-
expanded.push("bash");
|
|
721
|
-
toolNames = Array.from(new Set(expanded));
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const modelPatterns = normalizeModelPatterns(modelOverride ?? agent.model);
|
|
725
|
-
const sessionFile = subtaskSessionFile ?? null;
|
|
726
|
-
const spawnsEnv = atMaxDepth
|
|
727
|
-
? ""
|
|
728
|
-
: agent.spawns === undefined
|
|
729
|
-
? ""
|
|
730
|
-
: agent.spawns === "*"
|
|
731
|
-
? "*"
|
|
732
|
-
: agent.spawns.join(",");
|
|
733
|
-
|
|
734
|
-
const lspEnabled = enableLsp ?? true;
|
|
735
|
-
const ircEnabled = subagentSettings.get("irc.enabled") === true;
|
|
736
|
-
const contextFileForPrompt = ircEnabled ? undefined : options.contextFile;
|
|
737
|
-
const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
|
|
738
|
-
|
|
739
725
|
const outputChunks: string[] = [];
|
|
740
726
|
const finalOutputChunks: string[] = [];
|
|
741
727
|
const RECENT_OUTPUT_TAIL_BYTES = 8 * 1024;
|
|
742
728
|
let recentOutputTail = "";
|
|
743
|
-
let stderr = "";
|
|
744
729
|
let resolved = false;
|
|
745
|
-
type AbortReason = "signal" | "terminate" | "timeout";
|
|
746
730
|
let abortSent = false;
|
|
747
731
|
let abortReason: AbortReason | undefined;
|
|
748
732
|
let runtimeLimitExceeded = false;
|
|
@@ -751,11 +735,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
751
735
|
const abortController = new AbortController();
|
|
752
736
|
const abortSignal = abortController.signal;
|
|
753
737
|
let activeSession: AgentSession | null = null;
|
|
754
|
-
let unsubscribe: (() => void) | null = null;
|
|
755
738
|
let yieldCalled = false;
|
|
756
739
|
|
|
757
740
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
758
|
-
const accumulatedUsage = {
|
|
741
|
+
const accumulatedUsage: Usage = {
|
|
759
742
|
input: 0,
|
|
760
743
|
output: 0,
|
|
761
744
|
cacheRead: 0,
|
|
@@ -764,11 +747,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
764
747
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
765
748
|
};
|
|
766
749
|
let hasUsage = false;
|
|
750
|
+
let budgetSteerSent = false;
|
|
751
|
+
let budgetLimitExceeded = false;
|
|
752
|
+
let lastAssistantSalvageText: string | undefined;
|
|
767
753
|
|
|
768
754
|
const requestAbort = (reason: AbortReason) => {
|
|
769
755
|
if (reason === "timeout") {
|
|
770
756
|
runtimeLimitExceeded = true;
|
|
771
757
|
}
|
|
758
|
+
if (reason === "budget") {
|
|
759
|
+
budgetLimitExceeded = true;
|
|
760
|
+
}
|
|
772
761
|
if (abortSent) {
|
|
773
762
|
if (reason === "signal" && abortReason !== "signal" && abortReason !== "timeout") {
|
|
774
763
|
abortReason = "signal";
|
|
@@ -785,11 +774,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
785
774
|
};
|
|
786
775
|
|
|
787
776
|
// Handle abort signal
|
|
788
|
-
const onAbort = () => {
|
|
789
|
-
if (!resolved) requestAbort("signal");
|
|
790
|
-
};
|
|
791
777
|
if (signal) {
|
|
792
|
-
signal.addEventListener(
|
|
778
|
+
signal.addEventListener(
|
|
779
|
+
"abort",
|
|
780
|
+
() => {
|
|
781
|
+
if (!resolved) requestAbort("signal");
|
|
782
|
+
},
|
|
783
|
+
{ once: true, signal: listenerSignal },
|
|
784
|
+
);
|
|
793
785
|
}
|
|
794
786
|
|
|
795
787
|
// Wall-clock hard limit. Defense-in-depth for the case where a provider stream
|
|
@@ -825,6 +817,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
825
817
|
if (runtimeLimitExceeded) {
|
|
826
818
|
return `Subagent runtime limit exceeded (task.maxRuntimeMs=${maxRuntimeMs})`;
|
|
827
819
|
}
|
|
820
|
+
if (budgetLimitExceeded) {
|
|
821
|
+
return `Soft request budget exceeded (${progress.requests} requests; budget ${softRequestBudget})`;
|
|
822
|
+
}
|
|
828
823
|
return resolveSignalAbortReason();
|
|
829
824
|
};
|
|
830
825
|
const PROGRESS_COALESCE_MS = 150;
|
|
@@ -834,15 +829,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
834
829
|
const emitProgressNow = () => {
|
|
835
830
|
progress.durationMs = Date.now() - startTime;
|
|
836
831
|
onProgress?.({ ...progress });
|
|
837
|
-
if (
|
|
838
|
-
|
|
832
|
+
if (args.eventBus) {
|
|
833
|
+
args.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
|
|
839
834
|
index,
|
|
840
835
|
agent: agent.name,
|
|
841
836
|
agentSource: agent.source,
|
|
842
837
|
task,
|
|
838
|
+
parentToolCallId: args.parentToolCallId,
|
|
843
839
|
assignment,
|
|
844
840
|
progress: { ...progress },
|
|
845
|
-
sessionFile:
|
|
841
|
+
sessionFile: args.sessionFile,
|
|
846
842
|
});
|
|
847
843
|
}
|
|
848
844
|
lastProgressEmitMs = Date.now();
|
|
@@ -922,20 +918,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
922
918
|
progress.recentOutput = [];
|
|
923
919
|
};
|
|
924
920
|
|
|
921
|
+
const emitSubagentEvent = (event: AgentSessionEvent) => {
|
|
922
|
+
if (!args.eventBus) return;
|
|
923
|
+
args.eventBus.emit(TASK_SUBAGENT_EVENT_CHANNEL, {
|
|
924
|
+
id,
|
|
925
|
+
event,
|
|
926
|
+
});
|
|
927
|
+
};
|
|
928
|
+
|
|
925
929
|
const processEvent = (event: AgentEvent) => {
|
|
926
930
|
if (resolved) return;
|
|
927
|
-
|
|
928
|
-
if (options.eventBus) {
|
|
929
|
-
options.eventBus.emit(TASK_SUBAGENT_EVENT_CHANNEL, {
|
|
930
|
-
index,
|
|
931
|
-
agent: agent.name,
|
|
932
|
-
agentSource: agent.source,
|
|
933
|
-
task,
|
|
934
|
-
assignment,
|
|
935
|
-
event,
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
|
|
939
931
|
const now = Date.now();
|
|
940
932
|
let flushProgress = false;
|
|
941
933
|
|
|
@@ -1080,6 +1072,26 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1080
1072
|
case "message_end": {
|
|
1081
1073
|
// Extract text from assistant and toolResult messages (not user prompts)
|
|
1082
1074
|
const role = event.message?.role;
|
|
1075
|
+
if (role === "assistant") {
|
|
1076
|
+
progress.requests += 1;
|
|
1077
|
+
if (softRequestBudget > 0 && !abortSent) {
|
|
1078
|
+
if (progress.requests >= softRequestBudget * 1.5) {
|
|
1079
|
+
requestAbort("budget");
|
|
1080
|
+
} else if (!budgetSteerSent && progress.requests >= softRequestBudget) {
|
|
1081
|
+
budgetSteerSent = true;
|
|
1082
|
+
const steerSession = activeSession;
|
|
1083
|
+
if (steerSession) {
|
|
1084
|
+
void steerSession
|
|
1085
|
+
.sendUserMessage(buildBudgetNotice(progress.requests), { deliverAs: "steer" })
|
|
1086
|
+
.catch(err => {
|
|
1087
|
+
logger.warn("Subagent budget steer failed", {
|
|
1088
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1083
1095
|
if (role === "assistant") {
|
|
1084
1096
|
const messageContent =
|
|
1085
1097
|
getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
|
|
@@ -1149,113 +1161,646 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1149
1161
|
scheduleProgress(flushProgress);
|
|
1150
1162
|
};
|
|
1151
1163
|
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1164
|
+
const attach = (session: AgentSession): (() => void) =>
|
|
1165
|
+
session.subscribe(event => {
|
|
1166
|
+
emitSubagentEvent(event);
|
|
1167
|
+
if (event.type === "auto_retry_start") {
|
|
1168
|
+
progress.retryState = {
|
|
1169
|
+
attempt: event.attempt,
|
|
1170
|
+
maxAttempts: event.maxAttempts,
|
|
1171
|
+
delayMs: event.delayMs,
|
|
1172
|
+
errorMessage: event.errorMessage,
|
|
1173
|
+
startedAtMs: Date.now(),
|
|
1174
|
+
};
|
|
1175
|
+
progress.retryFailure = undefined;
|
|
1176
|
+
scheduleProgress(true);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (event.type === "auto_retry_end") {
|
|
1180
|
+
const attempt = progress.retryState?.attempt ?? event.attempt;
|
|
1181
|
+
progress.retryState = undefined;
|
|
1182
|
+
if (!event.success) {
|
|
1183
|
+
progress.retryFailure = {
|
|
1184
|
+
attempt,
|
|
1185
|
+
errorMessage: event.finalError ?? "Auto-retry failed",
|
|
1186
|
+
};
|
|
1169
1187
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1188
|
+
scheduleProgress(true);
|
|
1189
|
+
return;
|
|
1172
1190
|
}
|
|
1173
|
-
|
|
1174
|
-
const awaitAbortable = async <T>(promise: Promise<T>): Promise<T> => {
|
|
1175
|
-
checkAbort();
|
|
1176
|
-
const { promise: abortPromise, reject } = Promise.withResolvers<never>();
|
|
1177
|
-
const onAbort = () => {
|
|
1191
|
+
if (isAgentEvent(event)) {
|
|
1178
1192
|
try {
|
|
1179
|
-
|
|
1193
|
+
processEvent(event);
|
|
1180
1194
|
} catch (err) {
|
|
1181
|
-
|
|
1195
|
+
logger.error("Subagent event processing failed", {
|
|
1196
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1197
|
+
});
|
|
1198
|
+
requestAbort("terminate");
|
|
1182
1199
|
}
|
|
1183
|
-
};
|
|
1184
|
-
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1185
|
-
try {
|
|
1186
|
-
return await Promise.race([promise, abortPromise]);
|
|
1187
|
-
} finally {
|
|
1188
|
-
abortSignal.removeEventListener("abort", onAbort);
|
|
1189
1200
|
}
|
|
1190
|
-
};
|
|
1201
|
+
});
|
|
1191
1202
|
|
|
1203
|
+
const captureSalvage = (session: AgentSession): void => {
|
|
1204
|
+
// Best-effort salvage: capture the last assistant text so
|
|
1205
|
+
// cancelled/aborted children can surface "last activity" instead of
|
|
1206
|
+
// "(no output)".
|
|
1192
1207
|
try {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
"options.authStorage and options.modelRegistry.authStorage must be the same instance when both are provided",
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
checkAbort();
|
|
1206
|
-
if (!registryFromParent) {
|
|
1207
|
-
await awaitAbortable(modelRegistry.refresh());
|
|
1208
|
-
} else {
|
|
1209
|
-
logger.debug("runSubagent: reusing parent modelRegistry; skipping refresh");
|
|
1208
|
+
const lastContent = session.getLastAssistantMessage()?.content;
|
|
1209
|
+
if (Array.isArray(lastContent)) {
|
|
1210
|
+
const text = lastContent
|
|
1211
|
+
.map(block => (block.type === "text" && typeof block.text === "string" ? block.text : ""))
|
|
1212
|
+
.filter(Boolean)
|
|
1213
|
+
.join("\n");
|
|
1214
|
+
if (text.trim()) {
|
|
1215
|
+
lastAssistantSalvageText = text;
|
|
1216
|
+
}
|
|
1210
1217
|
}
|
|
1211
|
-
|
|
1218
|
+
} catch {
|
|
1219
|
+
// Salvage is best-effort; partial sessions may not implement it
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1212
1222
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1223
|
+
return {
|
|
1224
|
+
progress,
|
|
1225
|
+
abortSignal,
|
|
1226
|
+
accumulatedUsage,
|
|
1227
|
+
hasUsage: () => hasUsage,
|
|
1228
|
+
yieldCalled: () => yieldCalled,
|
|
1229
|
+
runtimeLimitExceeded: () => runtimeLimitExceeded,
|
|
1230
|
+
hasExplicitAbortReason: () => abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded,
|
|
1231
|
+
isAbortedRun: () =>
|
|
1232
|
+
abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded || abortReason === undefined,
|
|
1233
|
+
requestAbort,
|
|
1234
|
+
resolveSignalAbortReason,
|
|
1235
|
+
resolveAbortReasonText,
|
|
1236
|
+
setActiveSession: session => {
|
|
1237
|
+
activeSession = session;
|
|
1238
|
+
},
|
|
1239
|
+
takeActiveSession: () => {
|
|
1240
|
+
const session = activeSession;
|
|
1241
|
+
activeSession = null;
|
|
1242
|
+
return session;
|
|
1243
|
+
},
|
|
1244
|
+
attach,
|
|
1245
|
+
captureSalvage,
|
|
1246
|
+
lastAssistantSalvageText: () => lastAssistantSalvageText,
|
|
1247
|
+
rawOutput: () => (finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("")),
|
|
1248
|
+
scheduleProgress,
|
|
1249
|
+
finish: () => {
|
|
1250
|
+
resolved = true;
|
|
1251
|
+
listenerController.abort();
|
|
1252
|
+
if (runtimeTimeoutId !== undefined) {
|
|
1253
|
+
clearTimeout(runtimeTimeoutId);
|
|
1254
|
+
runtimeTimeoutId = undefined;
|
|
1236
1255
|
}
|
|
1237
|
-
if (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
: `${model.provider}/${model.id}`;
|
|
1256
|
+
if (progressTimeoutId) {
|
|
1257
|
+
clearTimeout(progressTimeoutId);
|
|
1258
|
+
progressTimeoutId = null;
|
|
1241
1259
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1260
|
+
},
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1245
1263
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1264
|
+
interface DriveOutcome {
|
|
1265
|
+
exitCode: number;
|
|
1266
|
+
error?: string;
|
|
1267
|
+
aborted: boolean;
|
|
1268
|
+
abortReasonText?: string;
|
|
1269
|
+
}
|
|
1252
1270
|
|
|
1253
|
-
|
|
1254
|
-
const enableMCP = !options.mcpManager;
|
|
1271
|
+
const MAX_YIELD_RETRIES = 3;
|
|
1255
1272
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1273
|
+
/**
|
|
1274
|
+
* Drive one assignment through a live session: send the prompt, wait for idle,
|
|
1275
|
+
* remind the agent to `yield` (up to {@link MAX_YIELD_RETRIES} times), then
|
|
1276
|
+
* classify the terminal assistant state.
|
|
1277
|
+
*/
|
|
1278
|
+
async function driveSessionToYield(
|
|
1279
|
+
session: AgentSession,
|
|
1280
|
+
monitor: SubagentRunMonitor,
|
|
1281
|
+
task: string,
|
|
1282
|
+
): Promise<DriveOutcome> {
|
|
1283
|
+
const abortSignal = monitor.abortSignal;
|
|
1284
|
+
let exitCode = 0;
|
|
1285
|
+
let error: string | undefined;
|
|
1286
|
+
let aborted = false;
|
|
1287
|
+
let abortReasonText: string | undefined;
|
|
1288
|
+
const checkAbort = () => {
|
|
1289
|
+
if (abortSignal.aborted) {
|
|
1290
|
+
aborted = monitor.isAbortedRun();
|
|
1291
|
+
if (aborted) {
|
|
1292
|
+
abortReasonText ??= monitor.resolveAbortReasonText();
|
|
1293
|
+
}
|
|
1294
|
+
exitCode = 1;
|
|
1295
|
+
throw new ToolAbortError();
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
const awaitAbortable = async <T>(promise: Promise<T>): Promise<T> => {
|
|
1299
|
+
checkAbort();
|
|
1300
|
+
const { promise: abortPromise, reject } = Promise.withResolvers<never>();
|
|
1301
|
+
const onAbort = () => {
|
|
1302
|
+
try {
|
|
1303
|
+
checkAbort();
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
reject(err);
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1309
|
+
try {
|
|
1310
|
+
return await Promise.race([promise, abortPromise]);
|
|
1311
|
+
} finally {
|
|
1312
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
try {
|
|
1317
|
+
await awaitAbortable(session.prompt(task, { attribution: "agent" }));
|
|
1318
|
+
await awaitAbortable(session.waitForIdle());
|
|
1319
|
+
|
|
1320
|
+
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
|
|
1321
|
+
|
|
1322
|
+
let retryCount = 0;
|
|
1323
|
+
while (!monitor.yieldCalled() && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
1324
|
+
// Skip reminders when the model returned a terminal error (e.g.
|
|
1325
|
+
// rate-limit cap hit, auth failure). Re-prompting would just
|
|
1326
|
+
// hit the same wall, multiplying the failure noise without
|
|
1327
|
+
// any chance of producing a yield.
|
|
1328
|
+
const lastBeforeReminder = session.getLastAssistantMessage();
|
|
1329
|
+
if (lastBeforeReminder?.stopReason === "error") break;
|
|
1330
|
+
try {
|
|
1331
|
+
retryCount++;
|
|
1332
|
+
const reminder = prompt.render(submitReminderTemplate, {
|
|
1333
|
+
retryCount,
|
|
1334
|
+
maxRetries: MAX_YIELD_RETRIES,
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
|
|
1338
|
+
await awaitAbortable(
|
|
1339
|
+
session.prompt(reminder, {
|
|
1340
|
+
attribution: "agent",
|
|
1341
|
+
synthetic: true,
|
|
1342
|
+
...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
|
|
1343
|
+
}),
|
|
1344
|
+
);
|
|
1345
|
+
await awaitAbortable(session.waitForIdle());
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
if (abortSignal.aborted || err instanceof ToolAbortError) {
|
|
1348
|
+
// Benign control-flow exit — user cancel (^C) or compaction aborting
|
|
1349
|
+
// pending operations both surface here as ToolAbortError. The outer
|
|
1350
|
+
// catch and finally already mark the run aborted; logging at ERROR
|
|
1351
|
+
// would spam operator dashboards with non-failures.
|
|
1352
|
+
logger.debug("Subagent prompt aborted");
|
|
1353
|
+
} else {
|
|
1354
|
+
logger.error("Subagent prompt failed", {
|
|
1355
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
await awaitAbortable(session.waitForIdle());
|
|
1362
|
+
|
|
1363
|
+
const lastAssistant = session.getLastAssistantMessage();
|
|
1364
|
+
if (lastAssistant) {
|
|
1365
|
+
if (lastAssistant.stopReason === "aborted") {
|
|
1366
|
+
aborted = monitor.isAbortedRun();
|
|
1367
|
+
if (aborted) {
|
|
1368
|
+
// A real caller signal or the wall-clock timer carries a precise
|
|
1369
|
+
// reason (signal.reason / "runtime limit exceeded"). An internal
|
|
1370
|
+
// turn abort does NOT — prefer the assistant message's own
|
|
1371
|
+
// errorMessage ("Request was aborted" or a specific stream error)
|
|
1372
|
+
// over the misleading "Cancelled by caller".
|
|
1373
|
+
abortReasonText ??= monitor.hasExplicitAbortReason()
|
|
1374
|
+
? monitor.resolveAbortReasonText()
|
|
1375
|
+
: lastAssistant.errorMessage?.trim() || monitor.resolveAbortReasonText();
|
|
1376
|
+
}
|
|
1377
|
+
exitCode = 1;
|
|
1378
|
+
} else if (lastAssistant.stopReason === "error") {
|
|
1379
|
+
exitCode = 1;
|
|
1380
|
+
error ??= lastAssistant.errorMessage || "Subagent failed";
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
exitCode = 1;
|
|
1385
|
+
if (!abortSignal.aborted) {
|
|
1386
|
+
error = err instanceof Error ? err.stack || err.message : String(err);
|
|
1387
|
+
}
|
|
1388
|
+
} finally {
|
|
1389
|
+
if (abortSignal.aborted) {
|
|
1390
|
+
aborted = monitor.isAbortedRun();
|
|
1391
|
+
if (aborted) {
|
|
1392
|
+
abortReasonText ??= monitor.resolveAbortReasonText();
|
|
1393
|
+
}
|
|
1394
|
+
if (exitCode === 0) exitCode = 1;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return { exitCode, error, aborted, abortReasonText };
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
interface FinalizeRunArgs {
|
|
1402
|
+
monitor: SubagentRunMonitor;
|
|
1403
|
+
done: { exitCode: number; error?: string; aborted?: boolean; abortReason?: string; durationMs: number };
|
|
1404
|
+
index: number;
|
|
1405
|
+
id: string;
|
|
1406
|
+
agent: AgentDefinition;
|
|
1407
|
+
task: string;
|
|
1408
|
+
assignment?: string;
|
|
1409
|
+
description?: string;
|
|
1410
|
+
modelOverride?: string | string[];
|
|
1411
|
+
outputSchema?: unknown;
|
|
1412
|
+
signal?: AbortSignal;
|
|
1413
|
+
artifactsDir?: string;
|
|
1414
|
+
eventBus?: EventBus;
|
|
1415
|
+
parentToolCallId?: string;
|
|
1416
|
+
sessionFile?: string;
|
|
1417
|
+
startTime: number;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Turn a settled run into a {@link SingleResult}: resolve the yield payload via
|
|
1422
|
+
* {@link finalizeSubprocessOutput}, salvage cancelled-run output, write the
|
|
1423
|
+
* `<id>.md` output artifact, flush final progress, and emit the lifecycle end
|
|
1424
|
+
* event.
|
|
1425
|
+
*/
|
|
1426
|
+
async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
1427
|
+
const { monitor, done, index, id, agent, task, assignment, signal, modelOverride } = args;
|
|
1428
|
+
const progress = monitor.progress;
|
|
1429
|
+
let exitCode = done.exitCode;
|
|
1430
|
+
let stderr = done.error ?? "";
|
|
1431
|
+
|
|
1432
|
+
// Use final output if available, otherwise accumulated output
|
|
1433
|
+
let rawOutput = monitor.rawOutput();
|
|
1434
|
+
const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
|
|
1435
|
+
const reportFindingDetails = progress.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
|
|
1436
|
+
const reportFindings: ReviewFinding[] | undefined = reportFindingDetails?.map(toReviewFinding);
|
|
1437
|
+
const finalized = finalizeSubprocessOutput({
|
|
1438
|
+
rawOutput,
|
|
1439
|
+
exitCode,
|
|
1440
|
+
stderr,
|
|
1441
|
+
doneAborted: Boolean(done.aborted),
|
|
1442
|
+
signalAborted: Boolean(signal?.aborted),
|
|
1443
|
+
yieldItems,
|
|
1444
|
+
reportFindings,
|
|
1445
|
+
outputSchema: args.outputSchema,
|
|
1446
|
+
});
|
|
1447
|
+
rawOutput = finalized.rawOutput;
|
|
1448
|
+
exitCode = finalized.exitCode;
|
|
1449
|
+
stderr = finalized.stderr;
|
|
1450
|
+
// Salvage for cancelled/aborted children that produced no completed output:
|
|
1451
|
+
// surface the last assistant text + stats instead of "(no output)" so the
|
|
1452
|
+
// parent doesn't redo work the child already finished.
|
|
1453
|
+
const salvageText = monitor.lastAssistantSalvageText();
|
|
1454
|
+
if (
|
|
1455
|
+
(done.aborted || signal?.aborted || monitor.runtimeLimitExceeded()) &&
|
|
1456
|
+
!rawOutput.trim() &&
|
|
1457
|
+
salvageText !== undefined
|
|
1458
|
+
) {
|
|
1459
|
+
rawOutput = `[cancelled after ${progress.requests} req, ${progress.tokens} tok — last activity: "${formatSalvageSnippet(salvageText)}"]`;
|
|
1460
|
+
}
|
|
1461
|
+
const lastYield = yieldItems?.[yieldItems.length - 1];
|
|
1462
|
+
const yieldAbortReason = lastYield?.status === "aborted" ? lastYield.error || "Subagent aborted task" : undefined;
|
|
1463
|
+
const { abortedViaYield, hasYield } = finalized;
|
|
1464
|
+
const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
|
|
1465
|
+
maxBytes: MAX_OUTPUT_BYTES,
|
|
1466
|
+
maxLines: MAX_OUTPUT_LINES,
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// Write output artifact (input and jsonl already written in real-time)
|
|
1470
|
+
// Compute output metadata for agent:// URL integration
|
|
1471
|
+
let outputMeta: { lineCount: number; charCount: number } | undefined;
|
|
1472
|
+
let outputPath: string | undefined;
|
|
1473
|
+
if (args.artifactsDir) {
|
|
1474
|
+
outputPath = path.join(args.artifactsDir, `${id}.md`);
|
|
1475
|
+
try {
|
|
1476
|
+
await Bun.write(outputPath, rawOutput);
|
|
1477
|
+
outputMeta = {
|
|
1478
|
+
lineCount: rawOutput.split("\n").length,
|
|
1479
|
+
charCount: rawOutput.length,
|
|
1480
|
+
};
|
|
1481
|
+
} catch {
|
|
1482
|
+
// Non-fatal
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Update final progress. A wall-clock timeout always wins: if the runtime
|
|
1487
|
+
// limit fired we report aborted/failed regardless of whether a yield landed
|
|
1488
|
+
// while we were tearing the session down. The yield data is still surfaced
|
|
1489
|
+
// to the caller via `progress.extractedToolData`, but the exit status must
|
|
1490
|
+
// reflect the timeout so on-call doesn't mistake a stuck run for success.
|
|
1491
|
+
const runtimeLimitExceeded = monitor.runtimeLimitExceeded();
|
|
1492
|
+
if (runtimeLimitExceeded && exitCode === 0) {
|
|
1493
|
+
exitCode = 1;
|
|
1494
|
+
}
|
|
1495
|
+
const wasAborted =
|
|
1496
|
+
runtimeLimitExceeded || abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
|
|
1497
|
+
const finalAbortReason = wasAborted
|
|
1498
|
+
? runtimeLimitExceeded
|
|
1499
|
+
? monitor.resolveAbortReasonText()
|
|
1500
|
+
: abortedViaYield
|
|
1501
|
+
? yieldAbortReason
|
|
1502
|
+
: (done.abortReason ??
|
|
1503
|
+
(signal?.aborted ? monitor.resolveSignalAbortReason() : monitor.resolveAbortReasonText()))
|
|
1504
|
+
: undefined;
|
|
1505
|
+
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1506
|
+
monitor.scheduleProgress(true);
|
|
1507
|
+
|
|
1508
|
+
// Emit lifecycle end event after finalization so yield status is reflected
|
|
1509
|
+
if (args.eventBus) {
|
|
1510
|
+
args.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
|
|
1511
|
+
id,
|
|
1512
|
+
agent: agent.name,
|
|
1513
|
+
parentToolCallId: args.parentToolCallId,
|
|
1514
|
+
agentSource: agent.source,
|
|
1515
|
+
description: args.description,
|
|
1516
|
+
status: progress.status as "completed" | "failed" | "aborted",
|
|
1517
|
+
sessionFile: args.sessionFile,
|
|
1518
|
+
index,
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return {
|
|
1523
|
+
index,
|
|
1524
|
+
id,
|
|
1525
|
+
agent: agent.name,
|
|
1526
|
+
agentSource: agent.source,
|
|
1527
|
+
task,
|
|
1528
|
+
assignment,
|
|
1529
|
+
description: args.description,
|
|
1530
|
+
lastIntent: progress.lastIntent,
|
|
1531
|
+
exitCode,
|
|
1532
|
+
output: truncatedOutput,
|
|
1533
|
+
stderr,
|
|
1534
|
+
truncated: Boolean(truncated),
|
|
1535
|
+
durationMs: Date.now() - args.startTime,
|
|
1536
|
+
tokens: progress.tokens,
|
|
1537
|
+
requests: progress.requests,
|
|
1538
|
+
contextTokens: progress.contextTokens,
|
|
1539
|
+
contextWindow: progress.contextWindow,
|
|
1540
|
+
modelOverride,
|
|
1541
|
+
resolvedModel: progress.resolvedModel,
|
|
1542
|
+
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1543
|
+
aborted: wasAborted,
|
|
1544
|
+
abortReason: finalAbortReason,
|
|
1545
|
+
usage: monitor.hasUsage() ? monitor.accumulatedUsage : undefined,
|
|
1546
|
+
outputPath,
|
|
1547
|
+
extractedToolData: progress.extractedToolData,
|
|
1548
|
+
retryFailure: progress.retryFailure,
|
|
1549
|
+
outputMeta,
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* Run a single agent in-process.
|
|
1555
|
+
*/
|
|
1556
|
+
export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
|
|
1557
|
+
const {
|
|
1558
|
+
cwd,
|
|
1559
|
+
agent,
|
|
1560
|
+
task,
|
|
1561
|
+
assignment,
|
|
1562
|
+
index,
|
|
1563
|
+
id,
|
|
1564
|
+
worktree,
|
|
1565
|
+
modelOverride,
|
|
1566
|
+
thinkingLevel,
|
|
1567
|
+
outputSchema,
|
|
1568
|
+
enableLsp,
|
|
1569
|
+
signal,
|
|
1570
|
+
onProgress,
|
|
1571
|
+
} = options;
|
|
1572
|
+
const startTime = Date.now();
|
|
1573
|
+
|
|
1574
|
+
// Check if already aborted
|
|
1575
|
+
if (signal?.aborted) {
|
|
1576
|
+
return {
|
|
1577
|
+
index,
|
|
1578
|
+
id,
|
|
1579
|
+
agent: agent.name,
|
|
1580
|
+
agentSource: agent.source,
|
|
1581
|
+
task,
|
|
1582
|
+
assignment,
|
|
1583
|
+
description: options.description,
|
|
1584
|
+
exitCode: 1,
|
|
1585
|
+
output: "",
|
|
1586
|
+
stderr: "Cancelled before start",
|
|
1587
|
+
truncated: false,
|
|
1588
|
+
durationMs: 0,
|
|
1589
|
+
tokens: 0,
|
|
1590
|
+
requests: 0,
|
|
1591
|
+
modelOverride,
|
|
1592
|
+
error: "Cancelled before start",
|
|
1593
|
+
aborted: true,
|
|
1594
|
+
abortReason: "Cancelled before start",
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Set up artifact paths and write input file upfront if artifacts dir provided
|
|
1599
|
+
let subtaskSessionFile: string | undefined;
|
|
1600
|
+
if (options.artifactsDir) {
|
|
1601
|
+
subtaskSessionFile = path.join(options.artifactsDir, `${id}.jsonl`);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const settings = options.settings ?? Settings.isolated();
|
|
1605
|
+
const subagentSettings = createSubagentSettings(
|
|
1606
|
+
settings,
|
|
1607
|
+
agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
|
|
1608
|
+
);
|
|
1609
|
+
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
1610
|
+
const maxRuntimeMs = Math.max(
|
|
1611
|
+
0,
|
|
1612
|
+
Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
|
|
1613
|
+
);
|
|
1614
|
+
// TTL before an adopted idle subagent is parked by the lifecycle manager.
|
|
1615
|
+
// <= 0 disables parking (the session stays live until process teardown).
|
|
1616
|
+
const agentIdleTtlMs = Math.trunc(Number(settings.get("task.agentIdleTtlMs") ?? 420_000) || 0);
|
|
1617
|
+
const configuredDefaultBudget = Math.max(
|
|
1618
|
+
0,
|
|
1619
|
+
Math.trunc(Number(settings.get("task.softRequestBudget") ?? SOFT_REQUEST_BUDGET.default) || 0),
|
|
1620
|
+
);
|
|
1621
|
+
const softRequestBudget =
|
|
1622
|
+
configuredDefaultBudget === 0 ? 0 : (SOFT_REQUEST_BUDGET[agent.name] ?? configuredDefaultBudget);
|
|
1623
|
+
const parentDepth = options.taskDepth ?? 0;
|
|
1624
|
+
const childDepth = parentDepth + 1;
|
|
1625
|
+
const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
|
|
1626
|
+
|
|
1627
|
+
// Add tools if specified
|
|
1628
|
+
let toolNames: string[] | undefined;
|
|
1629
|
+
if (agent.tools && agent.tools.length > 0) {
|
|
1630
|
+
toolNames = agent.tools;
|
|
1631
|
+
// Auto-include task tool if spawns defined but task not in tools
|
|
1632
|
+
if (agent.spawns !== undefined && !toolNames.includes("task") && !atMaxDepth) {
|
|
1633
|
+
toolNames = [...toolNames, "task"];
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (atMaxDepth && toolNames?.includes("task")) {
|
|
1638
|
+
toolNames = toolNames.filter(name => name !== "task");
|
|
1639
|
+
}
|
|
1640
|
+
// IRC is always available; the COOP prompt section advertises it, so a restricted
|
|
1641
|
+
// whitelist must still carry `irc` for the subagent to actually use it.
|
|
1642
|
+
if (toolNames && !toolNames.includes("irc")) {
|
|
1643
|
+
toolNames = [...toolNames, "irc"];
|
|
1644
|
+
}
|
|
1645
|
+
if (toolNames?.includes("exec")) {
|
|
1646
|
+
const allowEvalPy = settings.get("eval.py") ?? true;
|
|
1647
|
+
const allowEvalJs = settings.get("eval.js") ?? true;
|
|
1648
|
+
const expanded = toolNames.filter(name => name !== "exec");
|
|
1649
|
+
if (allowEvalPy || allowEvalJs) expanded.push("eval");
|
|
1650
|
+
expanded.push("bash");
|
|
1651
|
+
toolNames = Array.from(new Set(expanded));
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const modelPatterns = normalizeModelPatterns(modelOverride ?? agent.model);
|
|
1655
|
+
const sessionFile = subtaskSessionFile ?? null;
|
|
1656
|
+
const spawnsEnv = atMaxDepth
|
|
1657
|
+
? ""
|
|
1658
|
+
: agent.spawns === undefined
|
|
1659
|
+
? ""
|
|
1660
|
+
: agent.spawns === "*"
|
|
1661
|
+
? "*"
|
|
1662
|
+
: agent.spawns.join(",");
|
|
1663
|
+
|
|
1664
|
+
const lspEnabled = enableLsp ?? true;
|
|
1665
|
+
const ircEnabled = isIrcEnabled(subagentSettings, childDepth);
|
|
1666
|
+
const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
|
|
1667
|
+
|
|
1668
|
+
const monitor = createSubagentRunMonitor({
|
|
1669
|
+
index,
|
|
1670
|
+
id,
|
|
1671
|
+
agent,
|
|
1672
|
+
task,
|
|
1673
|
+
assignment,
|
|
1674
|
+
description: options.description,
|
|
1675
|
+
modelOverride,
|
|
1676
|
+
signal,
|
|
1677
|
+
onProgress,
|
|
1678
|
+
eventBus: options.eventBus,
|
|
1679
|
+
parentToolCallId: options.parentToolCallId,
|
|
1680
|
+
sessionFile: subtaskSessionFile,
|
|
1681
|
+
softRequestBudget,
|
|
1682
|
+
maxRuntimeMs,
|
|
1683
|
+
});
|
|
1684
|
+
const progress = monitor.progress;
|
|
1685
|
+
let unsubscribe: (() => void) | null = null;
|
|
1686
|
+
let reviveSession: (() => Promise<AgentSession>) | null = null;
|
|
1687
|
+
// Adopted (kept-alive) subagents flip registry status from session events on
|
|
1688
|
+
// later turns: revive/wake → running, turn drained → idle. The subscription
|
|
1689
|
+
// intentionally survives this run; a disposed session emits nothing, so it
|
|
1690
|
+
// needs no teardown.
|
|
1691
|
+
const installRegistryStatusSync = (target: AgentSession): void => {
|
|
1692
|
+
target.subscribe(event => {
|
|
1693
|
+
if (event.type === "agent_start") {
|
|
1694
|
+
AgentRegistry.global().setStatus(id, "running");
|
|
1695
|
+
} else if (event.type === "agent_end") {
|
|
1696
|
+
AgentRegistry.global().setStatus(id, "idle");
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
};
|
|
1700
|
+
|
|
1701
|
+
const runSubagent = async (): Promise<{
|
|
1702
|
+
exitCode: number;
|
|
1703
|
+
error?: string;
|
|
1704
|
+
aborted?: boolean;
|
|
1705
|
+
abortReason?: string;
|
|
1706
|
+
durationMs: number;
|
|
1707
|
+
}> => {
|
|
1708
|
+
const sessionAbortController = new AbortController();
|
|
1709
|
+
const abortSignal = monitor.abortSignal;
|
|
1710
|
+
let exitCode = 0;
|
|
1711
|
+
let error: string | undefined;
|
|
1712
|
+
let aborted = false;
|
|
1713
|
+
let abortReasonText: string | undefined;
|
|
1714
|
+
const checkAbort = () => {
|
|
1715
|
+
if (abortSignal.aborted) {
|
|
1716
|
+
throw new ToolAbortError();
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
const awaitAbortable = async <T>(promise: Promise<T>): Promise<T> => {
|
|
1720
|
+
checkAbort();
|
|
1721
|
+
const { promise: abortPromise, reject } = Promise.withResolvers<never>();
|
|
1722
|
+
const onAbort = () => {
|
|
1723
|
+
try {
|
|
1724
|
+
checkAbort();
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
reject(err);
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1730
|
+
try {
|
|
1731
|
+
return await Promise.race([promise, abortPromise]);
|
|
1732
|
+
} finally {
|
|
1733
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1737
|
+
try {
|
|
1738
|
+
checkAbort();
|
|
1739
|
+
// Pin authStorage to modelRegistry.authStorage — mirrors the createAgentSession invariant.
|
|
1740
|
+
const registryFromParent = options.modelRegistry !== undefined;
|
|
1741
|
+
const modelRegistry =
|
|
1742
|
+
options.modelRegistry ??
|
|
1743
|
+
new ModelRegistry(options.authStorage ?? (await awaitAbortable(discoverAuthStorage())));
|
|
1744
|
+
const authStorage = modelRegistry.authStorage;
|
|
1745
|
+
if (options.authStorage && options.authStorage !== authStorage) {
|
|
1746
|
+
throw new Error(
|
|
1747
|
+
"options.authStorage and options.modelRegistry.authStorage must be the same instance when both are provided",
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
checkAbort();
|
|
1751
|
+
if (!registryFromParent) {
|
|
1752
|
+
await awaitAbortable(modelRegistry.refresh());
|
|
1753
|
+
} else {
|
|
1754
|
+
logger.debug("runSubagent: reusing parent modelRegistry; skipping refresh");
|
|
1755
|
+
}
|
|
1756
|
+
checkAbort();
|
|
1757
|
+
|
|
1758
|
+
const {
|
|
1759
|
+
model,
|
|
1760
|
+
thinkingLevel: resolvedThinkingLevel,
|
|
1761
|
+
explicitThinkingLevel,
|
|
1762
|
+
authFallbackUsed,
|
|
1763
|
+
} = await awaitAbortable(
|
|
1764
|
+
resolveModelOverrideWithAuthFallback(
|
|
1765
|
+
modelPatterns,
|
|
1766
|
+
options.parentActiveModelPattern,
|
|
1767
|
+
modelRegistry,
|
|
1768
|
+
settings,
|
|
1769
|
+
),
|
|
1770
|
+
);
|
|
1771
|
+
if (authFallbackUsed && model) {
|
|
1772
|
+
logger.warn("Subagent model has no working credentials; falling back to parent session model", {
|
|
1773
|
+
requested: modelPatterns,
|
|
1774
|
+
parentModel: options.parentActiveModelPattern,
|
|
1775
|
+
resolvedProvider: model.provider,
|
|
1776
|
+
resolvedModel: model.id,
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
if (model?.contextWindow && model.contextWindow > 0) {
|
|
1780
|
+
progress.contextWindow = model.contextWindow;
|
|
1781
|
+
}
|
|
1782
|
+
if (model) {
|
|
1783
|
+
progress.resolvedModel = explicitThinkingLevel
|
|
1784
|
+
? `${model.provider}/${model.id}:${resolvedThinkingLevel}`
|
|
1785
|
+
: `${model.provider}/${model.id}`;
|
|
1786
|
+
}
|
|
1787
|
+
const effectiveThinkingLevel = explicitThinkingLevel
|
|
1788
|
+
? resolvedThinkingLevel
|
|
1789
|
+
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1790
|
+
|
|
1791
|
+
const sessionManager = sessionFile
|
|
1792
|
+
? await awaitAbortable(SessionManager.open(sessionFile))
|
|
1793
|
+
: SessionManager.inMemory(worktree ?? cwd);
|
|
1794
|
+
if (options.parentArtifactManager) {
|
|
1795
|
+
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
|
|
1799
|
+
const enableMCP = !options.mcpManager;
|
|
1800
|
+
|
|
1801
|
+
// Derive subagent-scoped telemetry from the parent's config so the
|
|
1802
|
+
// child loop's spans nest under the parent's active execute_tool span
|
|
1803
|
+
// (OTEL context propagation handles parent linkage automatically),
|
|
1259
1804
|
// carry the subagent's own agent identity, and use the subagent's
|
|
1260
1805
|
// own session id for `gen_ai.conversation.id`.
|
|
1261
1806
|
const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
|
|
@@ -1285,7 +1830,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1285
1830
|
|
|
1286
1831
|
const { normalized: normalizedOutputSchema } = normalizeSchema(outputSchema);
|
|
1287
1832
|
|
|
1288
|
-
|
|
1833
|
+
// Captured by the lifecycle reviver: rebuilding an equivalent session from
|
|
1834
|
+
// the same JSONL file re-invokes createAgentSession with the exact options
|
|
1835
|
+
// of the original run (same agent id, tools, model, system prompt,
|
|
1836
|
+
// artifacts dir) — only the SessionManager differs.
|
|
1837
|
+
const buildSubagentSessionOptions = (sessionManagerForRun: SessionManager): CreateAgentSessionOptions => ({
|
|
1289
1838
|
cwd: worktree ?? cwd,
|
|
1290
1839
|
authStorage,
|
|
1291
1840
|
modelRegistry,
|
|
@@ -1310,7 +1859,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1310
1859
|
planReferencePath: options.planReference?.path ?? "",
|
|
1311
1860
|
worktree: worktree ?? "",
|
|
1312
1861
|
outputSchema: normalizedOutputSchema,
|
|
1313
|
-
contextFile: contextFileForPrompt,
|
|
1314
1862
|
ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
|
|
1315
1863
|
ircSelfId: ircEnabled ? id : "",
|
|
1316
1864
|
});
|
|
@@ -1318,7 +1866,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1318
1866
|
? [subagentPrompt]
|
|
1319
1867
|
: [...defaultPrompt.slice(0, -1), subagentPrompt, defaultPrompt[defaultPrompt.length - 1]];
|
|
1320
1868
|
},
|
|
1321
|
-
sessionManager,
|
|
1869
|
+
sessionManager: sessionManagerForRun,
|
|
1322
1870
|
hasUI: false,
|
|
1323
1871
|
spawns: spawnsEnv,
|
|
1324
1872
|
taskDepth: childDepth,
|
|
@@ -1336,6 +1884,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1336
1884
|
telemetry: subagentTelemetry,
|
|
1337
1885
|
parentEvalSessionId: options.parentEvalSessionId,
|
|
1338
1886
|
});
|
|
1887
|
+
|
|
1888
|
+
const sessionPromise = createAgentSession(buildSubagentSessionOptions(sessionManager));
|
|
1339
1889
|
let session: AgentSession;
|
|
1340
1890
|
try {
|
|
1341
1891
|
({ session } = await awaitAbortable(sessionPromise));
|
|
@@ -1347,13 +1897,30 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1347
1897
|
throw err;
|
|
1348
1898
|
}
|
|
1349
1899
|
|
|
1350
|
-
|
|
1900
|
+
monitor.setActiveSession(session);
|
|
1901
|
+
installRegistryStatusSync(session);
|
|
1902
|
+
if (sessionFile !== null && worktree === undefined) {
|
|
1903
|
+
// Lifecycle reviver: park closed the JSONL writer, so reopening takes
|
|
1904
|
+
// the single-writer lock cleanly and restores the full message history
|
|
1905
|
+
// (createAgentSession → agent.replaceMessages). Isolated runs are not
|
|
1906
|
+
// resumable (worktree is merged + cleaned) and never get a reviver.
|
|
1907
|
+
reviveSession = async () => {
|
|
1908
|
+
const reopened = await SessionManager.open(sessionFile);
|
|
1909
|
+
if (options.parentArtifactManager) {
|
|
1910
|
+
reopened.adoptArtifactManager(options.parentArtifactManager);
|
|
1911
|
+
}
|
|
1912
|
+
const { session: revived } = await createAgentSession(buildSubagentSessionOptions(reopened));
|
|
1913
|
+
installRegistryStatusSync(revived);
|
|
1914
|
+
return revived;
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1351
1917
|
|
|
1352
1918
|
// Emit lifecycle start event
|
|
1353
1919
|
if (options.eventBus) {
|
|
1354
1920
|
options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
|
|
1355
1921
|
id,
|
|
1356
1922
|
agent: agent.name,
|
|
1923
|
+
parentToolCallId: options.parentToolCallId,
|
|
1357
1924
|
agentSource: agent.source,
|
|
1358
1925
|
description: options.description,
|
|
1359
1926
|
status: "started",
|
|
@@ -1450,43 +2017,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1450
2017
|
}
|
|
1451
2018
|
}
|
|
1452
2019
|
|
|
1453
|
-
|
|
1454
|
-
unsubscribe = session.subscribe(event => {
|
|
1455
|
-
if (event.type === "auto_retry_start") {
|
|
1456
|
-
progress.retryState = {
|
|
1457
|
-
attempt: event.attempt,
|
|
1458
|
-
maxAttempts: event.maxAttempts,
|
|
1459
|
-
delayMs: event.delayMs,
|
|
1460
|
-
errorMessage: event.errorMessage,
|
|
1461
|
-
startedAtMs: Date.now(),
|
|
1462
|
-
};
|
|
1463
|
-
progress.retryFailure = undefined;
|
|
1464
|
-
scheduleProgress(true);
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
if (event.type === "auto_retry_end") {
|
|
1468
|
-
const attempt = progress.retryState?.attempt ?? event.attempt;
|
|
1469
|
-
progress.retryState = undefined;
|
|
1470
|
-
if (!event.success) {
|
|
1471
|
-
progress.retryFailure = {
|
|
1472
|
-
attempt,
|
|
1473
|
-
errorMessage: event.finalError ?? "Auto-retry failed",
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
scheduleProgress(true);
|
|
1477
|
-
return;
|
|
1478
|
-
}
|
|
1479
|
-
if (isAgentEvent(event)) {
|
|
1480
|
-
try {
|
|
1481
|
-
processEvent(event);
|
|
1482
|
-
} catch (err) {
|
|
1483
|
-
logger.error("Subagent event processing failed", {
|
|
1484
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1485
|
-
});
|
|
1486
|
-
requestAbort("terminate");
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
});
|
|
2020
|
+
unsubscribe = monitor.attach(session);
|
|
1490
2021
|
|
|
1491
2022
|
checkAbort();
|
|
1492
2023
|
// Autoload skills via sendCustomMessage (same mechanic as /skill:<name>)
|
|
@@ -1504,78 +2035,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1504
2035
|
);
|
|
1505
2036
|
}
|
|
1506
2037
|
}
|
|
1507
|
-
await awaitAbortable(session.prompt(task, { attribution: "agent" }));
|
|
1508
|
-
await awaitAbortable(session.waitForIdle());
|
|
1509
|
-
|
|
1510
|
-
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
|
|
1511
|
-
|
|
1512
|
-
let retryCount = 0;
|
|
1513
|
-
while (!yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
1514
|
-
// Skip reminders when the model returned a terminal error (e.g.
|
|
1515
|
-
// rate-limit cap hit, auth failure). Re-prompting would just
|
|
1516
|
-
// hit the same wall, multiplying the failure noise without
|
|
1517
|
-
// any chance of producing a yield.
|
|
1518
|
-
const lastBeforeReminder = session.getLastAssistantMessage();
|
|
1519
|
-
if (lastBeforeReminder?.stopReason === "error") break;
|
|
1520
|
-
try {
|
|
1521
|
-
retryCount++;
|
|
1522
|
-
const reminder = prompt.render(submitReminderTemplate, {
|
|
1523
|
-
retryCount,
|
|
1524
|
-
maxRetries: MAX_YIELD_RETRIES,
|
|
1525
|
-
});
|
|
1526
|
-
|
|
1527
|
-
const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
|
|
1528
|
-
await awaitAbortable(
|
|
1529
|
-
session.prompt(reminder, {
|
|
1530
|
-
attribution: "agent",
|
|
1531
|
-
synthetic: true,
|
|
1532
|
-
...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
|
|
1533
|
-
}),
|
|
1534
|
-
);
|
|
1535
|
-
await awaitAbortable(session.waitForIdle());
|
|
1536
|
-
} catch (err) {
|
|
1537
|
-
if (abortSignal.aborted || err instanceof ToolAbortError) {
|
|
1538
|
-
// Benign control-flow exit — user cancel (^C) or compaction aborting
|
|
1539
|
-
// pending operations both surface here as ToolAbortError. The outer
|
|
1540
|
-
// catch and finally already mark the run aborted; logging at ERROR
|
|
1541
|
-
// would spam operator dashboards with non-failures.
|
|
1542
|
-
logger.debug("Subagent prompt aborted", {
|
|
1543
|
-
reason: abortReason ?? "signal",
|
|
1544
|
-
});
|
|
1545
|
-
} else {
|
|
1546
|
-
logger.error("Subagent prompt failed", {
|
|
1547
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
await awaitAbortable(session.waitForIdle());
|
|
1554
|
-
if (!yieldCalled && !abortSignal.aborted) {
|
|
1555
|
-
exitCode = 0;
|
|
1556
|
-
}
|
|
1557
2038
|
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
// A real caller signal or the wall-clock timer carries a precise
|
|
1564
|
-
// reason (signal.reason / "runtime limit exceeded"). An internal
|
|
1565
|
-
// turn abort (abortReason === undefined) does NOT — prefer the
|
|
1566
|
-
// assistant message's own errorMessage ("Request was aborted" or a
|
|
1567
|
-
// specific stream error) over the misleading "Cancelled by caller".
|
|
1568
|
-
abortReasonText ??=
|
|
1569
|
-
abortReason === "signal" || runtimeLimitExceeded
|
|
1570
|
-
? resolveAbortReasonText()
|
|
1571
|
-
: lastAssistant.errorMessage?.trim() || resolveAbortReasonText();
|
|
1572
|
-
}
|
|
1573
|
-
exitCode = 1;
|
|
1574
|
-
} else if (lastAssistant.stopReason === "error") {
|
|
1575
|
-
exitCode = 1;
|
|
1576
|
-
error ??= lastAssistant.errorMessage || "Subagent failed";
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
2039
|
+
const outcome = await driveSessionToYield(session, monitor, task);
|
|
2040
|
+
exitCode = outcome.exitCode;
|
|
2041
|
+
error = outcome.error;
|
|
2042
|
+
aborted = outcome.aborted;
|
|
2043
|
+
abortReasonText = outcome.abortReasonText;
|
|
1579
2044
|
} catch (err) {
|
|
1580
2045
|
exitCode = 1;
|
|
1581
2046
|
if (!abortSignal.aborted) {
|
|
@@ -1583,9 +2048,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1583
2048
|
}
|
|
1584
2049
|
} finally {
|
|
1585
2050
|
if (abortSignal.aborted) {
|
|
1586
|
-
aborted =
|
|
2051
|
+
aborted = monitor.isAbortedRun();
|
|
1587
2052
|
if (aborted) {
|
|
1588
|
-
abortReasonText ??= resolveAbortReasonText();
|
|
2053
|
+
abortReasonText ??= monitor.resolveAbortReasonText();
|
|
1589
2054
|
}
|
|
1590
2055
|
if (exitCode === 0) exitCode = 1;
|
|
1591
2056
|
}
|
|
@@ -1598,13 +2063,39 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1598
2063
|
}
|
|
1599
2064
|
unsubscribe = null;
|
|
1600
2065
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
2066
|
+
const session = monitor.takeActiveSession();
|
|
2067
|
+
if (session) {
|
|
2068
|
+
monitor.captureSalvage(session);
|
|
2069
|
+
const registry = AgentRegistry.global();
|
|
2070
|
+
if (aborted) {
|
|
2071
|
+
// Hard abort (caller signal / wall-clock / budget): terminal teardown.
|
|
2072
|
+
registry.setStatus(id, "aborted");
|
|
2073
|
+
try {
|
|
2074
|
+
await untilAborted(AbortSignal.timeout(5000), () => session.dispose());
|
|
2075
|
+
} catch {
|
|
2076
|
+
// Ignore cleanup errors
|
|
2077
|
+
}
|
|
2078
|
+
} else if (worktree !== undefined) {
|
|
2079
|
+
// Isolated run: the worktree is merged + cleaned after the run, so
|
|
2080
|
+
// the session is not resumable. Park the ref WITHOUT adopting — the
|
|
2081
|
+
// transcript stays reachable (history://), but ensureLive will throw.
|
|
2082
|
+
// Status must flip to "parked" before dispose so the sdk dispose
|
|
2083
|
+
// wrapper skips unregister.
|
|
2084
|
+
registry.setStatus(id, "parked");
|
|
2085
|
+
try {
|
|
2086
|
+
await untilAborted(AbortSignal.timeout(5000), () => session.dispose());
|
|
2087
|
+
} catch {
|
|
2088
|
+
// Ignore cleanup errors
|
|
2089
|
+
}
|
|
2090
|
+
registry.detachSession(id);
|
|
2091
|
+
} else {
|
|
2092
|
+
// Keep-alive: finished and failed subagents both stay interrogable.
|
|
2093
|
+
// The lifecycle manager owns idle-TTL parking + revival from here on.
|
|
2094
|
+
registry.setStatus(id, "idle");
|
|
2095
|
+
AgentLifecycleManager.global().adopt(id, {
|
|
2096
|
+
idleTtlMs: agentIdleTtlMs,
|
|
2097
|
+
revive: reviveSession ?? undefined,
|
|
2098
|
+
});
|
|
1608
2099
|
}
|
|
1609
2100
|
}
|
|
1610
2101
|
}
|
|
@@ -1619,125 +2110,24 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1619
2110
|
};
|
|
1620
2111
|
|
|
1621
2112
|
const done = await runSubagent();
|
|
1622
|
-
|
|
1623
|
-
listenerController.abort();
|
|
1624
|
-
if (runtimeTimeoutId !== undefined) {
|
|
1625
|
-
clearTimeout(runtimeTimeoutId);
|
|
1626
|
-
runtimeTimeoutId = undefined;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
if (progressTimeoutId) {
|
|
1630
|
-
clearTimeout(progressTimeoutId);
|
|
1631
|
-
progressTimeoutId = null;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
let exitCode = done.exitCode;
|
|
1635
|
-
if (done.error) {
|
|
1636
|
-
stderr = done.error;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// Use final output if available, otherwise accumulated output
|
|
1640
|
-
let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
|
|
1641
|
-
const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
|
|
1642
|
-
const reportFindingDetails = progress.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
|
|
1643
|
-
const reportFindings: ReviewFinding[] | undefined = reportFindingDetails?.map(toReviewFinding);
|
|
1644
|
-
const finalized = finalizeSubprocessOutput({
|
|
1645
|
-
rawOutput,
|
|
1646
|
-
exitCode,
|
|
1647
|
-
stderr,
|
|
1648
|
-
doneAborted: Boolean(done.aborted),
|
|
1649
|
-
signalAborted: Boolean(signal?.aborted),
|
|
1650
|
-
yieldItems,
|
|
1651
|
-
reportFindings,
|
|
1652
|
-
outputSchema,
|
|
1653
|
-
});
|
|
1654
|
-
rawOutput = finalized.rawOutput;
|
|
1655
|
-
exitCode = finalized.exitCode;
|
|
1656
|
-
stderr = finalized.stderr;
|
|
1657
|
-
const lastYield = yieldItems?.[yieldItems.length - 1];
|
|
1658
|
-
const yieldAbortReason = lastYield?.status === "aborted" ? lastYield.error || "Subagent aborted task" : undefined;
|
|
1659
|
-
const { abortedViaYield, hasYield } = finalized;
|
|
1660
|
-
const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
|
|
1661
|
-
maxBytes: MAX_OUTPUT_BYTES,
|
|
1662
|
-
maxLines: MAX_OUTPUT_LINES,
|
|
1663
|
-
});
|
|
1664
|
-
|
|
1665
|
-
// Write output artifact (input and jsonl already written in real-time)
|
|
1666
|
-
// Compute output metadata for agent:// URL integration
|
|
1667
|
-
let outputMeta: { lineCount: number; charCount: number } | undefined;
|
|
1668
|
-
let outputPath: string | undefined;
|
|
1669
|
-
if (options.artifactsDir) {
|
|
1670
|
-
outputPath = path.join(options.artifactsDir, `${id}.md`);
|
|
1671
|
-
try {
|
|
1672
|
-
await Bun.write(outputPath, rawOutput);
|
|
1673
|
-
outputMeta = {
|
|
1674
|
-
lineCount: rawOutput.split("\n").length,
|
|
1675
|
-
charCount: rawOutput.length,
|
|
1676
|
-
};
|
|
1677
|
-
} catch {
|
|
1678
|
-
// Non-fatal
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
// Update final progress. A wall-clock timeout always wins: if the runtime
|
|
1683
|
-
// limit fired we report aborted/failed regardless of whether a yield landed
|
|
1684
|
-
// while we were tearing the session down. The yield data is still surfaced
|
|
1685
|
-
// to the caller via `progress.extractedToolData`, but the exit status must
|
|
1686
|
-
// reflect the timeout so on-call doesn't mistake a stuck run for success.
|
|
1687
|
-
if (runtimeLimitExceeded && exitCode === 0) {
|
|
1688
|
-
exitCode = 1;
|
|
1689
|
-
}
|
|
1690
|
-
const wasAborted =
|
|
1691
|
-
runtimeLimitExceeded || abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
|
|
1692
|
-
const finalAbortReason = wasAborted
|
|
1693
|
-
? runtimeLimitExceeded
|
|
1694
|
-
? resolveAbortReasonText()
|
|
1695
|
-
: abortedViaYield
|
|
1696
|
-
? yieldAbortReason
|
|
1697
|
-
: (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : resolveAbortReasonText()))
|
|
1698
|
-
: undefined;
|
|
1699
|
-
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1700
|
-
scheduleProgress(true);
|
|
1701
|
-
|
|
1702
|
-
// Emit lifecycle end event after finalization so yield status is reflected
|
|
1703
|
-
if (options.eventBus) {
|
|
1704
|
-
options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
|
|
1705
|
-
id,
|
|
1706
|
-
agent: agent.name,
|
|
1707
|
-
agentSource: agent.source,
|
|
1708
|
-
description: options.description,
|
|
1709
|
-
status: progress.status as "completed" | "failed" | "aborted",
|
|
1710
|
-
sessionFile: subtaskSessionFile,
|
|
1711
|
-
index,
|
|
1712
|
-
});
|
|
1713
|
-
}
|
|
2113
|
+
monitor.finish();
|
|
1714
2114
|
|
|
1715
|
-
return {
|
|
2115
|
+
return finalizeRunResult({
|
|
2116
|
+
monitor,
|
|
2117
|
+
done,
|
|
1716
2118
|
index,
|
|
1717
2119
|
id,
|
|
1718
|
-
agent
|
|
1719
|
-
agentSource: agent.source,
|
|
2120
|
+
agent,
|
|
1720
2121
|
task,
|
|
1721
2122
|
assignment,
|
|
1722
2123
|
description: options.description,
|
|
1723
|
-
lastIntent: progress.lastIntent,
|
|
1724
|
-
exitCode,
|
|
1725
|
-
output: truncatedOutput,
|
|
1726
|
-
stderr,
|
|
1727
|
-
truncated: Boolean(truncated),
|
|
1728
|
-
durationMs: Date.now() - startTime,
|
|
1729
|
-
tokens: progress.tokens,
|
|
1730
|
-
contextTokens: progress.contextTokens,
|
|
1731
|
-
contextWindow: progress.contextWindow,
|
|
1732
2124
|
modelOverride,
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
outputMeta,
|
|
1742
|
-
};
|
|
2125
|
+
outputSchema,
|
|
2126
|
+
signal,
|
|
2127
|
+
artifactsDir: options.artifactsDir,
|
|
2128
|
+
eventBus: options.eventBus,
|
|
2129
|
+
parentToolCallId: options.parentToolCallId,
|
|
2130
|
+
sessionFile: subtaskSessionFile,
|
|
2131
|
+
startTime,
|
|
2132
|
+
});
|
|
1743
2133
|
}
|