pi-subagents 0.38.0 → 0.40.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 +38 -0
- package/README.md +85 -13
- package/agents/worker.md +1 -0
- package/package.json +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +4 -3
- package/skills/pi-subagents/references/execution-controls.md +22 -3
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -3
- package/src/agents/agent-management.ts +71 -30
- package/src/agents/agent-serializer.ts +4 -0
- package/src/agents/agents.ts +60 -1
- package/src/api/preflight.ts +16 -12
- package/src/extension/chain-validation.ts +17 -1
- package/src/extension/rpc.ts +2 -0
- package/src/extension/schemas.ts +16 -3
- package/src/extension/tool-description.ts +6 -5
- package/src/intercom/result-intercom.ts +33 -4
- package/src/runs/background/async-execution.ts +63 -16
- package/src/runs/background/async-job-tracker.ts +5 -1
- package/src/runs/background/async-resume.ts +1 -0
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/chain-append.ts +48 -5
- package/src/runs/background/control-channel.ts +60 -0
- package/src/runs/background/notify.ts +26 -4
- package/src/runs/background/result-watcher.ts +20 -3
- package/src/runs/background/run-status.ts +29 -3
- package/src/runs/background/subagent-runner.ts +302 -18
- package/src/runs/foreground/async-stop-action.ts +65 -0
- package/src/runs/foreground/chain-execution.ts +143 -43
- package/src/runs/foreground/execution.ts +384 -202
- package/src/runs/foreground/foreground-control.ts +30 -0
- package/src/runs/foreground/subagent-executor.ts +314 -77
- package/src/runs/shared/capability-ceiling.ts +51 -19
- package/src/runs/shared/chain-outputs.ts +3 -1
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/nested-events.ts +97 -20
- package/src/runs/shared/parallel-utils.ts +43 -12
- package/src/runs/shared/pi-args.ts +45 -3
- package/src/runs/shared/process-signal.ts +19 -0
- package/src/runs/shared/run-history.ts +45 -9
- package/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +30 -0
- package/src/runs/shared/usage-budget.ts +65 -0
- package/src/runs/shared/workflow-graph.ts +26 -1
- package/src/shared/settings.ts +17 -1
- package/src/shared/types.ts +124 -8
- package/src/shared/utils.ts +12 -1
- package/src/slash/slash-commands.ts +43 -1
- package/src/slash/slash-live-state.ts +5 -3
- package/src/tui/fleet-status.ts +12 -2
- package/src/tui/fleet.ts +181 -8
- package/src/tui/render.ts +4 -2
- package/src/watchdog/register-main.ts +14 -7
- package/src/watchdog/review.ts +4 -3
- package/src/watchdog/runtime.ts +170 -17
- package/src/watchdog/scope.ts +62 -0
- package/src/watchdog/settings.ts +41 -1
- package/src/watchdog/types.ts +10 -0
|
@@ -55,10 +55,12 @@ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
|
55
55
|
import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
|
|
56
56
|
import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
|
|
57
57
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
58
|
-
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
|
|
59
|
-
import {
|
|
58
|
+
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
|
|
59
|
+
import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
|
|
60
|
+
import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
|
|
60
61
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
61
62
|
import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
|
|
63
|
+
import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
62
64
|
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
63
65
|
import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
64
66
|
import {
|
|
@@ -121,6 +123,47 @@ function sumUsage(target: Usage, source: Usage): void {
|
|
|
121
123
|
target.turns += source.turns;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
function persistSingleResultMetadata(input: {
|
|
127
|
+
metadataPath?: string;
|
|
128
|
+
enabled: boolean;
|
|
129
|
+
runId?: string;
|
|
130
|
+
agent: string;
|
|
131
|
+
task: string;
|
|
132
|
+
result: SingleResult;
|
|
133
|
+
}): void {
|
|
134
|
+
if (!input.enabled || !input.metadataPath) return;
|
|
135
|
+
const target = input.result;
|
|
136
|
+
writeMetadata(input.metadataPath, {
|
|
137
|
+
runId: input.runId,
|
|
138
|
+
agent: input.agent,
|
|
139
|
+
task: input.task,
|
|
140
|
+
exitCode: target.exitCode,
|
|
141
|
+
processSignal: target.processSignal,
|
|
142
|
+
usage: target.usage,
|
|
143
|
+
model: target.model,
|
|
144
|
+
attemptedModels: target.attemptedModels,
|
|
145
|
+
modelAttempts: target.modelAttempts,
|
|
146
|
+
durationMs: target.progressSummary?.durationMs,
|
|
147
|
+
toolCount: target.progressSummary?.toolCount,
|
|
148
|
+
error: target.error,
|
|
149
|
+
agentContract: target.agentContract,
|
|
150
|
+
launchContractDigest: target.launchContractDigest,
|
|
151
|
+
launchResolvedExtensions: target.launchResolvedExtensions,
|
|
152
|
+
runtimeAcknowledgedExtensions: target.runtimeAcknowledgedExtensions,
|
|
153
|
+
execution: target.execution,
|
|
154
|
+
acceptance: target.acceptance,
|
|
155
|
+
capabilityCeiling: target.capabilityCeiling,
|
|
156
|
+
capabilityAudit: target.capabilityAudit,
|
|
157
|
+
review: target.review,
|
|
158
|
+
effects: target.effects,
|
|
159
|
+
transcriptPath: target.transcriptPath,
|
|
160
|
+
transcriptError: target.transcriptError,
|
|
161
|
+
skills: target.skills,
|
|
162
|
+
skillsWarning: target.skillsWarning,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
124
167
|
function formatTimeoutMessage(timeoutMs: number): string {
|
|
125
168
|
return `Subagent timed out after ${timeoutMs}ms.`;
|
|
126
169
|
}
|
|
@@ -148,6 +191,21 @@ function buildPendingAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): Acc
|
|
|
148
191
|
};
|
|
149
192
|
}
|
|
150
193
|
|
|
194
|
+
function buildInterruptedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
|
|
195
|
+
const pending = buildPendingAcceptanceLedger(acceptance);
|
|
196
|
+
if (acceptance.level === "none") {
|
|
197
|
+
pending.status = "not-required";
|
|
198
|
+
pending.evidenceStatus = "not-required";
|
|
199
|
+
} else {
|
|
200
|
+
pending.runtimeChecks = [{
|
|
201
|
+
id: "interrupted",
|
|
202
|
+
status: "not-applicable",
|
|
203
|
+
message: "Acceptance was not evaluated because the subagent was interrupted.",
|
|
204
|
+
}];
|
|
205
|
+
}
|
|
206
|
+
return pending;
|
|
207
|
+
}
|
|
208
|
+
|
|
151
209
|
function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
|
|
152
210
|
if (lines.length === 0) return;
|
|
153
211
|
progress.recentOutput.push(...lines.filter((line) => line.trim()));
|
|
@@ -203,7 +261,7 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
|
|
|
203
261
|
* unbounded `messages` transcript in favour of compact tool-call summaries so a
|
|
204
262
|
* single `tool_execution_update` line stays well under the child-stdout protocol
|
|
205
263
|
* cap (`MAX_CHILD_PENDING_LINE_BYTES`). Non-streaming consumers such as
|
|
206
|
-
*
|
|
264
|
+
* foreground detach receipts and terminal consumers use snapshotResult directly.
|
|
207
265
|
*/
|
|
208
266
|
function snapshotStreamResult(result: SingleResult, progress: AgentProgress): SingleResult {
|
|
209
267
|
const snapshot = snapshotResult(result, progress);
|
|
@@ -244,7 +302,7 @@ async function runSingleAttempt(
|
|
|
244
302
|
childIndex: options.index ?? 0,
|
|
245
303
|
})
|
|
246
304
|
: undefined;
|
|
247
|
-
const { args, env: sharedEnv, tempDir, toolDiagnosticPath, capabilityAudit } = buildPiArgs({
|
|
305
|
+
const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit } = buildPiArgs({
|
|
248
306
|
baseArgs: ["--mode", "json", "-p"],
|
|
249
307
|
task,
|
|
250
308
|
sessionEnabled: shared.sessionEnabled,
|
|
@@ -292,7 +350,9 @@ async function runSingleAttempt(
|
|
|
292
350
|
structuredOutput: Boolean(options.structuredOutput),
|
|
293
351
|
capabilityCeiling: options.capabilityCeiling,
|
|
294
352
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
353
|
+
agentName: agent.name,
|
|
295
354
|
});
|
|
355
|
+
const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
|
|
296
356
|
const launchContractDigest = launchBindingDigest({
|
|
297
357
|
definitionDigest: agentDefinitionDigest(agent),
|
|
298
358
|
task: shared.originalTask ?? task,
|
|
@@ -312,11 +372,14 @@ async function runSingleAttempt(
|
|
|
312
372
|
...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
|
|
313
373
|
});
|
|
314
374
|
const result: SingleResult = withRunContext({
|
|
375
|
+
index: options.index ?? 0,
|
|
315
376
|
agent: agent.name,
|
|
316
377
|
task: shared.originalTask ?? task,
|
|
317
378
|
...(options.agentContract ? { agentContract: options.agentContract } : {}),
|
|
318
379
|
launchContractDigest,
|
|
380
|
+
launchResolvedExtensions,
|
|
319
381
|
exitCode: 0,
|
|
382
|
+
outputState: "absent",
|
|
320
383
|
messages: [],
|
|
321
384
|
usage: emptyUsage(),
|
|
322
385
|
model: modelArg,
|
|
@@ -399,7 +462,7 @@ async function runSingleAttempt(
|
|
|
399
462
|
});
|
|
400
463
|
const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
|
|
401
464
|
let processClosed = false;
|
|
402
|
-
let
|
|
465
|
+
let lifecycleFinished = false;
|
|
403
466
|
let detached = false;
|
|
404
467
|
let intercomStarted = false;
|
|
405
468
|
let assistantError: string | undefined;
|
|
@@ -438,19 +501,43 @@ async function runSingleAttempt(
|
|
|
438
501
|
}
|
|
439
502
|
};
|
|
440
503
|
|
|
441
|
-
const
|
|
442
|
-
detached
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
504
|
+
const detachForeground = (reason: string): boolean => {
|
|
505
|
+
if (detached || processClosed || lifecycleFinished || options.signal?.aborted) return false;
|
|
506
|
+
const receiptProgress = snapshotProgress(progress);
|
|
507
|
+
receiptProgress.status = "detached";
|
|
508
|
+
receiptProgress.durationMs = Date.now() - startTime;
|
|
509
|
+
const receipt = snapshotResult(result, receiptProgress);
|
|
510
|
+
receipt.exitCode = -2;
|
|
511
|
+
receipt.detached = true;
|
|
512
|
+
receipt.detachedReason = reason;
|
|
513
|
+
receipt.finalOutput = reason === "intercom coordination"
|
|
514
|
+
? "Detached for intercom coordination before task completion."
|
|
515
|
+
: reason === "user request"
|
|
516
|
+
? "Detached at user request before task completion."
|
|
517
|
+
: `Detached for ${reason} before task completion.`;
|
|
518
|
+
receipt.outputMode = options.outputMode ?? "inline";
|
|
519
|
+
if (options.outputPath) {
|
|
520
|
+
receipt.outputSaveError = reason === "user request"
|
|
521
|
+
? "Output file was not finalized because the subagent detached at user request."
|
|
522
|
+
: `Output file was not finalized because the subagent detached for ${reason}.`;
|
|
523
|
+
}
|
|
524
|
+
receipt.progressSummary = {
|
|
525
|
+
toolCount: receiptProgress.toolCount,
|
|
526
|
+
tokens: receiptProgress.tokens,
|
|
527
|
+
durationMs: receiptProgress.durationMs,
|
|
452
528
|
};
|
|
453
|
-
|
|
529
|
+
// The attempt is not detached until the outer coordinator has accepted and
|
|
530
|
+
// synchronously published the receipt. A persistence/callback failure must
|
|
531
|
+
// leave this attempt attached and abortable rather than orphaning it.
|
|
532
|
+
let accepted = false;
|
|
533
|
+
try {
|
|
534
|
+
accepted = options.onDetachReceipt?.(receipt) === true;
|
|
535
|
+
} catch {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
if (!accepted) return false;
|
|
539
|
+
detached = true;
|
|
540
|
+
return true;
|
|
454
541
|
};
|
|
455
542
|
|
|
456
543
|
// If the child emits a terminal assistant stop but never exits,
|
|
@@ -491,9 +578,9 @@ async function runSingleAttempt(
|
|
|
491
578
|
armWatchdogTail();
|
|
492
579
|
return;
|
|
493
580
|
}
|
|
494
|
-
if (childExited || finalDrainTimer ||
|
|
581
|
+
if (childExited || finalDrainTimer || lifecycleFinished || processClosed) return;
|
|
495
582
|
finalDrainTimer = setTimeout(() => {
|
|
496
|
-
if (
|
|
583
|
+
if (lifecycleFinished || processClosed) return;
|
|
497
584
|
const termSent = trySignalChild(proc, "SIGTERM");
|
|
498
585
|
if (!termSent) return;
|
|
499
586
|
forcedTerminationSignal = true;
|
|
@@ -501,7 +588,7 @@ async function runSingleAttempt(
|
|
|
501
588
|
result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
|
|
502
589
|
}
|
|
503
590
|
finalHardKillTimer = setTimeout(() => {
|
|
504
|
-
if (
|
|
591
|
+
if (lifecycleFinished || processClosed) return;
|
|
505
592
|
forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
|
|
506
593
|
}, HARD_KILL_MS);
|
|
507
594
|
finalHardKillTimer.unref?.();
|
|
@@ -509,7 +596,7 @@ async function runSingleAttempt(
|
|
|
509
596
|
finalDrainTimer.unref?.();
|
|
510
597
|
};
|
|
511
598
|
function armWatchdogTail(): void {
|
|
512
|
-
if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer ||
|
|
599
|
+
if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished || processClosed) return;
|
|
513
600
|
watchdogTailTimer = setTimeout(() => {
|
|
514
601
|
watchdogTailTimer = undefined;
|
|
515
602
|
updateChildWatchdogState({
|
|
@@ -535,7 +622,7 @@ async function runSingleAttempt(
|
|
|
535
622
|
};
|
|
536
623
|
|
|
537
624
|
const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
538
|
-
if (!options.allowIntercomDetach ||
|
|
625
|
+
if (!options.allowIntercomDetach || processClosed) return;
|
|
539
626
|
if (!payload || typeof payload !== "object") return;
|
|
540
627
|
const event = payload as { requestId?: unknown; runId?: unknown; agent?: unknown; childIndex?: unknown };
|
|
541
628
|
const requestId = event.requestId;
|
|
@@ -546,13 +633,13 @@ async function runSingleAttempt(
|
|
|
546
633
|
if (typeof event.agent === "string" && event.agent !== agent.name) return;
|
|
547
634
|
if (typeof event.childIndex === "number" && event.childIndex !== (options.index ?? 0)) return;
|
|
548
635
|
} else if (!intercomStarted) return;
|
|
549
|
-
|
|
550
|
-
|
|
636
|
+
const accepted = detachForeground("intercom coordination");
|
|
637
|
+
options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted, runId: options.runId, agent: agent.name, childIndex: options.index ?? 0 });
|
|
551
638
|
});
|
|
552
639
|
|
|
553
640
|
const finish = (code: number) => {
|
|
554
|
-
if (
|
|
555
|
-
|
|
641
|
+
if (lifecycleFinished) return;
|
|
642
|
+
lifecycleFinished = true;
|
|
556
643
|
clearFinalDrainTimers();
|
|
557
644
|
clearWatchdogTailTimer();
|
|
558
645
|
clearStdioGuard();
|
|
@@ -637,7 +724,7 @@ async function runSingleAttempt(
|
|
|
637
724
|
};
|
|
638
725
|
const requestTurnBudgetAbort = (turnCount: number) => {
|
|
639
726
|
const budget = options.turnBudget;
|
|
640
|
-
if (!budget || result.timedOut || result.turnBudgetExceeded || interruptedByControl || processClosed ||
|
|
727
|
+
if (!budget || result.timedOut || result.turnBudgetExceeded || interruptedByControl || processClosed || lifecycleFinished) return;
|
|
641
728
|
const message = turnBudgetExceededMessage(budget, turnCount);
|
|
642
729
|
result.turnBudgetExceeded = true;
|
|
643
730
|
result.wrapUpRequested = true;
|
|
@@ -650,12 +737,12 @@ async function runSingleAttempt(
|
|
|
650
737
|
fireUpdate();
|
|
651
738
|
trySignalChild(proc, "SIGINT");
|
|
652
739
|
turnBudgetTerminationTimer = setTimeout(() => {
|
|
653
|
-
if (processClosed ||
|
|
740
|
+
if (processClosed || lifecycleFinished || result.timedOut) return;
|
|
654
741
|
trySignalChild(proc, "SIGTERM");
|
|
655
742
|
}, 1000);
|
|
656
743
|
turnBudgetTerminationTimer.unref?.();
|
|
657
744
|
turnBudgetHardKillTimer = setTimeout(() => {
|
|
658
|
-
if (processClosed ||
|
|
745
|
+
if (processClosed || lifecycleFinished || result.timedOut) return;
|
|
659
746
|
trySignalChild(proc, "SIGKILL");
|
|
660
747
|
}, 4000);
|
|
661
748
|
turnBudgetHardKillTimer.unref?.();
|
|
@@ -892,7 +979,7 @@ async function runSingleAttempt(
|
|
|
892
979
|
|
|
893
980
|
if (controlConfig.enabled) {
|
|
894
981
|
activityTimer = setInterval(() => {
|
|
895
|
-
if (processClosed ||
|
|
982
|
+
if (processClosed || lifecycleFinished) return;
|
|
896
983
|
const now = Date.now();
|
|
897
984
|
if (updateActivityState(now)) {
|
|
898
985
|
progress.durationMs = now - startTime;
|
|
@@ -904,7 +991,7 @@ async function runSingleAttempt(
|
|
|
904
991
|
|
|
905
992
|
if (attemptTimeout) {
|
|
906
993
|
timeoutTimer = setTimeout(() => {
|
|
907
|
-
if (processClosed ||
|
|
994
|
+
if (processClosed || lifecycleFinished || interruptedByControl) return;
|
|
908
995
|
result.timedOut = true;
|
|
909
996
|
result.error = attemptTimeout.message;
|
|
910
997
|
result.finalOutput = attemptTimeout.message;
|
|
@@ -914,12 +1001,12 @@ async function runSingleAttempt(
|
|
|
914
1001
|
fireUpdate();
|
|
915
1002
|
trySignalChild(proc, "SIGINT");
|
|
916
1003
|
timeoutTerminationTimer = setTimeout(() => {
|
|
917
|
-
if (processClosed ||
|
|
1004
|
+
if (processClosed || lifecycleFinished) return;
|
|
918
1005
|
trySignalChild(proc, "SIGTERM");
|
|
919
1006
|
}, 1000);
|
|
920
1007
|
timeoutTerminationTimer.unref?.();
|
|
921
1008
|
timeoutHardKillTimer = setTimeout(() => {
|
|
922
|
-
if (processClosed ||
|
|
1009
|
+
if (processClosed || lifecycleFinished) return;
|
|
923
1010
|
trySignalChild(proc, "SIGKILL");
|
|
924
1011
|
}, 4000);
|
|
925
1012
|
timeoutHardKillTimer.unref?.();
|
|
@@ -963,19 +1050,33 @@ async function runSingleAttempt(
|
|
|
963
1050
|
clearFinalDrainTimers();
|
|
964
1051
|
});
|
|
965
1052
|
proc.on("close", (code, signal) => {
|
|
1053
|
+
if (lifecycleFinished) return;
|
|
1054
|
+
processClosed = true;
|
|
966
1055
|
clearFinalDrainTimers();
|
|
967
1056
|
clearStdioGuard();
|
|
968
1057
|
void jsonlWriter.close().catch(() => {
|
|
969
1058
|
// JSONL artifact flush is best effort.
|
|
970
1059
|
});
|
|
971
1060
|
const toolDiagnosticError = readChildToolDiagnosticError(toolDiagnosticPath);
|
|
1061
|
+
result.runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
|
|
972
1062
|
cleanupTempDir(tempDir);
|
|
973
1063
|
stdoutReader.end();
|
|
974
1064
|
stderrReader.end();
|
|
975
1065
|
const stderr = stderrTail.text();
|
|
976
1066
|
const rawStdout = rawStdoutTail.text();
|
|
977
1067
|
let closeError = result.error ?? toolDiagnosticError ?? assistantError;
|
|
978
|
-
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
|
|
1068
|
+
const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
|
|
1069
|
+
if (signal) result.processSignal = signal;
|
|
1070
|
+
if (!closeError && isUnexplainedProcessSignal({
|
|
1071
|
+
processSignal: signal,
|
|
1072
|
+
interrupted: result.interrupted,
|
|
1073
|
+
timedOut: result.timedOut,
|
|
1074
|
+
stopped: result.stopped,
|
|
1075
|
+
turnBudgetExceeded: result.turnBudgetExceeded,
|
|
1076
|
+
forcedDrainAfterFinalSuccess,
|
|
1077
|
+
})) {
|
|
1078
|
+
closeError = formatProcessSignalError(signal!);
|
|
1079
|
+
}
|
|
979
1080
|
if (code !== 0 && rawStdout.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
|
|
980
1081
|
closeError = rawStdout.trim();
|
|
981
1082
|
}
|
|
@@ -983,61 +1084,12 @@ async function runSingleAttempt(
|
|
|
983
1084
|
closeError = stderr.trim();
|
|
984
1085
|
}
|
|
985
1086
|
const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
|
|
986
|
-
if (signal) result.processSignal = signal;
|
|
987
|
-
if (detached) {
|
|
988
|
-
const recoveredProgress = snapshotProgress(progress);
|
|
989
|
-
const recoveredResult = snapshotResult(result, recoveredProgress);
|
|
990
|
-
if (!recoveredResult.error && closeError) recoveredResult.error = closeError;
|
|
991
|
-
recoveredResult.exitCode = recoveredResult.error && finalCode === 0 ? 1 : finalCode;
|
|
992
|
-
recoveredProgress.status = recoveredResult.exitCode === 0 ? "completed" : "failed";
|
|
993
|
-
recoveredProgress.durationMs = Date.now() - startTime;
|
|
994
|
-
if (recoveredResult.error) recoveredProgress.error = recoveredResult.error;
|
|
995
|
-
recoveredResult.progressSummary = {
|
|
996
|
-
toolCount: recoveredProgress.toolCount,
|
|
997
|
-
tokens: recoveredProgress.tokens,
|
|
998
|
-
durationMs: recoveredProgress.durationMs,
|
|
999
|
-
};
|
|
1000
|
-
const acceptanceOutput = getFinalOutput(recoveredResult.messages ?? []).trim()
|
|
1001
|
-
|| recoveredResult.error
|
|
1002
|
-
|| recoveredResult.finalOutput
|
|
1003
|
-
|| "Detached child exited without final output.";
|
|
1004
|
-
acceptanceOutputByResult.set(recoveredResult, acceptanceOutput);
|
|
1005
|
-
let fullOutput = stripAcceptanceReport(acceptanceOutput);
|
|
1006
|
-
fullOutput = fullOutput.trim() || recoveredResult.error || recoveredResult.finalOutput || "Detached child exited without final output.";
|
|
1007
|
-
recoveredResult.outputMode = options.outputMode ?? "inline";
|
|
1008
|
-
if (options.outputPath && recoveredResult.exitCode === 0) {
|
|
1009
|
-
const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
|
|
1010
|
-
fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
|
|
1011
|
-
recoveredResult.savedOutputPath = resolvedOutput.savedPath;
|
|
1012
|
-
recoveredResult.outputSaveError = resolvedOutput.saveError;
|
|
1013
|
-
if (resolvedOutput.savedPath) {
|
|
1014
|
-
recoveredResult.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
|
|
1015
|
-
} else {
|
|
1016
|
-
recoveredResult.exitCode = 1;
|
|
1017
|
-
recoveredResult.error = `Output file was not finalized after detached child exit: ${resolvedOutput.saveError ?? options.outputPath}`;
|
|
1018
|
-
recoveredProgress.status = "failed";
|
|
1019
|
-
recoveredProgress.error = recoveredResult.error;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
recoveredResult.finalOutput = options.outputMode === "file-only" && recoveredResult.savedOutputPath && recoveredResult.outputReference
|
|
1023
|
-
? recoveredResult.outputReference.message
|
|
1024
|
-
: fullOutput;
|
|
1025
|
-
if (recoveredResult.artifactPaths && options.artifactConfig?.enabled !== false && options.artifactConfig?.includeOutput !== false) {
|
|
1026
|
-
try {
|
|
1027
|
-
writeArtifact(recoveredResult.artifactPaths.outputPath, fullOutput);
|
|
1028
|
-
} catch {
|
|
1029
|
-
// Detached children may outlive test/temp cleanup; recovered status is best-effort.
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
options.onDetachedExit?.(recoveredResult);
|
|
1033
|
-
finish(-2);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
1087
|
if (!result.error && closeError) result.error = closeError;
|
|
1037
|
-
processClosed = true;
|
|
1038
1088
|
finish(finalCode);
|
|
1039
1089
|
});
|
|
1040
1090
|
proc.on("error", (error) => {
|
|
1091
|
+
if (lifecycleFinished) return;
|
|
1092
|
+
processClosed = true;
|
|
1041
1093
|
clearFinalDrainTimers();
|
|
1042
1094
|
clearStdioGuard();
|
|
1043
1095
|
void jsonlWriter.close().catch(() => {
|
|
@@ -1054,7 +1106,7 @@ async function runSingleAttempt(
|
|
|
1054
1106
|
|
|
1055
1107
|
if (options.signal) {
|
|
1056
1108
|
const kill = () => {
|
|
1057
|
-
if (processClosed ||
|
|
1109
|
+
if (processClosed || lifecycleFinished) return;
|
|
1058
1110
|
proc.kill("SIGTERM");
|
|
1059
1111
|
setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
|
|
1060
1112
|
};
|
|
@@ -1067,7 +1119,7 @@ async function runSingleAttempt(
|
|
|
1067
1119
|
|
|
1068
1120
|
if (options.interruptSignal) {
|
|
1069
1121
|
const interrupt = () => {
|
|
1070
|
-
if (processClosed ||
|
|
1122
|
+
if (processClosed || lifecycleFinished) return;
|
|
1071
1123
|
if (result.timedOut) return;
|
|
1072
1124
|
interruptedByControl = true;
|
|
1073
1125
|
clearTimeoutTimers();
|
|
@@ -1079,7 +1131,7 @@ async function runSingleAttempt(
|
|
|
1079
1131
|
fireUpdate();
|
|
1080
1132
|
trySignalChild(proc, "SIGINT");
|
|
1081
1133
|
setTimeout(() => {
|
|
1082
|
-
if (
|
|
1134
|
+
if (lifecycleFinished || processClosed) return;
|
|
1083
1135
|
trySignalChild(proc, "SIGTERM");
|
|
1084
1136
|
}, 1000).unref?.();
|
|
1085
1137
|
};
|
|
@@ -1089,6 +1141,16 @@ async function runSingleAttempt(
|
|
|
1089
1141
|
removeInterruptListener = () => options.interruptSignal?.removeEventListener("abort", interrupt);
|
|
1090
1142
|
}
|
|
1091
1143
|
}
|
|
1144
|
+
|
|
1145
|
+
// Publish only after every callback and cleanup guard captured by detach or
|
|
1146
|
+
// later lifecycle events has initialized. Consumers may invoke synchronously.
|
|
1147
|
+
try {
|
|
1148
|
+
options.onDetachReady?.((reason = "user request") => detachForeground(reason));
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
// A consumer callback is advisory. Keep the child attached and observable
|
|
1151
|
+
// rather than rejecting this attempt and orphaning its live process.
|
|
1152
|
+
appendRecentOutput(progress, [`Foreground detach callback failed: ${error instanceof Error ? error.message : String(error)}`]);
|
|
1153
|
+
}
|
|
1092
1154
|
});
|
|
1093
1155
|
result.exitCode = exitCode;
|
|
1094
1156
|
if (interruptedByControl) {
|
|
@@ -1106,16 +1168,6 @@ async function runSingleAttempt(
|
|
|
1106
1168
|
};
|
|
1107
1169
|
return result;
|
|
1108
1170
|
}
|
|
1109
|
-
if (result.detached) {
|
|
1110
|
-
result.exitCode = -2;
|
|
1111
|
-
result.finalOutput = "Detached for intercom coordination before task completion.";
|
|
1112
|
-
result.outputMode = options.outputMode ?? "inline";
|
|
1113
|
-
if (options.outputPath) {
|
|
1114
|
-
result.outputSaveError = "Output file was not finalized because the subagent detached for intercom coordination.";
|
|
1115
|
-
}
|
|
1116
|
-
return result;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
1171
|
if (result.error && result.exitCode === 0) {
|
|
1120
1172
|
result.exitCode = 1;
|
|
1121
1173
|
}
|
|
@@ -1177,6 +1229,7 @@ async function runSingleAttempt(
|
|
|
1177
1229
|
|
|
1178
1230
|
const acceptanceOutput = getFinalOutput(result.messages ?? []);
|
|
1179
1231
|
let fullOutput = stripAcceptanceReport(acceptanceOutput);
|
|
1232
|
+
result.outputState = fullOutput.trim() || result.structuredOutput !== undefined ? "present" : "absent";
|
|
1180
1233
|
if (result.timedOut) {
|
|
1181
1234
|
const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
|
|
1182
1235
|
fullOutput = fullOutput.trim()
|
|
@@ -1236,6 +1289,7 @@ async function runSingleAttempt(
|
|
|
1236
1289
|
result.outputSaveError = resolvedOutput.saveError;
|
|
1237
1290
|
if (resolvedOutput.savedPath) {
|
|
1238
1291
|
result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
|
|
1292
|
+
if (result.outputState === "absent") result.outputState = "unknown";
|
|
1239
1293
|
}
|
|
1240
1294
|
}
|
|
1241
1295
|
artifactOutputByResult.set(result, fullOutput);
|
|
@@ -1265,7 +1319,7 @@ async function runSingleAttempt(
|
|
|
1265
1319
|
/**
|
|
1266
1320
|
* Run a subagent synchronously (blocking until complete)
|
|
1267
1321
|
*/
|
|
1268
|
-
|
|
1322
|
+
async function runSyncCompletion(
|
|
1269
1323
|
runtimeCwd: string,
|
|
1270
1324
|
agents: AgentConfig[],
|
|
1271
1325
|
agentName: string,
|
|
@@ -1274,11 +1328,12 @@ export async function runSync(
|
|
|
1274
1328
|
): Promise<SingleResult> {
|
|
1275
1329
|
options = {
|
|
1276
1330
|
...options,
|
|
1277
|
-
capabilityCeiling: options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId),
|
|
1331
|
+
capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])),
|
|
1278
1332
|
};
|
|
1279
1333
|
const agent = agents.find((a) => a.name === agentName);
|
|
1280
1334
|
if (!agent) {
|
|
1281
1335
|
return withRunContext({
|
|
1336
|
+
index: options.index ?? 0,
|
|
1282
1337
|
agent: agentName,
|
|
1283
1338
|
task,
|
|
1284
1339
|
exitCode: 1,
|
|
@@ -1287,9 +1342,23 @@ export async function runSync(
|
|
|
1287
1342
|
error: `Unknown agent: ${agentName}`,
|
|
1288
1343
|
}, options.context);
|
|
1289
1344
|
}
|
|
1345
|
+
try {
|
|
1346
|
+
assertAgentAllowedByCapabilityCeiling(agent.name, options.capabilityCeiling);
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
return withRunContext({
|
|
1349
|
+
agent: agent.name,
|
|
1350
|
+
task,
|
|
1351
|
+
exitCode: 1,
|
|
1352
|
+
messages: [],
|
|
1353
|
+
usage: emptyUsage(),
|
|
1354
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1355
|
+
...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
|
|
1356
|
+
}, options.context);
|
|
1357
|
+
}
|
|
1290
1358
|
const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
|
|
1291
1359
|
if (outputModeValidationError) {
|
|
1292
1360
|
return withRunContext({
|
|
1361
|
+
index: options.index ?? 0,
|
|
1293
1362
|
agent: agentName,
|
|
1294
1363
|
task,
|
|
1295
1364
|
exitCode: 1,
|
|
@@ -1326,6 +1395,7 @@ export async function runSync(
|
|
|
1326
1395
|
);
|
|
1327
1396
|
if (skillNames.some((skill) => skill.trim() === "pi-subagents") && missingSkills.includes("pi-subagents")) {
|
|
1328
1397
|
return withRunContext({
|
|
1398
|
+
index: options.index ?? 0,
|
|
1329
1399
|
agent: agentName,
|
|
1330
1400
|
task,
|
|
1331
1401
|
exitCode: 1,
|
|
@@ -1385,83 +1455,42 @@ export async function runSync(
|
|
|
1385
1455
|
}
|
|
1386
1456
|
|
|
1387
1457
|
const persistResultMetadata = (target: SingleResult): void => {
|
|
1388
|
-
|
|
1389
|
-
|
|
1458
|
+
persistSingleResultMetadata({
|
|
1459
|
+
metadataPath: artifactPathsResult?.metadataPath,
|
|
1460
|
+
enabled: options.artifactConfig?.enabled !== false && options.artifactConfig?.includeMetadata !== false,
|
|
1390
1461
|
runId: options.runId,
|
|
1391
1462
|
agent: agentName,
|
|
1392
1463
|
task,
|
|
1393
|
-
|
|
1394
|
-
processSignal: target.processSignal,
|
|
1395
|
-
usage: target.usage,
|
|
1396
|
-
model: target.model,
|
|
1397
|
-
attemptedModels: target.attemptedModels,
|
|
1398
|
-
modelAttempts: target.modelAttempts,
|
|
1399
|
-
durationMs: target.progressSummary?.durationMs,
|
|
1400
|
-
toolCount: target.progressSummary?.toolCount,
|
|
1401
|
-
error: target.error,
|
|
1402
|
-
agentContract: target.agentContract,
|
|
1403
|
-
launchContractDigest: target.launchContractDigest,
|
|
1404
|
-
execution: target.execution,
|
|
1405
|
-
acceptance: target.acceptance,
|
|
1406
|
-
capabilityCeiling: target.capabilityCeiling,
|
|
1407
|
-
capabilityAudit: target.capabilityAudit,
|
|
1408
|
-
review: target.review,
|
|
1409
|
-
effects: target.effects,
|
|
1410
|
-
...(transcriptWriter ? { transcriptPath: artifactPathsResult.transcriptPath } : {}),
|
|
1411
|
-
transcriptError: target.transcriptError,
|
|
1412
|
-
skills: target.skills,
|
|
1413
|
-
skillsWarning: target.skillsWarning,
|
|
1414
|
-
timestamp: Date.now(),
|
|
1464
|
+
result: target,
|
|
1415
1465
|
});
|
|
1416
1466
|
};
|
|
1417
1467
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
if (acceptanceFailure && recoveredResult.acceptance.explicit && recoveredResult.exitCode === 0 && !isAgentContractV1(options.agentContract)) {
|
|
1438
|
-
recoveredResult.exitCode = 1;
|
|
1439
|
-
recoveredResult.error = recoveredResult.error ? `${recoveredResult.error}\n${acceptanceFailure}` : acceptanceFailure;
|
|
1440
|
-
if (recoveredResult.progress) {
|
|
1441
|
-
recoveredResult.progress.status = "failed";
|
|
1442
|
-
recoveredResult.progress.error = recoveredResult.error;
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
if (isAgentContractV1(options.agentContract)) attachContractProjections(recoveredResult);
|
|
1446
|
-
persistResultMetadata(recoveredResult);
|
|
1447
|
-
options.onDetachedExit?.(recoveredResult);
|
|
1448
|
-
})().catch((error) => {
|
|
1449
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1450
|
-
recoveredResult.exitCode = 1;
|
|
1451
|
-
recoveredResult.error = recoveredResult.error ? `${recoveredResult.error}\nAcceptance evaluation failed: ${message}` : `Acceptance evaluation failed: ${message}`;
|
|
1452
|
-
options.onDetachedExit?.(recoveredResult);
|
|
1453
|
-
});
|
|
1454
|
-
},
|
|
1455
|
-
}
|
|
1456
|
-
: options;
|
|
1457
|
-
|
|
1468
|
+
let intercomDetached = false;
|
|
1469
|
+
let detachedReason: string | undefined;
|
|
1470
|
+
const attemptOptions: RunSyncOptions = {
|
|
1471
|
+
...options,
|
|
1472
|
+
onDetachReceipt: (receipt) => {
|
|
1473
|
+
receipt.acceptance = buildPendingAcceptanceLedger(effectiveAcceptance);
|
|
1474
|
+
try {
|
|
1475
|
+
persistResultMetadata(receipt);
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
receipt.metadataSaveError = error instanceof Error ? error.message : String(error);
|
|
1478
|
+
}
|
|
1479
|
+
const accepted = options.onDetachReceipt?.(receipt) === true;
|
|
1480
|
+
if (accepted) {
|
|
1481
|
+
detachedReason = receipt.detachedReason;
|
|
1482
|
+
if (receipt.detachedReason === "intercom coordination") intercomDetached = true;
|
|
1483
|
+
}
|
|
1484
|
+
return accepted;
|
|
1485
|
+
},
|
|
1486
|
+
};
|
|
1458
1487
|
let lastResult: SingleResult | undefined;
|
|
1459
1488
|
const modelsToTry = candidates.length > 0 ? candidates : [undefined];
|
|
1460
1489
|
modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
|
|
1461
1490
|
const candidate = modelsToTry[modelIndex];
|
|
1462
1491
|
for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
|
|
1463
1492
|
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
1464
|
-
const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate,
|
|
1493
|
+
const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, attemptOptions, {
|
|
1465
1494
|
sessionEnabled,
|
|
1466
1495
|
systemPrompt,
|
|
1467
1496
|
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
@@ -1493,7 +1522,10 @@ export async function runSync(
|
|
|
1493
1522
|
usage: { ...result.usage },
|
|
1494
1523
|
};
|
|
1495
1524
|
modelAttempts.push(attempt);
|
|
1496
|
-
|
|
1525
|
+
// Preserve the legacy intercom handoff contract: once this logical run has
|
|
1526
|
+
// been handed to a supervisor, terminating that attempt must not launch a
|
|
1527
|
+
// startup retry or model fallback. Explicit user detach retains fallback.
|
|
1528
|
+
if (intercomDetached || result.timedOut || result.turnBudgetExceeded) break modelAttemptsLoop;
|
|
1497
1529
|
if (attemptSucceeded) break modelAttemptsLoop;
|
|
1498
1530
|
|
|
1499
1531
|
const startupFailure = isRetryableSubagentStartupFailure({
|
|
@@ -1565,6 +1597,7 @@ export async function runSync(
|
|
|
1565
1597
|
}
|
|
1566
1598
|
|
|
1567
1599
|
const result = withRunContext(lastResult ?? {
|
|
1600
|
+
index: options.index ?? 0,
|
|
1568
1601
|
agent: agentName,
|
|
1569
1602
|
task,
|
|
1570
1603
|
exitCode: 1,
|
|
@@ -1584,25 +1617,30 @@ export async function runSync(
|
|
|
1584
1617
|
if (transcriptWriter) result.transcriptPath = artifactPathsResult?.transcriptPath;
|
|
1585
1618
|
if (transcriptWriter?.getError()) result.transcriptError = transcriptWriter.getError();
|
|
1586
1619
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1620
|
+
try {
|
|
1621
|
+
if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
|
|
1622
|
+
result.artifactPaths = artifactPathsResult;
|
|
1623
|
+
if (options.artifactConfig?.includeOutput !== false) {
|
|
1624
|
+
writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
|
|
1625
|
+
output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
|
|
1626
|
+
error: result.error,
|
|
1627
|
+
transcriptPath: result.transcriptPath,
|
|
1628
|
+
metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
|
|
1629
|
+
}));
|
|
1630
|
+
}
|
|
1631
|
+
if (options.maxOutput) {
|
|
1632
|
+
const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
|
|
1633
|
+
const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
|
|
1634
|
+
if (truncationResult.truncated) result.truncation = truncationResult;
|
|
1635
|
+
}
|
|
1636
|
+
} else if (options.maxOutput) {
|
|
1598
1637
|
const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
|
|
1599
|
-
const truncationResult = truncateOutput(result.finalOutput ?? "", config
|
|
1638
|
+
const truncationResult = truncateOutput(result.finalOutput ?? "", config);
|
|
1600
1639
|
if (truncationResult.truncated) result.truncation = truncationResult;
|
|
1601
1640
|
}
|
|
1602
|
-
}
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
if (truncationResult.truncated) result.truncation = truncationResult;
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
const message = `Artifact output post-processing failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1643
|
+
result.outputSaveError = result.outputSaveError ? `${result.outputSaveError}\n${message}` : message;
|
|
1606
1644
|
}
|
|
1607
1645
|
|
|
1608
1646
|
if (options.sessionFile && (existsSync(options.sessionFile) || result.messages?.length)) {
|
|
@@ -1615,28 +1653,36 @@ export async function runSync(
|
|
|
1615
1653
|
const childWrittenOutput = options.outputPath
|
|
1616
1654
|
? extractChildWrittenOutput(result.messages, options.outputPath, options.cwd ?? runtimeCwd)
|
|
1617
1655
|
: undefined;
|
|
1618
|
-
|
|
1619
|
-
result.
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
:
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1656
|
+
try {
|
|
1657
|
+
if (result.interrupted && detachedReason === "user request") {
|
|
1658
|
+
// Only an accepted user-detach receipt needs a non-rejecting terminal
|
|
1659
|
+
// ledger. Attached and background execution retain their baseline
|
|
1660
|
+
// acceptance behavior.
|
|
1661
|
+
result.acceptance = buildInterruptedAcceptanceLedger(effectiveAcceptance);
|
|
1662
|
+
} else if (result.stopped) {
|
|
1663
|
+
result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." });
|
|
1664
|
+
} else if (result.timedOut) {
|
|
1665
|
+
result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." });
|
|
1666
|
+
} else if (result.turnBudgetExceeded) {
|
|
1667
|
+
result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." });
|
|
1668
|
+
} else {
|
|
1669
|
+
result.acceptance = await evaluateAcceptance({
|
|
1670
|
+
acceptance: effectiveAcceptance,
|
|
1671
|
+
output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
|
|
1672
|
+
fileOutput: childWrittenOutput !== undefined && options.outputPath
|
|
1673
|
+
? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
|
|
1674
|
+
: undefined,
|
|
1675
|
+
cwd: options.cwd ?? runtimeCwd,
|
|
1676
|
+
reportOptional: isAgentContractV1(options.agentContract),
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
const message = `Acceptance evaluation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1681
|
+
result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "acceptance-evaluation", message });
|
|
1636
1682
|
}
|
|
1637
1683
|
const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
|
|
1638
1684
|
stripAcceptanceReportsFromMessages(result.messages);
|
|
1639
|
-
if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.
|
|
1685
|
+
if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
|
|
1640
1686
|
result.exitCode = 1;
|
|
1641
1687
|
result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
|
|
1642
1688
|
if (result.progress) {
|
|
@@ -1645,7 +1691,143 @@ export async function runSync(
|
|
|
1645
1691
|
}
|
|
1646
1692
|
}
|
|
1647
1693
|
if (isAgentContractV1(options.agentContract)) attachContractProjections(result);
|
|
1648
|
-
|
|
1694
|
+
try {
|
|
1695
|
+
persistResultMetadata(result);
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
result.metadataSaveError = error instanceof Error ? error.message : String(error);
|
|
1698
|
+
}
|
|
1649
1699
|
|
|
1650
1700
|
return result;
|
|
1651
1701
|
}
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Runs the authoritative completion pipeline independently from the foreground
|
|
1705
|
+
* receipt. Detachment is same-runtime only: it does not adopt or daemonize work.
|
|
1706
|
+
*/
|
|
1707
|
+
export async function runSync(
|
|
1708
|
+
runtimeCwd: string,
|
|
1709
|
+
agents: AgentConfig[],
|
|
1710
|
+
agentName: string,
|
|
1711
|
+
task: string,
|
|
1712
|
+
options: RunSyncOptions,
|
|
1713
|
+
): Promise<SingleResult> {
|
|
1714
|
+
// Capture the strict contract before consumer-owned objects can be mutated
|
|
1715
|
+
// after a detached receipt is published.
|
|
1716
|
+
const strictContract = isAgentContractV1(options.agentContract);
|
|
1717
|
+
let detachedReason: string | undefined;
|
|
1718
|
+
let publishedReceipt: SingleResult | undefined;
|
|
1719
|
+
let activeDetachAttempt: ((reason?: string) => boolean) | undefined;
|
|
1720
|
+
let detachReadyPublished = false;
|
|
1721
|
+
let resolveReceipt!: (result: SingleResult) => void;
|
|
1722
|
+
const receipt = new Promise<SingleResult>((resolve) => { resolveReceipt = resolve; });
|
|
1723
|
+
const originSignal = options.signal;
|
|
1724
|
+
const originController = new AbortController();
|
|
1725
|
+
const forwardOriginAbort = () => {
|
|
1726
|
+
if (!detachedReason) originController.abort(originSignal?.reason);
|
|
1727
|
+
};
|
|
1728
|
+
if (originSignal?.aborted) forwardOriginAbort();
|
|
1729
|
+
else originSignal?.addEventListener("abort", forwardOriginAbort, { once: true });
|
|
1730
|
+
|
|
1731
|
+
const completion = runSyncCompletion(runtimeCwd, agents, agentName, task, {
|
|
1732
|
+
...options,
|
|
1733
|
+
signal: originController.signal,
|
|
1734
|
+
onDetachedExit: undefined,
|
|
1735
|
+
onDetachReceipt: (detachedReceipt) => {
|
|
1736
|
+
if (detachedReason || publishedReceipt) return false;
|
|
1737
|
+
// Keep the authoritative result, fallback snapshot, and caller receipt
|
|
1738
|
+
// separately owned while the completion pipeline remains live.
|
|
1739
|
+
publishedReceipt = structuredClone(detachedReceipt);
|
|
1740
|
+
const callerReceipt = structuredClone(detachedReceipt);
|
|
1741
|
+
// A strict contract was already validated before detach; normalize private
|
|
1742
|
+
// and caller snapshots to the only safely known version.
|
|
1743
|
+
publishedReceipt.agentContract = strictContract ? { version: 1 } : undefined;
|
|
1744
|
+
callerReceipt.agentContract = strictContract ? { version: 1 } : undefined;
|
|
1745
|
+
detachedReason = detachedReceipt.detachedReason ?? "user request";
|
|
1746
|
+
resolveReceipt(callerReceipt);
|
|
1747
|
+
return true;
|
|
1748
|
+
},
|
|
1749
|
+
onDetachReady: (detachAttempt) => {
|
|
1750
|
+
activeDetachAttempt = detachAttempt;
|
|
1751
|
+
if (detachReadyPublished) return;
|
|
1752
|
+
detachReadyPublished = true;
|
|
1753
|
+
options.onDetachReady?.((reason = "user request") => {
|
|
1754
|
+
if (detachedReason || originController.signal.aborted) return false;
|
|
1755
|
+
return activeDetachAttempt?.(reason) ?? false;
|
|
1756
|
+
});
|
|
1757
|
+
},
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
const authoritativeCompletion = completion.catch((error: unknown) => {
|
|
1761
|
+
if (!publishedReceipt || !detachedReason) throw error;
|
|
1762
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1763
|
+
const failureMessage = `Detached completion pipeline failed after receipt: ${message}`;
|
|
1764
|
+
const failedProgress: AgentProgress = publishedReceipt.progress
|
|
1765
|
+
? { ...publishedReceipt.progress, status: "failed", error: failureMessage }
|
|
1766
|
+
: {
|
|
1767
|
+
index: options.index ?? 0,
|
|
1768
|
+
agent: publishedReceipt.agent,
|
|
1769
|
+
status: "failed",
|
|
1770
|
+
task: publishedReceipt.task,
|
|
1771
|
+
recentTools: [],
|
|
1772
|
+
recentOutput: [],
|
|
1773
|
+
toolCount: publishedReceipt.progressSummary?.toolCount ?? 0,
|
|
1774
|
+
tokens: publishedReceipt.progressSummary?.tokens ?? 0,
|
|
1775
|
+
durationMs: publishedReceipt.progressSummary?.durationMs ?? 0,
|
|
1776
|
+
error: failureMessage,
|
|
1777
|
+
};
|
|
1778
|
+
const failedResult: SingleResult = {
|
|
1779
|
+
...publishedReceipt,
|
|
1780
|
+
detached: undefined,
|
|
1781
|
+
detachedReason,
|
|
1782
|
+
exitCode: 1,
|
|
1783
|
+
error: failureMessage,
|
|
1784
|
+
finalOutput: failureMessage,
|
|
1785
|
+
progress: failedProgress,
|
|
1786
|
+
progressSummary: {
|
|
1787
|
+
toolCount: failedProgress.toolCount,
|
|
1788
|
+
tokens: failedProgress.tokens,
|
|
1789
|
+
durationMs: failedProgress.durationMs,
|
|
1790
|
+
},
|
|
1791
|
+
acceptance: publishedReceipt.acceptance
|
|
1792
|
+
? buildSkippedAcceptanceLedger(publishedReceipt.acceptance.effectiveAcceptance, { id: "completion-pipeline", message: failureMessage })
|
|
1793
|
+
: undefined,
|
|
1794
|
+
};
|
|
1795
|
+
if (strictContract) attachContractProjections(failedResult);
|
|
1796
|
+
try {
|
|
1797
|
+
// Replace the provisional detach receipt metadata with the authoritative
|
|
1798
|
+
// terminal failure. Persistence remains best-effort and cannot orphan work.
|
|
1799
|
+
persistSingleResultMetadata({
|
|
1800
|
+
metadataPath: failedResult.artifactPaths?.metadataPath,
|
|
1801
|
+
enabled: options.artifactConfig?.enabled !== false && options.artifactConfig?.includeMetadata !== false,
|
|
1802
|
+
runId: options.runId,
|
|
1803
|
+
agent: failedResult.agent,
|
|
1804
|
+
task: failedResult.task,
|
|
1805
|
+
result: failedResult,
|
|
1806
|
+
});
|
|
1807
|
+
} catch (metadataError) {
|
|
1808
|
+
failedResult.metadataSaveError = metadataError instanceof Error ? metadataError.message : String(metadataError);
|
|
1809
|
+
}
|
|
1810
|
+
return failedResult;
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
let terminalCallbackInvoked = false;
|
|
1814
|
+
void authoritativeCompletion.then((terminalResult) => {
|
|
1815
|
+
if (!detachedReason || terminalCallbackInvoked) return;
|
|
1816
|
+
terminalCallbackInvoked = true;
|
|
1817
|
+
terminalResult.detached = undefined;
|
|
1818
|
+
terminalResult.detachedReason = detachedReason;
|
|
1819
|
+
try {
|
|
1820
|
+
options.onDetachedExit?.(terminalResult);
|
|
1821
|
+
} catch {
|
|
1822
|
+
// The authoritative result has settled. Consumer callback failures are
|
|
1823
|
+
// contained here; each consumer owns cleanup through its own finally block.
|
|
1824
|
+
}
|
|
1825
|
+
}).catch(() => {
|
|
1826
|
+
// Attached completion rejection remains observable through the returned
|
|
1827
|
+
// promise without becoming an unhandled side-channel rejection.
|
|
1828
|
+
}).finally(() => {
|
|
1829
|
+
originSignal?.removeEventListener("abort", forwardOriginAbort);
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
return Promise.race([authoritativeCompletion, receipt]);
|
|
1833
|
+
}
|