pi-subagents 0.65.0 → 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 +87 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +17 -3
- package/docs/configuration.md +34 -0
- package/docs/extension-api.md +94 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +11 -5
- package/docs/workflows.md +22 -7
- package/package.json +4 -1
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/execution-controls.md +19 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- 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/preflight.ts +5 -1
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/config.ts +4 -2
- package/src/extension/index.ts +71 -4
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +9 -7
- package/src/extension/tool-description.ts +10 -5
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +102 -88
- package/src/intercom/supervisor-ui.ts +3 -2
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-execution.ts +8 -1
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-retention.ts +9 -0
- 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 +88 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +62 -33
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +46 -8
- 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 +430 -208
- 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 +116 -26
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +531 -227
- 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 +16 -3
- 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 +55 -24
- 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 +30 -20
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +41 -8
- package/src/runs/shared/nested-events.ts +8 -8
- 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 +3 -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-cleanup-plan.ts +6 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +403 -210
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/types.ts +89 -60
- package/src/shared/utils.ts +10 -2
- package/src/shared/watch-strategy.ts +2 -0
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +44 -15
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +70 -16
- package/src/workflows/workflow-checklist.ts +15 -18
- 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 => {
|
|
@@ -983,6 +996,20 @@ async function runSingleAttempt(
|
|
|
983
996
|
compactionStartedReceived = false;
|
|
984
997
|
afterCompactionSettlement = false;
|
|
985
998
|
}
|
|
999
|
+
if (evt.type === "agent_start") {
|
|
1000
|
+
const diagnostic = capture.toolDiagnostic();
|
|
1001
|
+
if (diagnostic) {
|
|
1002
|
+
const message = formatChildToolDiagnostic(diagnostic, { host: "parent" });
|
|
1003
|
+
toolAvailabilityError = message;
|
|
1004
|
+
result.error = message;
|
|
1005
|
+
result.finalOutput = message;
|
|
1006
|
+
progress.status = "failed";
|
|
1007
|
+
progress.error = message;
|
|
1008
|
+
fireUpdate();
|
|
1009
|
+
abortChild();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
986
1013
|
const lifecycleAction = projectChildLifecycle(evt, false, childLifecycleState);
|
|
987
1014
|
if (evt.type === "agent_settled" && lifecycleAction === "start-drain") {
|
|
988
1015
|
agentSettledReceived = true;
|
|
@@ -1086,11 +1113,15 @@ async function runSingleAttempt(
|
|
|
1086
1113
|
progress.model = evt.message.model;
|
|
1087
1114
|
if (!result.model) result.model = evt.message.model;
|
|
1088
1115
|
if (expectedModelForVerification && !hasToolCall) {
|
|
1089
|
-
const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, evt.message.model, options.availableModels);
|
|
1116
|
+
const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, evt.message.model, options.availableModels, options.modelResponseAliases);
|
|
1090
1117
|
if (modelVerificationError && !result.error) result.error = modelVerificationError;
|
|
1091
1118
|
}
|
|
1092
1119
|
}
|
|
1093
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
|
+
}
|
|
1094
1125
|
const assistantText = extractTextFromContent(evt.message.content);
|
|
1095
1126
|
appendRecentOutput(progress, assistantText.split("\n").slice(-10));
|
|
1096
1127
|
// Final assistant message: start the settle drain window.
|
|
@@ -1338,12 +1369,10 @@ async function runSingleAttempt(
|
|
|
1338
1369
|
|
|
1339
1370
|
void (async () => {
|
|
1340
1371
|
try {
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
},
|
|
1346
|
-
});
|
|
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);
|
|
1347
1376
|
if (lifecycleFinished) {
|
|
1348
1377
|
void created.dispose();
|
|
1349
1378
|
return;
|
|
@@ -1355,6 +1384,10 @@ async function runSingleAttempt(
|
|
|
1355
1384
|
abortChild();
|
|
1356
1385
|
}
|
|
1357
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.");
|
|
1358
1391
|
await created.prompt(`Task: ${task}`);
|
|
1359
1392
|
settle(undefined);
|
|
1360
1393
|
} catch (error) {
|
|
@@ -1483,7 +1516,7 @@ async function runSingleAttempt(
|
|
|
1483
1516
|
? `${timeoutMessage}\n\n${result.timeoutRecovery.message}\n\nPartial output before timeout:\n${fullOutput}`
|
|
1484
1517
|
: `${timeoutMessage}\n\n${result.timeoutRecovery.message}`;
|
|
1485
1518
|
}
|
|
1486
|
-
const completionGuardEnabled =
|
|
1519
|
+
const completionGuardEnabled = isAgentContract(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
|
|
1487
1520
|
const completionGuard = ((result.exitCode === 0 && !result.error) || toolAvailabilityError) && completionGuardEnabled
|
|
1488
1521
|
? evaluateCompletionMutationGuard({
|
|
1489
1522
|
agent: agent.name,
|
|
@@ -1523,7 +1556,7 @@ async function runSingleAttempt(
|
|
|
1523
1556
|
mutationAttemptObserved,
|
|
1524
1557
|
mutationEvidence,
|
|
1525
1558
|
arbiterRescued,
|
|
1526
|
-
|
|
1559
|
+
agentContractEnabled: isAgentContract(options.agentContract),
|
|
1527
1560
|
});
|
|
1528
1561
|
if (completionEvidence.fileMutation) {
|
|
1529
1562
|
result.effects = {
|
|
@@ -1719,7 +1752,7 @@ async function runSyncCompletionInner(
|
|
|
1719
1752
|
dynamicGroup: options.acceptanceContext?.dynamicGroup,
|
|
1720
1753
|
agentContract: options.agentContract,
|
|
1721
1754
|
});
|
|
1722
|
-
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional:
|
|
1755
|
+
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContract(options.agentContract), structuredOutput: Boolean(options.structuredOutput?.acceptanceReportPath) });
|
|
1723
1756
|
const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
|
|
1724
1757
|
options.onEffectivePrompt?.(taskWithAcceptance);
|
|
1725
1758
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
@@ -1870,19 +1903,33 @@ async function runSyncCompletionInner(
|
|
|
1870
1903
|
};
|
|
1871
1904
|
let lastResult: SingleResult | undefined;
|
|
1872
1905
|
const modelsToTry = candidates.length > 0 ? candidates : [undefined];
|
|
1873
|
-
let
|
|
1874
|
-
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;
|
|
1875
1920
|
modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
|
|
1876
1921
|
const candidate = modelsToTry[modelIndex];
|
|
1877
1922
|
// The inner loop re-runs the same candidate at most once, for abort recovery.
|
|
1878
1923
|
for (;;) {
|
|
1879
|
-
const recoveringAbort =
|
|
1924
|
+
const recoveringAbort = recoveryState === "abort-recovery";
|
|
1880
1925
|
const attemptTask = nextAttemptTask;
|
|
1881
1926
|
const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
|
|
1882
1927
|
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
1928
|
+
if (recoveryState === "readonly-continuation") attemptOptions.deadlineAt = continuationDeadline;
|
|
1883
1929
|
const result = await runSingleAttempt(runtimeCwd, agent, attemptTask, candidate, attemptOptions, {
|
|
1884
1930
|
sessionEnabled,
|
|
1885
1931
|
systemPrompt,
|
|
1932
|
+
acceptancePrompt,
|
|
1886
1933
|
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
1887
1934
|
skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
|
|
1888
1935
|
jsonlPath,
|
|
@@ -1897,6 +1944,9 @@ async function runSyncCompletionInner(
|
|
|
1897
1944
|
orcaProgressTab,
|
|
1898
1945
|
launchWarnings,
|
|
1899
1946
|
verifyModel,
|
|
1947
|
+
readonlyExpected,
|
|
1948
|
+
readonlyModel,
|
|
1949
|
+
readonlyHandoffAllowed: readonlyExpected ? readonlyHandoffAllowed : undefined,
|
|
1900
1950
|
});
|
|
1901
1951
|
lastResult = result;
|
|
1902
1952
|
if (!recoveringAbort) {
|
|
@@ -1915,6 +1965,46 @@ async function runSyncCompletionInner(
|
|
|
1915
1965
|
usage: { ...result.usage },
|
|
1916
1966
|
};
|
|
1917
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
|
+
}
|
|
1918
2008
|
if (!attemptSucceeded) {
|
|
1919
2009
|
const afterCompactionSettlement = (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT];
|
|
1920
2010
|
const abortRecovery = planAbortRecovery({
|
|
@@ -1922,7 +2012,7 @@ async function runSyncCompletionInner(
|
|
|
1922
2012
|
error: result.error,
|
|
1923
2013
|
processSignal: result.processSignal,
|
|
1924
2014
|
sessionAvailable: Boolean(options.sessionFile && existsSync(options.sessionFile)),
|
|
1925
|
-
alreadyResumed:
|
|
2015
|
+
alreadyResumed: recoveryState !== "unused",
|
|
1926
2016
|
stopped: result.stopped || result.detached || options.signal?.aborted,
|
|
1927
2017
|
interrupted: result.interrupted || intercomDetached || options.interruptSignal?.aborted,
|
|
1928
2018
|
timedOut: result.timedOut,
|
|
@@ -1934,7 +2024,7 @@ async function runSyncCompletionInner(
|
|
|
1934
2024
|
afterCompactionSettlement,
|
|
1935
2025
|
});
|
|
1936
2026
|
if (abortRecovery.action === "resume") {
|
|
1937
|
-
|
|
2027
|
+
recoveryState = "abort-recovery";
|
|
1938
2028
|
nextAttemptTask = abortRecovery.prompt;
|
|
1939
2029
|
attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
|
|
1940
2030
|
continue;
|
|
@@ -2045,7 +2135,7 @@ async function runSyncCompletionInner(
|
|
|
2045
2135
|
? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only", durable: result.savedOutputPath !== undefined }
|
|
2046
2136
|
: undefined,
|
|
2047
2137
|
cwd: options.cwd ?? runtimeCwd,
|
|
2048
|
-
reportOptional:
|
|
2138
|
+
reportOptional: isAgentContract(options.agentContract),
|
|
2049
2139
|
artifactsDir: options.artifactsDir,
|
|
2050
2140
|
runId: options.runId,
|
|
2051
2141
|
watchdog: result.watchdog,
|
|
@@ -2057,7 +2147,7 @@ async function runSyncCompletionInner(
|
|
|
2057
2147
|
}
|
|
2058
2148
|
const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
|
|
2059
2149
|
stripAcceptanceReportsFromMessages(result.messages);
|
|
2060
|
-
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)) {
|
|
2061
2151
|
result.exitCode = 1;
|
|
2062
2152
|
if (result.savedOutputPath) {
|
|
2063
2153
|
result.finalOutput = finalizeSingleOutput({
|
|
@@ -2090,7 +2180,7 @@ async function runSyncCompletionInner(
|
|
|
2090
2180
|
result.progress.error = result.error;
|
|
2091
2181
|
}
|
|
2092
2182
|
}
|
|
2093
|
-
if (
|
|
2183
|
+
if (isAgentContract(options.agentContract)) attachContractProjections(result);
|
|
2094
2184
|
redactResultPrompt(result);
|
|
2095
2185
|
try {
|
|
2096
2186
|
persistResultMetadata(result);
|
|
@@ -2135,7 +2225,7 @@ export async function runSync(
|
|
|
2135
2225
|
): Promise<SingleResult> {
|
|
2136
2226
|
// Capture the strict contract before consumer-owned objects can be mutated
|
|
2137
2227
|
// after a detached receipt is published.
|
|
2138
|
-
const strictContract =
|
|
2228
|
+
const strictContract = isAgentContract(options.agentContract);
|
|
2139
2229
|
let detachedReason: string | undefined;
|
|
2140
2230
|
let publishedReceipt: SingleResult | undefined;
|
|
2141
2231
|
let activeDetachAttempt: ((reason?: string) => boolean) | undefined;
|
|
@@ -2233,13 +2323,13 @@ export async function runSync(
|
|
|
2233
2323
|
});
|
|
2234
2324
|
|
|
2235
2325
|
let terminalCallbackInvoked = false;
|
|
2236
|
-
void authoritativeCompletion.then((terminalResult) => {
|
|
2326
|
+
void authoritativeCompletion.then(async (terminalResult) => {
|
|
2237
2327
|
if (!detachedReason || terminalCallbackInvoked) return;
|
|
2238
2328
|
terminalCallbackInvoked = true;
|
|
2239
2329
|
terminalResult.detached = undefined;
|
|
2240
2330
|
terminalResult.detachedReason = detachedReason;
|
|
2241
2331
|
try {
|
|
2242
|
-
options.onDetachedExit?.(terminalResult);
|
|
2332
|
+
await options.onDetachedExit?.(terminalResult);
|
|
2243
2333
|
} catch {
|
|
2244
2334
|
// The authoritative result has settled. Consumer callback failures are
|
|
2245
2335
|
// contained here; each consumer owns cleanup through its own finally block.
|
|
@@ -5,6 +5,7 @@ import { DIRS } from "../../shared/types.ts";
|
|
|
5
5
|
import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
6
6
|
import { utf8Tail } from "../../shared/utf8.ts";
|
|
7
7
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
8
|
+
import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
|
|
8
9
|
|
|
9
10
|
export const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
|
|
10
11
|
const HISTORY_VERSION = 1;
|
|
@@ -71,11 +72,12 @@ function isRestorableResumeContract(value: unknown): boolean {
|
|
|
71
72
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
72
73
|
try {
|
|
73
74
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > MAX_RESUME_CONTRACT_BYTES) return false;
|
|
75
|
+
validateModelResponseAliases((value as ForegroundResumeChild["resumeContract"])?.modelResponseAliases);
|
|
74
76
|
} catch {
|
|
75
77
|
return false;
|
|
76
78
|
}
|
|
77
79
|
const contract = value as NonNullable<ForegroundResumeChild["resumeContract"]>;
|
|
78
|
-
if (Object.keys(contract).some((key) => !["outputSchema", "agentContract", "acceptance", "output", "outputMode"].includes(key))) return false;
|
|
80
|
+
if (Object.keys(contract).some((key) => !["modelResponseAliases", "outputSchema", "agentContract", "acceptance", "output", "outputMode"].includes(key))) return false;
|
|
79
81
|
if (contract.outputSchema !== undefined && (!contract.outputSchema || typeof contract.outputSchema !== "object" || Array.isArray(contract.outputSchema))) return false;
|
|
80
82
|
if (contract.agentContract !== undefined && (!contract.agentContract || typeof contract.agentContract !== "object" || Array.isArray(contract.agentContract) || contract.agentContract.version !== 1)) return false;
|
|
81
83
|
if (validateAcceptanceInput(contract.acceptance).length > 0) return false;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Agent,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Agent, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
5
4
|
import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
|
|
6
5
|
import type { ForegroundRunControl } from "../../shared/types.ts";
|
|
7
6
|
export type PromptAuditView = "authored" | "runtime" | "effective";
|
|
@@ -21,7 +20,7 @@ export interface LivePromptAudit {
|
|
|
21
20
|
|
|
22
21
|
const livePrompts = new WeakMap<ForegroundRunControl, Map<number, LivePromptAudit>>();
|
|
23
22
|
|
|
24
|
-
type RegistryModel =
|
|
23
|
+
type RegistryModel = NonNullable<ExtensionContext["model"]>;
|
|
25
24
|
|
|
26
25
|
function fullModelId(model: Pick<RegistryModel, "provider" | "id">): string {
|
|
27
26
|
return `${model.provider}/${model.id}`;
|
|
@@ -64,6 +63,11 @@ export async function rewritePromptWithGuidance(input: {
|
|
|
64
63
|
}): Promise<string> {
|
|
65
64
|
const model = input.ctx.model;
|
|
66
65
|
if (!model) throw new Error("Prompt redo needs the current session model to rewrite the authored task.");
|
|
66
|
+
const [{ Agent }, { convertToLlm }, { streamSimple }] = await Promise.all([
|
|
67
|
+
import("@earendil-works/pi-agent-core"),
|
|
68
|
+
import("@earendil-works/pi-coding-agent"),
|
|
69
|
+
import("@earendil-works/pi-ai/compat"),
|
|
70
|
+
]);
|
|
67
71
|
const auth = await resolveRewriteAuth(input.ctx, model);
|
|
68
72
|
const registeredProvider = (input.ctx.modelRegistry as {
|
|
69
73
|
getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
|