pi-subagents 0.45.1 → 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 (48) hide show
  1. package/CHANGELOG.md +33 -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/resume-guidance.ts +33 -0
  32. package/src/runs/background/subagent-wait.ts +5 -2
  33. package/src/runs/background/wait-completions.ts +39 -5
  34. package/src/runs/background/wait-subscriptions.ts +21 -5
  35. package/src/runs/foreground/execution.ts +4 -0
  36. package/src/runs/foreground/foreground-history.ts +137 -0
  37. package/src/runs/foreground/subagent-executor.ts +326 -38
  38. package/src/runs/shared/subagent-prompt-runtime.ts +71 -5
  39. package/src/shared/fork-context.ts +13 -0
  40. package/src/shared/prompt-resources.ts +51 -0
  41. package/src/shared/types.ts +30 -1
  42. package/src/shared/utf8.ts +11 -0
  43. package/src/slash/prompt-workflows.ts +2 -15
  44. package/src/slash/slash-commands.ts +19 -1
  45. package/src/tui/fleet-status.ts +8 -2
  46. package/src/tui/fleet.ts +135 -25
  47. package/src/tui/render.ts +120 -7
  48. package/src/workflows/scripted-workflow.ts +187 -11
@@ -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(
@@ -139,6 +263,25 @@ function assertJsonValue(value, path = "emit", seen = new Set()) {
139
263
  seen.delete(value);
140
264
  }
141
265
 
266
+ function isPlainWorkflowObject(value) {
267
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
268
+ const prototype = Object.getPrototypeOf(value);
269
+ return prototype === null || prototype === Object.prototype || prototype === contextObjectPrototype;
270
+ }
271
+
272
+ function omitUndefinedWorkflowValues(value, seen = new Set()) {
273
+ if (value === null || typeof value !== "object") return value;
274
+ if (seen.has(value)) return value;
275
+ seen.add(value);
276
+ const normalized = Array.isArray(value)
277
+ ? value.map((entry) => entry === undefined ? null : omitUndefinedWorkflowValues(entry, seen))
278
+ : isPlainWorkflowObject(value) && Object.getOwnPropertySymbols(value).length === 0
279
+ ? Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => entry === undefined ? [] : [[key, omitUndefinedWorkflowValues(entry, seen)]]))
280
+ : value;
281
+ seen.delete(value);
282
+ return normalized;
283
+ }
284
+
142
285
  parentPort.on("message", async (message) => {
143
286
  if (message.type === "response") {
144
287
  const entry = pending.get(message.callId);
@@ -150,7 +293,7 @@ parentPort.on("message", async (message) => {
150
293
  }
151
294
  if (message.type !== "start") return;
152
295
  try {
153
- 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 };
154
297
  if (message.stateEnabled) sandbox.state = state;
155
298
  const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
156
299
  contextObjectPrototype = vm.runInContext("Object.prototype", context);
@@ -163,7 +306,7 @@ parentPort.on("message", async (message) => {
163
306
  return;
164
307
  }
165
308
  const value = await compiled.runInContext(context);
166
- const persistedValue = value === undefined ? null : value;
309
+ const persistedValue = value === undefined ? null : omitUndefinedWorkflowValues(value);
167
310
  assertJsonValue(persistedValue, "return");
168
311
  parentPort.postMessage({ type: "complete", value: persistedValue });
169
312
  } catch (error) {
@@ -175,6 +318,8 @@ parentPort.on("message", async (message) => {
175
318
  export interface WorkflowScriptChildResult {
176
319
  key: string;
177
320
  ok: boolean;
321
+ /** Canonical child agent name when launch resolution produced one. */
322
+ agent?: string;
178
323
  runId?: string;
179
324
  output: string;
180
325
  error?: string;
@@ -187,6 +332,8 @@ export interface WorkflowScriptTraceEntry {
187
332
  operation: "run" | "status";
188
333
  key: string;
189
334
  state: "started" | "completed" | "failed" | "reused";
335
+ /** Canonical child agent name when resolved launch or result data is available. */
336
+ agent?: string;
190
337
  runId?: string;
191
338
  durationMs?: number;
192
339
  phase?: string;
@@ -222,6 +369,9 @@ export interface RunWorkflowScriptOptions {
222
369
  get: (key: string) => unknown | Promise<unknown>;
223
370
  set: (key: string, value: unknown) => void | Promise<void>;
224
371
  };
372
+ prompts?: {
373
+ render: (ref: string, vars?: unknown) => string | Promise<string>;
374
+ };
225
375
  onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
226
376
  onEmit?: (emits: unknown[]) => void;
227
377
  }
@@ -336,7 +486,8 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
336
486
  const trace: WorkflowScriptTraceEntry[] = [];
337
487
  const children = new Map<string, WorkflowScriptChildResult>();
338
488
  const childOrder: string[] = [];
339
- 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>();
340
491
  const childController = new AbortController();
341
492
  let settled = false;
342
493
 
@@ -353,8 +504,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
353
504
  if (timer) clearTimeout(timer);
354
505
  options.signal?.removeEventListener("abort", onAbort);
355
506
  void worker.terminate();
356
- 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."));
357
512
  if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial()));
513
+ else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
358
514
  else resolve({ value: outcome.value, ...partial() });
359
515
  };
360
516
  const onAbort = () => finish({ error: new Error("Workflow script aborted.") });
@@ -399,6 +555,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
399
555
  return finish({ value: message.value });
400
556
  }
401
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
+ }
402
565
  if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
403
566
 
404
567
  const respond = (promise: Promise<unknown>) => {
@@ -412,6 +575,17 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
412
575
  );
413
576
  };
414
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
+
415
589
  if (message.method === "state.get" || message.method === "state.set") {
416
590
  if (!options.state) return respond(Promise.reject(new Error("Workflow state is unavailable without a mission.")));
417
591
  let key: string;
@@ -482,6 +656,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
482
656
  return respond(Promise.reject(new Error(`runs.run('${key}') resume requires a non-empty task follow-up.`)));
483
657
  }
484
658
  const collectFailure = message.args.collectFailure === true;
659
+ const callObserved = observedRunCalls.delete(message.callId);
485
660
  const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
486
661
  ? promise
487
662
  : promise.then((result) => {
@@ -492,6 +667,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
492
667
  const existing = launches.get(key);
493
668
  if (existing) {
494
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;
495
671
  trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
496
672
  traceChanged();
497
673
  return respond(deliver(existing.promise));
@@ -504,7 +680,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
504
680
  const promise = Promise.resolve().then(() => options.launch(key, { ...params, async: params.async ?? false }, childController.signal)).then((result) => {
505
681
  const normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
506
682
  children.set(key, normalized);
507
- 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 } : {}) });
508
684
  traceChanged();
509
685
  return normalized;
510
686
  }, (error: unknown) => {
@@ -515,7 +691,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
515
691
  traceChanged();
516
692
  return failure;
517
693
  });
518
- launches.set(key, { fingerprint, promise });
694
+ launches.set(key, { fingerprint, promise, observed: callObserved });
519
695
  respond(deliver(promise));
520
696
  });
521
697