pi-subagents 0.65.1 → 0.66.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 +55 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +16 -2
- package/docs/configuration.md +18 -0
- package/docs/extension-api.md +91 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +3 -3
- package/docs/workflows.md +14 -7
- package/package.json +2 -1
- package/skills/pi-subagents/references/execution-controls.md +14 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/src/agents/advertised-agent-prompt.ts +63 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +8 -0
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/index.ts +40 -2
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +8 -6
- package/src/extension/tool-description.ts +6 -5
- package/src/intercom/native-supervisor-channel.ts +82 -54
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +40 -29
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/notify.ts +86 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +60 -32
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +18 -7
- package/src/runs/background/runner-child-launch.ts +86 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/subagent-runner.ts +412 -209
- package/src/runs/background/subagent-wait.ts +3 -0
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/async-steering-action.ts +19 -0
- package/src/runs/foreground/execution.ts +101 -25
- package/src/runs/foreground/subagent-executor.ts +474 -197
- package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
- package/src/runs/shared/acceptance.ts +2 -2
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +47 -47
- package/src/runs/shared/child-hooks.ts +151 -2
- package/src/runs/shared/child-launch.ts +18 -13
- package/src/runs/shared/child-session.ts +42 -6
- package/src/runs/shared/child-tool-plan.ts +2 -2
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +1 -0
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +10 -9
- package/src/runs/shared/model-fallback.ts +10 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +6 -0
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +332 -204
- package/src/shared/types.ts +81 -60
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +23 -5
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +38 -10
- package/src/workflows/workflow-checklist.ts +11 -15
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
|
@@ -337,6 +337,9 @@ function completionUsage(completions: WaitCompletion[] | undefined): Usage | und
|
|
|
337
337
|
|
|
338
338
|
function result(text: string, isError = false, completions?: WaitCompletion[]): AgentToolResult<Details> {
|
|
339
339
|
const usage = completionUsage(completions);
|
|
340
|
+
for (const completion of completions ?? []) {
|
|
341
|
+
if (completion.workflowReceiptPath) text += `\nWorkflow receipt [${completion.runId}]: ${completion.workflowReceiptPath}`;
|
|
342
|
+
}
|
|
340
343
|
return {
|
|
341
344
|
content: [{ type: "text", text }],
|
|
342
345
|
...(isError ? { isError: true } : {}),
|
|
@@ -96,6 +96,9 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
96
96
|
})
|
|
97
97
|
: undefined;
|
|
98
98
|
const agent = asNonEmptyString(data.agent);
|
|
99
|
+
const receipt = data.workflowReceipt;
|
|
100
|
+
const workflowReceiptPath = receipt && typeof receipt === "object" && !Array.isArray(receipt)
|
|
101
|
+
? asNonEmptyString((receipt as Record<string, unknown>).path) : undefined;
|
|
99
102
|
const mode = asNonEmptyString(data.mode);
|
|
100
103
|
const state = asNonEmptyString(data.state);
|
|
101
104
|
const workflowChildren = parseWorkflowChildSummary(data.workflowChildren);
|
|
@@ -104,6 +107,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
104
107
|
runId,
|
|
105
108
|
...(agent ? { agent } : {}),
|
|
106
109
|
...(mode ? { mode } : {}),
|
|
110
|
+
...(workflowReceiptPath ? { workflowReceiptPath } : {}),
|
|
107
111
|
...(state ? { state } : {}),
|
|
108
112
|
...(typeof data.success === "boolean" ? { success: data.success } : {}),
|
|
109
113
|
...(results && results.length > 0 ? { results } : {}),
|
|
@@ -26,6 +26,7 @@ export async function steerAsyncRun(input: {
|
|
|
26
26
|
message: string;
|
|
27
27
|
mode?: SteerDeliveryMode;
|
|
28
28
|
index?: number;
|
|
29
|
+
findPendingAsks?: (target: { runId: string; agent: string; childIndex: number }) => string[];
|
|
29
30
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
30
31
|
location: { asyncDir: string | null };
|
|
31
32
|
signal?: AbortSignal;
|
|
@@ -44,6 +45,24 @@ export async function steerAsyncRun(input: {
|
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
const asyncDir = input.location.asyncDir;
|
|
48
|
+
const snapshot = input.findPendingAsks ? readStatus(asyncDir) : null;
|
|
49
|
+
if (snapshot?.mode === "single"
|
|
50
|
+
&& (!input.state.currentSessionId || snapshot.sessionId === input.state.currentSessionId)
|
|
51
|
+
&& (snapshot.state === "running" || snapshot.state === "queued")
|
|
52
|
+
&& snapshot.steps?.length === 1 && (input.index === undefined || input.index === 0)
|
|
53
|
+
&& (snapshot.steps[0]?.status === "running" || snapshot.steps[0]?.status === "pending")) {
|
|
54
|
+
const asks = input.findPendingAsks?.({ runId: snapshot.runId, agent: snapshot.steps[0].agent, childIndex: 0 }) ?? [];
|
|
55
|
+
if (asks.length > 0) {
|
|
56
|
+
const replies = asks.map(replyTo => `subagent_supervisor(${JSON.stringify({ action: "reply", replyTo, message: "<explicit answer>" })})`).join("\n");
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: `Steering not delivered or queued: async run ${snapshot.runId} is blocked on ${asks.length === 1 ? "a pending supervisor ask" : "ambiguous pending supervisor asks"}. No reply or recovery was attempted. Reply explicitly${asks.length > 1 ? " to the intended request ID" : ""}:\n${replies}` }],
|
|
59
|
+
isError: true,
|
|
60
|
+
details: { mode: "management", results: [] },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// The read-only snapshot precedes even stale-run reconciliation. An ask arriving
|
|
65
|
+
// later does not make a runner acknowledgement proof of consumption or unblocking.
|
|
47
66
|
const status = reconcileAsyncRun(asyncDir, { kill: input.kill }).status;
|
|
48
67
|
if (input.state.currentSessionId && status?.sessionId !== input.state.currentSessionId) {
|
|
49
68
|
return {
|
|
@@ -58,6 +58,9 @@ import { effectiveToolTimeoutMs, formatToolTimeoutMessage, resolveToolTimeoutMs,
|
|
|
58
58
|
import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
|
|
59
59
|
import { planCompletionEvidence } from "../shared/completion-evidence.ts";
|
|
60
60
|
import { planAbortRecovery } from "../shared/abort-recovery.ts";
|
|
61
|
+
import { planReadonlyModelContinuation, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
|
|
62
|
+
import { getReadonlySessionEvidence, requestReadonlySessionEvidence, type SettledReadonlyEvidence } from "../shared/readonly-session-evidence.ts";
|
|
63
|
+
import { getReadonlyChildModels } from "../shared/child-session.ts";
|
|
61
64
|
import { arbitrateCompletionGuardRescue } from "../shared/llm-intent-arbiter.ts";
|
|
62
65
|
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
63
66
|
import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
|
|
@@ -94,7 +97,7 @@ import {
|
|
|
94
97
|
} from "../shared/long-running-guard.ts";
|
|
95
98
|
import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport, validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
96
99
|
import { PROMPT_REDACTED } from "../../shared/utils.ts";
|
|
97
|
-
import { attachContractProjections,
|
|
100
|
+
import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
|
|
98
101
|
import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
|
|
99
102
|
import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
|
|
100
103
|
import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
@@ -109,7 +112,7 @@ import {
|
|
|
109
112
|
type ChildWatchdogStateSnapshot,
|
|
110
113
|
type ChildWatchdogStatusEvent,
|
|
111
114
|
} from "../../watchdog/child-status.ts";
|
|
112
|
-
import { buildInProcessChildLaunch } from "../shared/child-launch.ts";
|
|
115
|
+
import { buildInProcessChildLaunch, createReportedChildSessionInput } from "../shared/child-launch.ts";
|
|
113
116
|
import { childSessionFactory, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent } from "../shared/child-session.ts";
|
|
114
117
|
|
|
115
118
|
const artifactOutputByResult = new WeakMap<SingleResult, string>();
|
|
@@ -340,6 +343,7 @@ function structuredDelegationProgressChanged(
|
|
|
340
343
|
|
|
341
344
|
const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
|
|
342
345
|
type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
|
|
346
|
+
const settledReadonlySource = new WeakMap<SingleResult, ChildSession>();
|
|
343
347
|
|
|
344
348
|
const STOPPED_BEFORE_COMPLETION_ERROR = "Subagent stopped before completion.";
|
|
345
349
|
|
|
@@ -353,6 +357,7 @@ async function runSingleAttempt(
|
|
|
353
357
|
shared: {
|
|
354
358
|
sessionEnabled: boolean;
|
|
355
359
|
systemPrompt: string;
|
|
360
|
+
acceptancePrompt: string;
|
|
356
361
|
resolvedSkillNames?: string[];
|
|
357
362
|
modelCandidates?: string[];
|
|
358
363
|
skillsWarning?: string;
|
|
@@ -365,6 +370,9 @@ async function runSingleAttempt(
|
|
|
365
370
|
orcaProgressTab?: OrcaProgressTab;
|
|
366
371
|
launchWarnings: { emitted: boolean };
|
|
367
372
|
verifyModel: boolean;
|
|
373
|
+
readonlyExpected?: SettledReadonlyEvidence;
|
|
374
|
+
readonlyModel?: string;
|
|
375
|
+
readonlyHandoffAllowed?: () => boolean;
|
|
368
376
|
},
|
|
369
377
|
): Promise<SingleResult> {
|
|
370
378
|
const effectiveThinking = options.thinkingOverride ?? agent.thinking;
|
|
@@ -405,7 +413,8 @@ async function runSingleAttempt(
|
|
|
405
413
|
allowNestedSubagents: agent.allowNestedSubagents,
|
|
406
414
|
extensions: agent.extensions,
|
|
407
415
|
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
408
|
-
|
|
416
|
+
// Exact terminal protocol belongs to session resources, not compactable history.
|
|
417
|
+
systemPrompt: shared.acceptancePrompt ? `${shared.systemPrompt}\n${shared.acceptancePrompt}` : shared.systemPrompt,
|
|
409
418
|
mcpDirectTools: agent.mcpDirectTools,
|
|
410
419
|
cwd: options.cwd ?? runtimeCwd,
|
|
411
420
|
intercomSessionName: options.intercomSessionName,
|
|
@@ -425,7 +434,8 @@ async function runSingleAttempt(
|
|
|
425
434
|
permissionRules,
|
|
426
435
|
permissionAuditPath,
|
|
427
436
|
childWatchdog,
|
|
428
|
-
|
|
437
|
+
// registerChildWatchdog returns before reading the sink when no watchdog exists.
|
|
438
|
+
watchdogStatus: childWatchdog ? (event) => onWatchdogStatus?.(event) : undefined,
|
|
429
439
|
waitToolEnabled: options.waitToolEnabled,
|
|
430
440
|
waitToolDefaultTimeoutMs: options.waitToolDefaultTimeoutMs,
|
|
431
441
|
capabilityCeiling: options.capabilityCeiling,
|
|
@@ -780,7 +790,10 @@ async function runSingleAttempt(
|
|
|
780
790
|
// JSONL artifact flush is best effort.
|
|
781
791
|
});
|
|
782
792
|
// Report the run only after the child's extensions have shut down.
|
|
783
|
-
void Promise.resolve().then(() => session?.dispose()).catch(() => undefined).then(() =>
|
|
793
|
+
void Promise.resolve().then(() => session?.dispose()).catch(() => undefined).then(() => {
|
|
794
|
+
if (session && getReadonlySessionEvidence(session)) settledReadonlySource.set(result, session);
|
|
795
|
+
resolve(code);
|
|
796
|
+
});
|
|
784
797
|
};
|
|
785
798
|
|
|
786
799
|
const drainPendingControlEvents = (): ControlEvent[] | undefined => {
|
|
@@ -1105,6 +1118,10 @@ async function runSingleAttempt(
|
|
|
1105
1118
|
}
|
|
1106
1119
|
}
|
|
1107
1120
|
if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
|
|
1121
|
+
else if (hasToolCall && evt.message.stopReason === "toolUse") {
|
|
1122
|
+
// A recovered request can finish via a terminating tool, without a text stop.
|
|
1123
|
+
assistantError = undefined;
|
|
1124
|
+
}
|
|
1108
1125
|
const assistantText = extractTextFromContent(evt.message.content);
|
|
1109
1126
|
appendRecentOutput(progress, assistantText.split("\n").slice(-10));
|
|
1110
1127
|
// Final assistant message: start the settle drain window.
|
|
@@ -1352,12 +1369,10 @@ async function runSingleAttempt(
|
|
|
1352
1369
|
|
|
1353
1370
|
void (async () => {
|
|
1354
1371
|
try {
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
},
|
|
1360
|
-
});
|
|
1372
|
+
const input = createReportedChildSessionInput(launch, shared.transcriptWriter);
|
|
1373
|
+
requestReadonlySessionEvidence(input, shared.readonlyExpected);
|
|
1374
|
+
if (shared.readonlyHandoffAllowed && !shared.readonlyHandoffAllowed()) throw new Error("Read-only continuation handoff vetoed.");
|
|
1375
|
+
const created = await childSessions.create(input);
|
|
1361
1376
|
if (lifecycleFinished) {
|
|
1362
1377
|
void created.dispose();
|
|
1363
1378
|
return;
|
|
@@ -1369,6 +1384,10 @@ async function runSingleAttempt(
|
|
|
1369
1384
|
abortChild();
|
|
1370
1385
|
}
|
|
1371
1386
|
options.onChildSession?.({ steer: (text) => created.steer(text), followUp: (text) => created.followUp(text) });
|
|
1387
|
+
const actualReadonlyModel = shared.readonlyExpected && getReadonlyChildModels(created)?.current;
|
|
1388
|
+
if (shared.readonlyExpected && (!actualReadonlyModel || actualReadonlyModel.fullId !== shared.readonlyModel
|
|
1389
|
+
|| actualReadonlyModel.api !== shared.readonlyExpected.api || created.modelId !== shared.readonlyModel || abortedBySignal || interruptedByControl || result.timedOut
|
|
1390
|
+
|| !shared.readonlyHandoffAllowed?.())) throw new Error("Read-only continuation handoff vetoed.");
|
|
1372
1391
|
await created.prompt(`Task: ${task}`);
|
|
1373
1392
|
settle(undefined);
|
|
1374
1393
|
} catch (error) {
|
|
@@ -1497,7 +1516,7 @@ async function runSingleAttempt(
|
|
|
1497
1516
|
? `${timeoutMessage}\n\n${result.timeoutRecovery.message}\n\nPartial output before timeout:\n${fullOutput}`
|
|
1498
1517
|
: `${timeoutMessage}\n\n${result.timeoutRecovery.message}`;
|
|
1499
1518
|
}
|
|
1500
|
-
const completionGuardEnabled =
|
|
1519
|
+
const completionGuardEnabled = isAgentContract(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
|
|
1501
1520
|
const completionGuard = ((result.exitCode === 0 && !result.error) || toolAvailabilityError) && completionGuardEnabled
|
|
1502
1521
|
? evaluateCompletionMutationGuard({
|
|
1503
1522
|
agent: agent.name,
|
|
@@ -1537,7 +1556,7 @@ async function runSingleAttempt(
|
|
|
1537
1556
|
mutationAttemptObserved,
|
|
1538
1557
|
mutationEvidence,
|
|
1539
1558
|
arbiterRescued,
|
|
1540
|
-
|
|
1559
|
+
agentContractEnabled: isAgentContract(options.agentContract),
|
|
1541
1560
|
});
|
|
1542
1561
|
if (completionEvidence.fileMutation) {
|
|
1543
1562
|
result.effects = {
|
|
@@ -1733,7 +1752,7 @@ async function runSyncCompletionInner(
|
|
|
1733
1752
|
dynamicGroup: options.acceptanceContext?.dynamicGroup,
|
|
1734
1753
|
agentContract: options.agentContract,
|
|
1735
1754
|
});
|
|
1736
|
-
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional:
|
|
1755
|
+
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContract(options.agentContract), structuredOutput: Boolean(options.structuredOutput?.acceptanceReportPath) });
|
|
1737
1756
|
const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
|
|
1738
1757
|
options.onEffectivePrompt?.(taskWithAcceptance);
|
|
1739
1758
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
@@ -1884,19 +1903,33 @@ async function runSyncCompletionInner(
|
|
|
1884
1903
|
};
|
|
1885
1904
|
let lastResult: SingleResult | undefined;
|
|
1886
1905
|
const modelsToTry = candidates.length > 0 ? candidates : [undefined];
|
|
1887
|
-
let
|
|
1888
|
-
let
|
|
1906
|
+
let recoveryState: LogicalRecoveryState = "unused";
|
|
1907
|
+
let readonlyExpected: SettledReadonlyEvidence | undefined;
|
|
1908
|
+
let readonlyModel: string | undefined;
|
|
1909
|
+
let readonlySource: ChildSession | undefined;
|
|
1910
|
+
// Ordinary startup retries retain their per-attempt timeout. Only retained
|
|
1911
|
+
// continuation uses the original logical deadline, never a renewed allowance.
|
|
1912
|
+
const continuationDeadline = options.deadlineAt ?? (options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs);
|
|
1913
|
+
const readonlyHandoffAllowed = () => !options.signal?.aborted && !options.interruptSignal?.aborted
|
|
1914
|
+
&& !intercomDetached && !detachedReason && !options.workflowChildPermitLaunch
|
|
1915
|
+
&& options.usageBudget === undefined && options.toolBudget === undefined
|
|
1916
|
+
&& (continuationDeadline === undefined || Date.now() < continuationDeadline)
|
|
1917
|
+
&& (!readonlySource || getReadonlySessionEvidence(readonlySource) === readonlyExpected)
|
|
1918
|
+
&& !readonlySource?.detached && !readonlySource?.shutDown;
|
|
1919
|
+
let nextAttemptTask = task;
|
|
1889
1920
|
modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
|
|
1890
1921
|
const candidate = modelsToTry[modelIndex];
|
|
1891
1922
|
// The inner loop re-runs the same candidate at most once, for abort recovery.
|
|
1892
1923
|
for (;;) {
|
|
1893
|
-
const recoveringAbort =
|
|
1924
|
+
const recoveringAbort = recoveryState === "abort-recovery";
|
|
1894
1925
|
const attemptTask = nextAttemptTask;
|
|
1895
1926
|
const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
|
|
1896
1927
|
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
1928
|
+
if (recoveryState === "readonly-continuation") attemptOptions.deadlineAt = continuationDeadline;
|
|
1897
1929
|
const result = await runSingleAttempt(runtimeCwd, agent, attemptTask, candidate, attemptOptions, {
|
|
1898
1930
|
sessionEnabled,
|
|
1899
1931
|
systemPrompt,
|
|
1932
|
+
acceptancePrompt,
|
|
1900
1933
|
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
1901
1934
|
skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
|
|
1902
1935
|
jsonlPath,
|
|
@@ -1911,6 +1944,9 @@ async function runSyncCompletionInner(
|
|
|
1911
1944
|
orcaProgressTab,
|
|
1912
1945
|
launchWarnings,
|
|
1913
1946
|
verifyModel,
|
|
1947
|
+
readonlyExpected,
|
|
1948
|
+
readonlyModel,
|
|
1949
|
+
readonlyHandoffAllowed: readonlyExpected ? readonlyHandoffAllowed : undefined,
|
|
1914
1950
|
});
|
|
1915
1951
|
lastResult = result;
|
|
1916
1952
|
if (!recoveringAbort) {
|
|
@@ -1929,6 +1965,46 @@ async function runSyncCompletionInner(
|
|
|
1929
1965
|
usage: { ...result.usage },
|
|
1930
1966
|
};
|
|
1931
1967
|
modelAttempts.push(attempt);
|
|
1968
|
+
// A consumed retained continuation is terminal even on a startup error or abort.
|
|
1969
|
+
if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
|
|
1970
|
+
const source = settledReadonlySource.get(result);
|
|
1971
|
+
const evidence = source && getReadonlySessionEvidence(source);
|
|
1972
|
+
const models = source && getReadonlyChildModels(source);
|
|
1973
|
+
// Deny non-text retained inputs, including unknown blocks. Count bytes once,
|
|
1974
|
+
// not restored usage (the latter belongs to earlier attempts/runs).
|
|
1975
|
+
const textOnly = evidence && (JSON.parse(evidence.contextJson) as Array<{ role: string; content: unknown }>).every((message) =>
|
|
1976
|
+
typeof message.content === "string" || Array.isArray(message.content) && message.content.every((block) =>
|
|
1977
|
+
block?.type === "text" || message.role === "assistant" && block?.type === "toolCall"));
|
|
1978
|
+
const retainedBytes = evidence && models ? Buffer.byteLength(evidence.contextJson) + models.requestBytes : Infinity;
|
|
1979
|
+
const resolvedCandidates = modelsToTry.map((reference, index) => index === modelIndex ? models?.current : reference ? models?.resolve(reference) : undefined);
|
|
1980
|
+
const continuation = planReadonlyModelContinuation({
|
|
1981
|
+
source, recoveryState, currentIndex: modelIndex,
|
|
1982
|
+
candidates: resolvedCandidates.map((resolved, index) => {
|
|
1983
|
+
// A conservative byte ceiling includes serialized history, actual system
|
|
1984
|
+
// prompt/tools, framing/continuation headroom and full output allowance.
|
|
1985
|
+
const required = resolved?.maxTokens ? retainedBytes + 4096 + resolved.maxTokens : Infinity;
|
|
1986
|
+
return {
|
|
1987
|
+
resolved: resolved?.api ? { provider: resolved.provider, model: resolved.id, api: resolved.api } : undefined,
|
|
1988
|
+
tried: index <= modelIndex,
|
|
1989
|
+
compatibility: textOnly && resolved?.input?.includes("text") && (resolved.contextWindow ?? 0) >= required ? "compatible" as const : "unknown" as const,
|
|
1990
|
+
};
|
|
1991
|
+
}),
|
|
1992
|
+
lifecycleAllowsContinuation: !attemptSucceeded && readonlyHandoffAllowed() && !result.stopped && !result.detached && !result.interrupted && !result.timedOut,
|
|
1993
|
+
effectsAllowContinuation: !result.structuredOutputFailed && !result.toolBudgetBlocked && !result.progress?.currentTool
|
|
1994
|
+
&& !result.outputSaveError && (!result.effects?.fileMutation || result.effects.fileMutation.status === "not-applicable"),
|
|
1995
|
+
budget: options.toolBudget ? "tool-budget-configured" : options.usageBudget ? "unknown" : "unconfigured",
|
|
1996
|
+
knownContextOverflow: Boolean(result.contextOverflow || isContextOverflow(result.error)),
|
|
1997
|
+
});
|
|
1998
|
+
if (continuation.kind === "continue") {
|
|
1999
|
+
recoveryState = continuation.recoveryState; // consume BEFORE any sibling creation
|
|
2000
|
+
readonlyExpected = continuation.expected;
|
|
2001
|
+
readonlySource = source;
|
|
2002
|
+
readonlyModel = resolvedCandidates[continuation.candidateIndex]?.fullId;
|
|
2003
|
+
nextAttemptTask = continuation.prompt;
|
|
2004
|
+
attemptNotes.push(`[readonly-continuation] ${attempt.model} failed with HTTP 429 after read-only progress; continuing retained session once with ${readonlyModel}.`);
|
|
2005
|
+
modelIndex = continuation.candidateIndex - 1;
|
|
2006
|
+
continue modelAttemptsLoop;
|
|
2007
|
+
}
|
|
1932
2008
|
if (!attemptSucceeded) {
|
|
1933
2009
|
const afterCompactionSettlement = (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT];
|
|
1934
2010
|
const abortRecovery = planAbortRecovery({
|
|
@@ -1936,7 +2012,7 @@ async function runSyncCompletionInner(
|
|
|
1936
2012
|
error: result.error,
|
|
1937
2013
|
processSignal: result.processSignal,
|
|
1938
2014
|
sessionAvailable: Boolean(options.sessionFile && existsSync(options.sessionFile)),
|
|
1939
|
-
alreadyResumed:
|
|
2015
|
+
alreadyResumed: recoveryState !== "unused",
|
|
1940
2016
|
stopped: result.stopped || result.detached || options.signal?.aborted,
|
|
1941
2017
|
interrupted: result.interrupted || intercomDetached || options.interruptSignal?.aborted,
|
|
1942
2018
|
timedOut: result.timedOut,
|
|
@@ -1948,7 +2024,7 @@ async function runSyncCompletionInner(
|
|
|
1948
2024
|
afterCompactionSettlement,
|
|
1949
2025
|
});
|
|
1950
2026
|
if (abortRecovery.action === "resume") {
|
|
1951
|
-
|
|
2027
|
+
recoveryState = "abort-recovery";
|
|
1952
2028
|
nextAttemptTask = abortRecovery.prompt;
|
|
1953
2029
|
attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
|
|
1954
2030
|
continue;
|
|
@@ -2059,7 +2135,7 @@ async function runSyncCompletionInner(
|
|
|
2059
2135
|
? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only", durable: result.savedOutputPath !== undefined }
|
|
2060
2136
|
: undefined,
|
|
2061
2137
|
cwd: options.cwd ?? runtimeCwd,
|
|
2062
|
-
reportOptional:
|
|
2138
|
+
reportOptional: isAgentContract(options.agentContract),
|
|
2063
2139
|
artifactsDir: options.artifactsDir,
|
|
2064
2140
|
runId: options.runId,
|
|
2065
2141
|
watchdog: result.watchdog,
|
|
@@ -2071,7 +2147,7 @@ async function runSyncCompletionInner(
|
|
|
2071
2147
|
}
|
|
2072
2148
|
const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
|
|
2073
2149
|
stripAcceptanceReportsFromMessages(result.messages);
|
|
2074
|
-
if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.interrupted && !result.timedOut && !
|
|
2150
|
+
if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.interrupted && !result.timedOut && !isAgentContract(options.agentContract)) {
|
|
2075
2151
|
result.exitCode = 1;
|
|
2076
2152
|
if (result.savedOutputPath) {
|
|
2077
2153
|
result.finalOutput = finalizeSingleOutput({
|
|
@@ -2104,7 +2180,7 @@ async function runSyncCompletionInner(
|
|
|
2104
2180
|
result.progress.error = result.error;
|
|
2105
2181
|
}
|
|
2106
2182
|
}
|
|
2107
|
-
if (
|
|
2183
|
+
if (isAgentContract(options.agentContract)) attachContractProjections(result);
|
|
2108
2184
|
redactResultPrompt(result);
|
|
2109
2185
|
try {
|
|
2110
2186
|
persistResultMetadata(result);
|
|
@@ -2149,7 +2225,7 @@ export async function runSync(
|
|
|
2149
2225
|
): Promise<SingleResult> {
|
|
2150
2226
|
// Capture the strict contract before consumer-owned objects can be mutated
|
|
2151
2227
|
// after a detached receipt is published.
|
|
2152
|
-
const strictContract =
|
|
2228
|
+
const strictContract = isAgentContract(options.agentContract);
|
|
2153
2229
|
let detachedReason: string | undefined;
|
|
2154
2230
|
let publishedReceipt: SingleResult | undefined;
|
|
2155
2231
|
let activeDetachAttempt: ((reason?: string) => boolean) | undefined;
|
|
@@ -2247,13 +2323,13 @@ export async function runSync(
|
|
|
2247
2323
|
});
|
|
2248
2324
|
|
|
2249
2325
|
let terminalCallbackInvoked = false;
|
|
2250
|
-
void authoritativeCompletion.then((terminalResult) => {
|
|
2326
|
+
void authoritativeCompletion.then(async (terminalResult) => {
|
|
2251
2327
|
if (!detachedReason || terminalCallbackInvoked) return;
|
|
2252
2328
|
terminalCallbackInvoked = true;
|
|
2253
2329
|
terminalResult.detached = undefined;
|
|
2254
2330
|
terminalResult.detachedReason = detachedReason;
|
|
2255
2331
|
try {
|
|
2256
|
-
options.onDetachedExit?.(terminalResult);
|
|
2332
|
+
await options.onDetachedExit?.(terminalResult);
|
|
2257
2333
|
} catch {
|
|
2258
2334
|
// The authoritative result has settled. Consumer callback failures are
|
|
2259
2335
|
// contained here; each consumer owns cleanup through its own finally block.
|