pi-subagents 0.57.0 → 0.58.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 +39 -0
- package/docs/agents.md +13 -6
- package/docs/extension-api.md +39 -0
- package/docs/models.md +3 -3
- package/docs/tool-reference.md +3 -2
- package/docs/workflows.md +24 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +23 -0
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +38 -3
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +10 -2
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +26 -1
- package/src/extension/index.ts +15 -4
- package/src/extension/rpc.ts +35 -14
- package/src/extension/schemas.ts +2 -2
- package/src/extension/tool-description.ts +8 -4
- package/src/integrations/herdr-status.ts +5 -0
- package/src/runs/background/async-execution.ts +29 -12
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-status.ts +2 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-watcher.ts +27 -10
- package/src/runs/background/subagent-runner.ts +71 -21
- package/src/runs/foreground/execution.ts +20 -1
- package/src/runs/foreground/subagent-executor.ts +57 -26
- package/src/runs/foreground/workflow-detach-reconcile.ts +128 -17
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +12 -0
- package/src/runs/shared/model-fallback.ts +16 -4
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/formatters.ts +6 -0
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/types.ts +42 -1
- package/src/shared/utils.ts +6 -29
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/render.ts +14 -6
- package/src/workflows/scripted-workflow.ts +52 -5
- package/src/workflows/workflow-receipt.ts +39 -4
|
@@ -6,7 +6,7 @@ import { registerNativeSupervisorClient } from "../../intercom/native-supervisor
|
|
|
6
6
|
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
7
7
|
import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, PERMISSION_POLICY_ENV } from "./permissions.ts";
|
|
8
8
|
import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
|
|
9
|
-
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
9
|
+
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
10
10
|
import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
11
11
|
import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
12
12
|
import {
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, decodeToolBudgetEnv, shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
|
|
21
21
|
import type { JsonSchemaObject, ResolvedToolBudget, SubagentState } from "../../shared/types.ts";
|
|
22
22
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
23
|
-
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
23
|
+
import { getAgentDir, resolveWatchPath } from "../../shared/utils.ts";
|
|
24
24
|
import { registerChildWatchdog } from "../../watchdog/register-child.ts";
|
|
25
25
|
import { CHILD_WATCHDOG_CONFIG_ENV, decodeChildWatchdogConfig } from "../../watchdog/child-status.ts";
|
|
26
26
|
import { requestWatchdogPermission, type WatchdogPermissionRequest, type WatchdogPermissionResult } from "../../watchdog/permission-arbiter.ts";
|
|
@@ -68,7 +68,8 @@ const PARENT_ONLY_CUSTOM_MESSAGE_TYPES = new Set([
|
|
|
68
68
|
"subagent-control-notice",
|
|
69
69
|
]);
|
|
70
70
|
const SUBAGENT_ORCHESTRATION_SKILL_NAME_PATTERN = /<name>\s*pi-subagents\s*<\/name>/;
|
|
71
|
-
const
|
|
71
|
+
const PROJECT_CONTEXT_XML_HEADER = "\n\n<project_context>\n\n";
|
|
72
|
+
const PROJECT_CONTEXT_LEGACY_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
|
|
72
73
|
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
73
74
|
const DATE_HEADER = "\nCurrent date:";
|
|
74
75
|
|
|
@@ -148,10 +149,80 @@ function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
export function stripProjectContext(prompt: string): string {
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
const xmlStartIndex = prompt.indexOf(PROJECT_CONTEXT_XML_HEADER);
|
|
153
|
+
if (xmlStartIndex !== -1) {
|
|
154
|
+
const closingTag = "</project_context>";
|
|
155
|
+
const closingIndex = prompt.indexOf(closingTag, xmlStartIndex + PROJECT_CONTEXT_XML_HEADER.length);
|
|
156
|
+
if (closingIndex !== -1) {
|
|
157
|
+
return `${prompt.slice(0, xmlStartIndex)}${prompt.slice(closingIndex + closingTag.length)}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const legacyStartIndex = prompt.indexOf(PROJECT_CONTEXT_LEGACY_HEADER);
|
|
161
|
+
if (legacyStartIndex === -1) return prompt;
|
|
162
|
+
const endIndex = findSectionEnd(prompt, legacyStartIndex + PROJECT_CONTEXT_LEGACY_HEADER.length, [SKILLS_HEADER, DATE_HEADER]);
|
|
163
|
+
return `${prompt.slice(0, legacyStartIndex)}${prompt.slice(endIndex)}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const GLOBAL_CONTEXT_FILE_NAMES = new Set(["agents.md", "agents.override.md", "claude.md"]);
|
|
167
|
+
|
|
168
|
+
function canonicalDirectory(dir: string): string {
|
|
169
|
+
try {
|
|
170
|
+
return fs.realpathSync(dir);
|
|
171
|
+
} catch {
|
|
172
|
+
return path.resolve(dir);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function expandContextPath(filePath: string): string {
|
|
177
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
178
|
+
return filePath === "~"
|
|
179
|
+
? home ?? filePath
|
|
180
|
+
: /^~[\\/]/.test(filePath)
|
|
181
|
+
? path.join(home ?? "~", filePath.slice(2))
|
|
182
|
+
: filePath;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isContextFilePath(filePath: string): boolean {
|
|
186
|
+
return GLOBAL_CONTEXT_FILE_NAMES.has(path.basename(expandContextPath(filePath)).toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isGlobalContextFile(filePath: string): boolean {
|
|
190
|
+
const expanded = expandContextPath(filePath);
|
|
191
|
+
if (!isContextFilePath(filePath)) return false;
|
|
192
|
+
return canonicalDirectory(path.dirname(expanded)) === canonicalDirectory(getAgentDir());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function stripGlobalInstructionsFromXmlContext(context: string): string {
|
|
196
|
+
const block = /<project_instructions\s+path=(["'])(.*?)\1\s*>[\s\S]*?<\/project_instructions>\s*/gi;
|
|
197
|
+
return context.replace(block, (match, _quote: string, filePath: string) => isGlobalContextFile(filePath) ? "" : match);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stripGlobalInstructionsFromLegacyContext(context: string): string {
|
|
201
|
+
const sections = [...context.matchAll(/^## ([^\r\n]+)(?:\r?\n|$)/gm)]
|
|
202
|
+
.filter((match) => isContextFilePath(match[1]!.trim()));
|
|
203
|
+
let rewritten = "";
|
|
204
|
+
let cursor = 0;
|
|
205
|
+
for (let index = 0; index < sections.length; index++) {
|
|
206
|
+
const section = sections[index]!;
|
|
207
|
+
const start = section.index!;
|
|
208
|
+
const end = sections[index + 1]?.index ?? context.length;
|
|
209
|
+
rewritten += context.slice(cursor, start);
|
|
210
|
+
if (!isGlobalContextFile(section[1]!.trim())) rewritten += context.slice(start, end);
|
|
211
|
+
cursor = end;
|
|
212
|
+
}
|
|
213
|
+
return `${rewritten}${context.slice(cursor)}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function stripGlobalContext(prompt: string): string {
|
|
217
|
+
const rewrittenXml = prompt.replace(/<project_context>[\s\S]*?<\/project_context>/gi, (context) => {
|
|
218
|
+
const rewritten = stripGlobalInstructionsFromXmlContext(context);
|
|
219
|
+
return /<project_instructions\b/i.test(rewritten) ? rewritten : "";
|
|
220
|
+
});
|
|
221
|
+
const legacyStartIndex = rewrittenXml.indexOf(PROJECT_CONTEXT_LEGACY_HEADER);
|
|
222
|
+
if (legacyStartIndex === -1) return rewrittenXml;
|
|
223
|
+
const legacyEndIndex = findSectionEnd(rewrittenXml, legacyStartIndex + PROJECT_CONTEXT_LEGACY_HEADER.length, [SKILLS_HEADER, DATE_HEADER]);
|
|
224
|
+
const legacyContext = rewrittenXml.slice(legacyStartIndex, legacyEndIndex);
|
|
225
|
+
return `${rewrittenXml.slice(0, legacyStartIndex)}${stripGlobalInstructionsFromLegacyContext(legacyContext)}${rewrittenXml.slice(legacyEndIndex)}`;
|
|
155
226
|
}
|
|
156
227
|
|
|
157
228
|
export function stripInheritedSkills(prompt: string): string {
|
|
@@ -177,12 +248,15 @@ function stripChildBoundaryInstructions(prompt: string): string {
|
|
|
177
248
|
|
|
178
249
|
export function rewriteSubagentPrompt(
|
|
179
250
|
prompt: string,
|
|
180
|
-
options: { inheritProjectContext: boolean; inheritSkills: boolean; fanoutChild?: boolean },
|
|
251
|
+
options: { inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean; fanoutChild?: boolean },
|
|
181
252
|
): string {
|
|
182
253
|
let rewritten = prompt;
|
|
183
254
|
if (!options.inheritProjectContext) {
|
|
184
255
|
rewritten = stripProjectContext(rewritten);
|
|
185
256
|
}
|
|
257
|
+
if (!options.inheritGlobalContext) {
|
|
258
|
+
rewritten = stripGlobalContext(rewritten);
|
|
259
|
+
}
|
|
186
260
|
if (!options.inheritSkills) {
|
|
187
261
|
rewritten = stripInheritedSkills(rewritten);
|
|
188
262
|
}
|
|
@@ -688,12 +762,14 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
688
762
|
}
|
|
689
763
|
|
|
690
764
|
const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
|
|
765
|
+
const inheritGlobalContext = readBooleanEnv(SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV);
|
|
691
766
|
const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV);
|
|
692
767
|
const fanoutChild = readBooleanEnv(SUBAGENT_FANOUT_CHILD_ENV);
|
|
693
768
|
let rewritten = event.systemPrompt;
|
|
694
|
-
if (inheritProjectContext !== undefined || inheritSkills !== undefined || fanoutChild !== undefined) {
|
|
769
|
+
if (inheritProjectContext !== undefined || inheritGlobalContext !== undefined || inheritSkills !== undefined || fanoutChild !== undefined) {
|
|
695
770
|
rewritten = rewriteSubagentPrompt(event.systemPrompt, {
|
|
696
771
|
inheritProjectContext: inheritProjectContext ?? true,
|
|
772
|
+
inheritGlobalContext: inheritGlobalContext ?? true,
|
|
697
773
|
inheritSkills: inheritSkills ?? true,
|
|
698
774
|
fanoutChild: fanoutChild === true,
|
|
699
775
|
});
|
package/src/shared/formatters.ts
CHANGED
|
@@ -22,6 +22,12 @@ export function formatTokenUsage(usage: TokenUsage, legacyLabel = "tok"): string
|
|
|
22
22
|
? `${formatTokens(usage.window)} window · ${formatTokens(usage.total)} spent`
|
|
23
23
|
: `${formatTokens(usage.total)} ${legacyLabel}`;
|
|
24
24
|
}
|
|
25
|
+
export function formatContextUsage(usage: Pick<TokenUsage, "window" | "windowPeak">, contextLimit: number): string | undefined {
|
|
26
|
+
if (usage.window === undefined || !Number.isFinite(usage.window) || !Number.isFinite(contextLimit) || contextLimit <= 0) return undefined;
|
|
27
|
+
const peak = usage.windowPeak !== undefined && Number.isFinite(usage.windowPeak) ? usage.windowPeak : usage.window;
|
|
28
|
+
const used = Math.max(0, usage.window, peak);
|
|
29
|
+
return `ctx ${formatTokens(used)}/${formatTokens(contextLimit)} (${Math.round((used / contextLimit) * 100)}%)`;
|
|
30
|
+
}
|
|
25
31
|
|
|
26
32
|
export function formatModelThinking(model?: string, thinking?: string): string {
|
|
27
33
|
const parsed = model ? splitKnownThinkingSuffix(model) : undefined;
|
|
@@ -43,6 +43,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
43
43
|
systemPrompt: agent.systemPrompt,
|
|
44
44
|
systemPromptMode: agent.systemPromptMode,
|
|
45
45
|
inheritProjectContext: agent.inheritProjectContext,
|
|
46
|
+
inheritGlobalContext: agent.inheritGlobalContext,
|
|
46
47
|
inheritSkills: agent.inheritSkills,
|
|
47
48
|
model: agent.model,
|
|
48
49
|
modelProvider: agent.modelProvider,
|
|
@@ -53,6 +54,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
53
54
|
mcpDirectTools: agent.mcpDirectTools,
|
|
54
55
|
extensions: agent.extensions,
|
|
55
56
|
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
57
|
+
mutationTools: agent.mutationTools,
|
|
56
58
|
skills: agent.skills,
|
|
57
59
|
skillPath: agent.skillPath,
|
|
58
60
|
output: agent.output,
|
|
@@ -87,6 +89,7 @@ export interface LaunchBindingInput {
|
|
|
87
89
|
systemPrompt?: string | null;
|
|
88
90
|
systemPromptMode?: AgentConfig["systemPromptMode"];
|
|
89
91
|
inheritProjectContext: boolean;
|
|
92
|
+
inheritGlobalContext: boolean;
|
|
90
93
|
inheritSkills: boolean;
|
|
91
94
|
skills?: string[];
|
|
92
95
|
tools?: string[];
|
|
@@ -113,6 +116,7 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
|
|
|
113
116
|
systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : stableJsonDigest(input.systemPrompt),
|
|
114
117
|
systemPromptMode: input.systemPromptMode,
|
|
115
118
|
inheritProjectContext: input.inheritProjectContext,
|
|
119
|
+
inheritGlobalContext: input.inheritGlobalContext,
|
|
116
120
|
inheritSkills: input.inheritSkills,
|
|
117
121
|
skills: input.skills,
|
|
118
122
|
tools: input.tools,
|
package/src/shared/types.ts
CHANGED
|
@@ -76,6 +76,20 @@ export interface WorkflowGraphSnapshot {
|
|
|
76
76
|
|
|
77
77
|
export type WorkflowReceiptState = "complete" | "failed" | "paused" | "stopped";
|
|
78
78
|
|
|
79
|
+
export type WorkflowTerminalResolution = "settled-awaiting-resume" | "failed-child" | "interrupted-child";
|
|
80
|
+
|
|
81
|
+
export interface WorkflowTerminalOutcome {
|
|
82
|
+
state: "partial";
|
|
83
|
+
reason: "budget_exhausted" | "timeout";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface WorkflowRecoveryAction {
|
|
87
|
+
key: string;
|
|
88
|
+
call: "runs.run";
|
|
89
|
+
resume: { workflowRunId: string; key: string; latest: true };
|
|
90
|
+
taskRequired: true;
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
export interface WorkflowChildSummaryV1 {
|
|
80
94
|
version: 1;
|
|
81
95
|
parentToolCallId: string;
|
|
@@ -98,6 +112,7 @@ type WorkflowReceiptEntryResumability =
|
|
|
98
112
|
|
|
99
113
|
export type WorkflowReceiptEntry = WorkflowReceiptEntryResumability & {
|
|
100
114
|
key: string;
|
|
115
|
+
terminalOutcome?: WorkflowTerminalOutcome;
|
|
101
116
|
agent?: string;
|
|
102
117
|
requestedContext?: "fresh" | "fork";
|
|
103
118
|
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
@@ -113,6 +128,9 @@ export interface WorkflowReceipt {
|
|
|
113
128
|
createdAt: number;
|
|
114
129
|
entries: Record<string, WorkflowReceiptEntry>;
|
|
115
130
|
workflowChildren?: WorkflowChildSummaryV1;
|
|
131
|
+
workflowResolution?: WorkflowTerminalResolution;
|
|
132
|
+
terminalOutcome?: WorkflowTerminalOutcome;
|
|
133
|
+
recovery?: WorkflowRecoveryAction[];
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
export interface SavedOutputReference {
|
|
@@ -375,8 +393,24 @@ export interface FileMutationEffect {
|
|
|
375
393
|
evidence?: TrackedMutationEvidence;
|
|
376
394
|
}
|
|
377
395
|
|
|
396
|
+
export interface SettlementDiagnostic {
|
|
397
|
+
finalTextPresent: boolean;
|
|
398
|
+
mutation: {
|
|
399
|
+
expected: boolean;
|
|
400
|
+
attempted: boolean;
|
|
401
|
+
observed: boolean;
|
|
402
|
+
};
|
|
403
|
+
requiredOutput?: {
|
|
404
|
+
kind: "file-only" | "structured";
|
|
405
|
+
path: string;
|
|
406
|
+
missing: boolean;
|
|
407
|
+
};
|
|
408
|
+
afterCompactionSettlement?: boolean;
|
|
409
|
+
}
|
|
410
|
+
|
|
378
411
|
export interface EffectsProjection {
|
|
379
412
|
fileMutation?: FileMutationEffect;
|
|
413
|
+
settlementDiagnostic?: SettlementDiagnostic;
|
|
380
414
|
}
|
|
381
415
|
|
|
382
416
|
export interface TrackedMutationSnapshot {
|
|
@@ -605,9 +639,11 @@ export interface SteeringRecoveryDescriptor {
|
|
|
605
639
|
extensions?: string[];
|
|
606
640
|
subagentOnlyExtensions?: string[];
|
|
607
641
|
mcpDirectTools?: string[];
|
|
642
|
+
mutationTools?: string[];
|
|
608
643
|
systemPrompt?: string;
|
|
609
644
|
systemPromptMode: "append" | "replace";
|
|
610
645
|
inheritProjectContext: boolean;
|
|
646
|
+
inheritGlobalContext: boolean;
|
|
611
647
|
inheritSkills: boolean;
|
|
612
648
|
skills?: string[];
|
|
613
649
|
skillPath?: string[];
|
|
@@ -1622,6 +1658,7 @@ export interface AsyncStatus {
|
|
|
1622
1658
|
skills?: string[];
|
|
1623
1659
|
model?: string;
|
|
1624
1660
|
thinking?: string;
|
|
1661
|
+
contextLimit?: number;
|
|
1625
1662
|
thinkingCeiling?: ThinkingLevel;
|
|
1626
1663
|
attemptedModels?: string[];
|
|
1627
1664
|
modelAttempts?: ModelAttempt[];
|
|
@@ -2027,6 +2064,8 @@ export interface RunSyncOptions {
|
|
|
2027
2064
|
/** Resolved launch context for this child. */
|
|
2028
2065
|
context?: "fresh" | "fork";
|
|
2029
2066
|
cwd?: string;
|
|
2067
|
+
/** Original cwd input retained for launch diagnostics. */
|
|
2068
|
+
requestedCwd?: string;
|
|
2030
2069
|
signal?: AbortSignal;
|
|
2031
2070
|
interruptSignal?: AbortSignal;
|
|
2032
2071
|
timeoutMs?: number;
|
|
@@ -2085,9 +2124,11 @@ export interface RunSyncOptions {
|
|
|
2085
2124
|
/** Package-internal one-use authorization for one foreground workflow child. */
|
|
2086
2125
|
workflowChildPermitLaunch?: WorkflowChildPermitContext;
|
|
2087
2126
|
/** Registry models available for heuristic bare-model resolution */
|
|
2088
|
-
availableModels?: Array<{ provider: string; id: string; fullId: string }>;
|
|
2127
|
+
availableModels?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
|
|
2089
2128
|
/** Current parent-session provider to prefer for ambiguous bare model ids */
|
|
2090
2129
|
preferredModelProvider?: string;
|
|
2130
|
+
/** Parent Pi event host used to snapshot runtime-registered MCP servers before child launch. */
|
|
2131
|
+
runtimeSnapshotHost?: import("../runs/shared/mcp-direct-tool-allowlist.ts").McpRuntimeSnapshotHost;
|
|
2091
2132
|
/** Optional subagent model-scope enforcement for fallback candidates */
|
|
2092
2133
|
modelScope?: ModelScopeRule | ModelScopeRule[];
|
|
2093
2134
|
/** Skills to make available (overrides agent default if provided) */
|
package/src/shared/utils.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* General utility functions for the subagent extension
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import * as fs from "node:fs";
|
|
6
2
|
import * as os from "node:os";
|
|
7
3
|
import * as path from "node:path";
|
|
@@ -10,10 +6,6 @@ import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "./
|
|
|
10
6
|
import { formatToolCall } from "./formatters.ts";
|
|
11
7
|
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
|
|
12
8
|
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// File System Utilities
|
|
15
|
-
// ============================================================================
|
|
16
|
-
|
|
17
9
|
const DEFAULT_CONFIG_DIR_NAME = ".pi";
|
|
18
10
|
const PI_CODING_AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
19
11
|
export const PI_CODING_AGENT_PACKAGE_ROOT_ENV = "PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT";
|
|
@@ -96,9 +88,10 @@ export function getProjectConfigDir(projectRoot: string): string {
|
|
|
96
88
|
|
|
97
89
|
export function getAgentDir(): string {
|
|
98
90
|
const configured = process.env.PI_CODING_AGENT_DIR;
|
|
99
|
-
|
|
100
|
-
if (configured
|
|
101
|
-
|
|
91
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
92
|
+
if (configured === "~") return home;
|
|
93
|
+
if (configured?.startsWith("~/") || configured?.startsWith("~\\")) return path.join(home, configured.slice(2));
|
|
94
|
+
return configured || path.join(home, getConfigDirName(), "agent");
|
|
102
95
|
}
|
|
103
96
|
|
|
104
97
|
const statusCache = new Map<string, { mtime: number; ctime: number; size: number; ino: number; status: AsyncStatus }>();
|
|
@@ -253,10 +246,7 @@ function getOutputTail(outputFile: string | undefined, maxLines: number = 3): st
|
|
|
253
246
|
}
|
|
254
247
|
}
|
|
255
248
|
|
|
256
|
-
|
|
257
|
-
* Get human-readable last activity time for a file
|
|
258
|
-
*/
|
|
259
|
-
export function getLastActivity(outputFile: string | undefined): string {
|
|
249
|
+
export function getLastActivity(outputFile: string | undefined): string {
|
|
260
250
|
if (!outputFile) return "";
|
|
261
251
|
try {
|
|
262
252
|
const stat = fs.statSync(outputFile);
|
|
@@ -265,14 +255,11 @@ function getOutputTail(outputFile: string | undefined, maxLines: number = 3): st
|
|
|
265
255
|
if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
|
|
266
256
|
return `active ${Math.floor(ago / 60000)}m ago`;
|
|
267
257
|
} catch {
|
|
268
|
-
// Last-activity text is best effort; missing files should
|
|
258
|
+
// Last-activity text is best effort; missing files should omit the hint.
|
|
269
259
|
return "";
|
|
270
260
|
}
|
|
271
261
|
}
|
|
272
262
|
|
|
273
|
-
/**
|
|
274
|
-
* Find the latest session file in a directory
|
|
275
|
-
*/
|
|
276
263
|
export function findLatestSessionFile(sessionDir: string): string | null {
|
|
277
264
|
if (!fs.existsSync(sessionDir)) return null;
|
|
278
265
|
const files = fs.readdirSync(sessionDir)
|
|
@@ -289,9 +276,6 @@ export function findLatestSessionFile(sessionDir: string): string | null {
|
|
|
289
276
|
return latest ? latest.path : null;
|
|
290
277
|
}
|
|
291
278
|
|
|
292
|
-
/**
|
|
293
|
-
* Write a prompt to a temporary file
|
|
294
|
-
*/
|
|
295
279
|
function writePrompt(agent: string, prompt: string): { dir: string; path: string } {
|
|
296
280
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
297
281
|
const p = path.join(dir, `${agent.replace(/[^\w.-]/g, "_")}.md`);
|
|
@@ -299,13 +283,6 @@ function writePrompt(agent: string, prompt: string): { dir: string; path: string
|
|
|
299
283
|
return { dir, path: p };
|
|
300
284
|
}
|
|
301
285
|
|
|
302
|
-
// ============================================================================
|
|
303
|
-
// Message Parsing Utilities
|
|
304
|
-
// ============================================================================
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Get the final text output from a list of messages
|
|
308
|
-
*/
|
|
309
286
|
export function getFinalOutput(messages: Message[]): string {
|
|
310
287
|
const validTextParts: string[] = [];
|
|
311
288
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -89,6 +89,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
89
89
|
...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
|
|
90
90
|
systemPromptMode: agent.systemPromptMode,
|
|
91
91
|
inheritProjectContext: agent.inheritProjectContext,
|
|
92
|
+
inheritGlobalContext: agent.inheritGlobalContext,
|
|
92
93
|
inheritSkills: agent.inheritSkills,
|
|
93
94
|
...(agent.defaultContext !== undefined ? { defaultContext: agent.defaultContext } : {}),
|
|
94
95
|
...(agent.acceptanceRole !== undefined ? { acceptanceRole: agent.acceptanceRole } : {}),
|
|
@@ -98,6 +99,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
98
99
|
...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
|
|
99
100
|
...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
|
|
100
101
|
...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
|
|
102
|
+
...(agent.mutationTools !== undefined ? { mutationTools: [...agent.mutationTools] } : {}),
|
|
101
103
|
...(agent.completionGuard !== undefined ? { completionGuard: agent.completionGuard } : {}),
|
|
102
104
|
...(agent.toolBudget !== undefined ? { toolBudget: agent.toolBudget } : {}),
|
|
103
105
|
};
|
|
@@ -189,6 +191,7 @@ function metadataFor(agent: AgentConfig): string {
|
|
|
189
191
|
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
190
192
|
lines.push(`System prompt mode: ${agent.systemPromptMode}`);
|
|
191
193
|
lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
|
|
194
|
+
lines.push(`Inherit global context: ${agent.inheritGlobalContext ? "true" : "false"}`);
|
|
192
195
|
lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
|
|
193
196
|
if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
|
|
194
197
|
if (agent.output) lines.push(`Output: ${agent.output}`);
|
package/src/tui/render.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "../shared/types.ts";
|
|
24
24
|
import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
25
25
|
import { FLEET_OPEN_SHORTCUT, formatShortcutLabel } from "../shared/shortcuts.ts";
|
|
26
|
-
import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
|
|
26
|
+
import { formatContextUsage, formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
|
|
27
27
|
import { getDisplayItems, getSingleResultOutput, PROMPT_REDACTED } from "../shared/utils.ts";
|
|
28
28
|
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
29
29
|
import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
|
|
@@ -267,9 +267,13 @@ function oneLine(text: string): string {
|
|
|
267
267
|
const COMPACT_TASK_MAX_CHARS = 96;
|
|
268
268
|
|
|
269
269
|
export function compactTaskText(task: string | undefined, label?: string): string | undefined {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const
|
|
270
|
+
const taskText = task?.trim();
|
|
271
|
+
const labelText = label?.trim();
|
|
272
|
+
const normalizedTask = taskText && taskText !== PROMPT_REDACTED ? oneLine(taskText) : "";
|
|
273
|
+
const normalizedLabel = labelText && labelText !== PROMPT_REDACTED ? oneLine(labelText) : "";
|
|
274
|
+
const normalized = normalizedLabel && normalizedTask && normalizedLabel !== normalizedTask
|
|
275
|
+
? `${normalizedLabel} — ${normalizedTask}`
|
|
276
|
+
: normalizedLabel || normalizedTask;
|
|
273
277
|
if (!normalized) return undefined;
|
|
274
278
|
return previewDisplayText(normalized, COMPACT_TASK_MAX_CHARS);
|
|
275
279
|
}
|
|
@@ -794,7 +798,7 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
|
|
|
794
798
|
const activity = widgetStepActivity(step, job.updatedAt);
|
|
795
799
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
796
800
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
797
|
-
const label = compactTaskText(
|
|
801
|
+
const label = compactTaskText(step.description, step.label);
|
|
798
802
|
const display = label ? `${label} (${step.agent})` : step.agent;
|
|
799
803
|
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${display} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
800
804
|
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
|
|
@@ -1083,7 +1087,11 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
|
|
|
1083
1087
|
return statJoin(theme, [
|
|
1084
1088
|
step.turnCount !== undefined ? `${step.turnCount} turns` : "",
|
|
1085
1089
|
step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
|
|
1086
|
-
step.tokens
|
|
1090
|
+
step.tokens
|
|
1091
|
+
? step.contextLimit !== undefined
|
|
1092
|
+
? formatContextUsage(step.tokens, step.contextLimit) ?? formatTokenUsage(step.tokens, "token")
|
|
1093
|
+
: step.tokens.total ? formatTokenUsage(step.tokens, "token") : ""
|
|
1094
|
+
: "",
|
|
1087
1095
|
step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
|
|
1088
1096
|
]);
|
|
1089
1097
|
}
|
|
@@ -654,6 +654,7 @@ parentPort.on("message", async (message) => {
|
|
|
654
654
|
export interface WorkflowScriptChildResult {
|
|
655
655
|
key: string;
|
|
656
656
|
ok: boolean;
|
|
657
|
+
terminalOutcome?: import("../shared/types.ts").WorkflowTerminalOutcome;
|
|
657
658
|
stopped?: boolean;
|
|
658
659
|
/** Canonical child agent name when launch resolution produced one. */
|
|
659
660
|
agent?: string;
|
|
@@ -666,6 +667,7 @@ export interface WorkflowScriptChildResult {
|
|
|
666
667
|
requestedContext?: "fresh" | "fork";
|
|
667
668
|
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
668
669
|
outputReference?: string;
|
|
670
|
+
outputPathMapping?: { requestedPath: string; savedPath: string };
|
|
669
671
|
externalAdapter?: import("../shared/types.ts").ExternalCliReceiptMetadata;
|
|
670
672
|
resumability?: { state: "resumable" } | { state: "not-resumable"; reason: string };
|
|
671
673
|
continuation?: { runIds: string[] };
|
|
@@ -722,9 +724,9 @@ export interface WorkflowScriptResult {
|
|
|
722
724
|
|
|
723
725
|
export class WorkflowScriptError extends Error {
|
|
724
726
|
readonly partial: Omit<WorkflowScriptResult, "value">;
|
|
725
|
-
readonly errorKind?: "detached-child";
|
|
727
|
+
readonly errorKind?: "detached-child" | "timeout";
|
|
726
728
|
|
|
727
|
-
constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child") {
|
|
729
|
+
constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child" | "timeout") {
|
|
728
730
|
super(message);
|
|
729
731
|
this.name = "WorkflowScriptError";
|
|
730
732
|
this.partial = partial;
|
|
@@ -1109,6 +1111,41 @@ function resolveWorkflowParserEntry(): string {
|
|
|
1109
1111
|
}
|
|
1110
1112
|
}
|
|
1111
1113
|
|
|
1114
|
+
const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "turnBudget", "worktree"] as const;
|
|
1115
|
+
|
|
1116
|
+
function isZeroUsage(usage: unknown): boolean {
|
|
1117
|
+
if (!isRecord(usage)) return false;
|
|
1118
|
+
const cost = usage.cost;
|
|
1119
|
+
return (usage.input ?? 0) === 0
|
|
1120
|
+
&& (usage.output ?? 0) === 0
|
|
1121
|
+
&& (usage.cacheRead ?? 0) === 0
|
|
1122
|
+
&& (usage.cacheWrite ?? 0) === 0
|
|
1123
|
+
&& (!isRecord(cost) || (cost.total ?? 0) === 0);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function setupAbortResumeParams(params: Record<string, unknown>, result: WorkflowScriptChildResult, signal: AbortSignal): Record<string, unknown> | undefined {
|
|
1127
|
+
if (signal.aborted || result.ok || result.stopped || result.interrupted || !result.runId) return undefined;
|
|
1128
|
+
const childResult = Array.isArray(result.results) && result.results.length === 1 && isRecord(result.results[0]) ? result.results[0] : undefined;
|
|
1129
|
+
const error = typeof childResult?.error === "string" ? childResult.error : result.error;
|
|
1130
|
+
if (error !== "This operation was aborted" || !isZeroUsage(childResult?.usage)) return undefined;
|
|
1131
|
+
const messages = Array.isArray(childResult?.messages) ? childResult.messages : [];
|
|
1132
|
+
const message = messages.findLast((entry) => isRecord(entry) && entry.role === "assistant");
|
|
1133
|
+
if (message !== undefined) {
|
|
1134
|
+
if (!isRecord(message)) return undefined;
|
|
1135
|
+
if (message.stopReason !== "error" || message.errorMessage !== error) return undefined;
|
|
1136
|
+
if (!Array.isArray(message.content) || message.content.length > 0 || !isZeroUsage(message.usage)) return undefined;
|
|
1137
|
+
if (Object.hasOwn(message, "diagnostics") || Object.hasOwn(message, "responseId")) return undefined;
|
|
1138
|
+
}
|
|
1139
|
+
const task = typeof params.task === "string" && params.task.trim() ? params.task.trim() : "Continue after the setup abort.";
|
|
1140
|
+
const resumeParams: Record<string, unknown> = { resume: result.runId, task };
|
|
1141
|
+
for (const key of AUTO_RESUME_PARAM_KEYS) {
|
|
1142
|
+
if (Object.hasOwn(params, key)) resumeParams[key] = params[key];
|
|
1143
|
+
}
|
|
1144
|
+
resumeParams.resume = result.runId;
|
|
1145
|
+
resumeParams.task = task;
|
|
1146
|
+
return resumeParams;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1112
1149
|
export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
|
|
1113
1150
|
if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
|
|
1114
1151
|
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
|
|
@@ -1194,7 +1231,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1194
1231
|
return unobservedSteers.length > 0 ? new Error(`workflowScript completed with unawaited runs.steer call(s): ${unobservedSteers.map((key) => `'${key}'`).join(", ")}. Await or return each call.`) : undefined;
|
|
1195
1232
|
})()
|
|
1196
1233
|
: undefined;
|
|
1197
|
-
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child"
|
|
1234
|
+
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" || outcome.error.workflowErrorKind === "timeout" ? outcome.error.workflowErrorKind : undefined));
|
|
1198
1235
|
else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
|
|
1199
1236
|
else resolve({ value: outcome.value, ...partial() });
|
|
1200
1237
|
});
|
|
@@ -1225,7 +1262,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1225
1262
|
};
|
|
1226
1263
|
const timer = options.timeoutMs === undefined
|
|
1227
1264
|
? undefined
|
|
1228
|
-
: setTimeout(() =>
|
|
1265
|
+
: setTimeout(() => {
|
|
1266
|
+
const error = new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) as Error & { workflowErrorKind: "timeout" };
|
|
1267
|
+
error.workflowErrorKind = "timeout";
|
|
1268
|
+
finish({ error });
|
|
1269
|
+
}, options.timeoutMs);
|
|
1229
1270
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1230
1271
|
if (options.signal?.aborted) return onAbort();
|
|
1231
1272
|
|
|
@@ -1487,7 +1528,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1487
1528
|
if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
|
|
1488
1529
|
}
|
|
1489
1530
|
const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
|
|
1490
|
-
|
|
1531
|
+
const result = await options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1532
|
+
const autoResumeParams = setupAbortResumeParams(params, result, childSignal);
|
|
1533
|
+
if (!autoResumeParams) return result;
|
|
1534
|
+
resolvedResumeLineage = [...new Set([...(resolvedResumeLineage ?? []), result.runId!])];
|
|
1535
|
+
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(autoResumeParams), phase: "auto-resume", runId: result.runId });
|
|
1536
|
+
traceChanged();
|
|
1537
|
+
return options.launch(key, autoResumeParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1491
1538
|
}).then((result) => {
|
|
1492
1539
|
let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
1493
1540
|
if (resolvedResumeLineage?.length && normalized.runId) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
4
|
-
import type { ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState } from "../shared/types.ts";
|
|
4
|
+
import type { ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
|
|
5
5
|
import type { WorkflowReceiptResumeReference, WorkflowScriptChildResult } from "./scripted-workflow.ts";
|
|
6
6
|
import { parseWorkflowChildSummary } from "./workflow-child-summary.ts";
|
|
7
7
|
|
|
@@ -34,6 +34,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
34
34
|
state: WorkflowReceiptState;
|
|
35
35
|
children: WorkflowScriptChildResult[];
|
|
36
36
|
workflowChildren?: WorkflowReceipt["workflowChildren"];
|
|
37
|
+
terminalOutcome?: WorkflowTerminalOutcome;
|
|
37
38
|
createdAt?: number;
|
|
38
39
|
}): WorkflowReceipt {
|
|
39
40
|
const workflowRunId = assertSafeRunId(input.workflowRunId, "workflowRunId");
|
|
@@ -48,6 +49,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
48
49
|
if (resumability.state === "resumable" && !latestRunId) throw new Error(`Workflow receipt child '${key}' is resumable but has no retained run id.`);
|
|
49
50
|
const base = {
|
|
50
51
|
key,
|
|
52
|
+
...(child.terminalOutcome ? { terminalOutcome: child.terminalOutcome } : {}),
|
|
51
53
|
...(child.agent ? { agent: child.agent } : {}),
|
|
52
54
|
...(child.requestedContext ? { requestedContext: child.requestedContext } : {}),
|
|
53
55
|
...(child.resolvedContext ? { resolvedContext: child.resolvedContext } : {}),
|
|
@@ -59,7 +61,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
59
61
|
? { ...base, latestRunId: latestRunId!, resumability }
|
|
60
62
|
: { ...base, ...(latestRunId ? { latestRunId } : {}), resumability };
|
|
61
63
|
}
|
|
62
|
-
return { version: WORKFLOW_RECEIPT_VERSION, workflowRunId, state: input.state, createdAt: input.createdAt ?? Date.now(), entries, ...(input.workflowChildren ? { workflowChildren: input.workflowChildren } : {}) };
|
|
64
|
+
return { version: WORKFLOW_RECEIPT_VERSION, workflowRunId, state: input.state, createdAt: input.createdAt ?? Date.now(), entries, ...(input.workflowChildren ? { workflowChildren: input.workflowChildren } : {}), ...(input.terminalOutcome ? { terminalOutcome: input.terminalOutcome } : {}) };
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export function writeWorkflowReceipt(asyncDir: string, receipt: WorkflowReceipt): string {
|
|
@@ -170,6 +172,14 @@ function parseExternalCliReceiptMetadata(value: unknown, key: string, source: st
|
|
|
170
172
|
return value as ExternalCliReceiptMetadata;
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
function parseTerminalOutcome(value: unknown, label: string): WorkflowTerminalOutcome | undefined {
|
|
176
|
+
if (value === undefined) return undefined;
|
|
177
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
178
|
+
const outcome = value as Record<string, unknown>;
|
|
179
|
+
if (outcome.state !== "partial" || (outcome.reason !== "budget_exhausted" && outcome.reason !== "timeout")) throw new Error(`${label} is invalid.`);
|
|
180
|
+
return { state: "partial", reason: outcome.reason };
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
function parseEntry(value: unknown, key: string, source: string): WorkflowReceiptEntry {
|
|
174
184
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' must be an object.`);
|
|
175
185
|
const entry = value as Record<string, unknown>;
|
|
@@ -190,8 +200,30 @@ function parseEntry(value: unknown, key: string, source: string): WorkflowReceip
|
|
|
190
200
|
const reason = (resumability as Record<string, unknown>).reason;
|
|
191
201
|
if (state === "resumable" && latestRunId === undefined) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' resumable entry has no retained run id.`);
|
|
192
202
|
if (state === "not-resumable" && (typeof reason !== "string" || !reason.trim())) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' non-resumable reason is missing.`);
|
|
203
|
+
const terminalOutcome = parseTerminalOutcome(entry.terminalOutcome, `Invalid workflow receipt '${source}': entry '${key}' terminalOutcome`);
|
|
193
204
|
parseExternalCliReceiptMetadata(entry.externalAdapter, key, source);
|
|
194
|
-
return value as WorkflowReceiptEntry;
|
|
205
|
+
return { ...(value as WorkflowReceiptEntry), ...(terminalOutcome ? { terminalOutcome } : {}) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseWorkflowResolution(value: unknown, source: string): WorkflowTerminalResolution | undefined {
|
|
209
|
+
if (value === undefined) return undefined;
|
|
210
|
+
if (value !== "settled-awaiting-resume" && value !== "failed-child" && value !== "interrupted-child") throw new Error(`Invalid workflow receipt '${source}': workflowResolution is invalid.`);
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseRecovery(value: unknown, workflowRunId: string, entries: Record<string, WorkflowReceiptEntry>, source: string): WorkflowRecoveryAction[] | undefined {
|
|
215
|
+
if (value === undefined) return undefined;
|
|
216
|
+
if (!Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': recovery must be an array.`);
|
|
217
|
+
return value.map((item, index) => {
|
|
218
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error(`Invalid workflow receipt '${source}': recovery[${index}] must be an object.`);
|
|
219
|
+
const action = item as Record<string, unknown>;
|
|
220
|
+
const key = action.key;
|
|
221
|
+
const resume = action.resume;
|
|
222
|
+
if (typeof key !== "string" || !entries[key] || action.call !== "runs.run" || action.taskRequired !== true || !resume || typeof resume !== "object" || Array.isArray(resume)) throw new Error(`Invalid workflow receipt '${source}': recovery[${index}] is invalid.`);
|
|
223
|
+
const reference = resume as Record<string, unknown>;
|
|
224
|
+
if (reference.workflowRunId !== workflowRunId || reference.key !== key || reference.latest !== true || entries[key].resumability.state !== "resumable") throw new Error(`Invalid workflow receipt '${source}': recovery[${index}] does not identify a resumable entry.`);
|
|
225
|
+
return { key, call: "runs.run", resume: { workflowRunId, key, latest: true }, taskRequired: true };
|
|
226
|
+
});
|
|
195
227
|
}
|
|
196
228
|
|
|
197
229
|
export function readWorkflowReceipt(asyncDirRoot: string, workflowRunId: string): WorkflowReceipt {
|
|
@@ -222,7 +254,10 @@ export function readWorkflowReceipt(asyncDirRoot: string, workflowRunId: string)
|
|
|
222
254
|
for (const [key, entry] of Object.entries(receipt.entries as Record<string, unknown>)) entries[assertKey(key, "workflow receipt key")] = parseEntry(entry, key, receiptPath);
|
|
223
255
|
const workflowChildren = parseWorkflowChildSummary(receipt.workflowChildren);
|
|
224
256
|
if (workflowChildren && workflowChildren.workflowRunId !== workflowRunId) throw new Error(`Workflow receipt '${receiptPath}' is stale: workflowChildren.workflowRunId does not match.`);
|
|
225
|
-
|
|
257
|
+
const workflowResolution = parseWorkflowResolution(receipt.workflowResolution, receiptPath);
|
|
258
|
+
const terminalOutcome = parseTerminalOutcome(receipt.terminalOutcome, `Invalid workflow receipt '${receiptPath}': terminalOutcome`);
|
|
259
|
+
const recovery = parseRecovery(receipt.recovery, workflowRunId, entries, receiptPath);
|
|
260
|
+
return { version: 1, workflowRunId, state: receipt.state, createdAt: receipt.createdAt, entries, ...(workflowChildren ? { workflowChildren } : {}), ...(workflowResolution ? { workflowResolution } : {}), ...(terminalOutcome ? { terminalOutcome } : {}), ...(recovery ? { recovery } : {}) };
|
|
226
261
|
}
|
|
227
262
|
|
|
228
263
|
export function resolveWorkflowReceiptResumeEntry(input: {
|