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.
Files changed (45) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +342 -0
  4. package/docs/configuration.md +320 -0
  5. package/docs/extension-api.md +308 -0
  6. package/docs/missions.md +117 -0
  7. package/docs/models.md +190 -0
  8. package/docs/observability.md +174 -0
  9. package/docs/tool-reference.md +343 -0
  10. package/docs/watchdog.md +176 -0
  11. package/docs/workflows.md +163 -0
  12. package/package.json +4 -2
  13. package/skills/pi-subagents/references/execution-controls.md +2 -2
  14. package/src/agents/agents.ts +17 -8
  15. package/src/agents/frontmatter.ts +7 -3
  16. package/src/agents/skills.ts +2 -9
  17. package/src/api/project-panes.ts +30 -0
  18. package/src/extension/config.ts +15 -1
  19. package/src/extension/index.ts +36 -16
  20. package/src/extension/schemas.ts +3 -2
  21. package/src/extension/subagent-guide.ts +39 -0
  22. package/src/extension/tool-description.ts +4 -4
  23. package/src/inspectors/herdr/project-panes.ts +457 -62
  24. package/src/missions/actions.ts +25 -2
  25. package/src/missions/lifecycle.ts +21 -2
  26. package/src/missions/store.ts +77 -1
  27. package/src/missions/types.ts +33 -0
  28. package/src/runs/background/async-execution.ts +7 -1
  29. package/src/runs/background/completion-replay.ts +267 -0
  30. package/src/runs/background/result-watcher.ts +12 -4
  31. package/src/runs/background/wait-completions.ts +39 -5
  32. package/src/runs/background/wait-subscriptions.ts +18 -3
  33. package/src/runs/foreground/execution.ts +4 -0
  34. package/src/runs/foreground/foreground-history.ts +137 -0
  35. package/src/runs/foreground/subagent-executor.ts +310 -44
  36. package/src/shared/fork-context.ts +13 -0
  37. package/src/shared/prompt-resources.ts +51 -0
  38. package/src/shared/types.ts +30 -1
  39. package/src/shared/utf8.ts +11 -0
  40. package/src/slash/prompt-workflows.ts +2 -15
  41. package/src/slash/slash-commands.ts +19 -1
  42. package/src/tui/fleet-status.ts +8 -2
  43. package/src/tui/fleet.ts +135 -25
  44. package/src/tui/render.ts +120 -7
  45. 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?.slice(-5) ?? []) {
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
- for (const row of rows) {
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 ?? []).slice(-5)) {
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 recentLines = (rProg.recentOutput ?? []).slice(-5);
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 hostCall(method, args) {
21
- return new Promise((resolve, reject) => {
22
- const callId = ++nextCallId;
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
- return Promise.all(calls.map(({ key, params }) => hostCall("run", { key, params, collectFailure: true })));
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
- childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed; unawaited child launches are aborted."));
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