pi-subagents 0.45.2 → 0.46.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 +24 -0
- package/README.md +2 -0
- package/docs/agents.md +342 -0
- package/docs/configuration.md +320 -0
- package/docs/extension-api.md +308 -0
- package/docs/missions.md +117 -0
- package/docs/models.md +190 -0
- package/docs/observability.md +174 -0
- package/docs/tool-reference.md +343 -0
- package/docs/watchdog.md +176 -0
- package/docs/workflows.md +163 -0
- package/package.json +4 -2
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/src/agents/agents.ts +17 -8
- package/src/agents/frontmatter.ts +7 -3
- package/src/agents/skills.ts +2 -9
- package/src/api/project-panes.ts +30 -0
- package/src/extension/config.ts +15 -1
- package/src/extension/index.ts +36 -16
- package/src/extension/schemas.ts +3 -2
- package/src/extension/subagent-guide.ts +39 -0
- package/src/extension/tool-description.ts +4 -4
- package/src/inspectors/herdr/project-panes.ts +457 -62
- package/src/missions/actions.ts +25 -2
- package/src/missions/lifecycle.ts +21 -2
- package/src/missions/store.ts +77 -1
- package/src/missions/types.ts +33 -0
- package/src/runs/background/async-execution.ts +7 -1
- package/src/runs/background/completion-replay.ts +267 -0
- package/src/runs/background/result-watcher.ts +12 -4
- package/src/runs/background/wait-completions.ts +39 -5
- package/src/runs/background/wait-subscriptions.ts +18 -3
- package/src/runs/foreground/execution.ts +4 -0
- package/src/runs/foreground/foreground-history.ts +137 -0
- package/src/runs/foreground/subagent-executor.ts +310 -44
- package/src/shared/fork-context.ts +13 -0
- package/src/shared/prompt-resources.ts +51 -0
- package/src/shared/types.ts +30 -1
- package/src/shared/utf8.ts +11 -0
- package/src/slash/prompt-workflows.ts +2 -15
- package/src/slash/slash-commands.ts +19 -1
- package/src/tui/fleet-status.ts +8 -2
- package/src/tui/fleet.ts +135 -25
- package/src/tui/render.ts +120 -7
- package/src/workflows/scripted-workflow.ts +167 -10
package/src/tui/render.ts
CHANGED
|
@@ -202,6 +202,118 @@ function getToolCallLines(
|
|
|
202
202
|
return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
const ansiEscapePattern = /\x1b\[[0-9;]*m/g;
|
|
206
|
+
const noisyStatusPatterns = [
|
|
207
|
+
/^(?:i|we)\s+(?:will|need|can|should|am|are)\b/i,
|
|
208
|
+
/^i(?:'m|’m| am)\b/i,
|
|
209
|
+
/\bso i (?:will|need|can)\b/i,
|
|
210
|
+
/^(?:checking|fetching|reading|inspecting|verifying|collecting|confirming|polling)\b/i,
|
|
211
|
+
/^(?:async\s+subagent\s+)?[\w.-]+\s*·\s*(?:step|agent)\s+\d+\/\d+\s*·/i,
|
|
212
|
+
/^(?:Step|Agent)\s+\d+\/\d+:\s+[\w.-]+\s*·\s*(?:running|queued|pending|complete|completed)\b/i,
|
|
213
|
+
/^Press\s+\S+\s+for\s+live\s+detail$/i,
|
|
214
|
+
/^output:\s+.+\/async-subagent-runs\//i,
|
|
215
|
+
];
|
|
216
|
+
const liveOutputWordSignalPattern = /\b(?:access denied|denied|error|exception|fail(?:ed|ure)?|fatal|panic|rejected|timeout|timed out|unable|warning)\b/i;
|
|
217
|
+
const liveOutputCodeSignalPattern = /\bE[A-Z0-9_]{2,}\b/;
|
|
218
|
+
|
|
219
|
+
function oneLine(text: string): string {
|
|
220
|
+
return text.replace(ansiEscapePattern, "").replace(/\s+/g, " ").trim();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function hasLiveOutputSignal(line: string): boolean {
|
|
224
|
+
const clean = oneLine(line);
|
|
225
|
+
return liveOutputWordSignalPattern.test(clean) || liveOutputCodeSignalPattern.test(clean);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isNoisyStatusLine(line: string): boolean {
|
|
229
|
+
const clean = oneLine(line);
|
|
230
|
+
return clean.length > 0
|
|
231
|
+
&& clean.length <= 240
|
|
232
|
+
&& !hasLiveOutputSignal(clean)
|
|
233
|
+
&& noisyStatusPatterns.some((pattern) => pattern.test(clean));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function latestActivityText(line: string): string {
|
|
237
|
+
return oneLine(line)
|
|
238
|
+
.replace(/^i (?:will|can|need to|am going to)\s+/i, "")
|
|
239
|
+
.replace(/^i(?:'m|’m| am)\s+/i, "");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function progressUpdateSummary(lines: string[]): string {
|
|
243
|
+
const counts = new Map<string, number>();
|
|
244
|
+
for (const line of lines) counts.set(line.toLowerCase(), (counts.get(line.toLowerCase()) ?? 0) + 1);
|
|
245
|
+
const exactRepeatCount = Math.max(...counts.values());
|
|
246
|
+
const latest = latestActivityText(lines[lines.length - 1]!);
|
|
247
|
+
const repeat = exactRepeatCount > 1 ? ` · repeated ${exactRepeatCount}×` : "";
|
|
248
|
+
return `↻ ${lines.length} progress updates${repeat} · latest: ${latest}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function compactRecentOutputLines(recentOutput: string[] | undefined): string[] {
|
|
252
|
+
const lines: string[] = [];
|
|
253
|
+
const noisyLines: string[] = [];
|
|
254
|
+
const otherLines: string[] = [];
|
|
255
|
+
for (const rawLine of recentOutput ?? []) {
|
|
256
|
+
const line = oneLine(rawLine);
|
|
257
|
+
if (!line || line === "(running...)") continue;
|
|
258
|
+
lines.push(line);
|
|
259
|
+
(isNoisyStatusLine(line) ? noisyLines : otherLines).push(line);
|
|
260
|
+
}
|
|
261
|
+
if (noisyLines.length >= 4 && !otherLines.some(hasLiveOutputSignal)) {
|
|
262
|
+
if (otherLines.length === 0) {
|
|
263
|
+
return [
|
|
264
|
+
progressUpdateSummary(noisyLines),
|
|
265
|
+
"pattern: repeated short status lines",
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
const visibleTail = otherLines.slice(-3);
|
|
269
|
+
const hiddenSignals = otherLines.slice(0, -3).filter(hasLiveOutputSignal);
|
|
270
|
+
return [
|
|
271
|
+
progressUpdateSummary(noisyLines),
|
|
272
|
+
...(hiddenSignals.length > 0 ? [`… ${hiddenSignals.length} older signal ${hiddenSignals.length === 1 ? "line" : "lines"}: ${hiddenSignals.at(-1)}`] : []),
|
|
273
|
+
...visibleTail,
|
|
274
|
+
].slice(0, 5);
|
|
275
|
+
}
|
|
276
|
+
if (lines.length <= 5) return lines;
|
|
277
|
+
|
|
278
|
+
const tail = lines.slice(-5);
|
|
279
|
+
const hiddenSignals = lines.slice(0, -5).filter(hasLiveOutputSignal);
|
|
280
|
+
if (hiddenSignals.length === 0) return tail;
|
|
281
|
+
return [
|
|
282
|
+
`… ${hiddenSignals.length} older signal ${hiddenSignals.length === 1 ? "line" : "lines"}: ${hiddenSignals.at(-1)}`,
|
|
283
|
+
...lines.slice(-4),
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function compactWorkflowError(error: string): string {
|
|
288
|
+
const outputMatch = error.match(/(?:^|\n)Output:\s*([\s\S]+)/);
|
|
289
|
+
if (!outputMatch) return oneLine(error);
|
|
290
|
+
const prefix = oneLine(error.slice(0, outputMatch.index)).replace(/:$/, "") || "Failed";
|
|
291
|
+
const outputLines = outputMatch[1]!.split(/\r?\n/).map(oneLine).filter(Boolean);
|
|
292
|
+
const allOutputLinesAreNoisy = outputLines.length > 0 && outputLines.every(isNoisyStatusLine);
|
|
293
|
+
const latest = outputLines.at(-1);
|
|
294
|
+
return allOutputLinesAreNoisy && latest
|
|
295
|
+
? `${prefix} · latest: ${latestActivityText(latest)}`
|
|
296
|
+
: `${prefix} · ${oneLine(outputMatch[1] ?? "")}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const WORKFLOW_LIVE_ROW_LIMIT = 8;
|
|
300
|
+
|
|
301
|
+
function visibleWorkflowRows(rows: WorkflowChatProgressRow[]): { rows: WorkflowChatProgressRow[]; hiddenRows: number } {
|
|
302
|
+
if (rows.length <= WORKFLOW_LIVE_ROW_LIMIT) return { rows, hiddenRows: 0 };
|
|
303
|
+
const selected = new Set<string>();
|
|
304
|
+
const add = (row: WorkflowChatProgressRow): void => {
|
|
305
|
+
if (selected.size >= WORKFLOW_LIVE_ROW_LIMIT || selected.has(row.key)) return;
|
|
306
|
+
selected.add(row.key);
|
|
307
|
+
};
|
|
308
|
+
for (const row of [...rows].reverse()) {
|
|
309
|
+
if (row.state === "failed") add(row);
|
|
310
|
+
}
|
|
311
|
+
for (const row of [...rows].reverse()) add(row);
|
|
312
|
+
return {
|
|
313
|
+
rows: rows.filter((row) => selected.has(row.key)),
|
|
314
|
+
hiddenRows: rows.length - selected.size,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
205
317
|
|
|
206
318
|
function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStartedAt" | "durationMs" | "lastActivityAt">): number | undefined {
|
|
207
319
|
if (progress.currentToolStartedAt !== undefined && progress.durationMs !== undefined) return progress.currentToolStartedAt + progress.durationMs;
|
|
@@ -1038,7 +1150,7 @@ function foregroundStyleWidgetStepLines(
|
|
|
1038
1150
|
const argsPreview = tool.args.length <= maxArgsLen ? tool.args : `${tool.args.slice(0, maxArgsLen)}...`;
|
|
1039
1151
|
lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
|
|
1040
1152
|
}
|
|
1041
|
-
for (const line of step.recentOutput
|
|
1153
|
+
for (const line of compactRecentOutputLines(step.recentOutput)) {
|
|
1042
1154
|
lines.push(` ${theme.fg("dim", line)}`);
|
|
1043
1155
|
}
|
|
1044
1156
|
}
|
|
@@ -1513,12 +1625,14 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
|
|
|
1513
1625
|
c.addChild(new Text(truncLine(theme.fg("dim", " ◦ waiting for workflow child launches"), width), 0, 0));
|
|
1514
1626
|
return c;
|
|
1515
1627
|
}
|
|
1516
|
-
|
|
1628
|
+
const visible = visibleWorkflowRows(rows);
|
|
1629
|
+
if (visible.hiddenRows > 0) c.addChild(new Text(truncLine(theme.fg("dim", ` … ${visible.hiddenRows} older workflow rows hidden`), width), 0, 0));
|
|
1630
|
+
for (const row of visible.rows) {
|
|
1517
1631
|
const status = workflowRowStateLabel(row, theme);
|
|
1518
|
-
const label = row.label && row.label !== row.key ? ` ${row.label}` : "";
|
|
1632
|
+
const label = row.label && row.label !== row.key ? ` ${oneLine(row.label)}` : "";
|
|
1519
1633
|
const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
|
|
1520
1634
|
const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
|
|
1521
|
-
const error = row.error ? ` ${theme.fg("error", `· ${row.error}`)}` : "";
|
|
1635
|
+
const error = row.error ? ` ${theme.fg("error", `· ${compactWorkflowError(row.error)}`)}` : "";
|
|
1522
1636
|
c.addChild(new Text(truncLine(` ${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
|
|
1523
1637
|
}
|
|
1524
1638
|
if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", ` Emits ${workflow.emits.length}`), width), 0, 0));
|
|
@@ -1762,7 +1876,7 @@ export function renderSubagentResult(
|
|
|
1762
1876
|
c.addChild(new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0));
|
|
1763
1877
|
}
|
|
1764
1878
|
}
|
|
1765
|
-
for (const line of (r.progress.recentOutput
|
|
1879
|
+
for (const line of compactRecentOutputLines(r.progress.recentOutput)) {
|
|
1766
1880
|
c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
|
|
1767
1881
|
}
|
|
1768
1882
|
if (toolLine || liveStatusLine || r.progress.recentTools?.length || r.progress.recentOutput?.length || r.artifactPaths) {
|
|
@@ -1995,8 +2109,7 @@ export function renderSubagentResult(
|
|
|
1995
2109
|
c.addChild(new Text(fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)), 0, 0));
|
|
1996
2110
|
}
|
|
1997
2111
|
}
|
|
1998
|
-
const
|
|
1999
|
-
for (const line of recentLines) {
|
|
2112
|
+
for (const line of compactRecentOutputLines(rProg.recentOutput)) {
|
|
2000
2113
|
c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
|
|
2001
2114
|
}
|
|
2002
2115
|
}
|
|
@@ -10,6 +10,9 @@ const { inspect } = require("node:util");
|
|
|
10
10
|
let nextCallId = 0;
|
|
11
11
|
const pending = new Map();
|
|
12
12
|
const runKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
13
|
+
const trackedPromisePatch = Symbol("trackedPromisePatch");
|
|
14
|
+
const trackedPromiseObservations = new WeakMap();
|
|
15
|
+
let suppressRunObservation = 0;
|
|
13
16
|
|
|
14
17
|
function stableRunJson(value) {
|
|
15
18
|
if (Array.isArray(value)) return "[" + value.map(stableRunJson).join(",") + "]";
|
|
@@ -17,12 +20,124 @@ function stableRunJson(value) {
|
|
|
17
20
|
return JSON.stringify(value) ?? "undefined";
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
function isDirectWorkflowScriptPromiseHandlerCall() {
|
|
24
|
+
const stack = new Error().stack;
|
|
25
|
+
if (typeof stack !== "string") return false;
|
|
26
|
+
const frame = stack.split("\n").slice(1).map((line) => line.trim()).find((line) =>
|
|
27
|
+
line &&
|
|
28
|
+
!line.includes("isDirectWorkflowScriptPromiseHandlerCall") &&
|
|
29
|
+
!line.includes("markObserved") &&
|
|
30
|
+
!line.includes("promise.then") &&
|
|
31
|
+
!line.includes("promise.catch") &&
|
|
32
|
+
!line.includes("promise.finally")
|
|
33
|
+
);
|
|
34
|
+
return frame ? frame.includes("workflow-script.js") && !frame.includes("at async ") : false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mergeObservations(...groups) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const merged = [];
|
|
40
|
+
for (const group of groups) {
|
|
41
|
+
for (const observation of group) {
|
|
42
|
+
if (!observation || typeof observation.callId !== "number" || typeof observation.key !== "string" || seen.has(observation.callId)) continue;
|
|
43
|
+
seen.add(observation.callId);
|
|
44
|
+
merged.push(observation);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function trackedObservations(value) {
|
|
51
|
+
return value && (typeof value === "object" || typeof value === "function") ? trackedPromiseObservations.get(value) ?? [] : [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function withSuppressedRunObservation(callback) {
|
|
55
|
+
suppressRunObservation += 1;
|
|
56
|
+
try {
|
|
57
|
+
return callback();
|
|
58
|
+
} finally {
|
|
59
|
+
suppressRunObservation -= 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function trackRunObservation(observations, promise) {
|
|
64
|
+
const merged = mergeObservations(trackedObservations(promise), observations);
|
|
65
|
+
if (merged.length === 0 || !promise || typeof promise.then !== "function") return promise;
|
|
66
|
+
trackedPromiseObservations.set(promise, merged);
|
|
67
|
+
if (promise[trackedPromisePatch]) return promise;
|
|
68
|
+
|
|
69
|
+
const observedCallIds = new Set();
|
|
70
|
+
const markObserved = () => {
|
|
71
|
+
if (suppressRunObservation > 0 || isDirectWorkflowScriptPromiseHandlerCall()) return;
|
|
72
|
+
for (const observation of trackedObservations(promise)) {
|
|
73
|
+
if (observedCallIds.has(observation.callId)) continue;
|
|
74
|
+
observedCallIds.add(observation.callId);
|
|
75
|
+
parentPort.postMessage({ type: "runObserved", callId: observation.callId, key: observation.key });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const originalThen = promise.then.bind(promise);
|
|
79
|
+
const originalCatch = promise.catch.bind(promise);
|
|
80
|
+
const originalFinally = promise.finally.bind(promise);
|
|
81
|
+
Object.defineProperty(promise, trackedPromisePatch, { value: true });
|
|
82
|
+
promise.then = (onFulfilled, onRejected) => {
|
|
83
|
+
markObserved();
|
|
84
|
+
return trackRunObservation(trackedObservations(promise), originalThen(onFulfilled, onRejected));
|
|
85
|
+
};
|
|
86
|
+
promise.catch = (onRejected) => {
|
|
87
|
+
markObserved();
|
|
88
|
+
return trackRunObservation(trackedObservations(promise), originalCatch(onRejected));
|
|
89
|
+
};
|
|
90
|
+
promise.finally = (onFinally) => {
|
|
91
|
+
markObserved();
|
|
92
|
+
return trackRunObservation(trackedObservations(promise), originalFinally(onFinally));
|
|
93
|
+
};
|
|
94
|
+
return promise;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function trackPromiseCombinator(items, createPromise) {
|
|
98
|
+
const values = Array.from(items);
|
|
99
|
+
const observations = mergeObservations(...values.map(trackedObservations));
|
|
100
|
+
const promise = withSuppressedRunObservation(() => createPromise(values));
|
|
101
|
+
return observations.length > 0 ? trackRunObservation(observations, promise) : promise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const workflowPromise = new Proxy(Promise, {
|
|
105
|
+
construct(target, args) {
|
|
106
|
+
return new target(...args);
|
|
107
|
+
},
|
|
108
|
+
get(target, prop) {
|
|
109
|
+
if (prop === "all") return (items) => trackPromiseCombinator(items, (values) => target.all(values));
|
|
110
|
+
if (prop === "allSettled") return (items) => trackPromiseCombinator(items, (values) => target.allSettled(values));
|
|
111
|
+
if (prop === "race") return (items) => trackPromiseCombinator(items, (values) => target.race(values));
|
|
112
|
+
if (prop === "any") return (items) => trackPromiseCombinator(items, (values) => target.any(values));
|
|
113
|
+
if (prop === "resolve") return (value) => {
|
|
114
|
+
const observations = trackedObservations(value);
|
|
115
|
+
const promise = withSuppressedRunObservation(() => target.resolve(value));
|
|
116
|
+
return observations.length > 0 ? trackRunObservation(observations, promise) : promise;
|
|
117
|
+
};
|
|
118
|
+
const value = target[prop];
|
|
119
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function hostCall(method, args, observation) {
|
|
124
|
+
const callId = ++nextCallId;
|
|
125
|
+
const promise = new Promise((resolve, reject) => {
|
|
23
126
|
pending.set(callId, { resolve, reject });
|
|
24
127
|
parentPort.postMessage({ type: "call", callId, method, args });
|
|
25
128
|
});
|
|
129
|
+
return observation && typeof observation.key === "string"
|
|
130
|
+
? trackRunObservation([{ key: observation.key, callId }], promise)
|
|
131
|
+
: promise;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function runHostCall(key, params, collectFailure) {
|
|
135
|
+
const callId = ++nextCallId;
|
|
136
|
+
const promise = new Promise((resolve, reject) => {
|
|
137
|
+
pending.set(callId, { resolve, reject });
|
|
138
|
+
parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}) } });
|
|
139
|
+
});
|
|
140
|
+
return { key, callId, promise };
|
|
26
141
|
}
|
|
27
142
|
|
|
28
143
|
function formatRef(result) {
|
|
@@ -58,7 +173,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
58
173
|
const runs = Object.freeze({
|
|
59
174
|
run(key, params) {
|
|
60
175
|
validateRunCall(key, params, "runs.run", runFingerprints);
|
|
61
|
-
return hostCall("run", { key, params });
|
|
176
|
+
return hostCall("run", { key, params }, { key });
|
|
62
177
|
},
|
|
63
178
|
all(items) {
|
|
64
179
|
if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
|
|
@@ -73,7 +188,8 @@ const runs = Object.freeze({
|
|
|
73
188
|
calls.push({ key, params });
|
|
74
189
|
}
|
|
75
190
|
for (const { key, params } of calls) runFingerprints.set(key, stableRunJson(params));
|
|
76
|
-
|
|
191
|
+
const launched = calls.map(({ key, params }) => runHostCall(key, params, true));
|
|
192
|
+
return trackRunObservation(launched.map(({ key, callId }) => ({ key, callId })), Promise.all(launched.map(({ promise }) => promise)));
|
|
77
193
|
},
|
|
78
194
|
status(keyOrRunId) { return hostCall("status", { keyOrRunId }); },
|
|
79
195
|
ref: formatRef,
|
|
@@ -97,6 +213,14 @@ const state = Object.freeze({
|
|
|
97
213
|
},
|
|
98
214
|
});
|
|
99
215
|
|
|
216
|
+
const prompts = Object.freeze({
|
|
217
|
+
render(ref, vars) {
|
|
218
|
+
if (typeof ref !== "string" || !ref.trim()) throw new Error("prompts.render(ref, vars) requires a non-empty ref string.");
|
|
219
|
+
if (vars !== undefined) assertJsonValue(vars, "prompts.render vars");
|
|
220
|
+
return hostCall("prompts.render", { ref, vars });
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
100
224
|
let contextObjectPrototype;
|
|
101
225
|
|
|
102
226
|
const capturedConsole = Object.freeze(Object.fromEntries(
|
|
@@ -169,7 +293,7 @@ parentPort.on("message", async (message) => {
|
|
|
169
293
|
}
|
|
170
294
|
if (message.type !== "start") return;
|
|
171
295
|
try {
|
|
172
|
-
const sandbox = { runs, emit(value) { assertJsonValue(value); parentPort.postMessage({ type: "emit", value }); }, console: capturedConsole };
|
|
296
|
+
const sandbox = { runs, prompts, Promise: workflowPromise, emit(value) { assertJsonValue(value); parentPort.postMessage({ type: "emit", value }); }, console: capturedConsole };
|
|
173
297
|
if (message.stateEnabled) sandbox.state = state;
|
|
174
298
|
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
175
299
|
contextObjectPrototype = vm.runInContext("Object.prototype", context);
|
|
@@ -194,6 +318,8 @@ parentPort.on("message", async (message) => {
|
|
|
194
318
|
export interface WorkflowScriptChildResult {
|
|
195
319
|
key: string;
|
|
196
320
|
ok: boolean;
|
|
321
|
+
/** Canonical child agent name when launch resolution produced one. */
|
|
322
|
+
agent?: string;
|
|
197
323
|
runId?: string;
|
|
198
324
|
output: string;
|
|
199
325
|
error?: string;
|
|
@@ -206,6 +332,8 @@ export interface WorkflowScriptTraceEntry {
|
|
|
206
332
|
operation: "run" | "status";
|
|
207
333
|
key: string;
|
|
208
334
|
state: "started" | "completed" | "failed" | "reused";
|
|
335
|
+
/** Canonical child agent name when resolved launch or result data is available. */
|
|
336
|
+
agent?: string;
|
|
209
337
|
runId?: string;
|
|
210
338
|
durationMs?: number;
|
|
211
339
|
phase?: string;
|
|
@@ -241,6 +369,9 @@ export interface RunWorkflowScriptOptions {
|
|
|
241
369
|
get: (key: string) => unknown | Promise<unknown>;
|
|
242
370
|
set: (key: string, value: unknown) => void | Promise<void>;
|
|
243
371
|
};
|
|
372
|
+
prompts?: {
|
|
373
|
+
render: (ref: string, vars?: unknown) => string | Promise<string>;
|
|
374
|
+
};
|
|
244
375
|
onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
|
|
245
376
|
onEmit?: (emits: unknown[]) => void;
|
|
246
377
|
}
|
|
@@ -355,7 +486,8 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
355
486
|
const trace: WorkflowScriptTraceEntry[] = [];
|
|
356
487
|
const children = new Map<string, WorkflowScriptChildResult>();
|
|
357
488
|
const childOrder: string[] = [];
|
|
358
|
-
const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult
|
|
489
|
+
const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
|
|
490
|
+
const observedRunCalls = new Set<number>();
|
|
359
491
|
const childController = new AbortController();
|
|
360
492
|
let settled = false;
|
|
361
493
|
|
|
@@ -372,8 +504,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
372
504
|
if (timer) clearTimeout(timer);
|
|
373
505
|
options.signal?.removeEventListener("abort", onAbort);
|
|
374
506
|
void worker.terminate();
|
|
375
|
-
|
|
507
|
+
const unobservedKeys = "value" in outcome ? [...launches].filter(([, launch]) => !launch.observed).map(([key]) => key) : [];
|
|
508
|
+
const completionError = unobservedKeys.length > 0
|
|
509
|
+
? new Error(`workflowScript completed with unawaited runs.run launch(es): ${unobservedKeys.map((key) => `'${key}'`).join(", ")}. Await or return each launch.`)
|
|
510
|
+
: undefined;
|
|
511
|
+
childController.abort("error" in outcome ? outcome.error : completionError ?? new Error("Workflow script completed."));
|
|
376
512
|
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial()));
|
|
513
|
+
else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
|
|
377
514
|
else resolve({ value: outcome.value, ...partial() });
|
|
378
515
|
};
|
|
379
516
|
const onAbort = () => finish({ error: new Error("Workflow script aborted.") });
|
|
@@ -418,6 +555,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
418
555
|
return finish({ value: message.value });
|
|
419
556
|
}
|
|
420
557
|
if (message.type === "error") return finish({ error: new Error(typeof message.error === "string" ? message.error : "Workflow script failed.") });
|
|
558
|
+
if (message.type === "runObserved" && typeof message.callId === "number") {
|
|
559
|
+
const key = typeof message.key === "string" ? message.key : undefined;
|
|
560
|
+
const launch = key ? launches.get(key) : undefined;
|
|
561
|
+
if (launch) launch.observed = true;
|
|
562
|
+
else observedRunCalls.add(message.callId);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
421
565
|
if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
|
|
422
566
|
|
|
423
567
|
const respond = (promise: Promise<unknown>) => {
|
|
@@ -431,6 +575,17 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
431
575
|
);
|
|
432
576
|
};
|
|
433
577
|
|
|
578
|
+
if (message.method === "prompts.render") {
|
|
579
|
+
if (!options.prompts) return respond(Promise.reject(new Error("Workflow prompt rendering is unavailable.")));
|
|
580
|
+
const ref = message.args.ref;
|
|
581
|
+
const vars = message.args.vars;
|
|
582
|
+
if (typeof ref !== "string" || !ref.trim()) return respond(Promise.reject(new Error("prompts.render(ref, vars) requires a non-empty ref string.")));
|
|
583
|
+
return respond(Promise.resolve().then(() => options.prompts!.render(ref, vars)).then((rendered) => {
|
|
584
|
+
if (typeof rendered !== "string") throw new Error("prompts.render must return task text.");
|
|
585
|
+
return rendered;
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
|
|
434
589
|
if (message.method === "state.get" || message.method === "state.set") {
|
|
435
590
|
if (!options.state) return respond(Promise.reject(new Error("Workflow state is unavailable without a mission.")));
|
|
436
591
|
let key: string;
|
|
@@ -501,6 +656,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
501
656
|
return respond(Promise.reject(new Error(`runs.run('${key}') resume requires a non-empty task follow-up.`)));
|
|
502
657
|
}
|
|
503
658
|
const collectFailure = message.args.collectFailure === true;
|
|
659
|
+
const callObserved = observedRunCalls.delete(message.callId);
|
|
504
660
|
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
505
661
|
? promise
|
|
506
662
|
: promise.then((result) => {
|
|
@@ -511,6 +667,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
511
667
|
const existing = launches.get(key);
|
|
512
668
|
if (existing) {
|
|
513
669
|
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
670
|
+
if (callObserved) existing.observed = true;
|
|
514
671
|
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
|
|
515
672
|
traceChanged();
|
|
516
673
|
return respond(deliver(existing.promise));
|
|
@@ -523,7 +680,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
523
680
|
const promise = Promise.resolve().then(() => options.launch(key, { ...params, async: params.async ?? false }, childController.signal)).then((result) => {
|
|
524
681
|
const normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
525
682
|
children.set(key, normalized);
|
|
526
|
-
trace.push({ operation: "run", key, state: normalized.ok ? "completed" : "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
|
|
683
|
+
trace.push({ operation: "run", key, state: normalized.ok ? "completed" : "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
|
|
527
684
|
traceChanged();
|
|
528
685
|
return normalized;
|
|
529
686
|
}, (error: unknown) => {
|
|
@@ -534,7 +691,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
534
691
|
traceChanged();
|
|
535
692
|
return failure;
|
|
536
693
|
});
|
|
537
|
-
launches.set(key, { fingerprint, promise });
|
|
694
|
+
launches.set(key, { fingerprint, promise, observed: callObserved });
|
|
538
695
|
respond(deliver(promise));
|
|
539
696
|
});
|
|
540
697
|
|