pi-subagents 0.64.0 → 0.65.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 +33 -0
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +18 -16
- package/docs/configuration.md +7 -15
- package/docs/extension-api.md +15 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +7 -6
- package/docs/watchdog.md +1 -1
- package/docs/workflows.md +5 -3
- package/package.json +3 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +2 -3
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +27 -5
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -8
- package/src/intercom/native-supervisor-channel.ts +138 -60
- package/src/intercom/supervisor-ui.ts +243 -0
- package/src/runs/background/async-execution.ts +43 -24
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +11 -2
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/run-child-session.ts +613 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +125 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +184 -901
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +189 -377
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +100 -57
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +342 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +82 -14
- package/src/runs/shared/model-fallback.ts +47 -4
- package/src/runs/shared/nested-events.ts +29 -45
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +0 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
- package/src/runs/shared/worktree.ts +117 -30
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +55 -28
- package/src/shared/utils.ts +3 -4
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +233 -31
- package/src/watchdog/child-status.ts +0 -1
- package/src/watchdog/register-child.ts +12 -12
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1059
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Core execution logic for running subagents
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
6
|
import * as path from "node:path";
|
|
8
7
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
8
|
import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig } from "../../agents/agents.ts";
|
|
@@ -31,7 +30,6 @@ import {
|
|
|
31
30
|
type AcceptanceLedger,
|
|
32
31
|
type ResolvedAcceptanceConfig,
|
|
33
32
|
truncateOutput,
|
|
34
|
-
getSubagentDepthEnv,
|
|
35
33
|
} from "../../shared/types.ts";
|
|
36
34
|
import {
|
|
37
35
|
DEFAULT_CONTROL_CONFIG,
|
|
@@ -61,21 +59,18 @@ import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMuta
|
|
|
61
59
|
import { planCompletionEvidence } from "../shared/completion-evidence.ts";
|
|
62
60
|
import { planAbortRecovery } from "../shared/abort-recovery.ts";
|
|
63
61
|
import { arbitrateCompletionGuardRescue } from "../shared/llm-intent-arbiter.ts";
|
|
64
|
-
import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
|
|
65
62
|
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
66
63
|
import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
|
|
67
64
|
import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
|
|
68
|
-
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
69
65
|
import { resolvePermissionRules } from "../shared/permissions.ts";
|
|
70
|
-
import { applyThinkingSuffix,
|
|
66
|
+
import { applyThinkingSuffix, deriveForkPromptCacheKey } from "../shared/child-tool-plan.ts";
|
|
71
67
|
import { deriveChildSessionName } from "../../shared/child-session-name.ts";
|
|
72
|
-
import {
|
|
73
|
-
import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
|
|
68
|
+
import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
74
69
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
75
|
-
import { assertThinkingWithinCeiling,
|
|
76
|
-
import {
|
|
77
|
-
import { formatMidToolExitError,
|
|
78
|
-
import {
|
|
70
|
+
import { assertThinkingWithinCeiling, intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
|
|
71
|
+
import { MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR } from "../shared/structured-output.ts";
|
|
72
|
+
import { formatMidToolExitError, isOrdinaryToolForMidToolExit } from "../shared/process-signal.ts";
|
|
73
|
+
import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
|
|
79
74
|
import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
|
|
80
75
|
import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, hasSingleOutputChangedSinceSnapshot, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
81
76
|
import {
|
|
@@ -86,14 +81,6 @@ import {
|
|
|
86
81
|
isRetryableModelFailureAttempt,
|
|
87
82
|
recordRetryableModelFailure,
|
|
88
83
|
} from "../shared/model-fallback.ts";
|
|
89
|
-
import {
|
|
90
|
-
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
91
|
-
formatSubagentExtensionConflictError,
|
|
92
|
-
formatSubagentStartupRetryExhaustedError,
|
|
93
|
-
formatSubagentStartupRetryNote,
|
|
94
|
-
isRetryableSubagentStartupFailure,
|
|
95
|
-
waitForSubagentStartupRetry,
|
|
96
|
-
} from "../shared/subagent-startup-retry.ts";
|
|
97
84
|
import {
|
|
98
85
|
createMutatingFailureState,
|
|
99
86
|
didMutatingToolFail,
|
|
@@ -112,7 +99,7 @@ import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.t
|
|
|
112
99
|
import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
|
|
113
100
|
import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
114
101
|
import { consumeWorkflowChildPermit } from "../../shared/workflow-child-permit.ts";
|
|
115
|
-
import {
|
|
102
|
+
import { projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState } from "../shared/child-lifecycle.ts";
|
|
116
103
|
import {
|
|
117
104
|
acceptChildWatchdogEvent,
|
|
118
105
|
applyChildWatchdogMessage,
|
|
@@ -120,7 +107,10 @@ import {
|
|
|
120
107
|
isChildWatchdogStatusEvent,
|
|
121
108
|
resolveChildWatchdogConfig,
|
|
122
109
|
type ChildWatchdogStateSnapshot,
|
|
110
|
+
type ChildWatchdogStatusEvent,
|
|
123
111
|
} from "../../watchdog/child-status.ts";
|
|
112
|
+
import { buildInProcessChildLaunch } from "../shared/child-launch.ts";
|
|
113
|
+
import { childSessionFactory, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent } from "../shared/child-session.ts";
|
|
124
114
|
|
|
125
115
|
const artifactOutputByResult = new WeakMap<SingleResult, string>();
|
|
126
116
|
const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
|
|
@@ -288,8 +278,8 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
|
|
|
288
278
|
/**
|
|
289
279
|
* Streaming variant of snapshotResult for `onUpdate` progress events: it drops the
|
|
290
280
|
* unbounded `messages` transcript in favour of compact tool-call summaries so a
|
|
291
|
-
* single `tool_execution_update`
|
|
292
|
-
*
|
|
281
|
+
* single `tool_execution_update` event stays small; the parent records every
|
|
282
|
+
* one in its transcript and `events.jsonl`. Non-streaming consumers such as
|
|
293
283
|
* foreground detach receipts and terminal consumers use snapshotResult directly.
|
|
294
284
|
*/
|
|
295
285
|
function snapshotStreamResult(result: SingleResult, progress: AgentProgress): SingleResult {
|
|
@@ -351,6 +341,9 @@ function structuredDelegationProgressChanged(
|
|
|
351
341
|
const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
|
|
352
342
|
type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
|
|
353
343
|
|
|
344
|
+
const STOPPED_BEFORE_COMPLETION_ERROR = "Subagent stopped before completion.";
|
|
345
|
+
|
|
346
|
+
|
|
354
347
|
async function runSingleAttempt(
|
|
355
348
|
runtimeCwd: string,
|
|
356
349
|
agent: AgentConfig,
|
|
@@ -369,7 +362,6 @@ async function runSingleAttempt(
|
|
|
369
362
|
attemptNotes: string[];
|
|
370
363
|
outputSnapshot?: SingleOutputSnapshot;
|
|
371
364
|
originalTask?: string;
|
|
372
|
-
taskDelivery?: SubagentTaskDelivery;
|
|
373
365
|
orcaProgressTab?: OrcaProgressTab;
|
|
374
366
|
launchWarnings: { emitted: boolean };
|
|
375
367
|
verifyModel: boolean;
|
|
@@ -380,9 +372,9 @@ async function runSingleAttempt(
|
|
|
380
372
|
assertThinkingWithinCeiling({ model: modelArg, configThinking: effectiveThinking, ceiling: options.thinkingCeiling, agent: agent.name, runId: options.runId });
|
|
381
373
|
const expectedModelForVerification = shared.verifyModel ? modelArg : undefined;
|
|
382
374
|
const resolvedThinking = resolveEffectiveThinking(modelArg, effectiveThinking);
|
|
383
|
-
// Display name for the child session: applied inside the child
|
|
384
|
-
//
|
|
385
|
-
//
|
|
375
|
+
// Display name for the child session: applied inside the child through its
|
|
376
|
+
// runtime config and echoed back on the result payload so hosts can label
|
|
377
|
+
// this run without reading the child's session file.
|
|
386
378
|
const childSessionName = deriveChildSessionName({ agent: agent.name, task: shared.originalTask ?? task });
|
|
387
379
|
const watchdogConfig = resolveWatchdogConfig(options.cwd ?? runtimeCwd);
|
|
388
380
|
const childWatchdog = watchdogConfig.ok
|
|
@@ -397,15 +389,12 @@ async function runSingleAttempt(
|
|
|
397
389
|
const permissionAuditPath = permissionRules && options.artifactsDir
|
|
398
390
|
? path.join(options.artifactsDir, "permission-audit", `${options.runId}-${options.index ?? 0}.jsonl`)
|
|
399
391
|
: undefined;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
task,
|
|
403
|
-
taskDelivery: shared.taskDelivery,
|
|
392
|
+
let onWatchdogStatus: ((event: ChildWatchdogStatusEvent) => void) | undefined;
|
|
393
|
+
const launch = buildInProcessChildLaunch({
|
|
404
394
|
sessionEnabled: shared.sessionEnabled,
|
|
405
395
|
sessionDir: options.sessionDir,
|
|
406
396
|
sessionFile: options.sessionFile,
|
|
407
397
|
model: modelArg,
|
|
408
|
-
thinking: effectiveThinking,
|
|
409
398
|
systemPromptMode: agent.systemPromptMode,
|
|
410
399
|
inheritProjectContext: agent.inheritProjectContext,
|
|
411
400
|
inheritGlobalContext: agent.inheritGlobalContext,
|
|
@@ -419,63 +408,40 @@ async function runSingleAttempt(
|
|
|
419
408
|
systemPrompt: shared.systemPrompt,
|
|
420
409
|
mcpDirectTools: agent.mcpDirectTools,
|
|
421
410
|
cwd: options.cwd ?? runtimeCwd,
|
|
422
|
-
promptFileStem: agent.name,
|
|
423
411
|
intercomSessionName: options.intercomSessionName,
|
|
424
412
|
sessionName: childSessionName,
|
|
425
413
|
orchestratorIntercomTarget: options.orchestratorIntercomTarget,
|
|
426
414
|
runId: options.runId,
|
|
427
415
|
childAgentName: agent.name,
|
|
428
416
|
childIndex: options.index ?? 0,
|
|
429
|
-
|
|
430
|
-
parentControlInbox: options.nestedRoute?.controlInbox,
|
|
431
|
-
parentRootRunId: options.nestedRoute?.rootRunId,
|
|
432
|
-
parentCapabilityToken: options.nestedRoute?.capabilityToken,
|
|
417
|
+
nestedRoute: options.nestedRoute,
|
|
433
418
|
runFanoutBudget: options.runFanoutBudget,
|
|
434
419
|
parentSessionId: options.parentSessionId,
|
|
435
420
|
forkCacheKey: options.context === "fork" ? deriveForkPromptCacheKey(options.parentSessionId) : undefined,
|
|
436
|
-
steerInboxDir: options.steerInboxDir,
|
|
437
|
-
steerCapabilityPath: options.steerCapabilityPath,
|
|
438
|
-
steerAckDir: options.steerAckDir,
|
|
439
421
|
structuredOutput: options.structuredOutput,
|
|
440
422
|
fast: options.fast ?? agent.fast,
|
|
441
423
|
modelCandidates: shared.modelCandidates,
|
|
442
424
|
toolBudget: options.toolBudget,
|
|
443
|
-
allowZeroToolBudget: options.allowZeroToolBudget,
|
|
444
425
|
permissionRules,
|
|
445
426
|
permissionAuditPath,
|
|
446
427
|
childWatchdog,
|
|
428
|
+
watchdogStatus: (event) => onWatchdogStatus?.(event),
|
|
447
429
|
waitToolEnabled: options.waitToolEnabled,
|
|
448
430
|
waitToolDefaultTimeoutMs: options.waitToolDefaultTimeoutMs,
|
|
449
431
|
capabilityCeiling: options.capabilityCeiling,
|
|
450
432
|
thinkingCeiling: options.thinkingCeiling,
|
|
451
|
-
|
|
433
|
+
maxSubagentDepth: options.maxSubagentDepth,
|
|
452
434
|
runtimeSnapshotHost: options.runtimeSnapshotHost,
|
|
435
|
+
inherited: options.childRuntime,
|
|
436
|
+
host: "parent",
|
|
453
437
|
});
|
|
438
|
+
const { toolPlan, capabilityAudit, warnings, launchResolvedExtensions, capture } = launch;
|
|
454
439
|
if (!shared.launchWarnings.emitted && warnings.length > 0) {
|
|
455
440
|
for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
|
|
456
441
|
shared.launchWarnings.emitted = true;
|
|
457
442
|
}
|
|
458
443
|
|
|
459
444
|
const effectiveSystemPrompt = shared.systemPrompt;
|
|
460
|
-
const toolPlan = resolvePiLaunchToolPlan({
|
|
461
|
-
tools: agent.tools,
|
|
462
|
-
allowNestedSubagents: agent.allowNestedSubagents,
|
|
463
|
-
extensions: agent.extensions,
|
|
464
|
-
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
465
|
-
mcpDirectTools: agent.mcpDirectTools,
|
|
466
|
-
cwd: options.cwd ?? runtimeCwd,
|
|
467
|
-
requireReadTool: Boolean(shared.resolvedSkillNames?.length),
|
|
468
|
-
structuredOutput: Boolean(options.structuredOutput),
|
|
469
|
-
excludeTools: agent.excludeTools,
|
|
470
|
-
fast: options.fast ?? agent.fast,
|
|
471
|
-
model: modelArg,
|
|
472
|
-
modelCandidates: shared.modelCandidates,
|
|
473
|
-
capabilityCeiling: options.capabilityCeiling,
|
|
474
|
-
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
475
|
-
agentName: agent.name,
|
|
476
|
-
permissionRules,
|
|
477
|
-
runtimeSnapshotHost: options.runtimeSnapshotHost,
|
|
478
|
-
});
|
|
479
445
|
const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
|
|
480
446
|
const contractError = validateImplementationToolContract({
|
|
481
447
|
agent: agent.name,
|
|
@@ -488,7 +454,6 @@ async function runSingleAttempt(
|
|
|
488
454
|
completionGuard: agent.completionGuard,
|
|
489
455
|
});
|
|
490
456
|
if (contractError) {
|
|
491
|
-
cleanupTempDir(tempDir);
|
|
492
457
|
return {
|
|
493
458
|
index: options.index ?? 0,
|
|
494
459
|
agent: agent.name,
|
|
@@ -507,7 +472,6 @@ async function runSingleAttempt(
|
|
|
507
472
|
...(toolPlan.capabilityAudit ? { capabilityAudit: toolPlan.capabilityAudit } : {}),
|
|
508
473
|
};
|
|
509
474
|
}
|
|
510
|
-
const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
|
|
511
475
|
const launchContractDigest = launchBindingDigest({
|
|
512
476
|
definitionDigest: agentDefinitionDigest(agent),
|
|
513
477
|
task: shared.originalTask ?? task,
|
|
@@ -554,17 +518,6 @@ async function runSingleAttempt(
|
|
|
554
518
|
...(capabilityAudit ? { capabilityAudit } : {}),
|
|
555
519
|
}, options.context);
|
|
556
520
|
const startTime = Date.now();
|
|
557
|
-
if (options.structuredOutput) {
|
|
558
|
-
const cleanupError = clearStructuredOutputCaptures(options.structuredOutput);
|
|
559
|
-
if (cleanupError) {
|
|
560
|
-
cleanupTempDir(tempDir);
|
|
561
|
-
result.exitCode = 1;
|
|
562
|
-
result.error = cleanupError;
|
|
563
|
-
result.finalOutput = cleanupError;
|
|
564
|
-
result.progressSummary = { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime };
|
|
565
|
-
return result;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
521
|
const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
569
522
|
let interruptedByControl = false;
|
|
570
523
|
const allControlEvents: ControlEvent[] = [];
|
|
@@ -599,7 +552,6 @@ async function runSingleAttempt(
|
|
|
599
552
|
result.progress = progress;
|
|
600
553
|
const attemptTimeout = resolveAttemptTimeout(options);
|
|
601
554
|
if (attemptTimeout?.remainingMs === 0) {
|
|
602
|
-
cleanupTempDir(tempDir);
|
|
603
555
|
result.exitCode = 1;
|
|
604
556
|
result.timedOut = true;
|
|
605
557
|
result.error = attemptTimeout.message;
|
|
@@ -613,7 +565,6 @@ async function runSingleAttempt(
|
|
|
613
565
|
};
|
|
614
566
|
return result;
|
|
615
567
|
}
|
|
616
|
-
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
617
568
|
const mutationSnapshot = snapshotTrackedMutations(options.cwd ?? runtimeCwd);
|
|
618
569
|
let observedMutationAttempt = false;
|
|
619
570
|
let structuredOutputToolInvoked = false;
|
|
@@ -621,7 +572,6 @@ async function runSingleAttempt(
|
|
|
621
572
|
let toolAvailabilityError: string | undefined;
|
|
622
573
|
let abortedBySignal = options.signal?.aborted === true;
|
|
623
574
|
|
|
624
|
-
const spawnSpec = getPiSpawnCommand(args);
|
|
625
575
|
if (options.workflowChildPermitLaunch) {
|
|
626
576
|
const permitError = consumeWorkflowChildPermit(options.workflowChildPermitLaunch.permit, {
|
|
627
577
|
workflowRunId: options.workflowChildPermitLaunch.workflowRunId,
|
|
@@ -632,7 +582,6 @@ async function runSingleAttempt(
|
|
|
632
582
|
runner: "pi",
|
|
633
583
|
});
|
|
634
584
|
if (permitError) {
|
|
635
|
-
cleanupTempDir(tempDir);
|
|
636
585
|
result.exitCode = 1;
|
|
637
586
|
result.error = permitError;
|
|
638
587
|
result.finalOutput = permitError;
|
|
@@ -641,16 +590,13 @@ async function runSingleAttempt(
|
|
|
641
590
|
return result;
|
|
642
591
|
}
|
|
643
592
|
}
|
|
593
|
+
const childSessions = options.childSessionFactory ?? childSessionFactory();
|
|
644
594
|
let afterCompactionSettlement = false;
|
|
645
595
|
const exitCode = await new Promise<number>((resolve) => {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
windowsHide: true,
|
|
651
|
-
});
|
|
652
|
-
const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
|
|
653
|
-
let processClosed = false;
|
|
596
|
+
const jsonlWriter = createJsonlWriter(shared.jsonlPath, { pause() {}, resume() {} });
|
|
597
|
+
let session: ChildSession | undefined;
|
|
598
|
+
let unsubscribe: (() => void) | undefined;
|
|
599
|
+
let sessionSettled = false;
|
|
654
600
|
let lifecycleFinished = false;
|
|
655
601
|
let detached = false;
|
|
656
602
|
let intercomStarted = false;
|
|
@@ -659,26 +605,26 @@ async function runSingleAttempt(
|
|
|
659
605
|
let removeInterruptListener: (() => void) | undefined;
|
|
660
606
|
let activityTimer: NodeJS.Timeout | undefined;
|
|
661
607
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
662
|
-
let
|
|
663
|
-
let timeoutHardKillTimer: NodeJS.Timeout | undefined;
|
|
664
|
-
let protocolHardKillTimer: NodeJS.Timeout | undefined;
|
|
608
|
+
let timeoutHardFinishTimer: NodeJS.Timeout | undefined;
|
|
665
609
|
const clearTimeoutTimers = () => {
|
|
666
610
|
if (timeoutTimer) {
|
|
667
611
|
clearTimeout(timeoutTimer);
|
|
668
612
|
timeoutTimer = undefined;
|
|
669
613
|
}
|
|
670
|
-
if (
|
|
671
|
-
clearTimeout(
|
|
672
|
-
|
|
673
|
-
}
|
|
674
|
-
if (timeoutHardKillTimer) {
|
|
675
|
-
clearTimeout(timeoutHardKillTimer);
|
|
676
|
-
timeoutHardKillTimer = undefined;
|
|
614
|
+
if (timeoutHardFinishTimer) {
|
|
615
|
+
clearTimeout(timeoutHardFinishTimer);
|
|
616
|
+
timeoutHardFinishTimer = undefined;
|
|
677
617
|
}
|
|
678
618
|
};
|
|
619
|
+
const abortChild = (): void => {
|
|
620
|
+
if (!session || sessionSettled || lifecycleFinished) return;
|
|
621
|
+
void session.abort().catch(() => {
|
|
622
|
+
// The session settles through its prompt promise; abort failures are not separately actionable.
|
|
623
|
+
});
|
|
624
|
+
};
|
|
679
625
|
|
|
680
626
|
const detachForeground = (reason: string): boolean => {
|
|
681
|
-
if (detached ||
|
|
627
|
+
if (detached || sessionSettled || lifecycleFinished || options.signal?.aborted) return false;
|
|
682
628
|
const receiptProgress = snapshotProgress(progress);
|
|
683
629
|
receiptProgress.status = "detached";
|
|
684
630
|
receiptProgress.durationMs = Date.now() - startTime;
|
|
@@ -713,20 +659,21 @@ async function runSingleAttempt(
|
|
|
713
659
|
}
|
|
714
660
|
if (!accepted) return false;
|
|
715
661
|
detached = true;
|
|
662
|
+
if (session) session.detached = true;
|
|
716
663
|
return true;
|
|
717
664
|
};
|
|
718
665
|
|
|
719
|
-
// If the child emits a terminal assistant stop but never
|
|
720
|
-
//
|
|
666
|
+
// If the child emits a terminal assistant stop but its run never settles
|
|
667
|
+
// (a hook is stuck), abort it after a short grace period and then finish
|
|
668
|
+
// without it.
|
|
721
669
|
const FINAL_STOP_GRACE_MS = 1000;
|
|
722
|
-
const
|
|
723
|
-
let
|
|
724
|
-
let forcedTerminationSignal = false;
|
|
670
|
+
const HARD_FINISH_MS = 3000;
|
|
671
|
+
let forcedTermination = false;
|
|
725
672
|
let cleanTerminalAssistantStopReceived = false;
|
|
726
673
|
let agentSettledReceived = false;
|
|
727
674
|
let compactionStartedReceived = false;
|
|
728
675
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
729
|
-
let
|
|
676
|
+
let finalHardFinishTimer: NodeJS.Timeout | undefined;
|
|
730
677
|
let watchdogTailTimer: NodeJS.Timeout | undefined;
|
|
731
678
|
let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
|
|
732
679
|
const updateChildWatchdogState = (snapshot: ChildWatchdogStateSnapshot): void => {
|
|
@@ -745,9 +692,9 @@ async function runSingleAttempt(
|
|
|
745
692
|
clearTimeout(finalDrainTimer);
|
|
746
693
|
finalDrainTimer = undefined;
|
|
747
694
|
}
|
|
748
|
-
if (
|
|
749
|
-
clearTimeout(
|
|
750
|
-
|
|
695
|
+
if (finalHardFinishTimer) {
|
|
696
|
+
clearTimeout(finalHardFinishTimer);
|
|
697
|
+
finalHardFinishTimer = undefined;
|
|
751
698
|
}
|
|
752
699
|
};
|
|
753
700
|
const startFinalDrain = () => {
|
|
@@ -755,25 +702,24 @@ async function runSingleAttempt(
|
|
|
755
702
|
armWatchdogTail();
|
|
756
703
|
return;
|
|
757
704
|
}
|
|
758
|
-
if (
|
|
705
|
+
if (sessionSettled || finalDrainTimer || lifecycleFinished) return;
|
|
759
706
|
finalDrainTimer = setTimeout(() => {
|
|
760
|
-
if (lifecycleFinished ||
|
|
761
|
-
|
|
762
|
-
if (!termSent) return;
|
|
763
|
-
forcedTerminationSignal = true;
|
|
707
|
+
if (lifecycleFinished || sessionSettled) return;
|
|
708
|
+
forcedTermination = true;
|
|
764
709
|
if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !assistantError) {
|
|
765
|
-
result.error = result.error ?? `Subagent
|
|
710
|
+
result.error = result.error ?? `Subagent session did not settle within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Aborting it.`;
|
|
766
711
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
712
|
+
abortChild();
|
|
713
|
+
finalHardFinishTimer = setTimeout(() => {
|
|
714
|
+
if (lifecycleFinished || sessionSettled) return;
|
|
715
|
+
settle(undefined, true);
|
|
716
|
+
}, HARD_FINISH_MS);
|
|
717
|
+
finalHardFinishTimer.unref?.();
|
|
772
718
|
}, FINAL_STOP_GRACE_MS);
|
|
773
719
|
finalDrainTimer.unref?.();
|
|
774
720
|
};
|
|
775
721
|
function armWatchdogTail(): void {
|
|
776
|
-
if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished ||
|
|
722
|
+
if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished || sessionSettled) return;
|
|
777
723
|
watchdogTailTimer = setTimeout(() => {
|
|
778
724
|
watchdogTailTimer = undefined;
|
|
779
725
|
updateChildWatchdogState({
|
|
@@ -799,7 +745,7 @@ async function runSingleAttempt(
|
|
|
799
745
|
const childLifecycleState: ChildLifecycleState = { compactionRetryActive: false };
|
|
800
746
|
|
|
801
747
|
const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
802
|
-
if (!options.allowIntercomDetach ||
|
|
748
|
+
if (!options.allowIntercomDetach || sessionSettled) return;
|
|
803
749
|
if (!payload || typeof payload !== "object") return;
|
|
804
750
|
const event = payload as { requestId?: unknown; runId?: unknown; agent?: unknown; childIndex?: unknown };
|
|
805
751
|
const requestId = event.requestId;
|
|
@@ -819,13 +765,8 @@ async function runSingleAttempt(
|
|
|
819
765
|
lifecycleFinished = true;
|
|
820
766
|
clearFinalDrainTimers();
|
|
821
767
|
clearWatchdogTailTimer();
|
|
822
|
-
clearStdioGuard();
|
|
823
768
|
clearTimeoutTimers();
|
|
824
769
|
clearAllToolTimeouts();
|
|
825
|
-
if (protocolHardKillTimer) {
|
|
826
|
-
clearTimeout(protocolHardKillTimer);
|
|
827
|
-
protocolHardKillTimer = undefined;
|
|
828
|
-
}
|
|
829
770
|
if (activityTimer) {
|
|
830
771
|
clearInterval(activityTimer);
|
|
831
772
|
activityTimer = undefined;
|
|
@@ -833,7 +774,13 @@ async function runSingleAttempt(
|
|
|
833
774
|
unsubscribeIntercomDetach?.();
|
|
834
775
|
removeAbortListener?.();
|
|
835
776
|
removeInterruptListener?.();
|
|
836
|
-
|
|
777
|
+
unsubscribe?.();
|
|
778
|
+
onWatchdogStatus = undefined;
|
|
779
|
+
void jsonlWriter.close().catch(() => {
|
|
780
|
+
// JSONL artifact flush is best effort.
|
|
781
|
+
});
|
|
782
|
+
// Report the run only after the child's extensions have shut down.
|
|
783
|
+
void Promise.resolve().then(() => session?.dispose()).catch(() => undefined).then(() => resolve(code));
|
|
837
784
|
};
|
|
838
785
|
|
|
839
786
|
const drainPendingControlEvents = (): ControlEvent[] | undefined => {
|
|
@@ -996,7 +943,7 @@ async function runSingleAttempt(
|
|
|
996
943
|
|
|
997
944
|
let lastStructuredDelegationProgressState: StructuredDelegationProgressState | undefined;
|
|
998
945
|
const emitUpdateSnapshot = (text: string) => {
|
|
999
|
-
if (!options.onUpdate ||
|
|
946
|
+
if (!options.onUpdate || sessionSettled) return;
|
|
1000
947
|
const progressSnapshot = snapshotProgress(progress);
|
|
1001
948
|
const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
|
|
1002
949
|
const controlEvents = drainPendingControlEvents();
|
|
@@ -1012,7 +959,7 @@ async function runSingleAttempt(
|
|
|
1012
959
|
};
|
|
1013
960
|
|
|
1014
961
|
const fireUpdate = () => {
|
|
1015
|
-
if (!options.onUpdate ||
|
|
962
|
+
if (!options.onUpdate || sessionSettled) return;
|
|
1016
963
|
progress.durationMs = Date.now() - startTime;
|
|
1017
964
|
if (options.suppressUnchangedDelegationUpdates) {
|
|
1018
965
|
if (lastStructuredDelegationProgressState && !structuredDelegationProgressChanged(lastStructuredDelegationProgressState, progress, result)) return;
|
|
@@ -1022,20 +969,9 @@ async function runSingleAttempt(
|
|
|
1022
969
|
emitUpdateSnapshot(output || "(running...)");
|
|
1023
970
|
};
|
|
1024
971
|
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
jsonlWriter.writeLine(line);
|
|
1029
|
-
let evt: { type?: string; message?: Message; toolName?: string; toolCallId?: string; args?: unknown; willRetry?: unknown };
|
|
1030
|
-
try {
|
|
1031
|
-
evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; toolCallId?: string; args?: unknown; willRetry?: unknown };
|
|
1032
|
-
} catch {
|
|
1033
|
-
rawStdoutTail.push(`${line}\n`);
|
|
1034
|
-
shared.transcriptWriter?.writeStdoutLine(line);
|
|
1035
|
-
shared.orcaProgressTab?.append(`${line}\n`);
|
|
1036
|
-
// Non-JSON stdout lines are expected; only structured events are parsed.
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
972
|
+
const processEvent = (evt: ChildSessionEvent & { message?: Message; toolName?: string; toolCallId?: string; args?: unknown; willRetry?: unknown }) => {
|
|
973
|
+
if (lifecycleFinished) return;
|
|
974
|
+
jsonlWriter.writeLine(JSON.stringify(projectChildSessionEventForJson(evt)));
|
|
1039
975
|
shared.transcriptWriter?.writeChildEvent(evt);
|
|
1040
976
|
shared.orcaProgressTab?.event(evt);
|
|
1041
977
|
if (evt.type === "compaction_start") compactionStartedReceived = true;
|
|
@@ -1157,7 +1093,7 @@ async function runSingleAttempt(
|
|
|
1157
1093
|
if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
|
|
1158
1094
|
const assistantText = extractTextFromContent(evt.message.content);
|
|
1159
1095
|
appendRecentOutput(progress, assistantText.split("\n").slice(-10));
|
|
1160
|
-
// Final assistant message: start the
|
|
1096
|
+
// Final assistant message: start the settle drain window.
|
|
1161
1097
|
if (terminalAssistantStop) {
|
|
1162
1098
|
if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
|
|
1163
1099
|
cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
|
|
@@ -1219,11 +1155,12 @@ async function runSingleAttempt(
|
|
|
1219
1155
|
fireUpdate();
|
|
1220
1156
|
}
|
|
1221
1157
|
};
|
|
1158
|
+
onWatchdogStatus = (event) => processEvent(event as unknown as Parameters<typeof processEvent>[0]);
|
|
1222
1159
|
|
|
1223
1160
|
fireUpdate();
|
|
1224
1161
|
if (controlConfig.enabled || options.onUpdate) {
|
|
1225
1162
|
activityTimer = setInterval(() => {
|
|
1226
|
-
if (
|
|
1163
|
+
if (sessionSettled || lifecycleFinished) {
|
|
1227
1164
|
return;
|
|
1228
1165
|
}
|
|
1229
1166
|
updateActivityState(Date.now());
|
|
@@ -1234,7 +1171,7 @@ async function runSingleAttempt(
|
|
|
1234
1171
|
|
|
1235
1172
|
if (attemptTimeout) {
|
|
1236
1173
|
timeoutTimer = setTimeout(() => {
|
|
1237
|
-
if (
|
|
1174
|
+
if (sessionSettled || lifecycleFinished || interruptedByControl) return;
|
|
1238
1175
|
result.timedOut = true;
|
|
1239
1176
|
clearAllToolTimeouts();
|
|
1240
1177
|
result.error = attemptTimeout.message;
|
|
@@ -1243,17 +1180,12 @@ async function runSingleAttempt(
|
|
|
1243
1180
|
progress.error = attemptTimeout.message;
|
|
1244
1181
|
progress.durationMs = Date.now() - startTime;
|
|
1245
1182
|
fireUpdate();
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
}, 1000);
|
|
1251
|
-
timeoutTerminationTimer.unref?.();
|
|
1252
|
-
timeoutHardKillTimer = setTimeout(() => {
|
|
1253
|
-
if (processClosed || lifecycleFinished) return;
|
|
1254
|
-
trySignalChild(proc, "SIGKILL");
|
|
1183
|
+
abortChild();
|
|
1184
|
+
timeoutHardFinishTimer = setTimeout(() => {
|
|
1185
|
+
if (sessionSettled || lifecycleFinished) return;
|
|
1186
|
+
settle(undefined, true);
|
|
1255
1187
|
}, 4000);
|
|
1256
|
-
|
|
1188
|
+
timeoutHardFinishTimer.unref?.();
|
|
1257
1189
|
}, attemptTimeout.remainingMs);
|
|
1258
1190
|
timeoutTimer.unref?.();
|
|
1259
1191
|
}
|
|
@@ -1261,8 +1193,7 @@ async function runSingleAttempt(
|
|
|
1261
1193
|
let toolTimeoutSequence = 0;
|
|
1262
1194
|
const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
|
|
1263
1195
|
const activeToolTimeoutKeysByName = new Map<string, string[]>();
|
|
1264
|
-
let
|
|
1265
|
-
let toolTimeoutHardKillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1196
|
+
let toolTimeoutHardFinishTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1266
1197
|
const removeToolTimeoutKey = (key: string): void => {
|
|
1267
1198
|
const active = activeToolTimeouts.get(key);
|
|
1268
1199
|
if (!active) return;
|
|
@@ -1284,17 +1215,13 @@ async function runSingleAttempt(
|
|
|
1284
1215
|
};
|
|
1285
1216
|
const clearAllToolTimeouts = (): void => {
|
|
1286
1217
|
for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
|
|
1287
|
-
if (
|
|
1288
|
-
clearTimeout(
|
|
1289
|
-
|
|
1290
|
-
}
|
|
1291
|
-
if (toolTimeoutHardKillTimer) {
|
|
1292
|
-
clearTimeout(toolTimeoutHardKillTimer);
|
|
1293
|
-
toolTimeoutHardKillTimer = undefined;
|
|
1218
|
+
if (toolTimeoutHardFinishTimer) {
|
|
1219
|
+
clearTimeout(toolTimeoutHardFinishTimer);
|
|
1220
|
+
toolTimeoutHardFinishTimer = undefined;
|
|
1294
1221
|
}
|
|
1295
1222
|
};
|
|
1296
1223
|
const terminateForToolTimeout = (message: string): void => {
|
|
1297
|
-
if (
|
|
1224
|
+
if (sessionSettled || lifecycleFinished || interruptedByControl) return;
|
|
1298
1225
|
result.timedOut = true;
|
|
1299
1226
|
result.error = message;
|
|
1300
1227
|
result.finalOutput = message;
|
|
@@ -1302,17 +1229,12 @@ async function runSingleAttempt(
|
|
|
1302
1229
|
progress.error = message;
|
|
1303
1230
|
progress.durationMs = Date.now() - startTime;
|
|
1304
1231
|
fireUpdate();
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
if (
|
|
1308
|
-
|
|
1309
|
-
}, 1000);
|
|
1310
|
-
toolTimeoutTerminationTimer.unref?.();
|
|
1311
|
-
toolTimeoutHardKillTimer = setTimeout(() => {
|
|
1312
|
-
if (processClosed || lifecycleFinished) return;
|
|
1313
|
-
trySignalChild(proc, "SIGKILL");
|
|
1232
|
+
abortChild();
|
|
1233
|
+
toolTimeoutHardFinishTimer = setTimeout(() => {
|
|
1234
|
+
if (sessionSettled || lifecycleFinished) return;
|
|
1235
|
+
settle(undefined, true);
|
|
1314
1236
|
}, 4000);
|
|
1315
|
-
|
|
1237
|
+
toolTimeoutHardFinishTimer.unref?.();
|
|
1316
1238
|
};
|
|
1317
1239
|
const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
|
|
1318
1240
|
const timeoutForTool = effectiveToolTimeoutMs(event.toolName, options.toolTimeoutMs);
|
|
@@ -1333,111 +1255,42 @@ async function runSingleAttempt(
|
|
|
1333
1255
|
activeToolTimeoutKeysByName.set(toolName, keys);
|
|
1334
1256
|
};
|
|
1335
1257
|
|
|
1336
|
-
|
|
1337
|
-
const
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1340
|
-
result.error = formatProtocolOutputLimit(limit);
|
|
1341
|
-
progress.status = "failed";
|
|
1342
|
-
progress.error = result.error;
|
|
1343
|
-
progress.durationMs = Date.now() - startTime;
|
|
1344
|
-
fireUpdate();
|
|
1345
|
-
if (!childExited) {
|
|
1346
|
-
trySignalChild(proc, "SIGTERM");
|
|
1347
|
-
protocolHardKillTimer = setTimeout(() => {
|
|
1348
|
-
if (!childExited) trySignalChild(proc, "SIGKILL");
|
|
1349
|
-
}, 3000);
|
|
1350
|
-
protocolHardKillTimer.unref?.();
|
|
1351
|
-
}
|
|
1352
|
-
};
|
|
1353
|
-
const stdoutReader = createBoundedLineReader({
|
|
1354
|
-
oversizedLineProjector: PI_AGGREGATE_EVENT_PROJECTOR,
|
|
1355
|
-
onLine: processLine,
|
|
1356
|
-
onLimit: failProtocol,
|
|
1357
|
-
});
|
|
1358
|
-
const stderrReader = createBoundedLineReader({
|
|
1359
|
-
stream: "stderr",
|
|
1360
|
-
maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
|
|
1361
|
-
onLine: (line) => shared.transcriptWriter?.writeStderrLine(line),
|
|
1362
|
-
onLimit: (limit) => shared.transcriptWriter?.writeStderrLine(formatProtocolOutputLimit(limit)),
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
|
|
1366
|
-
proc.stdout.on("data", (chunk: Buffer) => stdoutReader.push(chunk));
|
|
1367
|
-
proc.stderr.on("data", (chunk: Buffer) => {
|
|
1368
|
-
stderrTail.push(chunk);
|
|
1369
|
-
stderrReader.push(chunk);
|
|
1370
|
-
shared.orcaProgressTab?.append(chunk.toString("utf-8"));
|
|
1371
|
-
});
|
|
1372
|
-
proc.on("exit", () => {
|
|
1373
|
-
childExited = true;
|
|
1374
|
-
clearFinalDrainTimers();
|
|
1375
|
-
});
|
|
1376
|
-
proc.on("close", (code, signal) => {
|
|
1377
|
-
if (lifecycleFinished) return;
|
|
1378
|
-
processClosed = true;
|
|
1258
|
+
/** The child run ended (or was forced to end); fold in the captures and finish. */
|
|
1259
|
+
const settle = (promptError: unknown, forced = false): void => {
|
|
1260
|
+
if (lifecycleFinished || sessionSettled) return;
|
|
1261
|
+
sessionSettled = true;
|
|
1379
1262
|
clearFinalDrainTimers();
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
// JSONL artifact flush is best effort.
|
|
1383
|
-
});
|
|
1384
|
-
const toolDiagnosticError = readChildToolDiagnosticError(toolDiagnosticPath);
|
|
1263
|
+
const diagnostic = capture.toolDiagnostic();
|
|
1264
|
+
const toolDiagnosticError = diagnostic ? formatChildToolDiagnostic(diagnostic, { host: "parent" }) : undefined;
|
|
1385
1265
|
toolAvailabilityError = toolDiagnosticError;
|
|
1386
|
-
result.runtimeAcknowledgedExtensions =
|
|
1387
|
-
cleanupTempDir(tempDir);
|
|
1388
|
-
stdoutReader.end();
|
|
1389
|
-
stderrReader.end();
|
|
1390
|
-
const stderr = stderrTail.text();
|
|
1391
|
-
const rawStdout = rawStdoutTail.text();
|
|
1266
|
+
result.runtimeAcknowledgedExtensions = capture.runtimeAcknowledgedExtensions();
|
|
1392
1267
|
let closeError = result.error ?? toolDiagnosticError ?? assistantError;
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
if (signal) result.processSignal = signal;
|
|
1396
|
-
if (!closeError && forcedDrainAfterEmptyTerminal && stderr.trim()) {
|
|
1397
|
-
closeError = stderr.trim();
|
|
1268
|
+
if (!closeError && promptError !== undefined) {
|
|
1269
|
+
closeError = promptError instanceof Error ? promptError.message : String(promptError);
|
|
1398
1270
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
stopped: result.stopped,
|
|
1404
|
-
forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
|
|
1405
|
-
})) {
|
|
1406
|
-
closeError = formatProcessSignalError(signal!);
|
|
1407
|
-
}
|
|
1408
|
-
if (code !== 0 && rawStdout.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
|
|
1409
|
-
closeError = rawStdout.trim();
|
|
1271
|
+
const forcedDrainAfterFinalSuccess = (forced || forcedTermination) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
|
|
1272
|
+
const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(result.messages ?? []);
|
|
1273
|
+
if (!closeError && (abortedBySignal || session?.shutDown) && !result.interrupted && !result.timedOut) {
|
|
1274
|
+
closeError = session?.shutDown ? "Subagent stopped because the parent session shut down." : STOPPED_BEFORE_COMPLETION_ERROR;
|
|
1410
1275
|
}
|
|
1411
|
-
if (
|
|
1412
|
-
closeError =
|
|
1276
|
+
if (!closeError && forced && !forcedDrainAfterFinalSuccess) {
|
|
1277
|
+
closeError = "Subagent session did not settle after it was aborted.";
|
|
1413
1278
|
}
|
|
1414
|
-
const finalCode = forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal ? 0 :
|
|
1279
|
+
const finalCode = forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal ? 0 : closeError || promptError !== undefined ? 1 : 0;
|
|
1415
1280
|
if (!result.error && closeError) result.error = closeError;
|
|
1416
1281
|
finish(finalCode);
|
|
1417
|
-
}
|
|
1418
|
-
proc.on("error", (error) => {
|
|
1419
|
-
if (lifecycleFinished) return;
|
|
1420
|
-
processClosed = true;
|
|
1421
|
-
clearFinalDrainTimers();
|
|
1422
|
-
clearStdioGuard();
|
|
1423
|
-
void jsonlWriter.close().catch(() => {
|
|
1424
|
-
// JSONL artifact flush is best effort.
|
|
1425
|
-
});
|
|
1426
|
-
cleanupTempDir(tempDir);
|
|
1427
|
-
stdoutReader.end();
|
|
1428
|
-
stderrReader.end();
|
|
1429
|
-
if (!result.error) {
|
|
1430
|
-
result.error = error instanceof Error ? error.message : String(error);
|
|
1431
|
-
}
|
|
1432
|
-
finish(1);
|
|
1433
|
-
});
|
|
1282
|
+
};
|
|
1434
1283
|
|
|
1435
1284
|
if (options.signal) {
|
|
1436
1285
|
const kill = () => {
|
|
1437
|
-
if (
|
|
1286
|
+
if (sessionSettled || lifecycleFinished) return;
|
|
1438
1287
|
abortedBySignal = true;
|
|
1439
|
-
|
|
1440
|
-
setTimeout(() =>
|
|
1288
|
+
abortChild();
|
|
1289
|
+
const hardTimer = setTimeout(() => {
|
|
1290
|
+
if (sessionSettled || lifecycleFinished) return;
|
|
1291
|
+
settle(undefined, true);
|
|
1292
|
+
}, 3000);
|
|
1293
|
+
hardTimer.unref?.();
|
|
1441
1294
|
};
|
|
1442
1295
|
if (options.signal.aborted) kill();
|
|
1443
1296
|
else {
|
|
@@ -1448,7 +1301,7 @@ async function runSingleAttempt(
|
|
|
1448
1301
|
|
|
1449
1302
|
if (options.interruptSignal) {
|
|
1450
1303
|
const interrupt = () => {
|
|
1451
|
-
if (
|
|
1304
|
+
if (sessionSettled || lifecycleFinished) return;
|
|
1452
1305
|
if (result.timedOut) return;
|
|
1453
1306
|
interruptedByControl = true;
|
|
1454
1307
|
clearTimeoutTimers();
|
|
@@ -1459,11 +1312,12 @@ async function runSingleAttempt(
|
|
|
1459
1312
|
result.finalOutput = "Interrupted. Waiting for explicit next action.";
|
|
1460
1313
|
progress.activityState = undefined;
|
|
1461
1314
|
fireUpdate();
|
|
1462
|
-
|
|
1463
|
-
setTimeout(() => {
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1466
|
-
},
|
|
1315
|
+
abortChild();
|
|
1316
|
+
const hardTimer = setTimeout(() => {
|
|
1317
|
+
if (sessionSettled || lifecycleFinished) return;
|
|
1318
|
+
settle(undefined, true);
|
|
1319
|
+
}, 3000);
|
|
1320
|
+
hardTimer.unref?.();
|
|
1467
1321
|
};
|
|
1468
1322
|
if (options.interruptSignal.aborted) interrupt();
|
|
1469
1323
|
else {
|
|
@@ -1478,9 +1332,35 @@ async function runSingleAttempt(
|
|
|
1478
1332
|
options.onDetachReady?.((reason = "user request") => detachForeground(reason));
|
|
1479
1333
|
} catch (error) {
|
|
1480
1334
|
// A consumer callback is advisory. Keep the child attached and observable
|
|
1481
|
-
// rather than rejecting this attempt and orphaning its live
|
|
1335
|
+
// rather than rejecting this attempt and orphaning its live session.
|
|
1482
1336
|
appendRecentOutput(progress, [`Foreground detach callback failed: ${error instanceof Error ? error.message : String(error)}`]);
|
|
1483
1337
|
}
|
|
1338
|
+
|
|
1339
|
+
void (async () => {
|
|
1340
|
+
try {
|
|
1341
|
+
const created = await childSessions.create({
|
|
1342
|
+
...launch.session,
|
|
1343
|
+
onExtensionError: (error) => {
|
|
1344
|
+
shared.transcriptWriter?.writeStderrLine(`Extension error (${error.extensionPath}, ${error.event}): ${error.error instanceof Error ? error.error.message : String(error.error)}`);
|
|
1345
|
+
},
|
|
1346
|
+
});
|
|
1347
|
+
if (lifecycleFinished) {
|
|
1348
|
+
void created.dispose();
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
session = created;
|
|
1352
|
+
created.detached = detached;
|
|
1353
|
+
unsubscribe = created.subscribe((event) => processEvent(event as Parameters<typeof processEvent>[0]));
|
|
1354
|
+
if (abortedBySignal || interruptedByControl || result.timedOut) {
|
|
1355
|
+
abortChild();
|
|
1356
|
+
}
|
|
1357
|
+
options.onChildSession?.({ steer: (text) => created.steer(text), followUp: (text) => created.followUp(text) });
|
|
1358
|
+
await created.prompt(`Task: ${task}`);
|
|
1359
|
+
settle(undefined);
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
settle(error ?? new Error("Child session failed."));
|
|
1362
|
+
}
|
|
1363
|
+
})();
|
|
1484
1364
|
});
|
|
1485
1365
|
result.exitCode = exitCode;
|
|
1486
1366
|
if (afterCompactionSettlement) {
|
|
@@ -1506,20 +1386,10 @@ async function runSingleAttempt(
|
|
|
1506
1386
|
&& !abortedBySignal
|
|
1507
1387
|
&& !result.timedOut
|
|
1508
1388
|
&& !result.stopped
|
|
1509
|
-
&& !result.protocolError
|
|
1510
1389
|
&& !toolAvailabilityError) {
|
|
1511
|
-
const processExitCode = result.exitCode;
|
|
1512
1390
|
result.exitCode = 1;
|
|
1513
|
-
result.error = formatMidToolExitError({
|
|
1514
|
-
toolName: progress.currentTool,
|
|
1515
|
-
exitCode: processExitCode,
|
|
1516
|
-
processSignal: result.processSignal,
|
|
1517
|
-
});
|
|
1391
|
+
result.error = formatMidToolExitError({ toolName: progress.currentTool });
|
|
1518
1392
|
}
|
|
1519
|
-
result.error = formatSubagentExtensionConflictError(result.error, {
|
|
1520
|
-
agent: agent.name,
|
|
1521
|
-
ambientExtensionsEnabled: !launchResolvedExtensions.disableAmbientExtensions,
|
|
1522
|
-
});
|
|
1523
1393
|
if (result.error && result.exitCode === 0) {
|
|
1524
1394
|
result.exitCode = 1;
|
|
1525
1395
|
}
|
|
@@ -1527,27 +1397,23 @@ async function runSingleAttempt(
|
|
|
1527
1397
|
if (options.structuredOutput && result.exitCode === 0 && !result.error) {
|
|
1528
1398
|
result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
|
|
1529
1399
|
result.structuredOutputPath = options.structuredOutput.outputPath;
|
|
1530
|
-
|
|
1400
|
+
const structured = capture.structuredOutput();
|
|
1401
|
+
if (!structuredOutputToolInvoked || !structured.called) {
|
|
1531
1402
|
result.exitCode = 1;
|
|
1532
1403
|
result.error = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
|
|
1533
1404
|
result.structuredOutputFailed = true;
|
|
1534
1405
|
} else {
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const acceptanceReport = readStructuredOutputAcceptanceReport(options.structuredOutput);
|
|
1547
|
-
(result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReport = acceptanceReport.value;
|
|
1548
|
-
(result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError = acceptanceReport.error;
|
|
1549
|
-
validatedStructuredOutput = true;
|
|
1550
|
-
}
|
|
1406
|
+
result.structuredOutput = structured.value;
|
|
1407
|
+
const acceptanceMode = options.structuredOutput.acceptanceReportPath
|
|
1408
|
+
? options.structuredOutput.acceptanceReportRequired ? "required" : "optional"
|
|
1409
|
+
: undefined;
|
|
1410
|
+
const acceptanceReportError = acceptanceMode === "required" && !structured.acceptanceReportProvided
|
|
1411
|
+
? MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR
|
|
1412
|
+
: undefined;
|
|
1413
|
+
(result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReport = acceptanceMode ? structured.acceptanceReport : undefined;
|
|
1414
|
+
(result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError = acceptanceReportError;
|
|
1415
|
+
writeStructuredOutputArtifacts(options.structuredOutput, structured.value, acceptanceMode ? structured.acceptanceReport : undefined);
|
|
1416
|
+
validatedStructuredOutput = true;
|
|
1551
1417
|
}
|
|
1552
1418
|
}
|
|
1553
1419
|
if (result.exitCode === 0 && !result.error) {
|
|
@@ -1600,7 +1466,7 @@ async function runSingleAttempt(
|
|
|
1600
1466
|
const outputChanged = hasSingleOutputChangedSinceSnapshot(options.outputPath, shared.outputSnapshot);
|
|
1601
1467
|
requiredOutputMissing = outputChanged === undefined ? undefined : !outputChanged;
|
|
1602
1468
|
} else if (options.structuredOutput) {
|
|
1603
|
-
requiredOutputMissing = !
|
|
1469
|
+
requiredOutputMissing = !capture.structuredOutput().called;
|
|
1604
1470
|
}
|
|
1605
1471
|
result.timeoutRecovery = buildTimeoutRecoverySummary({
|
|
1606
1472
|
termination: "timed-out",
|
|
@@ -1719,6 +1585,20 @@ async function runSingleAttempt(
|
|
|
1719
1585
|
return result;
|
|
1720
1586
|
}
|
|
1721
1587
|
|
|
1588
|
+
/** Keep the structured output artifact files other consumers read by path. */
|
|
1589
|
+
function writeStructuredOutputArtifacts(runtime: NonNullable<RunSyncOptions["structuredOutput"]>, value: unknown, acceptanceReport: unknown): void {
|
|
1590
|
+
try {
|
|
1591
|
+
mkdirSync(path.dirname(runtime.outputPath), { recursive: true });
|
|
1592
|
+
writeFileSync(runtime.outputPath, JSON.stringify(value), { mode: 0o600 });
|
|
1593
|
+
if (runtime.acceptanceReportPath) {
|
|
1594
|
+
if (acceptanceReport !== undefined) writeFileSync(runtime.acceptanceReportPath, JSON.stringify(acceptanceReport), { mode: 0o600 });
|
|
1595
|
+
else rmSync(runtime.acceptanceReportPath, { force: true });
|
|
1596
|
+
}
|
|
1597
|
+
} catch {
|
|
1598
|
+
// The captured value is already on the result; the file is observability only.
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1722
1602
|
/**
|
|
1723
1603
|
* Run a subagent synchronously (blocking until complete)
|
|
1724
1604
|
*/
|
|
@@ -1744,7 +1624,7 @@ async function runSyncCompletionInner(
|
|
|
1744
1624
|
}
|
|
1745
1625
|
options = {
|
|
1746
1626
|
...options,
|
|
1747
|
-
capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId),
|
|
1627
|
+
capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), options.childRuntime?.capabilityCeiling),
|
|
1748
1628
|
};
|
|
1749
1629
|
const childSessionName = deriveChildSessionName({ agent: agentName, task });
|
|
1750
1630
|
const agent = agents.find((a) => a.name === agentName);
|
|
@@ -1766,7 +1646,7 @@ async function runSyncCompletionInner(
|
|
|
1766
1646
|
thinkingCeiling: intersectThinkingCeilings(
|
|
1767
1647
|
options.thinkingCeiling,
|
|
1768
1648
|
agent.maxThinking,
|
|
1769
|
-
|
|
1649
|
+
options.childRuntime?.thinkingCeiling,
|
|
1770
1650
|
),
|
|
1771
1651
|
};
|
|
1772
1652
|
try {
|
|
@@ -1990,14 +1870,12 @@ async function runSyncCompletionInner(
|
|
|
1990
1870
|
};
|
|
1991
1871
|
let lastResult: SingleResult | undefined;
|
|
1992
1872
|
const modelsToTry = candidates.length > 0 ? candidates : [undefined];
|
|
1993
|
-
// Escalated to "file" after an unexplained zero-activity startup failure so
|
|
1994
|
-
// retries keep the task text out of argv (endpoint pre-exec scans may deny it).
|
|
1995
|
-
let taskDeliveryOverride: SubagentTaskDelivery | undefined;
|
|
1996
1873
|
let abortRecoveryAttempted = false;
|
|
1997
1874
|
let nextAttemptTask = taskWithAcceptance;
|
|
1998
1875
|
modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
|
|
1999
1876
|
const candidate = modelsToTry[modelIndex];
|
|
2000
|
-
|
|
1877
|
+
// The inner loop re-runs the same candidate at most once, for abort recovery.
|
|
1878
|
+
for (;;) {
|
|
2001
1879
|
const recoveringAbort = abortRecoveryAttempted;
|
|
2002
1880
|
const attemptTask = nextAttemptTask;
|
|
2003
1881
|
const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
|
|
@@ -2016,13 +1894,12 @@ async function runSyncCompletionInner(
|
|
|
2016
1894
|
.filter((modelCandidate): modelCandidate is string => Boolean(modelCandidate)),
|
|
2017
1895
|
outputSnapshot,
|
|
2018
1896
|
originalTask: task,
|
|
2019
|
-
taskDelivery: taskDeliveryOverride,
|
|
2020
1897
|
orcaProgressTab,
|
|
2021
1898
|
launchWarnings,
|
|
2022
1899
|
verifyModel,
|
|
2023
1900
|
});
|
|
2024
1901
|
lastResult = result;
|
|
2025
|
-
if (!recoveringAbort
|
|
1902
|
+
if (!recoveringAbort) {
|
|
2026
1903
|
if (result.model) attemptedModels.push(result.model);
|
|
2027
1904
|
else if (candidate) attemptedModels.push(candidate);
|
|
2028
1905
|
}
|
|
@@ -2072,75 +1949,10 @@ async function runSyncCompletionInner(
|
|
|
2072
1949
|
if (options.workflowChildPermitLaunch && !attemptSucceeded) break modelAttemptsLoop;
|
|
2073
1950
|
// Preserve the legacy intercom handoff contract: once this logical run has
|
|
2074
1951
|
// been handed to a supervisor, terminating that attempt must not launch a
|
|
2075
|
-
//
|
|
1952
|
+
// model fallback. Explicit user detach retains fallback.
|
|
2076
1953
|
if (intercomDetached || result.timedOut) break modelAttemptsLoop;
|
|
2077
1954
|
if (attemptSucceeded) break modelAttemptsLoop;
|
|
2078
1955
|
|
|
2079
|
-
const startupFailure = isRetryableSubagentStartupFailure({
|
|
2080
|
-
exitCode: result.exitCode,
|
|
2081
|
-
error: result.error,
|
|
2082
|
-
finalOutput: result.finalOutput,
|
|
2083
|
-
messageCount: result.messages?.length ?? 0,
|
|
2084
|
-
toolCount: result.progressSummary?.toolCount ?? 0,
|
|
2085
|
-
usage: result.usage,
|
|
2086
|
-
durationMs: result.progressSummary?.durationMs ?? Number.POSITIVE_INFINITY,
|
|
2087
|
-
protocolError: result.protocolError,
|
|
2088
|
-
processSignal: result.processSignal,
|
|
2089
|
-
detached: result.detached,
|
|
2090
|
-
interrupted: result.interrupted,
|
|
2091
|
-
timedOut: result.timedOut,
|
|
2092
|
-
stopped: result.stopped,
|
|
2093
|
-
});
|
|
2094
|
-
const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
|
|
2095
|
-
if (startupFailure && retryDelayMs !== undefined) {
|
|
2096
|
-
const retryNote = formatSubagentStartupRetryNote({
|
|
2097
|
-
model: attempt.model,
|
|
2098
|
-
attempt: startupAttemptIndex + 1,
|
|
2099
|
-
maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
|
|
2100
|
-
delayMs: retryDelayMs,
|
|
2101
|
-
});
|
|
2102
|
-
const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [options.signal, options.interruptSignal]);
|
|
2103
|
-
if (!shouldRetry) {
|
|
2104
|
-
if (options.interruptSignal?.aborted) {
|
|
2105
|
-
result.exitCode = 0;
|
|
2106
|
-
result.interrupted = true;
|
|
2107
|
-
result.error = undefined;
|
|
2108
|
-
result.finalOutput = "Interrupted. Waiting for explicit next action.";
|
|
2109
|
-
if (result.progress) {
|
|
2110
|
-
result.progress.status = "running";
|
|
2111
|
-
result.progress.error = undefined;
|
|
2112
|
-
}
|
|
2113
|
-
} else {
|
|
2114
|
-
const cancellationError = "Subagent startup retry cancelled before relaunch.";
|
|
2115
|
-
result.error = cancellationError;
|
|
2116
|
-
result.finalOutput = cancellationError;
|
|
2117
|
-
attempt.error = cancellationError;
|
|
2118
|
-
if (result.progress) result.progress.error = cancellationError;
|
|
2119
|
-
}
|
|
2120
|
-
break modelAttemptsLoop;
|
|
2121
|
-
}
|
|
2122
|
-
if (!taskDeliveryOverride && result.processSignal === "SIGKILL") {
|
|
2123
|
-
taskDeliveryOverride = "file";
|
|
2124
|
-
attemptNotes.push("[startup-retry] retrying with file task delivery to keep the task text out of the child process argv.");
|
|
2125
|
-
}
|
|
2126
|
-
attempt.error = retryNote;
|
|
2127
|
-
attemptNotes.push(retryNote);
|
|
2128
|
-
continue;
|
|
2129
|
-
}
|
|
2130
|
-
if (startupFailure) {
|
|
2131
|
-
const startupError = formatSubagentStartupRetryExhaustedError({
|
|
2132
|
-
model: attempt.model,
|
|
2133
|
-
attempts: startupAttemptIndex + 1,
|
|
2134
|
-
});
|
|
2135
|
-
result.error = startupError;
|
|
2136
|
-
result.finalOutput = startupError;
|
|
2137
|
-
if (result.progress) {
|
|
2138
|
-
result.progress.error = startupError;
|
|
2139
|
-
result.progress.status = "failed";
|
|
2140
|
-
}
|
|
2141
|
-
attempt.error = startupError;
|
|
2142
|
-
break modelAttemptsLoop;
|
|
2143
|
-
}
|
|
2144
1956
|
const retryableModelFailure = isRetryableModelFailureAttempt({ error: result.error, messages: result.messages, toolCount: result.progressSummary?.toolCount });
|
|
2145
1957
|
if (retryableModelFailure) recordRetryableModelFailure(result.model ?? candidate, result.error);
|
|
2146
1958
|
if (isContextOverflow(result.error)) {
|