pi-extensible-workflows 1.0.1 → 3.0.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 (47) hide show
  1. package/README.md +9 -2
  2. package/dist/src/agent-execution.d.ts +13 -14
  3. package/dist/src/agent-execution.js +110 -197
  4. package/dist/src/budget.d.ts +38 -0
  5. package/dist/src/budget.js +160 -0
  6. package/dist/src/cli.d.ts +9 -0
  7. package/dist/src/cli.js +536 -6
  8. package/dist/src/doctor.d.ts +4 -4
  9. package/dist/src/doctor.js +9 -29
  10. package/dist/src/execution.d.ts +17 -0
  11. package/dist/src/execution.js +630 -0
  12. package/dist/src/herdr.d.ts +12 -0
  13. package/dist/src/herdr.js +74 -0
  14. package/dist/src/host.d.ts +62 -0
  15. package/dist/src/host.js +2696 -0
  16. package/dist/src/index.d.ts +11 -557
  17. package/dist/src/index.js +8 -3974
  18. package/dist/src/persistence.d.ts +32 -19
  19. package/dist/src/persistence.js +310 -70
  20. package/dist/src/registry.d.ts +31 -0
  21. package/dist/src/registry.js +169 -0
  22. package/dist/src/session-inspector.d.ts +1 -0
  23. package/dist/src/session-inspector.js +4 -1
  24. package/dist/src/types.d.ts +565 -0
  25. package/dist/src/types.js +27 -0
  26. package/dist/src/utils.d.ts +26 -0
  27. package/dist/src/utils.js +128 -0
  28. package/dist/src/validation.d.ts +24 -0
  29. package/dist/src/validation.js +740 -0
  30. package/dist/src/workflow-evals.js +2 -1
  31. package/package.json +4 -3
  32. package/skills/pi-extensible-workflows/SKILL.md +52 -67
  33. package/src/agent-execution.ts +84 -147
  34. package/src/budget.ts +75 -0
  35. package/src/cli.ts +427 -6
  36. package/src/doctor.ts +13 -32
  37. package/src/execution.ts +540 -0
  38. package/src/herdr.ts +73 -0
  39. package/src/host.ts +2265 -0
  40. package/src/index.ts +11 -3453
  41. package/src/persistence.ts +255 -47
  42. package/src/registry.ts +157 -0
  43. package/src/session-inspector.ts +5 -1
  44. package/src/types.ts +113 -0
  45. package/src/utils.ts +108 -0
  46. package/src/validation.ts +616 -0
  47. package/src/workflow-evals.ts +2 -1
package/dist/src/index.js CHANGED
@@ -1,3977 +1,11 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- import { fork } from "node:child_process";
3
- import { randomUUID } from "node:crypto";
4
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
5
- import { homedir, tmpdir } from "node:os";
6
- import { basename, dirname, extname, join, resolve } from "node:path";
7
- import { fileURLToPath } from "node:url";
8
- import * as acorn from "acorn";
9
- import { Script } from "node:vm";
10
- import { Type } from "@earendil-works/pi-ai";
11
- import { Value } from "typebox/value";
12
- import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode, SessionManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
13
- import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
14
- import { transcriptLines } from "./session-inspector.js";
15
- import { acquireSessionLease, atomicWriteFile, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
16
- export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
17
- export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"];
18
- export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
19
- export const WORKFLOW_RUN_STARTED_EVENT = "workflow:run-started";
20
- export const WORKFLOW_RUN_RESUMED_EVENT = "workflow:run-resumed";
21
- export const WORKFLOW_RUN_STATE_CHANGED_EVENT = "workflow:run-state-changed";
22
- export const WORKFLOW_RUN_COMPLETED_EVENT = "workflow:run-completed";
23
- export const WORKFLOW_RUN_FAILED_EVENT = "workflow:run-failed";
24
- export const WORKFLOW_AGENT_STATE_CHANGED_EVENT = "workflow:agent-state-changed";
25
- export const WORKFLOW_PHASE_CHANGED_EVENT = "workflow:phase-changed";
26
- export const WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT = "workflow:checkpoint-state-changed";
27
- export const WORKFLOW_BUDGET_EVENT = "workflow:budget-event";
28
- export const WORKFLOW_WORKTREE_CREATED_EVENT = "workflow:worktree-created";
29
- const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
30
- export const ERROR_CODES = [
31
- "CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
32
- "RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
33
- "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
34
- ];
35
- export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 3;
36
- export class WorkflowError extends Error {
37
- code;
38
- constructor(code, message) {
39
- super(message);
40
- this.code = code;
41
- this.name = "WorkflowError";
42
- }
43
- }
44
- const WORKFLOW_AUTHORED_ERROR = Symbol("workflowAuthoredError");
45
- function errorText(error) { return error && typeof error === "object" && typeof error.message === "string" ? error.message : error instanceof Error ? error.message : String(error); }
46
- function errorCode(error) {
47
- if (error instanceof WorkflowError)
48
- return ERROR_CODES.includes(error.code) ? error.code : undefined;
49
- if (!error || typeof error !== "object")
50
- return undefined;
51
- const code = error.code;
52
- return typeof code === "string" && ERROR_CODES.includes(code) ? code : undefined;
53
- }
54
- function markWorkflowAuthored(error, authored = false) {
55
- if (authored)
56
- Object.defineProperty(error, WORKFLOW_AUTHORED_ERROR, { value: true });
57
- return error;
58
- }
59
- function isWorkflowAuthored(error) { return Boolean(error && typeof error === "object" && WORKFLOW_AUTHORED_ERROR in error); }
60
- function workflowDetail(message) {
61
- const detail = message.trim().replace(new RegExp(`\\b(?:${ERROR_CODES.join("|")})\\b:?\\s*`, "g"), "").replace(/^\s*[A-Z][A-Z0-9_]+:\s*/, "").split("\n").filter((line) => !/^\s*at\s/.test(line)).join("\n").replace(/^Run \S+(?=\s(?:exceeded|is))/i, "Run").replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, "the workflow").replace(/^(?:Pi )session \S+(?=\s(?:is|has))/i, "session").replace(/^(Unknown scheduler run|Missing production ownership record|Persisted agent belongs to another run):\s*\S+/i, "$1").replace(/\b(?:runId|sessionId|callSite|occurrence|failedAt|id)[:=]\s*\S+/gi, "").replace(/\s{2,}/g, " ").trim();
62
- return detail || "No further details were provided";
63
- }
64
- const WORKFLOW_ERROR_PROSE = {
65
- CONFIG_ERROR: (detail) => `The workflow configuration is invalid: ${detail}.`,
66
- INVALID_SETTINGS: (detail) => `The workflow settings are invalid: ${detail}.`,
67
- INVALID_SYNTAX: (detail) => `The workflow source is invalid: ${detail}.`,
68
- INVALID_METADATA: (detail) => `The workflow metadata is invalid: ${detail}.`,
69
- DUPLICATE_NAME: (detail) => `The workflow contains a duplicate name: ${detail}.`,
70
- INVALID_SCHEMA: (detail) => `The workflow schema is invalid: ${detail}.`,
71
- REGISTRY_FROZEN: (detail) => `Workflow extension registration is closed: ${detail}.`,
72
- GLOBAL_COLLISION: (detail) => `The workflow global name is already in use: ${detail}.`,
73
- MISSING_WORKFLOW: (detail) => `The workflow primitive is unavailable: ${detail}.`,
74
- UNKNOWN_MODEL: (detail) => `The workflow requested the unavailable model ${detail.replace(/^(?:Unknown model(?: for role [^:]+)?|Invalid model spec):\s*/, "")}.`,
75
- UNKNOWN_TOOL: (detail) => `The workflow requested the unavailable tool ${detail.replace(/^Unknown tool:\s*/, "")}.`,
76
- UNKNOWN_AGENT_TYPE: (detail) => `The workflow requested the unavailable agent role ${detail.replace(/^Unknown agent role:\s*/, "")}.`,
77
- RUN_OWNED: (detail) => /already owned|active ownership/.test(detail) ? "The workflow session is already in use." : `The workflow session is already in use: ${detail}.`,
78
- RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
79
- AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
80
- AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
81
- RESULT_INVALID: (detail) => `The workflow produced an invalid result: ${detail}.`,
82
- CANCELLED: (detail) => `The workflow was cancelled: ${detail}.`,
83
- WORKER_UNRESPONSIVE: (detail) => `The workflow worker stopped responding: ${detail}.`,
84
- WORKTREE_FAILED: (detail) => `The workflow worktree operation failed: ${detail}.`,
85
- RESUME_INCOMPATIBLE: (detail) => `The workflow cannot resume this run: ${detail}.`,
86
- BUDGET_EXHAUSTED: (detail) => `The workflow budget was exhausted: ${detail}.`,
87
- INTERNAL_ERROR: (detail) => `The workflow encountered an internal error: ${detail}.`,
88
- };
89
- export function formatWorkflowFailure(error) {
90
- if (isWorkflowAuthored(error))
91
- return errorText(error);
92
- const code = errorCode(error);
93
- if (code)
94
- return WORKFLOW_ERROR_PROSE[code](workflowDetail(errorText(error)));
95
- if (error instanceof Error)
96
- return error.message || "The workflow failed without an error message.";
97
- return `The workflow failed with value ${String(error)}.`;
98
- }
99
- function workflowErrorFromWorker(error) {
100
- const code = errorCode(error);
101
- const typed = markWorkflowAuthored(new WorkflowError(code ?? "INTERNAL_ERROR", error.message), error.authored || !code);
102
- return error.failedAt === undefined ? typed : Object.assign(typed, { failedAt: error.failedAt });
103
- }
104
- function asWorkflowError(error) {
105
- const code = errorCode(error);
106
- return markWorkflowAuthored(error instanceof WorkflowError && code ? error : new WorkflowError(code ?? "INTERNAL_ERROR", errorText(error)), isWorkflowAuthored(error) || !code);
107
- }
108
- function mainAgentError(error) {
109
- const typed = asWorkflowError(error);
110
- const presented = new WorkflowError(typed.code, formatWorkflowFailure(typed));
111
- Object.assign(presented, typed);
112
- presented.message = formatWorkflowFailure(typed);
113
- return presented;
114
- }
115
- export class RunLifecycle {
116
- changed;
117
- #state;
118
- #active = 0;
119
- #waiters = [];
120
- constructor(state = "running", changed) {
121
- this.changed = changed;
122
- this.#state = state;
123
- }
124
- get state() { return this.#state; }
125
- async enter() {
126
- while (this.#state === "pausing" || this.#state === "paused" || this.#state === "awaiting_input")
127
- await new Promise((resolve) => { this.#waiters.push(resolve); });
128
- if (this.#state !== "running")
129
- throw new WorkflowError("CANCELLED", `Run is ${this.#state}`);
130
- this.#active += 1;
131
- }
132
- async leave() {
133
- if (this.#active > 0)
134
- this.#active -= 1;
135
- if (this.#state === "pausing" && this.#active === 0)
136
- await this.#set("paused", "pause");
137
- }
138
- async enterAwaitingInput() {
139
- while (this.#state === "pausing" || this.#state === "paused")
140
- await new Promise((resolve) => { this.#waiters.push(resolve); });
141
- if (this.#state === "awaiting_input")
142
- return;
143
- if (this.#state !== "running")
144
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot await input for ${this.#state} run`);
145
- await this.#set("awaiting_input", "awaiting_input");
146
- }
147
- async resolveAwaitingInput() {
148
- if (this.#state !== "awaiting_input")
149
- return;
150
- await this.#set("running", "checkpoint_resolved");
151
- for (const resolve of this.#waiters.splice(0))
152
- resolve();
153
- }
154
- async pause() {
155
- if (this.#state !== "running")
156
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot pause ${this.#state} run`);
157
- await this.#set("pausing", "pause");
158
- if (this.#active === 0)
159
- await this.#set("paused", "pause");
160
- }
161
- async resume() {
162
- if (this.#state !== "paused" && this.#state !== "interrupted" && this.#state !== "budget_exhausted")
163
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot resume ${this.#state} run`);
164
- await this.#set("running", "resume");
165
- for (const resolve of this.#waiters.splice(0))
166
- resolve();
167
- }
168
- async providerPause() {
169
- await this.leave();
170
- if (this.#state === "running")
171
- await this.pause();
172
- await this.enter();
173
- }
174
- async terminal(state, reason) {
175
- if (["completed", "failed", "stopped"].includes(this.#state))
176
- throw new WorkflowError("RESUME_INCOMPATIBLE", `${this.#state} runs are terminal`);
177
- await this.#set(state, reason ?? state);
178
- for (const resolve of this.#waiters.splice(0))
179
- resolve();
180
- }
181
- async #set(state, reason) {
182
- const previousState = this.#state;
183
- this.#state = state;
184
- await this.changed?.(state, previousState, reason);
185
- }
186
- }
187
- export const DEFAULT_SETTINGS = Object.freeze({ concurrency: 8 });
188
- function fail(code, message) { throw new WorkflowError(code, message); }
189
- function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
190
- export { object as isObject };
191
- function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0; }
192
- function nonNegativeFinite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
193
- export function validateBudget(value) {
194
- if (value === undefined)
195
- return undefined;
196
- if (!object(value))
197
- fail("INVALID_METADATA", "budget must be an object");
198
- const result = {};
199
- for (const [dimension, raw] of Object.entries(value)) {
200
- if (!["tokens", "costUsd", "durationMs", "agentLaunches"].includes(dimension))
201
- fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
202
- if (!object(raw))
203
- fail("INVALID_METADATA", `${dimension} budget must be an object`);
204
- if (Object.keys(raw).some((key) => key !== "soft" && key !== "hard"))
205
- fail("INVALID_METADATA", `${dimension} budget has an unknown limit`);
206
- const isCost = dimension === "costUsd";
207
- for (const key of ["soft", "hard"])
208
- if (raw[key] !== undefined && !(isCost ? nonNegativeFinite(raw[key]) : nonNegativeInteger(raw[key])))
209
- fail("INVALID_METADATA", `${dimension}.${key} must be a non-negative ${isCost ? "finite number" : "integer"}`);
210
- if (raw.soft !== undefined && raw.soft !== null && raw.hard !== undefined && raw.hard !== null && raw.soft >= raw.hard)
211
- fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
212
- const limits = {};
213
- if (raw.soft !== undefined)
214
- limits.soft = raw.soft;
215
- if (raw.hard !== undefined)
216
- limits.hard = raw.hard;
217
- if (Object.keys(limits).length)
218
- result[dimension] = limits;
219
- }
220
- return Object.freeze(result);
221
- }
222
- export function validateBudgetPatch(value) {
223
- if (!object(value))
224
- fail("INVALID_METADATA", "budget patch must be an object");
225
- const result = {};
226
- for (const [dimension, raw] of Object.entries(value)) {
227
- if (!["tokens", "costUsd", "durationMs", "agentLaunches"].includes(dimension))
228
- fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
229
- if (raw === null) {
230
- result[dimension] = null;
231
- continue;
232
- }
233
- if (!object(raw) || Object.keys(raw).some((key) => key !== "soft" && key !== "hard"))
234
- fail("INVALID_METADATA", `${dimension} budget patch must contain only soft and hard`);
235
- const limits = {};
236
- for (const key of ["soft", "hard"])
237
- if (Object.prototype.hasOwnProperty.call(raw, key)) {
238
- if (raw[key] === null)
239
- limits[key] = null;
240
- else {
241
- const checked = validateBudget({ [dimension]: { [key]: raw[key] } })?.[dimension];
242
- if (checked?.[key] !== undefined)
243
- limits[key] = checked[key];
244
- }
245
- }
246
- if (limits.soft !== null && limits.hard !== null && limits.soft !== undefined && limits.hard !== undefined && limits.soft >= limits.hard)
247
- fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
248
- result[dimension] = limits;
249
- }
250
- return result;
251
- }
252
- function budgetUsage(value) { return { tokens: value?.tokens ?? 0, costUsd: value?.costUsd ?? 0, durationMs: value?.durationMs ?? 0, agentLaunches: value?.agentLaunches ?? 0 }; }
253
- export class WorkflowBudgetRuntime {
254
- budget;
255
- version;
256
- #now;
257
- #onChange;
258
- #injected = new Set();
259
- #seen = new Set();
260
- #active;
261
- #activeSince;
262
- #usage;
263
- #events;
264
- #turnAccounting;
265
- constructor(budget, version = 1, usage, events = [], options = {}) {
266
- this.budget = budget;
267
- this.version = version;
268
- this.#now = options.now ?? (() => Date.now());
269
- this.#onChange = options.onChange;
270
- this.#active = options.active ?? true;
271
- this.#activeSince = this.#active ? this.#now() : undefined;
272
- this.#usage = budgetUsage(usage);
273
- this.#events = [...events];
274
- for (const event of events)
275
- if (event.budgetVersion === version)
276
- this.#seen.add(event.type);
277
- }
278
- get usage() { this.#syncDuration(); return { ...this.#usage }; }
279
- get events() { return this.#events; }
280
- get hardExhausted() { return this.#events.some((event) => event.type === "hard_exhausted" && event.budgetVersion === this.version); }
281
- checkAgentLaunch() { this.#checkHard(["agentLaunches"]); }
282
- beforeAttempt() { this.#checkHard(["agentLaunches"]); this.#usage.agentLaunches += 1; this.#evaluate(); }
283
- beforeTurn() { this.#syncDuration(); this.#evaluate(); this.#checkHard(["tokens", "costUsd", "durationMs"]); }
284
- afterTurn(accounting, final) { this.#syncDuration(); this.#applyTurn(accounting, final, this.#turnAccounting); this.#turnAccounting = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }
285
- #applyTurn(accounting, final, previous = { input: 0, output: 0, cost: 0 }) {
286
- this.#usage.tokens += Math.max(0, accounting.input - previous.input) + Math.max(0, accounting.output - previous.output);
287
- this.#usage.costUsd += Math.max(0, accounting.cost - previous.cost);
288
- this.#evaluate();
289
- if (!final)
290
- this.#checkHard(["tokens", "costUsd", "durationMs"]);
291
- }
292
- instruction(agentId = "agent") {
293
- if (!this.#hasSoftCrossed() || this.#injected.has(agentId))
294
- return undefined;
295
- this.#injected.add(agentId);
296
- return `The workflow budget soft limit has been reached. Finish the requested output now, preserving any required output schema. Current usage: ${JSON.stringify(this.usage)}. Do not start additional model work unless it is required to produce the final requested result.`;
297
- }
298
- forAgent(agentId) {
299
- let attempt = 0;
300
- let previous;
301
- return {
302
- beforeAttempt: () => { attempt += 1; previous = undefined; this.beforeAttempt(); },
303
- beforeTurn: () => { this.beforeTurn(); },
304
- afterTurn: (accounting, final) => { this.#applyTurn(accounting, final, previous); previous = { input: accounting.input, output: accounting.output, cost: accounting.cost }; },
305
- instruction: () => this.instruction(`${agentId}:${String(attempt + 1)}`),
306
- };
307
- }
308
- transition(state) {
309
- const active = state === "running";
310
- if (active === this.#active)
311
- return;
312
- if (active) {
313
- this.#active = true;
314
- this.#activeSince = this.#now();
315
- }
316
- else {
317
- this.#syncDuration();
318
- this.#evaluate();
319
- this.#active = false;
320
- this.#activeSince = undefined;
321
- }
322
- this.#onChange?.();
323
- }
324
- #syncDuration() { if (this.#active && this.#activeSince !== undefined) {
325
- const now = this.#now();
326
- this.#usage.durationMs += Math.max(0, now - this.#activeSince);
327
- this.#activeSince = now;
328
- } }
329
- #hasSoftCrossed() { return !!this.budget && Object.entries(this.budget).some(([dimension, limits]) => limits.soft !== undefined && this.#usage[dimension] >= limits.soft); }
330
- #checkHard(dimensions) {
331
- const exhausted = dimensions.filter((dimension) => { const hard = this.budget?.[dimension]?.hard; return hard !== undefined && this.#usage[dimension] >= hard; });
332
- if (!exhausted.length)
333
- return;
334
- this.#record("hard_exhausted", exhausted);
335
- const detail = exhausted.map((dimension) => `${dimension} usage=${String(this.#usage[dimension])} hard=${String(this.budget?.[dimension]?.hard)}`).join(", ");
336
- throw new WorkflowError("BUDGET_EXHAUSTED", `Budget version ${String(this.version)} exhausted: ${detail}`);
337
- }
338
- #evaluate() {
339
- const budget = this.budget;
340
- if (!budget)
341
- return;
342
- const soft = Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.soft !== undefined && this.#usage[dimension] >= limits.soft; });
343
- if (soft.length)
344
- this.#record("soft_crossed", soft);
345
- const overrun = Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.hard !== undefined && this.#usage[dimension] > limits.hard; });
346
- if (overrun.length)
347
- this.#record("hard_overrun", overrun);
348
- }
349
- #record(type, dimensions) { if (this.#seen.has(type))
350
- return; this.#seen.add(type); this.#events.push({ type, budgetVersion: this.version, dimensions: [...dimensions], usage: this.usage, limits: structuredClone(this.budget ?? {}), at: this.#now() }); this.#onChange?.(); }
351
- recordEvent(event) { this.#events.push(structuredClone(event)); }
352
- snapshot() { return { usage: this.usage, budgetEvents: [...this.#events] }; }
353
- }
354
- export function mergeBudget(budget, patch) {
355
- const merged = structuredClone(budget ?? {});
356
- for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"])
357
- if (Object.prototype.hasOwnProperty.call(patch, dimension)) {
358
- const value = patch[dimension];
359
- if (value === null) {
360
- Reflect.deleteProperty(merged, dimension);
361
- continue;
362
- }
363
- const next = { ...(merged[dimension] ?? {}) };
364
- for (const key of ["soft", "hard"])
365
- if (value && Object.prototype.hasOwnProperty.call(value, key)) {
366
- const limit = value[key];
367
- if (limit === null)
368
- Reflect.deleteProperty(next, key);
369
- else if (limit !== undefined)
370
- next[key] = limit;
371
- }
372
- if (Object.keys(next).length)
373
- merged[dimension] = next;
374
- else
375
- Reflect.deleteProperty(merged, dimension);
376
- }
377
- return validateBudget(merged);
378
- }
379
- export function budgetRelaxed(previous, next) { for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"]) {
380
- const oldLimit = previous?.[dimension];
381
- const newLimit = next?.[dimension];
382
- for (const key of ["soft", "hard"])
383
- if ((oldLimit?.[key] !== undefined && newLimit?.[key] === undefined) || (oldLimit?.[key] !== undefined && newLimit?.[key] !== undefined && newLimit[key] > oldLimit[key]))
384
- return true;
385
- } return false; }
386
- export function exhaustedBudgetDimensions(budget, usage) {
387
- if (!budget)
388
- return [];
389
- return Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.hard !== undefined && usage[dimension] >= limits.hard; });
390
- }
391
- export function resumeBudgetAllowed(budget, usage) { return exhaustedBudgetDimensions(budget, usage).length === 0; }
392
- function positiveInteger(value) { return Number.isInteger(value) && value > 0; }
393
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
394
- const MODEL_ALIAS_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
395
- export function parseModelReference(value) {
396
- const match = /^([^/:\s]+)\/([^:\s]+)(?::([^:\s]+))?$/.exec(value);
397
- if (!match?.[1] || !match[2])
398
- fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
399
- const thinking = match[3];
400
- if (thinking && !THINKING_LEVELS.includes(thinking))
401
- fail("UNKNOWN_MODEL", `Invalid thinking level: ${thinking}`);
402
- return { provider: match[1], model: match[2], ...(thinking ? { thinking: thinking } : {}) };
403
- }
404
- function aliasError(message, settingsPath) { fail("CONFIG_ERROR", `${message} (settings: ${settingsPath})`); }
405
- export function validateModelAliases(value, settingsPath = "workflow settings") {
406
- if (!object(value))
407
- aliasError("modelAliases must be an object", settingsPath);
408
- const aliases = {};
409
- for (const [name, target] of Object.entries(value)) {
410
- if (!MODEL_ALIAS_NAME.test(name))
411
- aliasError(`Invalid model alias name: ${name}`, settingsPath);
412
- if (typeof target !== "string" || !target.trim())
413
- aliasError(`Invalid model alias target for ${name}`, settingsPath);
414
- try {
415
- parseModelReference(target);
416
- }
417
- catch (error) {
418
- aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath);
419
- }
420
- aliases[name] = target;
421
- }
422
- return Object.freeze(aliases);
423
- }
424
- function unknownModel(value, target, settingsPath) {
425
- const resolved = target ? ` resolved to ${target}` : "";
426
- const path = settingsPath ? ` (settings: ${settingsPath})` : "";
427
- fail("UNKNOWN_MODEL", `Unknown model${target ? " alias" : ""} ${value}${resolved}${path}`);
428
- }
429
- export function resolveModelReference(value, aliases = {}, knownModels, settingsPath) {
430
- const target = Object.prototype.hasOwnProperty.call(aliases, value) ? aliases[value] : undefined;
431
- if (target !== undefined) {
432
- try {
433
- return parseModelReference(target);
434
- }
435
- catch {
436
- unknownModel(value, target, settingsPath);
437
- }
438
- }
439
- if (value.includes("/"))
440
- return parseModelReference(value);
441
- const match = /^([^:\s]+)(?::([^:\s]+))?$/.exec(value);
442
- const thinking = match?.[2];
443
- if (!match?.[1] || thinking && !THINKING_LEVELS.includes(thinking))
444
- unknownModel(value, undefined, settingsPath);
445
- const candidates = [...(knownModels ?? [])].filter((model) => model.slice(model.indexOf("/") + 1) === match[1]);
446
- if (candidates.length === 1) {
447
- const parsed = parseModelReference(candidates[0]);
448
- return thinking ? { ...parsed, thinking: thinking } : parsed;
449
- }
450
- unknownModel(value, undefined, settingsPath);
451
- }
452
- function modelCapability(value, aliases, knownModels, settingsPath) {
453
- const parsed = resolveModelReference(value, aliases, knownModels, settingsPath);
454
- return `${parsed.provider}/${parsed.model}`;
455
- }
456
- function aliasDrift(previous, current) {
457
- return [...new Set([...Object.keys(previous), ...Object.keys(current)])].sort().flatMap((name) => previous[name] === current[name] ? [] : [`${name}: ${previous[name] ?? "(missing)"} -> ${current[name] ?? "(missing)"}`]);
458
- }
459
- export function validateCheckpoint(value) {
460
- if (!object(value) || Object.keys(value).some((key) => !["name", "prompt", "context"].includes(key)) || typeof value.name !== "string" || value.name.trim() === "" || typeof value.prompt !== "string" || !jsonValue(value.context))
461
- fail("INVALID_METADATA", "checkpoint requires only name, prompt, and JSON context");
462
- if (Buffer.byteLength(value.prompt) > 1024)
463
- fail("INVALID_METADATA", "checkpoint prompt exceeds 1024 UTF-8 bytes");
464
- if (Buffer.byteLength(JSON.stringify(value.context)) > 4096)
465
- fail("INVALID_METADATA", "checkpoint context exceeds 4096 UTF-8 bytes");
466
- return { name: value.name, prompt: value.prompt, context: value.context };
467
- }
468
- export function workflowSettingsPath(agentDir = getAgentDir()) { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
469
- export function workflowProjectSettingsPath(cwd) { return join(cwd, ".pi", ROLE_DIRECTORY, "settings.json"); }
470
- const EMPTY_AGENT_RESOURCE_EXCLUSIONS = Object.freeze({ skills: [], extensions: [] });
471
- function normalizedResourcePath(value, settingsPath) {
472
- let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
473
- if (expanded.startsWith("file://"))
474
- expanded = fileURLToPath(expanded);
475
- const resolved = resolve(dirname(settingsPath), expanded);
476
- try {
477
- return realpathSync(resolved);
478
- }
479
- catch {
480
- return resolved;
481
- }
482
- }
483
- export function mergeAgentResourceExclusions(...values) {
484
- return { skills: [...new Set(values.flatMap((value) => value?.skills ?? []))], extensions: [...new Set(values.flatMap((value) => value?.extensions ?? []))] };
485
- }
486
- function validateAgentResourceExclusions(value, settingsPath, errorCode = "INVALID_SETTINGS") {
487
- if (value === undefined)
488
- return undefined;
489
- const base = `${settingsPath}.disabledAgentResources`;
490
- if (!object(value))
491
- fail(errorCode, `${base} must be an object`);
492
- for (const key of Object.keys(value))
493
- if (key !== "skills" && key !== "extensions")
494
- fail(errorCode, `${base}.${key} is not supported`);
495
- const normalized = { skills: [], extensions: [] };
496
- for (const kind of ["skills", "extensions"]) {
497
- const entries = value[kind];
498
- if (entries === undefined)
499
- continue;
500
- if (!Array.isArray(entries))
501
- fail(errorCode, `${base}.${kind} must be an array`);
502
- const seen = new Set();
503
- for (const [index, entry] of entries.entries()) {
504
- if (typeof entry !== "string" || !entry.trim())
505
- fail(errorCode, `${base}.${kind}[${String(index)}] must be a non-empty string`);
506
- let selector = entry.trim();
507
- if (kind === "extensions") {
508
- try {
509
- selector = normalizedResourcePath(selector, settingsPath);
510
- }
511
- catch (error) {
512
- fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`);
513
- }
514
- }
515
- if (!seen.has(selector)) {
516
- seen.add(selector);
517
- normalized[kind].push(selector);
518
- }
519
- }
520
- }
521
- return Object.freeze({ skills: Object.freeze(normalized.skills), extensions: Object.freeze(normalized.extensions) });
522
- }
523
- export function loadSettings(path = workflowSettingsPath()) {
524
- let parsed;
525
- try {
526
- parsed = JSON.parse(readFileSync(path, "utf8"));
527
- }
528
- catch (error) {
529
- if (error.code === "ENOENT")
530
- return DEFAULT_SETTINGS;
531
- fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
532
- }
533
- if (!object(parsed))
534
- fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
535
- const allowed = new Set(["concurrency", "modelAliases", "disabledAgentResources"]);
536
- const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
537
- if (unknown)
538
- fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
539
- const concurrency = parsed.concurrency === undefined ? DEFAULT_SETTINGS.concurrency : parsed.concurrency;
540
- if (!positiveInteger(concurrency) || concurrency > 16)
541
- fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
542
- const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
543
- const disabledAgentResources = validateAgentResourceExclusions(parsed.disabledAgentResources, path);
544
- return Object.freeze({ concurrency, ...(modelAliases ? { modelAliases } : {}), ...(disabledAgentResources ? { disabledAgentResources } : {}) });
545
- }
546
- export function resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
547
- const projectSettingsPath = workflowProjectSettingsPath(cwd);
548
- const global = loadSettings(globalSettingsPath).disabledAgentResources ?? EMPTY_AGENT_RESOURCE_EXCLUSIONS;
549
- const project = projectTrusted ? loadSettings(projectSettingsPath).disabledAgentResources ?? EMPTY_AGENT_RESOURCE_EXCLUSIONS : EMPTY_AGENT_RESOURCE_EXCLUSIONS;
550
- const effective = mergeAgentResourceExclusions(global, project);
551
- return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [] };
552
- }
553
- export function saveModelAliases(path = workflowSettingsPath(), aliases = {}) {
554
- const normalized = validateModelAliases(aliases, path);
555
- let parsed = {};
556
- try {
557
- loadSettings(path);
558
- parsed = JSON.parse(readFileSync(path, "utf8"));
559
- }
560
- catch (error) {
561
- if (error.code !== "ENOENT")
562
- throw error;
563
- }
564
- mkdirSync(dirname(path), { recursive: true });
565
- atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
566
- }
567
- export function parseRoleMarkdown(content, strict = false, rolePath) {
568
- if (!strict) {
569
- if (!content.startsWith("---\n"))
570
- return { prompt: content };
571
- const end = content.indexOf("\n---", 4);
572
- if (end < 0)
573
- return { prompt: content };
574
- const meta = {};
575
- for (const line of content.slice(4, end).split("\n")) {
576
- const match = /^(model|thinking|tools|description)\s*:\s*(.+)$/.exec(line.trim());
577
- if (match?.[1] && match[2])
578
- meta[match[1]] = match[2].trim();
579
- }
580
- const tools = meta.tools ? meta.tools.replace(/^\[|\]$/g, "").split(",").map((tool) => tool.trim().replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "")).filter(Boolean) : undefined;
581
- const thinking = meta.thinking?.replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
582
- if (thinking && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinking))
583
- fail("INVALID_METADATA", `Invalid role thinking level: ${thinking}`);
584
- const definition = { prompt: content.slice(end + 4).replace(/^\n/, "") };
585
- if (meta.model)
586
- definition.model = meta.model.replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
587
- if (meta.description)
588
- definition.description = meta.description.replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
589
- if (thinking)
590
- definition.thinking = thinking;
591
- if (tools)
592
- definition.tools = tools;
593
- return definition;
594
- }
595
- const normalized = content.replace(/\r\n?/g, "\n");
596
- if (normalized.startsWith("---\n") && normalized.indexOf("\n---", 3) < 0)
597
- fail("INVALID_METADATA", "Role frontmatter is missing its closing delimiter");
598
- let parsed;
599
- try {
600
- parsed = parseFrontmatter(content);
601
- }
602
- catch (error) {
603
- fail("INVALID_METADATA", `Invalid role frontmatter: ${errorText(error)}`);
604
- }
605
- if (!object(parsed.frontmatter))
606
- fail("INVALID_METADATA", "Role frontmatter must be an object");
607
- const { model, thinking, tools, description, disabledAgentResources } = parsed.frontmatter;
608
- if (model !== undefined && (typeof model !== "string" || model.trim() === ""))
609
- fail("INVALID_METADATA", "Role model must be a non-empty string");
610
- if (thinking !== undefined && (typeof thinking !== "string" || !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinking)))
611
- fail("INVALID_METADATA", `Invalid role thinking level: ${typeof thinking === "string" ? thinking : typeof thinking}`);
612
- if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description)))
613
- fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
614
- if (tools !== undefined && (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string" || tool.trim() === "")))
615
- fail("INVALID_METADATA", "Role tools must be an array of non-empty strings");
616
- const normalizedResources = validateAgentResourceExclusions(disabledAgentResources, rolePath ?? "<role>", "INVALID_METADATA");
617
- return { prompt: parsed.body, ...(typeof description === "string" ? { description: description.trim() } : {}), ...(typeof model === "string" ? { model: model.trim() } : {}), ...(typeof thinking === "string" ? { thinking: thinking } : {}), ...(Array.isArray(tools) ? { tools: tools.map((tool) => tool.trim()) } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
618
- }
619
- const ROLE_DIRECTORY = "pi-extensible-workflows";
620
- export function workflowRoleDirectories(agentDir = getAgentDir()) {
621
- return [join(agentDir, ROLE_DIRECTORY, "roles")];
622
- }
623
- function projectRoleDirectories(root) {
624
- return [join(root, ROLE_DIRECTORY, "roles")];
625
- }
626
- function readAgentDefinitions(dir) {
627
- try {
628
- return Object.fromEntries(readdirSync(dir, { withFileTypes: true })
629
- .filter((entry) => entry.isFile() && extname(entry.name) === ".md")
630
- .map((entry) => { const path = join(dir, entry.name); return [basename(entry.name, ".md"), parseRoleMarkdown(readFileSync(path, "utf8"), true, path)]; }));
631
- }
632
- catch (error) {
633
- if (error.code === "ENOENT")
634
- return {};
635
- throw error;
636
- }
637
- }
638
- function readRoleDefinitions(dirs) {
639
- return Object.fromEntries(dirs.flatMap((dir) => Object.entries(readAgentDefinitions(dir))));
640
- }
641
- export function loadAgentDefinitions(cwd, agentDir = getAgentDir(), projectTrusted = true) {
642
- return deepFreeze({ ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
643
- }
644
- function validateRolePolicies(definitions, roles, availableModels, rootTools, aliases = {}, knownModels = availableModels, settingsPath) {
645
- for (const role of roles) {
646
- const definition = definitions[role];
647
- if (!definition)
648
- continue;
649
- if (definition.model !== undefined) {
650
- const resolved = modelCapability(definition.model, aliases, knownModels, settingsPath);
651
- if (!availableModels.has(resolved)) {
652
- if (Object.prototype.hasOwnProperty.call(aliases, definition.model))
653
- unknownModel(definition.model, resolved, settingsPath);
654
- fail("UNKNOWN_MODEL", `Unknown model for role ${role}: ${resolved}`);
655
- }
656
- }
657
- const missingTool = (definition.tools ?? [...rootTools]).find((tool) => !rootTools.has(tool));
658
- if (missingTool)
659
- fail("UNKNOWN_TOOL", `Unknown tool for role ${role}: ${missingTool}`);
660
- }
661
- }
662
- function validateWorkflowMetadata(value) {
663
- if (!object(value) || typeof value.name !== "string" || value.name.trim() === "")
664
- fail("INVALID_METADATA", "Workflow metadata requires a non-empty name");
665
- if (value.description !== undefined && (typeof value.description !== "string" || value.description.trim() === ""))
666
- fail("INVALID_METADATA", "Workflow description must be a non-empty string when provided");
667
- if (Object.keys(value).some((key) => !["name", "description"].includes(key)))
668
- fail("INVALID_METADATA", "Unknown workflow metadata");
669
- return Object.freeze({ name: value.name.trim(), ...(typeof value.description === "string" ? { description: value.description.trim() } : {}) });
670
- }
671
- function workflowBody(script) {
672
- if (typeof script !== "string" || script.trim() === "")
673
- fail("INVALID_SYNTAX", "Workflow script must be non-empty");
674
- try {
675
- const program = acorn.parse(script, { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true });
676
- const first = program.body[0];
677
- if (first?.type === "ExportNamedDeclaration" && first.declaration?.type === "VariableDeclaration") {
678
- const declarator = first.declaration.declarations[0];
679
- if (declarator?.id.type === "Identifier" && declarator.id.name === "meta")
680
- return script.slice(first.end).replace(/^\s*/, "");
681
- }
682
- return script;
683
- }
684
- catch (error) {
685
- fail("INVALID_SYNTAX", `Invalid workflow syntax: ${errorText(error)}`);
686
- }
687
- }
688
- function parseWorkflow(script) {
689
- const body = workflowBody(script);
690
- try {
691
- new Script(`(async()=>{${body}\n})`);
692
- return acorn.parse(body, { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true });
693
- }
694
- catch (error) {
695
- fail("INVALID_SYNTAX", `Invalid workflow syntax: ${errorText(error)}`);
696
- }
697
- }
698
- function astNode(value) {
699
- return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
700
- }
701
- function astChildren(node) {
702
- const children = [];
703
- for (const value of Object.values(node)) {
704
- if (Array.isArray(value)) {
705
- for (const child of value)
706
- if (astNode(child))
707
- children.push(child);
708
- }
709
- else if (astNode(value))
710
- children.push(value);
711
- }
712
- return children;
713
- }
714
- function workflowCallKind(node) {
715
- if (node.type !== "CallExpression" || node.callee.type !== "Identifier")
716
- return undefined;
717
- const kind = node.callee.name;
718
- return WORKFLOW_CALL_KINDS.includes(kind) ? kind : undefined;
719
- }
720
- function workflowCalls(program) {
721
- const calls = [];
722
- const visit = (node) => {
723
- if (workflowCallKind(node))
724
- calls.push(node);
725
- for (const child of astChildren(node))
726
- visit(child);
727
- };
728
- visit(program);
729
- return calls.sort((left, right) => left.start - right.start);
730
- }
731
- function workflowCallsWithStructure(program) {
732
- const calls = [];
733
- const visit = (node, context) => {
734
- let current = context;
735
- if (node.type === "Property" && current.structure.length) {
736
- const scope = current.structure.at(-1);
737
- const key = node.key.type === "Identifier" ? node.key.name : node.key.type === "Literal" ? String(node.key.value) : undefined;
738
- if (scope?.key === null && key)
739
- current = { ...current, structure: [...current.structure.slice(0, -1), { ...scope, key }] };
740
- }
741
- const operation = workflowCallKind(node);
742
- if (operation) {
743
- const call = node;
744
- const execution = operation === "parallel" ? "parallel" : operation === "pipeline" ? "sequential" : current.execution;
745
- calls.push({ call, execution, structure: current.structure });
746
- for (const [index, argument] of call.arguments.entries()) {
747
- if (argument.type === "SpreadElement")
748
- continue;
749
- const scopeKind = operation === "parallel" && index === 1 ? "parallel" : operation === "pipeline" && index === 2 ? "pipeline" : undefined;
750
- visit(argument, scopeKind ? { execution, structure: [...current.structure, { kind: scopeKind, name: staticString(callArgument(call, 0)), key: null }] } : current);
751
- }
752
- return;
753
- }
754
- for (const child of astChildren(node))
755
- visit(child, current);
756
- };
757
- visit(program, { execution: "sequential", structure: [] });
758
- return calls.sort((left, right) => left.call.start - right.call.start);
759
- }
760
- function validateDirectPrimitiveReferences(program, name) {
761
- const visit = (node, parent) => {
762
- if (node.type === "Identifier" && node.name === name) {
763
- const directCall = parent?.type === "CallExpression" && parent.callee === node;
764
- const propertyKey = parent?.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand;
765
- if (!directCall && !propertyKey)
766
- fail("INVALID_METADATA", `${name} calls must use a direct ${name}(...) call; aliases and indirect calls are unsupported`);
767
- }
768
- for (const child of astChildren(node))
769
- visit(child, node);
770
- };
771
- visit(program);
772
- }
773
- function hasIdentifier(node, name) {
774
- if (node.type === "Identifier" && node.name === name)
775
- return true;
776
- return astChildren(node).some((child) => hasIdentifier(child, name));
777
- }
778
- const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
779
- const INTERNAL_CONVERSATION_NAME = "__pi_extensible_workflows_conversation";
780
- const INTERNAL_WORKTREE_NAME = "__pi_extensible_workflows_withWorktree";
781
- function callHasTrailingComma(source, call) {
782
- let previous;
783
- let current;
784
- for (const token of acorn.tokenizer(source.slice(call.start, call.end), { ecmaVersion: "latest", sourceType: "module" })) {
785
- previous = current;
786
- current = token;
787
- }
788
- return current?.type.label === ")" && previous?.type.label === ",";
789
- }
790
- function instrumentWorkflow(script) {
791
- const body = workflowBody(script);
792
- if (!body.trim())
793
- return body;
794
- const program = parseWorkflow(body);
795
- if (hasIdentifier(program, INTERNAL_AGENT_NAME))
796
- fail("INVALID_METADATA", `${INTERNAL_AGENT_NAME} is reserved for workflow agent instrumentation`);
797
- if (hasIdentifier(program, INTERNAL_CONVERSATION_NAME))
798
- fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
799
- if (hasIdentifier(program, INTERNAL_WORKTREE_NAME))
800
- fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
801
- const calls = workflowCalls(program).filter((call) => ["agent", "conversation", "withWorktree"].includes(call.callee.name));
802
- const edits = calls.flatMap((call) => {
803
- const callSite = `${String(call.start)}:${String(call.end)}`;
804
- const hiddenArgument = call.arguments.length === 0 || callHasTrailingComma(body, call) ? "" : ", ";
805
- return [
806
- { start: call.callee.start, end: call.callee.end, text: call.callee.name === "agent" ? INTERNAL_AGENT_NAME : call.callee.name === "conversation" ? INTERNAL_CONVERSATION_NAME : INTERNAL_WORKTREE_NAME },
807
- { start: call.end - 1, end: call.end - 1, text: `${hiddenArgument}${JSON.stringify(callSite)}` },
808
- ];
809
- }).sort((left, right) => right.start - left.start);
810
- let instrumented = body;
811
- for (const edit of edits)
812
- instrumented = instrumented.slice(0, edit.start) + edit.text + instrumented.slice(edit.end);
813
- return instrumented;
814
- }
815
- function literalString(node) {
816
- return node?.type === "Literal" && typeof node.value === "string" ? node.value : undefined;
817
- }
818
- function propertyNode(node, name) {
819
- if (node?.type !== "ObjectExpression")
820
- return undefined;
821
- for (let index = node.properties.length - 1; index >= 0; index -= 1) {
822
- const property = node.properties[index];
823
- if (!property || property.type === "SpreadElement" || property.computed)
824
- return undefined;
825
- const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
826
- if (key === name)
827
- return property.value;
828
- }
829
- return undefined;
830
- }
831
- function stableName(node) {
832
- if (!node)
833
- return false;
834
- if (node.type !== "ObjectExpression") {
835
- if (["Literal", "ArrayExpression", "ArrowFunctionExpression", "FunctionExpression", "ClassExpression", "TemplateLiteral", "UnaryExpression", "UpdateExpression", "BinaryExpression"].includes(node.type))
836
- return false;
837
- return undefined;
838
- }
839
- let result = false;
840
- for (const property of node.properties) {
841
- if (property.type === "SpreadElement" || property.computed) {
842
- result = undefined;
843
- continue;
844
- }
845
- const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
846
- if (key !== "name")
847
- continue;
848
- const value = literalString(property.value);
849
- result = value === undefined ? property.value.type === "Literal" ? false : undefined : value.trim() !== "";
850
- }
851
- return result;
852
- }
853
- function jsonValue(value, seen = new Set()) {
854
- if (value === null || typeof value === "boolean" || typeof value === "string")
855
- return true;
856
- if (typeof value === "number")
857
- return Number.isFinite(value);
858
- if (typeof value !== "object" || seen.has(value))
859
- return false;
860
- if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
861
- return false;
862
- const keys = Reflect.ownKeys(value);
863
- if (keys.some((key) => typeof key !== "string"))
864
- return false;
865
- seen.add(value);
866
- const values = Array.isArray(value) ? Array.from(value) : keys.map((key) => value[key]);
867
- const valid = values.every((item) => jsonValue(item, seen));
868
- seen.delete(value);
869
- return valid;
870
- }
871
- function workflowPrompt(template, values) {
872
- if (typeof template !== "string")
873
- fail("INVALID_METADATA", "prompt() template must be a string");
874
- if (!object(values) || Array.isArray(values) || !jsonValue(values))
875
- fail("INVALID_METADATA", "prompt() values must be a plain JSON-compatible object");
876
- const placeholders = [...template.matchAll(/{{|}}|{([A-Za-z_$][\w$]*)}/g)].flatMap((match) => match[1] === undefined ? [] : [match[1]]);
877
- const used = new Set(placeholders);
878
- const keys = Object.keys(values);
879
- const missing = placeholders.find((key) => !Object.prototype.hasOwnProperty.call(values, key));
880
- if (missing)
881
- fail("INVALID_METADATA", `Missing prompt value "${missing}"`);
882
- const unused = keys.find((key) => !used.has(key));
883
- if (unused !== undefined)
884
- fail("INVALID_METADATA", `Unused prompt value "${unused}"`);
885
- return template.replace(/{{|}}|{([A-Za-z_$][\w$]*)}/g, (match, key) => match === "{{" ? "{" : match === "}}" ? "}" : typeof values[key] === "string" ? values[key] : JSON.stringify(values[key], null, 2));
886
- }
887
- function validateSchema(schema, at = "schema") {
888
- if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema))
889
- fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
890
- if (typeof schema.type !== "string" && !Array.isArray(schema.type) && schema.$ref === undefined && schema.anyOf === undefined && schema.oneOf === undefined && schema.allOf === undefined && schema.const === undefined && schema.enum === undefined)
891
- fail("INVALID_SCHEMA", `${at} has no JSON Schema shape`);
892
- if (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string")))
893
- fail("INVALID_SCHEMA", `${at}.required must be an array of strings`);
894
- if (schema.properties !== undefined && !object(schema.properties))
895
- fail("INVALID_SCHEMA", `${at}.properties must be an object`);
896
- }
897
- const AGENT_OPTION_KEYS = new Set(["label", "model", "thinking", "tools", "role", "outputSchema", "retries", "timeoutMs"]);
898
- function validateAgentOption(key, value, aliases, knownModels, settingsPath) {
899
- switch (key) {
900
- case "label":
901
- if (typeof value !== "string" || !value.trim())
902
- fail("INVALID_METADATA", "agent label must be a non-empty string");
903
- break;
904
- case "model":
905
- if (typeof value !== "string" || !value.trim())
906
- fail("INVALID_METADATA", "agent model must be a non-empty string");
907
- if (aliases !== undefined)
908
- resolveModelReference(value, aliases, knownModels, settingsPath);
909
- break;
910
- case "thinking":
911
- if (typeof value !== "string" || !parseThinking(value))
912
- fail("INVALID_METADATA", "agent thinking must be off, minimal, low, medium, high, xhigh, or max");
913
- break;
914
- case "tools":
915
- if (!Array.isArray(value) || value.some((tool) => typeof tool !== "string"))
916
- fail("INVALID_METADATA", "agent tools must be an array of strings");
917
- break;
918
- case "role":
919
- if (typeof value !== "string" || !value.trim())
920
- fail("INVALID_METADATA", "agent role must be a non-empty string");
921
- break;
922
- case "outputSchema":
923
- validateSchema(value, "agent outputSchema");
924
- break;
925
- case "retries":
926
- if (!Number.isInteger(value) || value < 0)
927
- fail("INVALID_METADATA", "agent retries must be a non-negative integer");
928
- break;
929
- case "timeoutMs":
930
- if (value !== null && !positiveInteger(value))
931
- fail("INVALID_METADATA", "agent timeoutMs must be null or a positive integer");
932
- break;
933
- }
934
- }
935
- function validateAgentOptions(value) {
936
- if (!object(value) || !jsonValue(value))
937
- fail("INVALID_METADATA", "agent options must be a JSON object");
938
- for (const [key, option] of Object.entries(value))
939
- if (AGENT_OPTION_KEYS.has(key))
940
- validateAgentOption(key, option);
941
- if (typeof value.role === "string" && ["model", "thinking", "tools"].some((key) => Object.prototype.hasOwnProperty.call(value, key)))
942
- fail("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
943
- return value;
944
- }
945
- function staticValue(node) {
946
- if (!node)
947
- return { known: false };
948
- if (node.type === "Literal")
949
- return { known: true, value: node.value };
950
- if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")) {
951
- const argument = staticValue(node.argument);
952
- return argument.known && typeof argument.value === "number" ? { known: true, value: node.operator === "-" ? -argument.value : argument.value } : { known: false };
953
- }
954
- if (node.type === "ArrayExpression") {
955
- const values = [];
956
- for (const element of node.elements) {
957
- if (!element || element.type === "SpreadElement")
958
- return { known: false };
959
- const value = staticValue(element);
960
- if (!value.known)
961
- return { known: false };
962
- values.push(value.value);
963
- }
964
- return { known: true, value: values };
965
- }
966
- if (node.type === "ObjectExpression") {
967
- const value = {};
968
- for (const property of node.properties) {
969
- if (property.type === "SpreadElement" || property.computed)
970
- return { known: false };
971
- const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
972
- const child = staticValue(property.value);
973
- if (!key || !child.known)
974
- return { known: false };
975
- value[key] = child.value;
976
- }
977
- return { known: true, value };
978
- }
979
- return { known: false };
980
- }
981
- function callArgument(call, index) {
982
- const argument = call.arguments[index];
983
- return argument?.type === "SpreadElement" ? undefined : argument;
984
- }
985
- function staticString(node) {
986
- const value = staticValue(node);
987
- return value.known && typeof value.value === "string" ? value.value : null;
988
- }
989
- export function inspectWorkflowScript(script) {
990
- return workflowCallsWithStructure(parseWorkflow(script)).map(({ call, execution, structure }) => {
991
- const kind = call.callee.name;
992
- const first = callArgument(call, 0);
993
- const options = callArgument(call, 1);
994
- const placement = { execution, structure };
995
- if (kind === "agent" || kind === "conversation") {
996
- const retries = staticValue(propertyNode(options, "retries"));
997
- const outputSchema = staticValue(propertyNode(options, "outputSchema"));
998
- const optionKeys = options?.type === "ObjectExpression" ? options.properties.flatMap((property) => {
999
- if (property.type === "SpreadElement" || property.computed)
1000
- return [];
1001
- const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
1002
- return key ? [key] : [];
1003
- }) : [];
1004
- const knownOptions = Object.fromEntries(optionKeys.flatMap((key) => { const value = staticValue(propertyNode(options, key)); return value.known && jsonValue(value.value) ? [[key, value.value]] : []; }));
1005
- const base = { ...placement, kind, start: call.start, end: call.end, name: kind === "conversation" ? staticString(first) : null, prompt: kind === "conversation" ? null : staticString(first), model: staticString(propertyNode(options, "model")), label: staticString(propertyNode(options, "label")), role: staticString(propertyNode(options, "role")) };
1006
- return { ...base, ...(retries.known && typeof retries.value === "number" ? { retries: retries.value } : {}), ...(outputSchema.known && object(outputSchema.value) ? { outputSchema: outputSchema.value } : {}), ...(optionKeys.length ? { options: knownOptions, optionKeys } : {}) };
1007
- }
1008
- if (kind === "checkpoint")
1009
- return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null, role: null };
1010
- return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
1011
- });
1012
- }
1013
- function validateStaticAgentOptions(node, aliases = {}, knownModels, settingsPath) {
1014
- if (node?.type !== "ObjectExpression")
1015
- return;
1016
- const options = staticValue(node);
1017
- if (options.known && object(options.value) && typeof options.value.role === "string" && ["model", "thinking", "tools"].some((key) => Object.prototype.hasOwnProperty.call(options.value, key)))
1018
- fail("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
1019
- for (const key of AGENT_OPTION_KEYS) {
1020
- const value = staticValue(propertyNode(node, key));
1021
- if (value.known)
1022
- validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
1023
- }
1024
- }
1025
- function validateStaticWithWorktree(call) {
1026
- if (call.arguments.some((argument) => argument.type === "SpreadElement"))
1027
- return;
1028
- if (call.arguments.length !== 1 && call.arguments.length !== 2)
1029
- fail("INVALID_METADATA", "withWorktree requires a callback or a name and callback");
1030
- const callback = call.arguments[call.arguments.length - 1];
1031
- if (staticValue(callback).known)
1032
- fail("INVALID_METADATA", "withWorktree callback must be a function");
1033
- if (call.arguments.length === 2) {
1034
- const name = staticValue(call.arguments[0]);
1035
- if (name.known && (typeof name.value !== "string" || !name.value.trim()))
1036
- fail("INVALID_METADATA", "withWorktree name must be a non-empty string");
1037
- }
1038
- }
1039
- const RESERVED_GLOBALS = new Set(["agent", "conversation", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
1040
- const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
1041
- const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
1042
- export class WorkflowRegistry {
1043
- #extensions = new Set();
1044
- #globals = new Map();
1045
- #workflows = new Map();
1046
- #hooks = new Map();
1047
- #frozen = false;
1048
- get frozen() { return this.#frozen; }
1049
- freeze() { this.#frozen = true; }
1050
- register(extension) {
1051
- if (this.#frozen)
1052
- fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
1053
- if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "workflows", "agentSetupHooks"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
1054
- fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
1055
- const functions = extension.functions ?? {};
1056
- const variables = extension.variables ?? {};
1057
- const workflows = extension.workflows ?? {};
1058
- const agentSetupHooks = extension.agentSetupHooks ?? {};
1059
- if (!object(functions) || !object(variables) || !object(workflows) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(workflows).length === 0 && Object.keys(agentSetupHooks).length === 0))
1060
- fail("INVALID_METADATA", "Workflow extensions require functions, variables, workflows, or agent setup hooks");
1061
- const names = [...Object.keys(functions), ...Object.keys(variables)];
1062
- if (new Set(names).size !== names.length)
1063
- fail("GLOBAL_COLLISION", "Global name collision inside extension");
1064
- for (const name of names) {
1065
- if (!IDENTIFIER.test(name) || name.startsWith("__pi_extensible_workflows_"))
1066
- fail("INVALID_METADATA", `Invalid global name: ${name}`);
1067
- if (RESERVED_GLOBALS.has(name))
1068
- fail("GLOBAL_COLLISION", `Global name is reserved: ${name}`);
1069
- if (this.#globals.has(name))
1070
- fail("GLOBAL_COLLISION", `Global name is already registered: ${name}`);
1071
- }
1072
- for (const [name, fn] of Object.entries(functions)) {
1073
- if (!object(fn) || Object.keys(fn).some((key) => !["description", "input", "output", "run"].includes(key)) || typeof fn.description !== "string" || !fn.description.trim() || typeof fn.run !== "function")
1074
- fail("INVALID_METADATA", `Invalid workflow function: ${name}`);
1075
- validateSchema(fn.input, `${name} input`);
1076
- validateSchema(fn.output, `${name} output`);
1077
- if (fn.input.type !== "object")
1078
- fail("INVALID_SCHEMA", `${name} input must describe one object`);
1079
- }
1080
- for (const [name, variable] of Object.entries(variables)) {
1081
- if (!object(variable) || Object.keys(variable).some((key) => !["description", "schema", "resolve"].includes(key)) || typeof variable.description !== "string" || !variable.description.trim() || typeof variable.resolve !== "function")
1082
- fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
1083
- validateSchema(variable.schema, `${name} schema`);
1084
- }
1085
- for (const [name, workflow] of Object.entries(workflows)) {
1086
- if (!IDENTIFIER.test(name) || !object(workflow) || Object.keys(workflow).some((key) => !["description", "script"].includes(key)) || typeof workflow.description !== "string" || !workflow.description.trim() || typeof workflow.script !== "string" || !workflow.script.trim())
1087
- fail("INVALID_METADATA", `Invalid workflow script: ${name}`);
1088
- if (this.#workflows.has(name))
1089
- fail("DUPLICATE_NAME", `Reusable workflow already registered: ${name}`);
1090
- parseWorkflow(workflow.script);
1091
- }
1092
- for (const [name, hook] of Object.entries(agentSetupHooks)) {
1093
- if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority)))
1094
- fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
1095
- if (this.#hooks.has(name))
1096
- fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
1097
- }
1098
- const stored = deepFreeze({ ...extension, functions, variables, workflows, agentSetupHooks });
1099
- this.#extensions.add(stored);
1100
- for (const name of names)
1101
- this.#globals.set(name, name);
1102
- for (const [name, workflow] of Object.entries(workflows))
1103
- this.#workflows.set(name, workflow);
1104
- for (const [name, hook] of Object.entries(agentSetupHooks))
1105
- this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
1106
- }
1107
- workflow(name) {
1108
- if (!IDENTIFIER.test(name))
1109
- fail("MISSING_WORKFLOW", `Registered workflows require an unqualified name: ${name}`);
1110
- const workflow = this.#workflows.get(name);
1111
- if (!workflow)
1112
- fail("MISSING_WORKFLOW", `Workflow is unavailable: ${name}`);
1113
- return workflow;
1114
- }
1115
- workflows() {
1116
- return Object.freeze(Object.fromEntries(this.#workflows));
1117
- }
1118
- catalog() {
1119
- const functions = [];
1120
- const variables = [];
1121
- const workflows = [];
1122
- for (const extension of this.#extensions) {
1123
- for (const [name, fn] of Object.entries(extension.functions ?? {}))
1124
- functions.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: fn.description, input: structuredClone(fn.input), output: structuredClone(fn.output) });
1125
- for (const [name, variable] of Object.entries(extension.variables ?? {}))
1126
- variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
1127
- for (const [name, workflow] of Object.entries(extension.workflows ?? {}))
1128
- workflows.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: workflow.description });
1129
- }
1130
- let aliases;
1131
- try {
1132
- aliases = loadSettings().modelAliases;
1133
- }
1134
- catch {
1135
- aliases = undefined;
1136
- }
1137
- const sort = (left, right) => left.name.localeCompare(right.name);
1138
- return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort), workflows: workflows.sort(sort), ...(aliases ? { modelAliases: structuredClone(aliases) } : {}) });
1139
- }
1140
- globals() {
1141
- return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.keys(extension.functions ?? {}).map((name) => [name, { name }]))));
1142
- }
1143
- async invokeFunction(name, input, context, path, journal) {
1144
- const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
1145
- if (!fn)
1146
- fail("MISSING_WORKFLOW", `Workflow function is unavailable: ${name}`);
1147
- if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input))
1148
- fail("RESULT_INVALID", `Invalid input for ${name}`);
1149
- const replayed = journal.get(path);
1150
- if (replayed !== undefined) {
1151
- if (!jsonValue(replayed) || !Value.Check(fn.output, replayed))
1152
- fail("RESULT_INVALID", `Invalid replay for ${name}`);
1153
- return structuredClone(replayed);
1154
- }
1155
- const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, invoke: context.invoke, agent: context.agent, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
1156
- if (!jsonValue(result) || !Value.Check(fn.output, result))
1157
- fail("RESULT_INVALID", `Invalid output from ${name}`);
1158
- const stored = structuredClone(result);
1159
- journal.put(path, stored);
1160
- return structuredClone(stored);
1161
- }
1162
- variables() {
1163
- return [...this.#extensions].flatMap((extension) => Object.entries(extension.variables ?? {}).map(([name, variable]) => ({ name, variable })));
1164
- }
1165
- agentSetupHooks() {
1166
- return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
1167
- }
1168
- }
1169
- const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
1170
- const globalRegistry = globalThis;
1171
- function createWorkflowRegistryApi(registry) {
1172
- return {
1173
- get frozen() { return registry.frozen; },
1174
- freeze: () => { registry.freeze(); },
1175
- register: (extension) => { registry.register(extension); },
1176
- workflow: (name) => registry.workflow(name),
1177
- workflows: () => registry.workflows(),
1178
- catalog: () => registry.catalog(),
1179
- globals: () => registry.globals(),
1180
- invokeFunction: (...args) => registry.invokeFunction(...args),
1181
- variables: () => registry.variables(),
1182
- agentSetupHooks: () => registry.agentSetupHooks(),
1183
- };
1184
- }
1185
- function workflowRegistryHost() {
1186
- return globalRegistry[WORKFLOW_REGISTRY_KEY] ??= { api: createWorkflowRegistryApi(new WorkflowRegistry()) };
1187
- }
1188
- function resetWorkflowRegistry() {
1189
- workflowRegistryHost().api = createWorkflowRegistryApi(new WorkflowRegistry());
1190
- }
1191
- function beginWorkflowExtensionLoading() {
1192
- if (workflowRegistryHost().api.frozen)
1193
- resetWorkflowRegistry();
1194
- }
1195
- function loadingRegistry() { return workflowRegistryHost().api; }
1196
- beginWorkflowExtensionLoading();
1197
- export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
1198
- export function workflowCatalog() { return loadingRegistry().catalog(); }
1199
- export function registeredWorkflowDefinitions() { return loadingRegistry().workflows(); }
1200
- export function formatWorkflowPreview(args) {
1201
- const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : typeof args.workflow === "string" && args.workflow.trim() ? args.workflow : "workflow";
1202
- if (typeof args.script !== "string" || !args.script.trim())
1203
- return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered workflow" : ""}`;
1204
- return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
1205
- }
1206
- export const WORKFLOW_TOOL_LABEL = "Workflow";
1207
- export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
1208
- export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that orchestrates subagents. Inline scripts require a name; registered workflows use their workflow name. Runs in the background by default; completion arrives as a follow-up message.";
1209
- export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
1210
- name: Type.Optional(Type.String({ description: "Workflow name for inline scripts" })),
1211
- description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
1212
- script: Type.Optional(Type.String({ description: "Immutable workflow source without metadata" })),
1213
- workflow: Type.Optional(Type.String({ description: "Registered reusable workflow as an unqualified name" })),
1214
- args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
1215
- foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of running in the background" })),
1216
- concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16 })),
1217
- budget: Type.Optional(Type.Unknown({ description: "Optional aggregate soft and hard run budgets" })),
1218
- });
1219
- function hasDynamicAgentRole(node) {
1220
- if (!node)
1221
- return false;
1222
- if (node.type !== "ObjectExpression")
1223
- return true;
1224
- for (let index = node.properties.length - 1; index >= 0; index -= 1) {
1225
- const property = node.properties[index];
1226
- if (!property || property.type === "SpreadElement" || property.computed)
1227
- return true;
1228
- const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
1229
- if (key === "role")
1230
- return literalString(property.value) === undefined;
1231
- }
1232
- return false;
1233
- }
1234
- export function preflight(script, capabilities, schemas = [], metadata = { name: "workflow" }) {
1235
- const checkedMetadata = validateWorkflowMetadata(metadata);
1236
- const program = parseWorkflow(script);
1237
- if (hasIdentifier(program, INTERNAL_AGENT_NAME))
1238
- fail("INVALID_METADATA", `${INTERNAL_AGENT_NAME} is reserved for workflow agent instrumentation`);
1239
- if (hasIdentifier(program, INTERNAL_CONVERSATION_NAME))
1240
- fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
1241
- if (hasIdentifier(program, INTERNAL_WORKTREE_NAME))
1242
- fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
1243
- validateDirectPrimitiveReferences(program, "withWorktree");
1244
- validateDirectPrimitiveReferences(program, "conversation");
1245
- for (const [index, schema] of schemas.entries())
1246
- validateSchema(schema, `schema[${String(index)}]`);
1247
- const calls = workflowCalls(program);
1248
- const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase) => phase !== undefined);
1249
- for (const call of calls) {
1250
- const operation = call.callee.name;
1251
- if (operation === "agent" || operation === "conversation") {
1252
- if (operation === "conversation" && (!literalString(call.arguments[0])?.trim() || call.arguments.length > 2))
1253
- fail("INVALID_METADATA", "conversation requires a stable name and optional options object");
1254
- validateStaticAgentOptions(call.arguments[1], capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
1255
- }
1256
- if (operation === "withWorktree")
1257
- validateStaticWithWorktree(call);
1258
- if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement"))
1259
- continue;
1260
- if (operation === "checkpoint" && stableName(call.arguments[0]) === false)
1261
- fail("INVALID_METADATA", `${operation} requires a stable explicit name`);
1262
- if (operation === "parallel" && (call.arguments.length !== 2 || !literalString(call.arguments[0])?.trim() || call.arguments[1]?.type !== "ObjectExpression"))
1263
- fail("INVALID_METADATA", "parallel requires an operation name string and tasks record");
1264
- if (operation === "pipeline" && (call.arguments.length !== 3 || !literalString(call.arguments[0])?.trim() || call.arguments[1]?.type !== "ObjectExpression" || call.arguments[2]?.type !== "ObjectExpression"))
1265
- fail("INVALID_METADATA", "pipeline requires an operation name string, items record, and stages record");
1266
- }
1267
- const agentCalls = calls.filter((call) => call.callee.name === "agent" || call.callee.name === "conversation");
1268
- const dynamicAgentRoles = agentCalls.some((call) => hasDynamicAgentRole(call.arguments[1]));
1269
- const staticSchemas = agentCalls.flatMap((call) => { const value = staticValue(propertyNode(call.arguments[1], "outputSchema")); return value.known ? [value.value] : []; });
1270
- for (const [index, schema] of staticSchemas.entries())
1271
- validateSchema(schema, `agent outputSchema[${String(index)}]`);
1272
- const checkedSchemas = [...schemas, ...staticSchemas];
1273
- const modelRefs = agentCalls.flatMap((call) => { const requested = literalString(propertyNode(call.arguments[1], "model")); return requested === undefined ? [] : [{ requested, resolved: modelCapability(requested, capabilities.modelAliases, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath) }]; });
1274
- const models = modelRefs.map(({ resolved }) => resolved);
1275
- const tools = agentCalls.flatMap((call) => {
1276
- const value = propertyNode(call.arguments[1], "tools");
1277
- return value?.type === "ArrayExpression" ? value.elements.flatMap((element) => { const tool = element && element.type !== "SpreadElement" ? literalString(element) : undefined; return tool === undefined ? [] : [tool]; }) : [];
1278
- });
1279
- const agentTypes = agentCalls.flatMap((call) => { const value = literalString(propertyNode(call.arguments[1], "role")); return value === undefined ? [] : [value]; });
1280
- const missingModel = capabilities.skipModelAvailability ? undefined : modelRefs.find(({ resolved }) => !capabilities.models.has(resolved));
1281
- if (missingModel) {
1282
- if (Object.prototype.hasOwnProperty.call(capabilities.modelAliases ?? {}, missingModel.requested))
1283
- unknownModel(missingModel.requested, missingModel.resolved, capabilities.settingsPath);
1284
- fail("UNKNOWN_MODEL", `Unknown model: ${missingModel.resolved}`);
1285
- }
1286
- const missingTool = tools.find((tool) => !capabilities.tools.has(tool));
1287
- if (missingTool)
1288
- fail("UNKNOWN_TOOL", `Unknown tool: ${missingTool}`);
1289
- const missingType = agentTypes.find((type) => !capabilities.agentTypes.has(type));
1290
- if (missingType)
1291
- fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
1292
- return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas), dynamicAgentRoles });
1293
- }
1294
- export function validateWorkflowLaunch(params, context) {
1295
- return validateWorkflowLaunchWithRegistry(params, context, loadingRegistry());
1296
- }
1297
- function validateWorkflowLaunchWithRegistry(params, context, registry) {
1298
- if (Object.prototype.hasOwnProperty.call(params, "maxAgentLaunches"))
1299
- fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
1300
- if (params.script !== undefined && params.workflow !== undefined)
1301
- fail("INVALID_METADATA", "Provide either script or workflow, not both");
1302
- const definition = typeof params.workflow === "string" ? registry.workflow(params.workflow) : undefined;
1303
- const script = typeof params.script === "string" && params.script.trim() ? params.script : definition?.script ?? "";
1304
- if (!script)
1305
- fail("INVALID_SYNTAX", "Provide script or registered workflow");
1306
- const workflowName = typeof params.name === "string" && params.name.trim() ? params.name.trim() : typeof params.workflow === "string" ? params.workflow : "";
1307
- if (!workflowName)
1308
- fail("INVALID_METADATA", "Inline workflows require name");
1309
- const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : definition?.description ? { description: definition.description } : {}) });
1310
- const globalAgentDefinitions = loadAgentDefinitions(context.cwd, undefined, false);
1311
- const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
1312
- const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
1313
- const aliases = context.modelAliases ?? {};
1314
- const knownModels = context.knownModels ?? context.availableModels;
1315
- const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, agentTypes: new Set(Object.keys(agentDefinitions)), modelAliases: aliases, knownModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
1316
- const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
1317
- validateRolePolicies(agentDefinitions, roleNames, context.availableModels, context.rootTools, aliases, knownModels, context.settingsPath);
1318
- return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
1319
- }
1320
- function deepFreeze(value) {
1321
- if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
1322
- Object.freeze(value);
1323
- for (const child of Object.values(value))
1324
- deepFreeze(child);
1325
- }
1326
- return value;
1327
- }
1328
- export function createLaunchSnapshot(input) {
1329
- return deepFreeze(structuredClone({ ...input, identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
1330
- }
1331
- export function loadLaunchSnapshot(input) {
1332
- return deepFreeze(structuredClone(input));
1333
- }
1334
- export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
1335
- export const HEARTBEAT_TIMEOUT_MS = 5000;
1336
- const OUTCOME_ERRORS = new Set(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
1337
- const WORK_RESULT_BRAND = "__workResult";
1338
- const childSource = String.raw `
1339
- "use strict";
1340
- const { AsyncLocalStorage } = require("node:async_hooks");
1341
- const vm = require("node:vm");
1342
- const LIMIT = parseInt(process.argv[2], 10);
1343
- const config = JSON.parse(process.argv[3]);
1344
- for (const key of ["getBuiltinModule","binding","_linkedBinding","dlopen","kill","abort","exit","reallyExit","_kill","umask","chdir","setuid","setgid","seteuid","setegid","setgroups","initgroups"]) {
1345
- if (key in process) process[key] = undefined;
1346
- }
1347
- let nextId = 0;
1348
- let cancelled = false;
1349
- const pending = new Map();
1350
- const inflight = new Set();
1351
- const hasMessage = error => Boolean(error && typeof error === "object" && typeof error.message === "string");
1352
- const errorText = error => hasMessage(error) ? error.message : String(error);
1353
- const errorCode = error => { if (!error || typeof error !== "object") return undefined; const code = error.code; return typeof code === "string" ? code : undefined; };
1354
- const errorAuthored = error => Boolean(error && typeof error === "object" && error.authored === true);
1355
- const workerError = error => { const code = errorCode(error); return { code: code || "INTERNAL_ERROR", message: errorText(error), ...(error && typeof error === "object" && typeof error.failedAt === "string" ? { failedAt: error.failedAt } : {}), ...(errorAuthored(error) || hasMessage(error) && !code ? { authored: true } : {}) }; };
1356
- const workflowError = error => Object.assign(new Error(errorText(error)), workerError(error));
1357
- function send(value) {
1358
- const json = JSON.stringify(value);
1359
- if (json === undefined || Buffer.byteLength(json) > LIMIT) throw Object.assign(new Error("RPC value exceeds the 10 MB JSON boundary"), { code: "RPC_LIMIT_EXCEEDED" });
1360
- process.send(json);
1361
- }
1362
- function rpc(method, args) {
1363
- if (cancelled) throw Object.assign(new Error("Workflow cancelled"), { code: "CANCELLED" });
1364
- const id = ++nextId;
1365
- send({ type: "rpc", id, method, args });
1366
- const promise = new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
1367
- inflight.add(promise);
1368
- void promise.then(() => inflight.delete(promise), () => inflight.delete(promise));
1369
- return promise;
1370
- }
1371
- process.on("message", raw => {
1372
- let message;
1373
- try {
1374
- if (typeof raw !== "string" || Buffer.byteLength(raw) > LIMIT) throw Object.assign(new Error("RPC value exceeds the 10 MB JSON boundary"), { code: "RPC_LIMIT_EXCEEDED" });
1375
- message = JSON.parse(raw);
1376
- } catch (error) { send({ type: "error", error: workerError(error) }); return; }
1377
- if (message.type === "cancel") { cancelled = true; for (const { reject } of pending.values()) reject(Object.assign(new Error("Workflow cancelled"), { code: "CANCELLED" })); pending.clear(); return; }
1378
- if (message.type !== "rpcResult") return;
1379
- const request = pending.get(message.id);
1380
- if (!request) return;
1381
- pending.delete(message.id);
1382
- if (message.ok) request.resolve(message.value);
1383
- else request.reject(workflowError(message.error));
1384
- });
1385
- const heartbeat = setInterval(() => send({ type: "heartbeat" }), 1000);
1386
- send({ type: "heartbeat" });
1387
- const BRAND = "${WORK_RESULT_BRAND}";
1388
- const workError = (code, message) => Object.assign(new Error(message), { code });
1389
- const isBranded = value => value && typeof value === "object" && value[BRAND] === true;
1390
- const unwrap = result => {
1391
- if (!isBranded(result)) return result;
1392
- if (result.ok) return result.value;
1393
- throw Object.assign(workflowError(result.error), { failedAt: result.failedAt });
1394
- };
1395
- const named = (value, kind) => { if (typeof value !== "string" || !value.trim()) throw workError("INVALID_METADATA", kind + " requires a stable explicit name"); return value; };
1396
- const path = (...names) => names.map(encodeURIComponent).join("/");
1397
- const inheritedAgentPath = new AsyncLocalStorage();
1398
- const agentOccurrences = new Map();
1399
- const conversationOccurrences = new Map();
1400
- const worktreeOwners = new AsyncLocalStorage();
1401
- const worktreeOccurrences = new Map();
1402
- const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
1403
- const rejectWorktree = () => { throw workError("INVALID_METADATA", "withWorktree calls must use a direct withWorktree(...) call; aliases and indirect calls are unsupported"); };
1404
- const internalWithWorktree = async (...values) => {
1405
- const callSite = values.pop();
1406
- if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing withWorktree call-site identity");
1407
- if (values.length !== 1 && values.length !== 2) throw workError("INVALID_METADATA", "withWorktree requires a callback or a name and callback");
1408
- const callback = values[values.length - 1];
1409
- if (typeof callback !== "function") throw workError("INVALID_METADATA", "withWorktree callback must be a function");
1410
- let owner;
1411
- if (values.length === 2) {
1412
- if (typeof values[0] !== "string" || !values[0].trim()) throw workError("INVALID_METADATA", "withWorktree name must be a non-empty string");
1413
- owner = path("worktree", "named", values[0].trim());
1414
- } else {
1415
- const inherited = inheritedAgentPath.getStore() || [];
1416
- const occurrenceKey = JSON.stringify([inherited, callSite]);
1417
- const occurrence = (worktreeOccurrences.get(occurrenceKey) || 0) + 1;
1418
- worktreeOccurrences.set(occurrenceKey, occurrence);
1419
- owner = path("worktree", "unnamed", ...inherited, "callsite:" + callSite, "occurrence:" + String(occurrence));
1420
- }
1421
- return await worktreeOwners.run(owner, callback);
1422
- };
1423
- const internalConversation = (...values) => {
1424
- const callSite = values.pop();
1425
- if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow conversation call-site identity");
1426
- const name = values[0];
1427
- if (typeof name !== "string" || !name.trim()) throw workError("INVALID_METADATA", "conversation requires a non-empty name");
1428
- const conversationOptions = values.length < 2 || values[1] === undefined ? {} : values[1];
1429
- if (!conversationOptions || typeof conversationOptions !== "object" || Array.isArray(conversationOptions)) throw workError("INVALID_METADATA", "conversation options must be a JSON object");
1430
- const inherited = inheritedAgentPath.getStore() || [];
1431
- const occurrenceKey = JSON.stringify([inherited, callSite, name]);
1432
- const occurrence = (conversationOccurrences.get(occurrenceKey) || 0) + 1;
1433
- conversationOccurrences.set(occurrenceKey, occurrence);
1434
- const fixedOptions = structuredClone(conversationOptions);
1435
- const defaultTimeout = fixedOptions.timeoutMs;
1436
- const defaultRetries = fixedOptions.retries;
1437
- delete fixedOptions.timeoutMs;
1438
- delete fixedOptions.retries;
1439
- const worktreeOwner = worktreeOwners.getStore();
1440
- let turn = 0;
1441
- let active = false;
1442
- return Object.freeze({
1443
- run(prompt, turnOptions = {}) {
1444
- if (typeof prompt !== "string") throw workError("INVALID_METADATA", "conversation.run prompt must be a string");
1445
- if (!turnOptions || typeof turnOptions !== "object" || Array.isArray(turnOptions) || Object.keys(turnOptions).some(key => key !== "timeoutMs" && key !== "retries")) throw workError("INVALID_METADATA", "conversation.run options only support timeoutMs and retries");
1446
- if (active) throw workError("RESUME_INCOMPATIBLE", "Conversation turns cannot overlap");
1447
- active = true;
1448
- const turnNumber = turn + 1;
1449
- const options = { ...fixedOptions, ...(defaultTimeout !== undefined && turnOptions.timeoutMs === undefined ? { timeoutMs: defaultTimeout } : {}), ...(defaultRetries !== undefined && turnOptions.retries === undefined ? { retries: defaultRetries } : {}), ...turnOptions };
1450
- const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}), conversation: { name: name.trim(), turn: turnNumber } };
1451
- const result = rpc("agent", [prompt, options, identity]).then(value => { const unwrapped = unwrap(value); turn = turnNumber; return unwrapped; }).finally(() => { active = false; });
1452
- Object.defineProperties(result, {
1453
- toJSON: { value() { throw workError("INVALID_METADATA", "Workflow conversation result is a Promise; await it before serialization"); } },
1454
- toString: { value() { throw workError("INVALID_METADATA", "Workflow conversation result is a Promise; await it before interpolation"); } },
1455
- [Symbol.toPrimitive]: { value() { throw workError("INVALID_METADATA", "Workflow conversation result is a Promise; await it before interpolation"); } },
1456
- });
1457
- return result;
1458
- },
1459
- });
1460
- };
1461
- const internalAgent = (...values) => {
1462
- const callSite = values.pop();
1463
- if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow agent call-site identity");
1464
- const inherited = inheritedAgentPath.getStore() || [];
1465
- // ponytail: same-callsite races outside parallel/pipeline lack a stable structural scope and are unsupported.
1466
- const occurrenceKey = JSON.stringify([inherited, callSite]);
1467
- const occurrence = (agentOccurrences.get(occurrenceKey) || 0) + 1;
1468
- agentOccurrences.set(occurrenceKey, occurrence);
1469
- const options = values.length < 2 || values[1] === undefined ? {} : values[1];
1470
- const worktreeOwner = worktreeOwners.getStore();
1471
- const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
1472
- const result = rpc("agent", [values[0], options, identity]).then(unwrap);
1473
- Object.defineProperties(result, {
1474
- toJSON: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before serialization"); } },
1475
- toString: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before interpolation"); } },
1476
- [Symbol.toPrimitive]: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before interpolation"); } },
1477
- });
1478
- return result;
1479
- };
1480
- const agent = rejectAgent;
1481
- const promptPath = (at, key) => /^[A-Za-z_$][\w$]*$/.test(key) ? at + "." + key : at + "[" + JSON.stringify(key) + "]";
1482
- const plainPromptObject = value => {
1483
- const proto = Object.getPrototypeOf(value);
1484
- return proto === null || Object.getPrototypeOf(proto) === null && Object.prototype.hasOwnProperty.call(proto, "constructor") && typeof proto.constructor === "function" && Function.prototype.toString.call(proto.constructor) === Function.prototype.toString.call(Object);
1485
- };
1486
- const promptValue = (value, at, seen) => {
1487
- if (value === null || typeof value === "string" || typeof value === "boolean") return;
1488
- if (typeof value === "number") { if (!Number.isFinite(value)) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" must be a finite number"); return; }
1489
- if (typeof value !== "object") throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" cannot be " + typeof value);
1490
- if (typeof value.then === "function") throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" is a Promise or thenable; await it before calling prompt()");
1491
- if (!Array.isArray(value) && !plainPromptObject(value)) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" must be a plain object");
1492
- if (seen.has(value)) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" contains a cycle");
1493
- const keys = Reflect.ownKeys(value);
1494
- const symbol = keys.find(key => typeof key === "symbol");
1495
- if (symbol) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" contains a symbol key");
1496
- seen.add(value);
1497
- if (Array.isArray(value)) {
1498
- for (let index = 0; index < value.length; index += 1) promptProperty(value, String(index), at + "[" + index + "]", seen);
1499
- for (const key of keys) {
1500
- const index = Number(key);
1501
- if (key !== "length" && !(Number.isInteger(index) && index >= 0 && index < value.length && String(index) === key)) promptProperty(value, key, promptPath(at, key), seen);
1502
- }
1503
- } else for (const key of keys) promptProperty(value, key, promptPath(at, key), seen);
1504
- seen.delete(value);
1505
- };
1506
- const promptProperty = (value, key, at, seen) => {
1507
- const descriptor = Object.getOwnPropertyDescriptor(value, key);
1508
- if (descriptor && (descriptor.get || descriptor.set)) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" cannot use getters or setters");
1509
- promptValue(descriptor && descriptor.value, at, seen);
1510
- };
1511
- const prompt = (template, values) => {
1512
- if (typeof template !== "string") throw workError("INVALID_METADATA", "prompt() template must be a string");
1513
- if (!values || typeof values !== "object" || Array.isArray(values) || !plainPromptObject(values)) throw workError("INVALID_METADATA", "prompt() values must be a plain object");
1514
- const placeholders = [...template.matchAll(/{{|}}|{([A-Za-z_$][\w$]*)}/g)].flatMap(match => match[1] === undefined ? [] : [match[1]]);
1515
- const used = new Set(placeholders);
1516
- const keys = Reflect.ownKeys(values);
1517
- const symbol = keys.find(key => typeof key === "symbol");
1518
- if (symbol) throw workError("INVALID_METADATA", "prompt() values must use string keys");
1519
- const missing = placeholders.find(key => !Object.prototype.hasOwnProperty.call(values, key));
1520
- if (missing) throw workError("INVALID_METADATA", "Missing prompt value \"" + missing + "\"");
1521
- const unused = keys.find(key => !used.has(key));
1522
- if (unused !== undefined) throw workError("INVALID_METADATA", "Unused prompt value \"" + unused + "\"");
1523
- for (const key of keys) promptProperty(values, key, key, new Set());
1524
- return template.replace(/{{|}}|{([A-Za-z_$][\w$]*)}/g, (match, key) => match === "{{" ? "{" : match === "}}" ? "}" : typeof values[key] === "string" ? values[key] : JSON.stringify(values[key], null, 2));
1525
- };
1526
- const checkpoint = input => rpc("checkpoint", [input]).then(unwrap);
1527
- const phase = name => rpc("phase", [name]);
1528
- const log = message => rpc("log", [message]);
1529
- const functionOccurrences = new Map();
1530
- const functionPath = name => {
1531
- const inherited = inheritedAgentPath.getStore() || [];
1532
- const key = JSON.stringify([inherited, name]);
1533
- const occurrence = (functionOccurrences.get(key) || 0) + 1;
1534
- functionOccurrences.set(key, occurrence);
1535
- return path("function", ...inherited, name, String(occurrence));
1536
- };
1537
- const functions = Object.freeze(Object.fromEntries(Object.entries(config.functions || {}).map(([local, target]) => [local, (...values) => {
1538
- if (values.length !== 1 || !values[0] || typeof values[0] !== "object" || Array.isArray(values[0])) throw workError("RESULT_INVALID", local + " requires exactly one JSON object argument");
1539
- const inherited = inheritedAgentPath.getStore() || [];
1540
- const result = rpc("function", [target.name, values[0], functionPath(target.name), worktreeOwners.getStore() || null, inherited]).then(unwrap);
1541
- Object.defineProperty(result, "toJSON", { value() { throw workError("INVALID_METADATA", "Workflow function result is a Promise; await it before serialization"); } });
1542
- return result;
1543
- }])));
1544
- const freeze = value => { if (value && typeof value === "object" && !Object.isFrozen(value)) { Object.freeze(value); for (const child of Object.values(value)) freeze(child); } return value; };
1545
- const recordEntries = (value, kind) => {
1546
- if (!value || typeof value !== "object" || Array.isArray(value)) throw workError("INVALID_METADATA", kind + " must be a record");
1547
- return Object.entries(value);
1548
- };
1549
- const parallel = async (operationName, tasks) => {
1550
- named(operationName, "parallel");
1551
- const entries = recordEntries(tasks, "parallel tasks");
1552
- for (const [name, run] of entries) {
1553
- named(name, "parallel task");
1554
- if (typeof run !== "function") throw workError("INVALID_METADATA", "parallel task values must be run functions");
1555
- }
1556
- const results = await Promise.all(entries.map(async ([name, run]) => {
1557
- try {
1558
- const parent = inheritedAgentPath.getStore() || [];
1559
- return { name, ok: true, value: await inheritedAgentPath.run([...parent, operationName, name], run) };
1560
- } catch (error) {
1561
- if (errorCode(error) === "CANCELLED") throw error;
1562
- const failedAt = error && typeof error === "object" && typeof error.failedAt === "string" ? error.failedAt : undefined;
1563
- return { name, ok: false, failedAt: failedAt ? path(operationName, name, failedAt) : path(operationName, name), error: workerError(error) };
1564
- }
1565
- }));
1566
- const failure = results.find(result => !result.ok);
1567
- if (failure) throw Object.assign(workflowError(failure.error), { failedAt: failure.failedAt });
1568
- return Object.fromEntries(results.map(result => [result.name, result.value]));
1569
- };
1570
- const pipeline = async (operationName, items, stages) => {
1571
- named(operationName, "pipeline");
1572
- const itemEntries = recordEntries(items, "pipeline items");
1573
- const stageEntries = recordEntries(stages, "pipeline stages");
1574
- if (!stageEntries.length) throw workError("INVALID_METADATA", "pipeline requires at least one stage");
1575
- for (const [name] of itemEntries) named(name, "pipeline item");
1576
- for (const [stageName, run] of stageEntries) {
1577
- named(stageName, "pipeline stage");
1578
- if (typeof run !== "function") throw workError("INVALID_METADATA", "pipeline stage values must be run functions");
1579
- }
1580
- const results = await Promise.all(itemEntries.map(async ([name, initial]) => {
1581
- let value = initial;
1582
- let failedAt = path(operationName, name);
1583
- try {
1584
- for (const [stageName, run] of stageEntries) {
1585
- failedAt = path(operationName, name, stageName);
1586
- const parent = inheritedAgentPath.getStore() || [];
1587
- value = await inheritedAgentPath.run([...parent, operationName, name, stageName], () => run(value));
1588
- }
1589
- return { name, ok: true, value };
1590
- } catch (error) {
1591
- if (errorCode(error) === "CANCELLED") throw error;
1592
- const nestedFailedAt = error && typeof error === "object" && typeof error.failedAt === "string" ? error.failedAt : undefined;
1593
- return { name, ok: false, failedAt: nestedFailedAt ? path(failedAt, nestedFailedAt) : failedAt, error: workerError(error) };
1594
- }
1595
- }));
1596
- const failure = results.find(result => !result.ok);
1597
- if (failure) throw Object.assign(workflowError(failure.error), { failedAt: failure.failedAt });
1598
- return Object.fromEntries(results.map(result => [result.name, result.value]));
1599
- };
1600
- const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
1601
- const sandbox = { agent, conversation: internalConversation, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
1602
- for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
1603
- for (const [name, value] of Object.entries(config.variables || {})) Object.defineProperty(sandbox, name, { value: freeze(value), writable: false, configurable: false });
1604
- for (const name of ["Date","eval","Function","WebAssembly","process","require","module","exports","console","fetch","XMLHttpRequest","WebSocket","performance","crypto","setTimeout","setInterval","setImmediate","queueMicrotask","Intl","SharedArrayBuffer","Atomics"]) sandbox[name] = undefined;
1605
- const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
1606
- const body = config.script;
1607
- Promise.resolve().then(() => new vm.Script("(async(__pi_extensible_workflows_agent,__pi_extensible_workflows_conversation,__pi_extensible_workflows_withWorktree)=>{" + body + "\n})", { filename: "workflow.js" }).runInContext(context)(internalAgent, internalConversation, internalWithWorktree))
1608
- .then(async value => { await Promise.all(inflight); send({ type: "result", value: value === undefined ? null : value }); })
1609
- .catch(error => send({ type: "error", error: workerError(error) }))
1610
- .finally(() => clearInterval(heartbeat));
1611
- `;
1612
- function encoded(value) {
1613
- if (!jsonValue(value))
1614
- fail("RPC_LIMIT_EXCEEDED", "RPC values must be JSON-compatible");
1615
- const json = JSON.stringify(value);
1616
- if (Buffer.byteLength(json) > RPC_LIMIT_BYTES)
1617
- fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
1618
- return json;
1619
- }
1620
- function readAgentIdentity(value) {
1621
- if (!object(value))
1622
- fail("INTERNAL_ERROR", "Invalid workflow agent identity");
1623
- const structuralPath = value.structuralPath;
1624
- const callSite = value.callSite;
1625
- const occurrence = value.occurrence;
1626
- const worktreeOwner = value.worktreeOwner;
1627
- const parentBreadcrumb = value.parentBreadcrumb;
1628
- const conversation = value.conversation;
1629
- const parsedConversation = object(conversation) && typeof conversation.name === "string" && Boolean(conversation.name.trim()) && positiveInteger(conversation.turn) ? { name: conversation.name, turn: conversation.turn } : undefined;
1630
- if (!Array.isArray(structuralPath) || !structuralPath.every((part) => typeof part === "string" && Boolean(part.trim())) || typeof callSite !== "string" || !callSite || !positiveInteger(occurrence) || parentBreadcrumb !== undefined && (typeof parentBreadcrumb !== "string" || !parentBreadcrumb.trim()) || worktreeOwner !== undefined && (typeof worktreeOwner !== "string" || !worktreeOwner) || conversation !== undefined && !parsedConversation)
1631
- fail("INTERNAL_ERROR", "Invalid workflow agent identity");
1632
- return { structuralPath: [...structuralPath], callSite, occurrence, ...(typeof parentBreadcrumb === "string" ? { parentBreadcrumb } : {}), ...(typeof worktreeOwner === "string" ? { worktreeOwner } : {}), ...(parsedConversation ? { conversation: parsedConversation } : {}) };
1633
- }
1634
- function agentIdentityPath(identity) {
1635
- return operationPath("agent", ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
1636
- }
1637
- function conversationIdentityPath(identity) {
1638
- if (!identity.conversation)
1639
- throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
1640
- return operationPath("conversation", identity.conversation.name, ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
1641
- }
1642
- function conversationTurnPath(identity) {
1643
- if (!identity.conversation)
1644
- throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
1645
- return operationPath(conversationIdentityPath(identity), `turn:${String(identity.conversation.turn)}`);
1646
- }
1647
- function agentWorktree(identity) {
1648
- return identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {};
1649
- }
1650
- export function runWorkflow(script, args = null, bridge = {}, signal) {
1651
- encoded(args);
1652
- const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
1653
- const childDir = mkdtempSync(join(tmpdir(), "pi-wf-"));
1654
- const childFile = join(childDir, "child.cjs");
1655
- writeFileSync(childFile, childSource);
1656
- const child = fork(childFile, [String(RPC_LIMIT_BYTES), config], {
1657
- execArgv: (() => {
1658
- const filtered = [];
1659
- const skip = new Set(["--input-type", "-e", "--eval", "-p", "--print"]);
1660
- let skipNext = false;
1661
- for (const arg of process.execArgv) {
1662
- if (skipNext) {
1663
- skipNext = false;
1664
- continue;
1665
- }
1666
- if (skip.has(arg) || skip.has(arg.split("=")[0] ?? "")) {
1667
- if (!arg.includes("="))
1668
- skipNext = true;
1669
- continue;
1670
- }
1671
- filtered.push(arg);
1672
- }
1673
- return [...filtered, "--max-old-space-size=128", "--permission", `--allow-fs-read=${childDir}`];
1674
- })(),
1675
- stdio: ["ignore", "ignore", "ignore", "ipc"],
1676
- serialization: "advanced",
1677
- });
1678
- const controller = new AbortController();
1679
- let settled = false;
1680
- let rejectResult = () => undefined;
1681
- let watchdog = setTimeout(() => { stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat"); }, HEARTBEAT_TIMEOUT_MS);
1682
- const result = new Promise((resolve, reject) => {
1683
- rejectResult = reject;
1684
- child.on("message", (raw) => {
1685
- try {
1686
- if (typeof raw !== "string" || Buffer.byteLength(raw) > RPC_LIMIT_BYTES)
1687
- fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
1688
- const message = JSON.parse(raw);
1689
- if (!jsonValue(message))
1690
- fail("RPC_LIMIT_EXCEEDED", "Worker RPC must contain JSON-compatible values");
1691
- if (message.type === "heartbeat") {
1692
- clearTimeout(watchdog);
1693
- watchdog = setTimeout(() => { stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat"); }, HEARTBEAT_TIMEOUT_MS);
1694
- return;
1695
- }
1696
- if (message.type === "result") {
1697
- encoded(message.value);
1698
- finish();
1699
- resolve(message.value ?? null);
1700
- return;
1701
- }
1702
- if (message.type === "error") {
1703
- finish();
1704
- reject(workflowErrorFromWorker(message.error ?? { code: "INTERNAL_ERROR", message: "Worker failed" }));
1705
- return;
1706
- }
1707
- if (message.type === "rpc" && message.id !== undefined)
1708
- void handleRpc(message.id, message.method ?? "", message.args ?? []);
1709
- }
1710
- catch (error) {
1711
- stop(error instanceof WorkflowError ? error.code : "INTERNAL_ERROR", error instanceof Error ? error.message : String(error));
1712
- }
1713
- });
1714
- child.on("error", (error) => { stop("INTERNAL_ERROR", error.message); });
1715
- child.on("exit", (code) => { if (!settled && code !== 0)
1716
- stop("INTERNAL_ERROR", `Workflow child exited with code ${String(code)}`); });
1717
- });
1718
- function killChild() {
1719
- if (!child.killed) {
1720
- child.kill("SIGTERM");
1721
- setTimeout(() => { if (!child.killed)
1722
- child.kill("SIGKILL"); }, 1000).unref();
1723
- }
1724
- }
1725
- function finish() { settled = true; clearTimeout(watchdog); signal?.removeEventListener("abort", cancel); killChild(); rmSync(childDir, { recursive: true, force: true }); }
1726
- function stop(code, message) { if (settled)
1727
- return; controller.abort(); finish(); rejectResult(new WorkflowError(code, message)); }
1728
- function branded(result) { return { ...result, [WORK_RESULT_BRAND]: true }; }
1729
- async function handleRpc(id, method, values) {
1730
- try {
1731
- encoded(values);
1732
- let value = null;
1733
- if (method === "agent") {
1734
- if (!bridge.agent)
1735
- fail("AGENT_FAILED", "No agent bridge is available");
1736
- if (typeof values[0] !== "string")
1737
- fail("INTERNAL_ERROR", "agent prompt must be a string");
1738
- const opts = validateAgentOptions(values[1]);
1739
- const identity = readAgentIdentity(values[2]);
1740
- const path = agentIdentityPath(identity);
1741
- const label = typeof opts.label === "string" ? opts.label : typeof opts.role === "string" ? opts.role : "agent";
1742
- try {
1743
- const result = await bridge.agent(values[0], opts, controller.signal, identity);
1744
- value = branded({ name: label, ok: true, value: result ?? null });
1745
- }
1746
- catch (error) {
1747
- const typed = asWorkflowError(error);
1748
- if (!OUTCOME_ERRORS.has(typed.code))
1749
- throw typed;
1750
- value = branded({ name: label, ok: false, failedAt: path, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
1751
- }
1752
- }
1753
- else if (method === "checkpoint") {
1754
- if (!bridge.checkpoint || !object(values[0]))
1755
- fail("INTERNAL_ERROR", "checkpoint requires an available bridge and object input");
1756
- const name = typeof values[0].name === "string" ? values[0].name : "checkpoint";
1757
- try {
1758
- const result = await bridge.checkpoint(values[0], controller.signal);
1759
- if (typeof result !== "boolean")
1760
- fail("INTERNAL_ERROR", "checkpoint must return a boolean");
1761
- value = branded({ name, ok: true, value: result ? "approved" : "rejected" });
1762
- }
1763
- catch (error) {
1764
- const typed = asWorkflowError(error);
1765
- if (!OUTCOME_ERRORS.has(typed.code))
1766
- throw typed;
1767
- value = branded({ name, ok: false, failedAt: name, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
1768
- }
1769
- }
1770
- else if (method === "function") {
1771
- const worktreeOwner = values[3] === undefined || values[3] === null ? undefined : typeof values[3] === "string" && values[3] ? values[3] : fail("INTERNAL_ERROR", "function worktree scope is invalid");
1772
- const structuralPath = values[4] === undefined ? [] : values[4];
1773
- if (!Array.isArray(structuralPath) || !structuralPath.every((part) => typeof part === "string" && Boolean(part.trim())))
1774
- fail("INTERNAL_ERROR", "function structural scope is invalid");
1775
- if (!bridge.function || typeof values[0] !== "string" || !object(values[1]) || typeof values[2] !== "string")
1776
- fail("INTERNAL_ERROR", "function requires an available bridge, name, object input, and path");
1777
- const name = values[0];
1778
- try {
1779
- const result = await bridge.function(values[0], values[1], values[2], controller.signal, worktreeOwner, structuralPath);
1780
- value = branded({ name, ok: true, value: result ?? null });
1781
- }
1782
- catch (error) {
1783
- const typed = asWorkflowError(error);
1784
- if (!OUTCOME_ERRORS.has(typed.code))
1785
- throw typed;
1786
- value = branded({ name, ok: false, failedAt: name, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
1787
- }
1788
- }
1789
- else if (method === "phase") {
1790
- if (typeof values[0] !== "string")
1791
- fail("INTERNAL_ERROR", "phase name must be a string");
1792
- await bridge.phase?.(values[0]);
1793
- }
1794
- else if (method === "log") {
1795
- if (typeof values[0] !== "string")
1796
- fail("INTERNAL_ERROR", "log message must be a string");
1797
- await bridge.log?.(values[0]);
1798
- }
1799
- else
1800
- fail("INTERNAL_ERROR", `Unknown worker RPC method: ${method}`);
1801
- encoded(value);
1802
- child.send(encoded({ type: "rpcResult", id, ok: true, value }));
1803
- }
1804
- catch (error) {
1805
- const typed = asWorkflowError(error);
1806
- child.send(encoded({ type: "rpcResult", id, ok: false, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } }));
1807
- }
1808
- }
1809
- function cancel() {
1810
- if (settled)
1811
- return;
1812
- controller.abort();
1813
- child.send(encoded({ type: "cancel" }));
1814
- stop("CANCELLED", "Workflow cancelled");
1815
- }
1816
- if (signal?.aborted)
1817
- cancel();
1818
- else
1819
- signal?.addEventListener("abort", cancel, { once: true });
1820
- return { result, cancel };
1821
- }
1822
- function nativeSessionReference(attempt) {
1823
- return { sessionId: attempt.sessionId, sessionFile: attempt.sessionFile };
1824
- }
1825
- export async function persistActiveAgentAttempt(store, id, active) {
1826
- await store.updateState((run) => {
1827
- const agent = run.agents.find((candidate) => candidate.id === id);
1828
- if (!agent)
1829
- throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
1830
- const accounting = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
1831
- const details = [...(agent.attemptDetails ?? []).filter((candidate) => candidate.attempt !== active.attempt), { ...active, accounting }];
1832
- const nativeSessions = run.nativeSessions.some(({ sessionId }) => sessionId === active.sessionId) ? run.nativeSessions : [...run.nativeSessions, nativeSessionReference(active)];
1833
- return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: Math.max(candidate.attempts, active.attempt), attemptDetails: details } : candidate), nativeSessions };
1834
- });
1835
- }
1836
- export async function persistAgentAttempts(store, id, attempts) {
1837
- await store.updateState((run) => {
1838
- const agent = run.agents.find((candidate) => candidate.id === id);
1839
- if (!agent)
1840
- throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
1841
- const total = attempts.reduce((sum, attempt) => ({ input: sum.input + attempt.accounting.input, output: sum.output + attempt.accounting.output, cacheRead: sum.cacheRead + attempt.accounting.cacheRead, cacheWrite: sum.cacheWrite + attempt.accounting.cacheWrite, cost: sum.cost + attempt.accounting.cost }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
1842
- const attemptDetails = attempts.map(({ attempt, sessionId, sessionFile, error, accounting, setup }) => ({ attempt, sessionId, sessionFile, ...(error ? { error } : {}), accounting, ...(setup ? { setup } : {}) }));
1843
- const sessionIds = new Set(attempts.map(({ sessionId }) => sessionId));
1844
- return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: attempts.length, attemptDetails, accounting: total } : candidate), nativeSessions: [...run.nativeSessions.filter(({ sessionId }) => !sessionIds.has(sessionId)), ...attempts.map((attempt) => nativeSessionReference(attempt))] };
1845
- });
1846
- }
1847
- function agentGroupKey(agent) { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
1848
- function agentGroupLabel(agents) {
1849
- const structural = agents[0]?.structuralPath ?? [];
1850
- const breadcrumbs = [...new Set(agents.map((agent) => agent.parentBreadcrumb).filter((value) => Boolean(value)))];
1851
- return [...(structural.length ? [structural.join(" > ")] : []), ...(breadcrumbs.length === 1 ? breadcrumbs : breadcrumbs.length ? [breadcrumbs.join(" | ")] : [])].join(" > ") || "Agents";
1852
- }
1853
- function agentGroups(agents) {
1854
- const byId = new Map(agents.map((agent) => [agent.id, agent]));
1855
- const groups = new Map();
1856
- for (const [index, agent] of agents.entries()) {
1857
- let depth = 0;
1858
- for (let parent = agent.parentId; parent && byId.has(parent); parent = byId.get(parent)?.parentId)
1859
- depth += 1;
1860
- const key = agentGroupKey(agent);
1861
- const group = groups.get(key) ?? { agents: [] };
1862
- group.agents.push({ agent, index, depth });
1863
- groups.set(key, group);
1864
- }
1865
- return [...groups].map(([, group]) => ({ label: agentGroupLabel(group.agents.map(({ agent }) => agent)), entries: group.agents }));
1866
- }
1867
- function renderGroupedAgents(agents, render) {
1868
- const groups = agentGroups(agents);
1869
- const grouped = groups.length > 1 || groups.some(({ label }) => label !== "Agents");
1870
- return groups.flatMap((group) => [
1871
- ...(grouped ? [` ${group.label}`] : []),
1872
- ...group.entries.map((entry) => render(entry, grouped)),
1873
- ]);
1874
- }
1875
- export function formatWorkflowProgress(run, spinner = "◇") {
1876
- const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
1877
- const lines = [`${run.state === "completed" ? "✓" : run.state === "failed" || run.state === "stopped" ? "✗" : run.state === "budget_exhausted" ? "!" : run.state === "running" ? spinner : "◆"} Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`];
1878
- if (run.phase)
1879
- lines.push(` Phase: ${run.phase}`);
1880
- lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${line}`));
1881
- const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
1882
- lines.push(...renderGroupedAgents(run.agents, ({ agent, index, depth }, grouped) => {
1883
- const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? spinner : "○";
1884
- const indent = " ".repeat((grouped ? 2 : 1) + depth);
1885
- const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner);
1886
- const name = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
1887
- return `${indent}#${String(index + 1)} ${icon} ${name} [${agent.state}]${activity ? ` ${activity}` : ""}`;
1888
- }));
1889
- return lines.join("\n");
1890
- }
1891
- function workflowToolUpdate(run) {
1892
- return { content: [{ type: "text", text: formatWorkflowProgress(run) }], details: { runId: run.id, run } };
1893
- }
1894
- const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
1895
- function textBlock(text) {
1896
- return {
1897
- render(width) {
1898
- return text.split("\n").map((line) => line.length <= width ? line : `${line.slice(0, Math.max(0, width - 1))}…`);
1899
- },
1900
- invalidate() { },
1901
- };
1902
- }
1903
- function workflowProgressBlock(run) {
1904
- return {
1905
- render(width) {
1906
- const frame = workflowSpinner[Math.floor(Date.now() / 80) % workflowSpinner.length] ?? "◇";
1907
- return formatWorkflowProgress(run, frame).split("\n").map((line) => line.length <= width ? line : `${line.slice(0, Math.max(0, width - 1))}…`);
1908
- },
1909
- invalidate() { },
1910
- };
1911
- }
1912
- export function formatBudgetStatus(run) {
1913
- const usage = budgetUsage(run.usage);
1914
- if (!run.budget || !Object.keys(run.budget).length)
1915
- return ["Budget: unlimited"];
1916
- const lines = [`Budget version ${String(run.budgetVersion ?? 1)}`];
1917
- for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"]) {
1918
- const limits = run.budget[dimension];
1919
- if (!limits || (limits.soft === undefined && limits.hard === undefined))
1920
- continue;
1921
- const limit = limits.hard ?? limits.soft;
1922
- const percent = limit === undefined ? "" : ` ${limit === 0 ? "100.0" : ((usage[dimension] / limit) * 100).toFixed(1)}%`;
1923
- const state = (run.budgetEvents ?? []).filter((event) => event.dimensions.includes(dimension)).at(-1)?.type;
1924
- lines.push(` ${dimension}: ${String(usage[dimension])}${limits.soft !== undefined ? ` soft=${String(limits.soft)}` : ""}${limits.hard !== undefined ? ` hard=${String(limits.hard)}` : ""}${percent}${state ? ` state=${state}` : ""}`);
1925
- }
1926
- const events = run.budgetEvents ?? [];
1927
- if (events.length)
1928
- lines.push(` events: ${events.map((event) => `${event.type}@v${String(event.budgetVersion)}`).join(", ")}`);
1929
- return lines;
1930
- }
1931
- function formatCompactBudgetStatus(run) {
1932
- if (!Object.values(run.budget ?? {}).some((limits) => limits.soft !== undefined || limits.hard !== undefined))
1933
- return [];
1934
- return formatBudgetStatus(run);
1935
- }
1936
- const ATTENTION_ORDER = { awaiting_input: 0, budget_exhausted: 1, running: 2, pausing: 3, paused: 4, interrupted: 5, failed: 6, queued: 7, stopped: 8, completed: 9 };
1937
- function navigatorAttentionSort(entries) {
1938
- return [...entries].sort((a, b) => (ATTENTION_ORDER[a.loaded.run.state] ?? 9) - (ATTENTION_ORDER[b.loaded.run.state] ?? 9));
1939
- }
1940
- function navigatorRunLabels(entries) {
1941
- const nameCount = new Map();
1942
- for (const { loaded: { run } } of entries)
1943
- nameCount.set(run.workflowName, (nameCount.get(run.workflowName) ?? 0) + 1);
1944
- return entries.map(({ store, loaded: { run } }) => {
1945
- const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
1946
- const glyph = run.state === "completed" ? "✓" : run.state === "failed" || run.state === "stopped" ? "✗" : run.state === "budget_exhausted" ? "!" : run.state === "running" ? "⠦" : run.state === "awaiting_input" ? "●" : "◆";
1947
- const suffix = (nameCount.get(run.workflowName) ?? 0) > 1 ? ` ${store.runId.slice(0, 8)}` : "";
1948
- const cost = run.agents.reduce((sum, a) => sum + (a.accounting?.cost ?? 0), 0);
1949
- const costStr = cost > 0 ? ` $${cost.toFixed(2)}` : "";
1950
- return `${glyph} ${run.workflowName}${suffix} ${run.state} ${run.phase ?? ""} ${String(done)}/${String(run.agents.length)} agents${costStr}`;
1951
- });
1952
- }
1953
- function agentBreadcrumb(agent, byId) {
1954
- const name = agent.label ?? agent.name;
1955
- const parts = agent.parentBreadcrumb ? [agent.parentBreadcrumb] : [];
1956
- const seen = new Set([agent.id]);
1957
- for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
1958
- if (seen.has(parentId))
1959
- break; // ponytail: cycle guard for corrupt data
1960
- seen.add(parentId);
1961
- const parent = byId.get(parentId);
1962
- if (parent)
1963
- parts.push(parent.label ?? parent.name);
1964
- else
1965
- break;
1966
- }
1967
- parts.push(name);
1968
- return parts.length > 1 ? parts.join(" > ") : name;
1969
- }
1970
- function formatAgentActivity(agent, spinner) {
1971
- if (agent.activity?.kind === "reasoning")
1972
- return `${spinner} reasoning`;
1973
- if (agent.activity?.kind === "text")
1974
- return `${spinner} responding`;
1975
- if (agent.activity?.kind === "tool")
1976
- return `${spinner} ${agent.activity.text}`;
1977
- const tool = [...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running");
1978
- return tool ? `${spinner} ${tool.name}` : "";
1979
- }
1980
- function formatAccounting(accounting) {
1981
- const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
1982
- return `${new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(total).toLowerCase()} tok`;
1983
- }
1984
- export function formatNavigatorDashboard(run, checkpoints, worktrees) {
1985
- void worktrees;
1986
- const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
1987
- const totalAccounting = run.agents.reduce((sum, a) => ({ input: sum.input + (a.accounting?.input ?? 0), output: sum.output + (a.accounting?.output ?? 0), cacheRead: sum.cacheRead + (a.accounting?.cacheRead ?? 0), cacheWrite: sum.cacheWrite + (a.accounting?.cacheWrite ?? 0), cost: sum.cost + (a.accounting?.cost ?? 0) }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
1988
- const hasAccounting = run.agents.some((a) => a.accounting);
1989
- const glyph = run.state === "completed" ? "✓" : run.state === "failed" || run.state === "stopped" ? "✗" : run.state === "budget_exhausted" ? "!" : run.state === "running" ? "⠦" : run.state === "awaiting_input" ? "●" : "◆";
1990
- const header = `${glyph} ${run.workflowName}`;
1991
- const meta = [run.state, run.phase ? `phase: ${run.phase}` : "", `${String(done)}/${String(run.agents.length)} agents`, hasAccounting ? formatAccounting(totalAccounting) : "", totalAccounting.cost > 0 ? `$${totalAccounting.cost.toFixed(2)}` : ""].filter(Boolean).join(" · ");
1992
- const lines = [header, meta, ...formatCompactBudgetStatus(run)];
1993
- if (run.error)
1994
- lines.push(`Error: ${run.error.code}: ${run.error.message}`);
1995
- if (run.events?.length)
1996
- lines.push(...run.events.map((event) => `Warning: ${event.message}`));
1997
- lines.push("");
1998
- const byId = new Map(run.agents.map((a) => [a.id, a]));
1999
- const render = ({ agent, depth }, grouped) => {
2000
- const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
2001
- const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
2002
- const setup = agent.attemptDetails?.at(-1)?.setup;
2003
- const thinking = setup?.model.thinking ?? agent.model.thinking;
2004
- const model = `${setup?.model.provider ?? agent.model.provider}/${setup?.model.model ?? agent.model.model}${thinking ? `:${thinking}` : ""}`;
2005
- const policy = [`model=${model}`, agent.requestedModel ? `requested=${agent.requestedModel}` : "", `tools=${(setup?.tools ?? agent.tools).join(",") || "(none)"}`, agent.role ? `role=${agent.role}` : ""];
2006
- const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
2007
- const indent = " ".repeat((grouped ? 2 : 1) + depth);
2008
- const result = [`${indent}${icon} ${breadcrumb} · ${agent.state} · ${policy.filter(Boolean).join(" · ")}${tokens ? ` · ${tokens}` : ""}`];
2009
- if (agent.state === "failed" && agent.attemptDetails?.length) {
2010
- const last = agent.attemptDetails[agent.attemptDetails.length - 1];
2011
- if (last?.error)
2012
- result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
2013
- }
2014
- const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦") : "";
2015
- if (activity)
2016
- result.push(`${indent} ${activity}`);
2017
- return result.join("\n");
2018
- };
2019
- lines.push(...renderGroupedAgents(run.agents, render));
2020
- if (checkpoints.length) {
2021
- lines.push("");
2022
- for (const cp of checkpoints)
2023
- lines.push(`● checkpoint ${cp.name}: ${cp.prompt}`);
2024
- }
2025
- return lines.join("\n");
2026
- }
2027
- export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
2028
- const { run, snapshot } = loaded;
2029
- const lines = [
2030
- `Workflow: ${run.workflowName}`,
2031
- `Run: ${run.id}`,
2032
- `Status: ${run.state}`,
2033
- `Phase: ${run.phase ?? "(none)"}`,
2034
- `Launch cwd: ${run.cwd}`,
2035
- ...formatCompactBudgetStatus(run),
2036
- `Launch models: ${snapshot.models.join(", ") || "(none)"}`,
2037
- ];
2038
- if (run.error)
2039
- lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
2040
- if (run.events?.length)
2041
- lines.push(...run.events.map((event) => `Warning: ${event.message}`));
2042
- const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
2043
- if (aliases && Object.keys(aliases).length)
2044
- lines.push(`Model aliases: ${Object.entries(aliases).map(([name, target]) => `${name}=${target}`).join(", ")}`);
2045
- lines.push("Agents / ownership:");
2046
- if (!run.agents.length)
2047
- lines.push(" (none)");
2048
- const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
2049
- lines.push(...renderGroupedAgents(run.agents, ({ agent, index, depth }, grouped) => {
2050
- const model = `${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`;
2051
- const role = agent.role ? ` role=${agent.role}` : "";
2052
- const tools = ` tools=${agent.tools.join(",") || "(none)"}`;
2053
- const accounting = agent.accounting ? ` input=${String(agent.accounting.input)} output=${String(agent.accounting.output)} cache-read=${String(agent.accounting.cacheRead)} cache-write=${String(agent.accounting.cacheWrite)} cost=${String(agent.accounting.cost)}` : "";
2054
- const indent = " ".repeat((grouped ? 2 : 1) + depth);
2055
- const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${role}${tools} attempts=${String(agent.attempts)} retries=${String(Math.max(0, agent.attempts - 1))}${accounting}`];
2056
- for (const attempt of agent.attemptDetails ?? [])
2057
- result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
2058
- for (const call of agent.toolCalls ?? [])
2059
- result.push(`${indent} tool ${call.name} state=${call.state}`);
2060
- return result.join("\n");
2061
- }));
2062
- lines.push("Checkpoints:");
2063
- if (!checkpoints.length)
2064
- lines.push(" (none)");
2065
- for (const checkpoint of checkpoints)
2066
- lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
2067
- lines.push(`Worktrees: ${String(_worktrees.length)}`);
2068
- lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
2069
- return lines.join("\n");
2070
- }
2071
- function formatCheckpointReview(checkpoint) {
2072
- return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, "Context:", JSON.stringify(checkpoint.context, null, 2)].join("\n");
2073
- }
2074
- const DELIVERY_LIMIT_BYTES = 4 * 1024;
2075
- const WORKFLOW_LOG_ENTRY = "workflow-log";
2076
- function completionDelivery(name, value, resultPath, worktrees) {
2077
- const locations = worktrees.length ? ` Changes: ${worktrees.map(({ branch, path }) => `${branch} (${path})`).join(", ")}.` : "";
2078
- const message = `Workflow ${name} completed: ${JSON.stringify(value)}${locations}`;
2079
- if (Buffer.byteLength(message) <= DELIVERY_LIMIT_BYTES)
2080
- return message;
2081
- const suffix = `... Full result: ${resultPath}${locations}`;
2082
- const suffixBytes = Buffer.byteLength(suffix);
2083
- if (suffixBytes >= DELIVERY_LIMIT_BYTES)
2084
- return utf8Prefix(suffix, DELIVERY_LIMIT_BYTES);
2085
- return utf8Prefix(message, DELIVERY_LIMIT_BYTES - suffixBytes) + suffix;
2086
- }
2087
- function utf8Prefix(value, maxBytes) {
2088
- const bytes = Buffer.from(value);
2089
- let end = Math.min(bytes.length, maxBytes);
2090
- while (end < bytes.length && end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80)
2091
- end -= 1;
2092
- return bytes.subarray(0, end).toString("utf8");
2093
- }
2094
- function deliver(pi, content) {
2095
- pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
2096
- }
2097
- function deliverFailure(pi, name, error) {
2098
- deliver(pi, `Workflow ${name} failed: ${formatWorkflowFailure(error)}`);
2099
- }
2100
- function safeEventError(error) {
2101
- const code = errorCode(error) ?? "INTERNAL_ERROR";
2102
- return { code, message: `Workflow execution failed (${code})` };
2103
- }
2104
- class WorkflowEventPublisher {
2105
- sink;
2106
- #queues = new Map();
2107
- #budgetEvents = new Map();
2108
- #worktrees = new Map();
2109
- constructor(sink) {
2110
- this.sink = sink;
2111
- }
2112
- seedBudget(runId, events) {
2113
- const seen = this.#budgetEvents.get(runId) ?? new Set();
2114
- for (const event of events ?? [])
2115
- seen.add(this.budgetKey(event));
2116
- this.#budgetEvents.set(runId, seen);
2117
- }
2118
- async runStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {}); }
2119
- async runResumed(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_RESUMED_EVENT, {}); }
2120
- async runState(store, metadata, previousState, state, reason) {
2121
- await this.#publish(store, metadata, WORKFLOW_RUN_STATE_CHANGED_EVENT, { previousState, state, ...(reason ? { reason } : {}), ...(ERROR_CODES.includes(reason) ? { errorCode: reason } : {}) });
2122
- if ((previousState === "paused" || previousState === "interrupted" || previousState === "budget_exhausted") && state === "running")
2123
- await this.runResumed(store, metadata);
2124
- }
2125
- async runCompleted(store, metadata, resultPath) { await this.#publish(store, metadata, WORKFLOW_RUN_COMPLETED_EVENT, { resultPath }); }
2126
- async runFailed(store, metadata, error, state) {
2127
- if (state === "failed")
2128
- await this.#publish(store, metadata, WORKFLOW_RUN_FAILED_EVENT, { error: safeEventError(error) });
2129
- }
2130
- async agentState(store, metadata, previous, agent) {
2131
- await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name, ...(agent.role ? { role: agent.role } : {}), structuralPath: [...(agent.structuralPath ?? [])], ...(agent.parentId ? { parentId: agent.parentId } : {}), ...(agent.parentBreadcrumb ? { parentBreadcrumb: agent.parentBreadcrumb } : {}), ...(agent.worktreeOwner ? { worktreeOwner: agent.worktreeOwner } : {}), ...(previous ? { previousState: previous.state } : {}), state: agent.state, attempt: agent.attempts });
2132
- }
2133
- async agentStates(store, metadata, previous, current) {
2134
- const previousById = new Map(previous.map((agent) => [agent.id, agent]));
2135
- for (const agent of current) {
2136
- const old = previousById.get(agent.id);
2137
- if (!old || old.state !== agent.state || old.attempts !== agent.attempts)
2138
- await this.agentState(store, metadata, old, agent);
2139
- }
2140
- }
2141
- async phase(store, metadata, previousPhase, phase) {
2142
- if (previousPhase !== phase)
2143
- await this.#publish(store, metadata, WORKFLOW_PHASE_CHANGED_EVENT, { ...(previousPhase !== undefined ? { previousPhase } : {}), phase });
2144
- }
2145
- async checkpoint(store, metadata, name, state) { await this.#publish(store, metadata, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, { name, state }); }
2146
- async budget(store, metadata, run) {
2147
- const seen = this.#budgetEvents.get(store.runId) ?? new Set();
2148
- this.#budgetEvents.set(store.runId, seen);
2149
- for (const event of run.budgetEvents ?? []) {
2150
- const key = this.budgetKey(event);
2151
- if (seen.has(key))
2152
- continue;
2153
- seen.add(key);
2154
- await this.#publish(store, metadata, WORKFLOW_BUDGET_EVENT, { ...event, timestamp: event.at });
2155
- }
2156
- }
2157
- async worktree(store, metadata, worktree) {
2158
- const seen = this.#worktrees.get(store.runId) ?? new Set();
2159
- this.#worktrees.set(store.runId, seen);
2160
- if (seen.has(worktree.owner))
2161
- return;
2162
- seen.add(worktree.owner);
2163
- await this.#publish(store, metadata, WORKFLOW_WORKTREE_CREATED_EVENT, { owner: worktree.owner, branch: worktree.branch, path: worktree.path, base: worktree.base });
2164
- }
2165
- async #publish(store, metadata, name, payload) {
2166
- const base = { runId: store.runId, sessionId: store.sessionId, workflowName: metadata.name, cwd: store.cwd, runDirectory: store.directory, timestamp: Date.now() };
2167
- const previous = this.#queues.get(store.runId) ?? Promise.resolve();
2168
- const next = previous.then(() => {
2169
- try {
2170
- void Promise.resolve(this.sink?.emit(name, { ...base, ...payload })).catch(() => undefined);
2171
- }
2172
- catch { /* Best effort: listeners cannot affect a run. */ }
2173
- });
2174
- this.#queues.set(store.runId, next.catch(() => undefined));
2175
- await next;
2176
- }
2177
- budgetKey(event) { return `${String(event.budgetVersion)}:${event.type}:${event.proposalId ?? ""}`; }
2178
- }
2179
- const inheritedHostAgentPath = new AsyncLocalStorage();
2180
- const inheritedHostWorktreeOwner = new AsyncLocalStorage();
2181
- function namedRecord(value, kind) {
2182
- if (!object(value))
2183
- fail("INVALID_METADATA", `${kind} must be a record`);
2184
- return Object.entries(value);
2185
- }
2186
- function hostWithWorktree(args, identity, occurrences) {
2187
- if (args.length !== 1 && args.length !== 2)
2188
- fail("INVALID_METADATA", "withWorktree requires a callback or a name and callback");
2189
- const callback = args[args.length - 1];
2190
- if (typeof callback !== "function")
2191
- fail("INVALID_METADATA", "withWorktree callback must be a function");
2192
- let owner;
2193
- if (args.length === 2) {
2194
- if (typeof args[0] !== "string" || !args[0].trim())
2195
- fail("INVALID_METADATA", "withWorktree name must be a non-empty string");
2196
- owner = operationPath("worktree", "named", args[0].trim());
2197
- }
2198
- else {
2199
- const structuralPath = inheritedHostAgentPath.getStore() ?? [];
2200
- const key = `${identity}\0${JSON.stringify(structuralPath)}`;
2201
- const occurrence = (occurrences.get(key) ?? 0) + 1;
2202
- occurrences.set(key, occurrence);
2203
- owner = operationPath("worktree", "unnamed", "function", identity, ...structuralPath, `occurrence:${String(occurrence)}`);
2204
- }
2205
- return inheritedHostWorktreeOwner.run(owner, async () => await callback());
2206
- }
2207
- function workflowRunContext(cwd, sessionId, runId, workflow, args, signal) {
2208
- return Object.freeze({ cwd, sessionId, runId, workflow: deepFreeze(structuredClone(workflow)), args: deepFreeze(structuredClone(args)), signal });
2209
- }
2210
- async function resolveWorkflowVariables(run, controller, registry) {
2211
- let first;
2212
- const tasks = registry.variables().map(async ({ name, variable }) => {
2213
- try {
2214
- const result = await variable.resolve(run);
2215
- if (!jsonValue(result) || !Value.Check(variable.schema, result))
2216
- fail("RESULT_INVALID", `Invalid output from ${name}`);
2217
- return [name, deepFreeze(structuredClone(result))];
2218
- }
2219
- catch (error) {
2220
- const typed = errorCode(error) ? new WorkflowError(errorCode(error), `${name}: ${errorText(error)}`) : new WorkflowError("INTERNAL_ERROR", `${name}: ${errorText(error)}`);
2221
- if (!first) {
2222
- first = typed;
2223
- controller.abort();
2224
- }
2225
- throw typed;
2226
- }
2227
- });
2228
- await Promise.allSettled(tasks);
2229
- if (first)
2230
- throw first;
2231
- return Object.freeze(Object.fromEntries((await Promise.all(tasks)).map(([name, value]) => [name, value])));
2232
- }
2233
- async function hostParallel(rawOperation, rawTasks) {
2234
- if (typeof rawOperation !== "string" || !rawOperation.trim())
2235
- fail("INVALID_METADATA", "parallel requires a stable explicit name");
2236
- const tasks = namedRecord(rawTasks, "parallel tasks");
2237
- for (const [name, run] of tasks) {
2238
- if (!name.trim())
2239
- fail("INVALID_METADATA", "parallel task requires a stable explicit name");
2240
- if (typeof run !== "function")
2241
- fail("INVALID_METADATA", "parallel task values must be run functions");
2242
- }
2243
- const results = await Promise.all(tasks.map(async ([name, run]) => {
2244
- try {
2245
- const parent = inheritedHostAgentPath.getStore() ?? [];
2246
- return { name, value: await inheritedHostAgentPath.run([...parent, rawOperation, name], run) };
2247
- }
2248
- catch (error) {
2249
- const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", error instanceof Error ? error.message : String(error));
2250
- if (typed.code === "CANCELLED")
2251
- throw typed;
2252
- return { name, error: typed };
2253
- }
2254
- }));
2255
- const failure = results.find((result) => result.error);
2256
- if (failure?.error)
2257
- throw failure.error;
2258
- return Object.fromEntries(results.map((result) => [result.name, result.value]));
2259
- }
2260
- async function hostPipeline(rawOperation, rawItems, rawStages) {
2261
- if (typeof rawOperation !== "string" || !rawOperation.trim())
2262
- fail("INVALID_METADATA", "pipeline requires a stable explicit name");
2263
- const items = namedRecord(rawItems, "pipeline items");
2264
- const stages = namedRecord(rawStages, "pipeline stages");
2265
- if (!stages.length)
2266
- fail("INVALID_METADATA", "pipeline requires at least one stage");
2267
- for (const [name] of items)
2268
- if (!name.trim())
2269
- fail("INVALID_METADATA", "pipeline item requires a stable explicit name");
2270
- for (const [stageName, run] of stages) {
2271
- if (!stageName.trim())
2272
- fail("INVALID_METADATA", "pipeline stage requires a stable explicit name");
2273
- if (typeof run !== "function")
2274
- fail("INVALID_METADATA", "pipeline stage values must be run functions");
2275
- }
2276
- const results = await Promise.all(items.map(async ([name, initial]) => {
2277
- let current = initial;
2278
- try {
2279
- for (const [stageName, run] of stages) {
2280
- const parent = inheritedHostAgentPath.getStore() ?? [];
2281
- current = await inheritedHostAgentPath.run([...parent, rawOperation, name, stageName], () => run(current));
2282
- }
2283
- return { name, value: current };
2284
- }
2285
- catch (error) {
2286
- const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", error instanceof Error ? error.message : String(error));
2287
- if (typed.code === "CANCELLED")
2288
- throw typed;
2289
- return { name, error: typed };
2290
- }
2291
- }));
2292
- const failure = results.find((result) => result.error);
2293
- if (failure?.error)
2294
- throw failure.error;
2295
- return Object.fromEntries(results.map((result) => [result.name, result.value]));
2296
- }
2297
- function nextNamedOccurrence(counters, label) {
2298
- const count = (counters.get(label) ?? 0) + 1;
2299
- counters.set(label, count);
2300
- return count === 1 ? label : `${label}#${String(count)}`;
2301
- }
2302
- function withWorkflowFunctions(bridge, store, runContext, variables, registry) {
2303
- const functionAgentOccurrences = new Map();
2304
- const functionWorktreeOccurrences = new Map();
2305
- const functionInvokeOccurrences = new Map();
2306
- const invokeFunction = async (name, input, path, signal, worktreeOwner, structuralPath = [], breadcrumb) => {
2307
- const replayed = await store.replay(path);
2308
- let stored;
2309
- const sideEffects = [];
2310
- const functionBreadcrumb = breadcrumb ?? name;
2311
- const context = {
2312
- run: runContext,
2313
- invoke: async (targetName, targetInput) => {
2314
- const inherited = inheritedHostAgentPath.getStore() ?? structuralPath;
2315
- const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2316
- const key = JSON.stringify([path, inherited, targetName]);
2317
- const occurrence = (functionInvokeOccurrences.get(key) ?? 0) + 1;
2318
- functionInvokeOccurrences.set(key, occurrence);
2319
- const nestedPath = operationPath("function", "nested", path, ...inherited, targetName, `occurrence:${String(occurrence)}`);
2320
- return invokeFunction(targetName, targetInput, nestedPath, signal, scopedWorktreeOwner, inherited, `${functionBreadcrumb} > ${targetName}`);
2321
- },
2322
- agent: async (...args) => {
2323
- if (!bridge.agent || typeof args[0] !== "string")
2324
- fail("AGENT_FAILED", "No agent bridge is available");
2325
- const options = validateAgentOptions(args[1] === undefined ? {} : args[1]);
2326
- const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2327
- const inherited = inheritedHostAgentPath.getStore() ?? [];
2328
- const key = `${path}\0${JSON.stringify(inherited)}`;
2329
- const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
2330
- functionAgentOccurrences.set(key, occurrence);
2331
- return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: functionBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
2332
- },
2333
- prompt: workflowPrompt,
2334
- parallel: (...args) => hostParallel(args[0], args[1]),
2335
- pipeline: (...args) => hostPipeline(args[0], args[1], args[2]),
2336
- withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences),
2337
- checkpoint: async (...args) => {
2338
- if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0]))
2339
- fail("INTERNAL_ERROR", "No checkpoint bridge is available");
2340
- return bridge.checkpoint(args[0], signal);
2341
- },
2342
- phase: (name) => { sideEffects.push(Promise.resolve(bridge.phase?.(name))); },
2343
- log: (message) => { sideEffects.push(Promise.resolve(bridge.log?.(message))); },
2344
- };
2345
- const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
2346
- await Promise.all(sideEffects);
2347
- if (!replayed)
2348
- await store.complete(path, stored ?? result);
2349
- return result;
2350
- };
2351
- return { ...bridge, functions: registry.globals(), variables, function: invokeFunction };
2352
- }
2353
- function projectTrusted(ctx) {
2354
- const check = object(ctx) ? ctx.isProjectTrusted : undefined;
2355
- return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
2356
- }
2357
- function isEntryRenderer(value) { return typeof value === "function"; }
2358
- function isWorkflowEventSink(value) { return object(value) && typeof value.emit === "function"; }
2359
- function piHostCapabilities(pi) {
2360
- if (!object(pi))
2361
- return {};
2362
- const registerEntryRenderer = pi.registerEntryRenderer;
2363
- const events = pi.events;
2364
- return { ...(isEntryRenderer(registerEntryRenderer) ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
2365
- }
2366
- function isModelRegistryGetter(value) { return typeof value === "function"; }
2367
- function contextHostCapabilities(ctx) {
2368
- if (!object(ctx) || !object(ctx.modelRegistry))
2369
- return {};
2370
- const registry = ctx.modelRegistry;
2371
- const getAll = registry.getAll;
2372
- const getAvailable = registry.getAvailable;
2373
- return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
2374
- }
2375
- function isUiSelect(value) { return typeof value === "function"; }
2376
- function isUiInput(value) { return typeof value === "function"; }
2377
- function isUiSetStatus(value) { return typeof value === "function"; }
2378
- function uiHostCapabilities(ui) {
2379
- if (!object(ui))
2380
- return undefined;
2381
- const select = ui.select;
2382
- const input = ui.input;
2383
- const setStatus = ui.setStatus;
2384
- return { ...(isUiSelect(select) ? { select } : {}), ...(isUiInput(input) ? { input } : {}), ...(isUiSetStatus(setStatus) ? { setStatus } : {}) };
2385
- }
2386
- function tuiHostCapabilities(tui) {
2387
- if (!object(tui) || !object(tui.terminal))
2388
- return {};
2389
- return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
2390
- }
2391
- function tuiRows(tui) { const rows = tuiHostCapabilities(tui).terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? rows : 24; }
2392
- function isKeybindingGetter(value) { return typeof value === "function"; }
2393
- function keybindingsHostCapabilities(keybindings) {
2394
- if (!object(keybindings) || !isKeybindingGetter(keybindings.getKeys))
2395
- return {};
2396
- return { getKeys: keybindings.getKeys };
2397
- }
2398
- function keybindingKeys(keybindings, name) { const getKeys = keybindingsHostCapabilities(keybindings).getKeys; return typeof getKeys === "function" ? getKeys.call(keybindings, name) : undefined; }
2399
- function parseThinking(value) {
2400
- switch (value) {
2401
- case "off":
2402
- case "minimal":
2403
- case "low":
2404
- case "medium":
2405
- case "high":
2406
- case "xhigh":
2407
- case "max": return value;
2408
- default: return undefined;
2409
- }
2410
- }
2411
- export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession) {
2412
- beginWorkflowExtensionLoading();
2413
- const registry = loadingRegistry();
2414
- const registerEntryRenderer = piHostCapabilities(pi).registerEntryRenderer;
2415
- registerEntryRenderer?.(WORKFLOW_LOG_ENTRY, (entry) => {
2416
- const data = entry.data;
2417
- return textBlock(data ? `Workflow ${data.workflowName}: ${data.message}` : "");
2418
- });
2419
- const logBridge = (lifecycle, workflowName) => async (message) => {
2420
- await lifecycle.enter();
2421
- try {
2422
- pi.appendEntry(WORKFLOW_LOG_ENTRY, { workflowName, message: utf8Prefix(message, DELIVERY_LIMIT_BYTES) });
2423
- }
2424
- finally {
2425
- await lifecycle.leave();
2426
- }
2427
- };
2428
- const eventPublisher = new WorkflowEventPublisher(piHostCapabilities(pi).events);
2429
- pi.on("resources_discover", () => {
2430
- if (!pi.getActiveTools().includes("workflow"))
2431
- return;
2432
- const extensionDir = dirname(fileURLToPath(import.meta.url));
2433
- const skillPath = [join(extensionDir, "../skills"), join(extensionDir, "../../skills")].find((path) => existsSync(path));
2434
- return skillPath ? { skillPaths: [skillPath] } : undefined;
2435
- });
2436
- const runs = new Map();
2437
- const liveActivities = new Map();
2438
- const setLiveActivity = (runId, agentId, activity) => {
2439
- const activities = liveActivities.get(runId);
2440
- if (activity) {
2441
- if (activities)
2442
- activities.set(agentId, activity);
2443
- else
2444
- liveActivities.set(runId, new Map([[agentId, activity]]));
2445
- }
2446
- else {
2447
- activities?.delete(agentId);
2448
- if (activities?.size === 0)
2449
- liveActivities.delete(runId);
2450
- }
2451
- };
2452
- const withLiveActivities = (run) => {
2453
- const activities = liveActivities.get(run.id);
2454
- return activities?.size ? { ...run, agents: run.agents.map((agent) => {
2455
- const activity = activities.get(agent.id);
2456
- return activity ? { ...agent, activity } : agent;
2457
- }) } : run;
2458
- };
2459
- const conversationLocks = new Set();
2460
- const terminalRunStates = new Map();
2461
- let sessionLease;
2462
- let sessionLeasePromise;
2463
- const ensureSessionLease = async (cwd, sessionId) => {
2464
- if (sessionLease?.active)
2465
- return;
2466
- const pending = sessionLeasePromise ?? (sessionLeasePromise = acquireSessionLease(cwd, sessionId, home));
2467
- try {
2468
- sessionLease = await pending;
2469
- }
2470
- finally {
2471
- if (sessionLeasePromise === pending)
2472
- sessionLeasePromise = undefined;
2473
- }
2474
- };
2475
- const releaseSessionLease = async () => {
2476
- const lease = sessionLease ?? await sessionLeasePromise?.catch(() => undefined);
2477
- sessionLease = undefined;
2478
- sessionLeasePromise = undefined;
2479
- await lease?.release();
2480
- };
2481
- const persistRunState = async (store, metadata, update) => {
2482
- const persisted = await store.updateState(update);
2483
- await eventPublisher.budget(store, metadata, persisted);
2484
- return persisted;
2485
- };
2486
- const persistWorktree = async (store, metadata, owner) => {
2487
- const existing = (await store.worktrees()).some((worktree) => worktree.owner === owner);
2488
- const worktree = await store.worktree(owner);
2489
- if (!existing)
2490
- await eventPublisher.worktree(store, metadata, worktree);
2491
- return worktree;
2492
- };
2493
- const lifecycleFor = (store, state, budget, metadata) => new RunLifecycle(state, async (next, previous, reason) => {
2494
- if (next !== "pausing")
2495
- budget.transition(next);
2496
- const persisted = await persistRunState(store, metadata, (current) => {
2497
- const nextRun = { ...current, state: next, ...budget.snapshot() };
2498
- if (next === "running" || next === "completed")
2499
- delete nextRun.error;
2500
- return nextRun;
2501
- });
2502
- await eventPublisher.runState(store, metadata, previous, next, reason);
2503
- runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2504
- });
2505
- const scheduler = new FairAgentScheduler(async ({ id, runId, parentId, prompt, options, signal, setSteer }) => {
2506
- const run = runs.get(runId);
2507
- if (!run)
2508
- throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${runId}`);
2509
- try {
2510
- const budget = run.budget.forAgent(id);
2511
- const onProgress = async (progress) => {
2512
- let runState;
2513
- if (progress.persist) {
2514
- runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) } : current);
2515
- }
2516
- else {
2517
- const loaded = await run.store.load();
2518
- if (!loaded.run.agents.some((agent) => agent.id === id))
2519
- return;
2520
- runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) };
2521
- }
2522
- if (!runState.agents.some((agent) => agent.id === id))
2523
- return;
2524
- setLiveActivity(runId, id, progress.activity);
2525
- run.update?.(workflowToolUpdate(withLiveActivities(runState)));
2526
- };
2527
- const onAttempt = async (attempt) => {
2528
- await scheduler.flush();
2529
- scheduler.attemptStarted(id);
2530
- await scheduler.flush();
2531
- const before = (await run.store.load()).run;
2532
- await persistActiveAgentAttempt(run.store, id, attempt);
2533
- const active = (await run.store.load()).run;
2534
- await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
2535
- const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2536
- run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2537
- };
2538
- const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.conversation ? { conversation: options.conversation } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
2539
- const before = (await run.store.load()).run;
2540
- await persistAgentAttempts(run.store, id, result.attempts);
2541
- const completed = (await run.store.load()).run;
2542
- await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
2543
- const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2544
- setLiveActivity(runId, id);
2545
- run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2546
- return result.value;
2547
- }
2548
- catch (error) {
2549
- const attempts = error.attempts;
2550
- if (attempts?.length) {
2551
- const before = (await run.store.load()).run;
2552
- await persistAgentAttempts(run.store, id, attempts);
2553
- const failed = (await run.store.load()).run;
2554
- await eventPublisher.agentStates(run.store, run.metadata, before.agents, failed.agents);
2555
- }
2556
- const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2557
- setLiveActivity(runId, id);
2558
- run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2559
- throw error;
2560
- }
2561
- }, 16, async (runId, ownership) => {
2562
- const run = runs.get(runId);
2563
- if (!run)
2564
- return;
2565
- await run.store.saveOwnership(ownership);
2566
- let previousAgents = [];
2567
- const runState = await persistRunState(run.store, run.metadata, (current) => {
2568
- previousAgents = current.agents;
2569
- const existing = new Map(current.agents.map((agent) => [agent.id, agent]));
2570
- const agents = ownership.map((node) => {
2571
- const previous = existing.get(node.id);
2572
- const requested = { label: node.options.label, workflowName: run.metadata.name, ...(node.options.model ? { model: node.options.model } : {}), ...(node.options.thinking ? { thinking: node.options.thinking } : {}), ...(node.options.role ? { role: node.options.role } : {}), effectiveTools: node.options.tools };
2573
- let effective;
2574
- try {
2575
- effective = run.executor.resolve(requested);
2576
- }
2577
- catch {
2578
- effective = previous ? { model: previous.model, ...(previous.requestedModel ? { requestedModel: previous.requestedModel } : {}), tools: previous.tools } : { model: node.options.model ? modelSpec(node.options.model, run.model) : { ...run.model, ...(node.options.thinking ? { thinking: node.options.thinking } : {}) }, ...(node.options.model ? { requestedModel: node.options.model } : {}), tools: node.options.tools };
2579
- }
2580
- return { id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}) };
2581
- });
2582
- return { ...current, agents };
2583
- });
2584
- await eventPublisher.agentStates(run.store, run.metadata, previousAgents, runState.agents);
2585
- run.update?.(workflowToolUpdate(withLiveActivities(runState)));
2586
- });
2587
- const stopWorkflowRun = async (runId) => {
2588
- const run = runs.get(runId);
2589
- const terminalState = terminalRunStates.get(runId);
2590
- if (!run)
2591
- return terminalState ? { runId, state: terminalState, stopped: false, reason: "already_terminal" } : { runId, state: "unknown", stopped: false, reason: "unknown_run" };
2592
- const state = run.lifecycle.state;
2593
- if (state === "completed" || state === "failed" || state === "stopped")
2594
- return { runId, state, stopped: false, reason: "already_terminal" };
2595
- await run.lifecycle.terminal("stopped");
2596
- run.abortController.abort();
2597
- run.execution?.cancel();
2598
- await scheduler.cancelRun(run.store.runId);
2599
- await scheduler.flush();
2600
- return { runId, state: "stopped", stopped: true };
2601
- };
2602
- const answerCheckpoint = async (runId, name, approved, silent = false) => {
2603
- const run = runs.get(runId);
2604
- if (!run)
2605
- return false;
2606
- const checkpoint = await run.store.answerCheckpoint(name, approved);
2607
- if (!checkpoint)
2608
- return false;
2609
- await eventPublisher.checkpoint(run.store, run.metadata, checkpoint.name, approved ? "approved" : "rejected");
2610
- if ((await run.store.awaitingCheckpoints()).length === 0)
2611
- await run.lifecycle.resolveAwaitingInput();
2612
- run.checkpointResolvers.get(checkpoint.path)?.(approved);
2613
- run.checkpointResolvers.delete(checkpoint.path);
2614
- if (!silent)
2615
- deliver(pi, `Workflow ${run.metadata.name} checkpoint ${name}: ${approved ? "Approved" : "Rejected"}.`);
2616
- return true;
2617
- };
2618
- const budgetDecisionDelivery = (metadata, request) => `Workflow ${metadata.name} budget adjustment ${request.proposalId} for run ${request.runId} requires approval. Consumed usage: ${JSON.stringify(request.consumed)}. Previous limits: ${JSON.stringify(request.previous)}. Proposed limits: ${JSON.stringify(request.proposed)}. Respond with workflow_respond using proposalId ${request.proposalId}.`;
2619
- const appendBudgetDecisionEvent = async (run, request, type) => {
2620
- run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
2621
- await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2622
- };
2623
- const answerBudgetDecision = async (runId, proposalId, approved, silent = false) => {
2624
- const run = runs.get(runId);
2625
- if (!run)
2626
- return undefined;
2627
- const request = await run.store.answerWorkflowDecision(proposalId, approved);
2628
- if (!request)
2629
- return undefined;
2630
- await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
2631
- const result = await applyBudgetDecision(request, approved);
2632
- run.budgetResolvers.get(proposalId)?.(result);
2633
- run.budgetResolvers.delete(proposalId);
2634
- if (!silent)
2635
- deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
2636
- return result;
2637
- };
2638
- const checkpointBridge = (runId, store, metadata, foreground, ui) => {
2639
- const checkpointCounters = new Map();
2640
- return async (raw, signal) => {
2641
- const input = validateCheckpoint(raw);
2642
- const label = nextNamedOccurrence(checkpointCounters, input.name);
2643
- const path = operationPath("checkpoint", label);
2644
- if (foreground && !ui?.select)
2645
- fail("RESUME_INCOMPATIBLE", "Foreground checkpoints require UI");
2646
- const alreadyAwaiting = (await store.awaitingCheckpoints()).some((checkpoint) => checkpoint.path === path);
2647
- const replayed = await store.awaitCheckpoint({ ...input, name: label, path });
2648
- if (replayed !== undefined)
2649
- return replayed;
2650
- if (!alreadyAwaiting)
2651
- await eventPublisher.checkpoint(store, metadata, label, "awaiting");
2652
- const run = runs.get(runId);
2653
- await run?.lifecycle.enterAwaitingInput();
2654
- if (!alreadyAwaiting && !ui?.select)
2655
- deliver(pi, `Workflow ${metadata.name} checkpoint ${label}: ${input.prompt}\nContext: ${JSON.stringify(input.context)}\nRespond with workflow_respond.`);
2656
- const decision = new Promise((resolve, reject) => {
2657
- run?.checkpointResolvers.set(path, resolve);
2658
- if (signal.aborted)
2659
- reject(new WorkflowError("CANCELLED", "Workflow cancelled"));
2660
- else
2661
- signal.addEventListener("abort", () => { run?.checkpointResolvers.delete(path); reject(new WorkflowError("CANCELLED", "Workflow cancelled")); }, { once: true });
2662
- });
2663
- const answered = await store.awaitCheckpoint({ ...input, name: label, path });
2664
- if (answered !== undefined) {
2665
- if ((await store.awaitingCheckpoints()).length === 0)
2666
- await run?.lifecycle.resolveAwaitingInput();
2667
- run?.checkpointResolvers.get(path)?.(answered);
2668
- run?.checkpointResolvers.delete(path);
2669
- }
2670
- if (ui?.select)
2671
- void (async () => {
2672
- while (!signal.aborted && run?.checkpointResolvers.has(path)) {
2673
- const choice = await ui.select?.(input.prompt, ["Approve", "Reject"]);
2674
- if (!choice) {
2675
- if (foreground)
2676
- continue; // foreground: retry until answered
2677
- deliver(pi, `Workflow ${metadata.name} checkpoint ${label}: ${input.prompt}\nContext: ${JSON.stringify(input.context)}\nRespond with workflow_respond.`);
2678
- return;
2679
- }
2680
- if (await answerCheckpoint(runId, label, choice === "Approve", true))
2681
- return;
2682
- }
2683
- })().catch(() => undefined);
2684
- return decision;
2685
- };
2686
- };
2687
- pi.registerTool({
2688
- name: "workflow_respond",
2689
- label: "Workflow Respond",
2690
- description: "Approve or reject one pending workflow checkpoint or budget decision",
2691
- parameters: Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }),
2692
- async execute(_id, params) {
2693
- try {
2694
- if (params.proposalId) {
2695
- const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved);
2696
- if (!result) {
2697
- const denied = { state: "budget_exhausted", approved: false, reason: "proposal_not_pending" };
2698
- return { content: [{ type: "text", text: JSON.stringify(denied) }], details: denied };
2699
- }
2700
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { ...result, reason: params.approved ? "approved" : "rejected" } };
2701
- }
2702
- if (!params.name)
2703
- throw new WorkflowError("INVALID_METADATA", "workflow_respond requires name or proposalId");
2704
- const accepted = await answerCheckpoint(params.runId, params.name, params.approved);
2705
- return { content: [{ type: "text", text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response." }], details: { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" } };
2706
- }
2707
- catch (error) {
2708
- throw mainAgentError(error);
2709
- }
2710
- },
2711
- });
2712
- pi.registerTool({
2713
- name: "workflow_stop",
2714
- label: "Workflow Stop",
2715
- description: "Stop an active workflow run by ID",
2716
- parameters: Type.Object({ runId: Type.String() }, { additionalProperties: false }),
2717
- async execute(_id, params) {
2718
- try {
2719
- const result = await stopWorkflowRun(params.runId);
2720
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
2721
- }
2722
- catch (error) {
2723
- throw mainAgentError(error);
2724
- }
2725
- },
2726
- });
2727
- let catalogRegistered = false;
2728
- let sessionStarted = false;
2729
- const registerCatalog = () => {
2730
- if (catalogRegistered || !pi.getActiveTools().includes("workflow"))
2731
- return;
2732
- const catalog = registry.catalog();
2733
- const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
2734
- if (!catalog.functions.length && !catalog.variables.length && !catalog.workflows.length && !hasAliases)
2735
- return;
2736
- pi.registerTool({
2737
- name: "workflow_catalog",
2738
- label: "Workflow Catalog",
2739
- description: "List global workflow functions, variables, and reusable workflows",
2740
- parameters: Type.Object({}, { additionalProperties: false }),
2741
- async execute() { return { content: [{ type: "text", text: JSON.stringify(registry.catalog()) }], details: {} }; }
2742
- });
2743
- catalogRegistered = true;
2744
- };
2745
- const refreshPausedRunAliases = async (run, context) => {
2746
- const loaded = await run.store.load();
2747
- const active = new Set(pi.getActiveTools().filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_stop" && tool !== "workflow_catalog"));
2748
- const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
2749
- if (missing)
2750
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
2751
- const settingsPath = workflowSettingsPath();
2752
- const currentSettings = loadSettings(settingsPath);
2753
- resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath);
2754
- const currentAliases = currentSettings.modelAliases ?? {};
2755
- const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
2756
- const modelRegistry = context?.modelRegistry;
2757
- const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`));
2758
- if (context?.model)
2759
- knownModels.add(`${context.model.provider}/${context.model.id}`);
2760
- const resumeModels = modelRegistry ? knownModels : new Set([...loaded.snapshot.models, ...knownModels]);
2761
- const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
2762
- const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
2763
- const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
2764
- await run.store.saveSnapshot(snapshot);
2765
- run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
2766
- run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
2767
- const drift = aliasDrift(previousAliases, currentAliases);
2768
- if (drift.length)
2769
- await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
2770
- };
2771
- const coldResumeRun = async (run, hasUI, ui, trustedProject, context) => {
2772
- const loaded = await run.store.load();
2773
- if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
2774
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
2775
- if (loaded.snapshot.roles === undefined)
2776
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow role definitions are missing from the launch snapshot");
2777
- if ((loaded.snapshot.projectRoles?.length ?? 0) > 0 && !trustedProject)
2778
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot restore project roles in an untrusted project");
2779
- const missingRole = loaded.snapshot.agentTypes.find((role) => !loaded.snapshot.roles?.[role]);
2780
- if (missingRole)
2781
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
2782
- const active = new Set(pi.getActiveTools().filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_stop" && tool !== "workflow_catalog"));
2783
- const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
2784
- if (missing)
2785
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
2786
- const settingsPath = workflowSettingsPath();
2787
- const currentSettings = loadSettings(settingsPath);
2788
- resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
2789
- const currentAliases = currentSettings.modelAliases ?? {};
2790
- const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
2791
- const modelRegistry = context?.modelRegistry;
2792
- const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`));
2793
- if (context?.model)
2794
- knownModels.add(`${context.model.provider}/${context.model.id}`);
2795
- const resumeModels = modelRegistry ? knownModels : new Set([...loaded.snapshot.models, ...knownModels]);
2796
- const resumeAliases = { ...previousAliases, ...currentAliases };
2797
- const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
2798
- const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
2799
- preflight(loaded.snapshot.script, { models: resumeModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels: resumeModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata);
2800
- const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
2801
- await run.store.saveSnapshot(snapshot);
2802
- run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
2803
- const drift = aliasDrift(previousAliases, currentAliases);
2804
- if (drift.length)
2805
- await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
2806
- const controller = new AbortController();
2807
- run.abortController = controller;
2808
- const runContext = workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, controller.signal);
2809
- run.executor.setRunContext(runContext);
2810
- let variables;
2811
- try {
2812
- variables = await resolveWorkflowVariables(runContext, controller, registry);
2813
- }
2814
- catch (error) {
2815
- const typed = asWorkflowError(error);
2816
- if (!["completed", "failed", "stopped"].includes(run.lifecycle.state)) {
2817
- await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
2818
- const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } }));
2819
- await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
2820
- run.update?.(workflowToolUpdate(persisted));
2821
- }
2822
- throw typed;
2823
- }
2824
- await scheduler.cancelRun(run.store.runId);
2825
- await run.lifecycle.resume();
2826
- const execution = runWorkflow(loaded.snapshot.script, loaded.snapshot.args, withWorkflowFunctions({ agent: async (prompt, options, signal, identity) => {
2827
- await run.lifecycle.enter();
2828
- const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
2829
- const conversationLock = conversationId ? `${run.store.runId}:${conversationId}` : "";
2830
- try {
2831
- const path = conversationId ? conversationTurnPath(identity) : agentIdentityPath(identity);
2832
- const replayed = await run.store.replay(path);
2833
- if (replayed) {
2834
- if (conversationId) {
2835
- const conversation = await run.store.conversation(conversationId);
2836
- if (!conversation || conversation.head.turn < (identity.conversation?.turn ?? 0))
2837
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Completed conversation turn has no persisted head");
2838
- }
2839
- return replayed.value;
2840
- }
2841
- if (conversationId) {
2842
- if (conversationLocks.has(conversationLock))
2843
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation turns cannot overlap");
2844
- const conversation = await run.store.conversation(conversationId);
2845
- if (conversation ? conversation.head.turn + 1 !== identity.conversation?.turn : identity.conversation?.turn !== 1)
2846
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation turn does not continue its persisted head");
2847
- conversationLocks.add(conversationLock);
2848
- }
2849
- const worktree = agentWorktree(identity);
2850
- const cwd = worktree.worktreeOwner ? (await persistWorktree(run.store, run.metadata, worktree.worktreeOwner)).cwd : run.store.cwd;
2851
- const role = typeof options.role === "string" ? options.role : undefined;
2852
- const model = typeof options.model === "string" ? options.model : undefined;
2853
- const thinking = parseThinking(options.thinking);
2854
- const requestedLabel = typeof options.label === "string" ? options.label : undefined;
2855
- const resolved = run.executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
2856
- const label = displayAgentName(requestedLabel, role, resolved.model);
2857
- const tools = resolved.tools;
2858
- const schema = object(options.outputSchema) ? options.outputSchema : undefined;
2859
- const spawned = scheduler.spawn(run.store.runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd, tools, ...worktree, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(schema ? { schema } : {}), ...(typeof options.retries === "number" ? { retries: options.retries } : {}), ...(positiveInteger(options.timeoutMs) || options.timeoutMs === null ? { timeoutMs: options.timeoutMs } : {}), ...(conversationId ? { conversation: { id: conversationId, turn: identity.conversation?.turn ?? 0 } } : {}), agentOptions: options, agentIdentity: identity });
2860
- const cancel = () => { scheduler.cancel(spawned.id); };
2861
- signal.addEventListener("abort", cancel, { once: true });
2862
- const outcome = await spawned.result.finally(() => { signal.removeEventListener("abort", cancel); });
2863
- if (!outcome.ok)
2864
- throw new WorkflowError(outcome.error.code, outcome.error.message);
2865
- await run.store.complete(path, outcome.value);
2866
- return outcome.value;
2867
- }
2868
- finally {
2869
- if (conversationLock)
2870
- conversationLocks.delete(conversationLock);
2871
- await run.lifecycle.leave();
2872
- }
2873
- }, checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, false, hasUI ? ui : undefined), phase: async (phase) => { await run.lifecycle.enter(); try {
2874
- let previousPhase;
2875
- const persisted = await persistRunState(run.store, run.metadata, (current) => { previousPhase = current.phase; return { ...current, phase }; });
2876
- await eventPublisher.phase(run.store, run.metadata, previousPhase, phase);
2877
- runs.get(run.store.runId)?.update?.(workflowToolUpdate(persisted));
2878
- }
2879
- finally {
2880
- await run.lifecycle.leave();
2881
- } }, log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
2882
- run.execution = execution;
2883
- const completion = execution.result.then(async (value) => {
2884
- await scheduler.flush();
2885
- if (run.budget.hardExhausted)
2886
- throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
2887
- const resultPath = await run.store.saveResult(value);
2888
- await run.lifecycle.terminal("completed", "completed");
2889
- await eventPublisher.runCompleted(run.store, run.metadata, resultPath);
2890
- return { value, resultPath };
2891
- }).catch(async (error) => {
2892
- await scheduler.flush();
2893
- const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
2894
- if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
2895
- await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
2896
- const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), error: { code: typed.code, message: typed.message } }));
2897
- const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
2898
- await eventPublisher.runFailed(run.store, run.metadata, typed, state);
2899
- run.update?.(workflowToolUpdate(persisted));
2900
- if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
2901
- deliverFailure(pi, run.metadata.name, error);
2902
- });
2903
- void completion;
2904
- };
2905
- const applyBudgetDecision = async (request, approved) => {
2906
- const run = runs.get(request.runId);
2907
- if (!run)
2908
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
2909
- if (!approved)
2910
- return { state: "budget_exhausted", approved: false };
2911
- const nextBudget = validateBudget(request.proposed);
2912
- const nextVersion = request.budgetVersion + 1;
2913
- const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, request.consumed, run.budget.events, { active: false });
2914
- run.budget = runtime;
2915
- await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget)
2916
- next.budget = nextBudget;
2917
- else
2918
- delete next.budget; return next; });
2919
- await coldResumeRun(run, false, {}, true);
2920
- return { state: "running", approved: true };
2921
- };
2922
- const resumeWorkflowRun = async (runId, rawPatch) => {
2923
- const run = runs.get(runId);
2924
- if (!run)
2925
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${runId}`);
2926
- const loaded = await run.store.load();
2927
- if (loaded.run.state !== "budget_exhausted")
2928
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Only budget-exhausted runs can be resumed with workflow_resume");
2929
- const currentBudget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
2930
- const patch = rawPatch === undefined ? {} : validateBudgetPatch(rawPatch);
2931
- const nextBudget = mergeBudget(currentBudget, patch);
2932
- const usage = budgetUsage(loaded.run.usage);
2933
- if (!resumeBudgetAllowed(nextBudget, usage))
2934
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Every exhausted hard budget must be raised above retained usage or removed");
2935
- if (budgetRelaxed(currentBudget, nextBudget)) {
2936
- const proposalId = randomUUID();
2937
- const request = { kind: "budget", proposalId, runId, consumed: usage, previous: currentBudget ?? {}, proposed: nextBudget ?? {}, budgetVersion: loaded.run.budgetVersion ?? 1 };
2938
- const decision = new Promise((resolve) => { run.budgetResolvers.set(proposalId, resolve); });
2939
- try {
2940
- await run.store.requestWorkflowDecision(request);
2941
- await appendBudgetDecisionEvent(run, request, "adjustment_requested");
2942
- }
2943
- catch (error) {
2944
- run.budgetResolvers.delete(proposalId);
2945
- throw error;
2946
- }
2947
- deliver(pi, budgetDecisionDelivery(run.metadata, request));
2948
- const decisionResult = await decision;
2949
- return { state: decisionResult.state };
2950
- }
2951
- const changed = JSON.stringify(currentBudget ?? {}) !== JSON.stringify(nextBudget ?? {});
2952
- if (changed) {
2953
- const nextVersion = (loaded.run.budgetVersion ?? 1) + 1;
2954
- const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, usage, loaded.run.budgetEvents, { active: false });
2955
- run.budget = runtime;
2956
- await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget)
2957
- next.budget = nextBudget;
2958
- else
2959
- delete next.budget; return next; });
2960
- }
2961
- await coldResumeRun(run, false, {}, true);
2962
- return { state: "running" };
2963
- };
2964
- pi.registerTool({
2965
- name: "workflow_resume",
2966
- label: "Workflow Resume",
2967
- description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
2968
- parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
2969
- async execute(_id, params) {
2970
- try {
2971
- const result = await resumeWorkflowRun(params.runId, params.budget);
2972
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
2973
- }
2974
- catch (error) {
2975
- throw mainAgentError(error);
2976
- }
2977
- },
2978
- });
2979
- pi.on("session_start", async (_event, ctx) => {
2980
- if (sessionStarted)
2981
- return;
2982
- sessionStarted = true;
2983
- registry.freeze();
2984
- registerCatalog();
2985
- await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
2986
- try {
2987
- for (const runId of await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)) {
2988
- if (runs.has(runId))
2989
- continue;
2990
- const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
2991
- let loaded;
2992
- try {
2993
- loaded = await store.load();
2994
- }
2995
- catch {
2996
- if (!await store.isComplete())
2997
- await store.delete(true).catch(() => undefined);
2998
- continue;
2999
- }
3000
- if (loaded.run.state === "completed" || loaded.run.state === "failed" || loaded.run.state === "stopped") {
3001
- terminalRunStates.set(runId, loaded.run.state);
3002
- continue;
3003
- }
3004
- if (loaded.run.state !== "interrupted" && loaded.run.state !== "budget_exhausted") {
3005
- const previousState = loaded.run.state;
3006
- await store.updateState((current) => ["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state) ? current : { ...current, state: "interrupted" });
3007
- loaded = { ...loaded, run: (await store.load()).run };
3008
- await eventPublisher.runState(store, loaded.snapshot.metadata, previousState, "interrupted", "session_shutdown");
3009
- loaded = { ...loaded, run: (await store.load()).run };
3010
- }
3011
- const model = modelSpec(loaded.snapshot.models[0] ?? "", { provider: ctx.model?.provider ?? "", model: ctx.model?.id ?? "", thinking: pi.getThinkingLevel() });
3012
- const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
3013
- eventPublisher.seedBudget(runId, loaded.run.budgetEvents);
3014
- const budgetRuntime = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents, { active: loaded.run.state === "running" });
3015
- const lifecycle = lifecycleFor(store, loaded.run.state, budgetRuntime, loaded.snapshot.metadata);
3016
- const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
3017
- const roleDefinitions = loaded.snapshot.roles ?? {};
3018
- runs.set(runId, { executor: new WorkflowAgentExecutor({ cwd: ctx.cwd, model, tools: new Set(loaded.snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), agentDefinitions: roleDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(store.cwd, projectTrusted(ctx)) }, createSession), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController: new AbortController(), projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), budgetResolvers: new Map() });
3019
- for (const checkpoint of await store.awaitingCheckpoints())
3020
- deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
3021
- for (const decision of await store.pendingWorkflowDecisions())
3022
- deliver(pi, budgetDecisionDelivery(loaded.snapshot.metadata, decision));
3023
- scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch());
3024
- }
3025
- const resumeSelect = uiHostCapabilities(ctx.ui)?.select;
3026
- if (ctx.hasUI && resumeSelect) {
3027
- const interrupted = [...runs.values()].filter((r) => r.lifecycle.state === "interrupted");
3028
- if (interrupted.length > 0) {
3029
- const labels = interrupted.map((r) => `Resume: ${r.metadata.name} (${r.store.runId.slice(0, 8)})`);
3030
- const options = [...labels, ...(interrupted.length > 1 ? ["Resume all"] : []), "Skip"];
3031
- const choice = await resumeSelect(`${String(interrupted.length)} interrupted workflow${interrupted.length > 1 ? "s" : ""} found`, options);
3032
- if (choice && choice !== "Skip") {
3033
- const toResume = choice === "Resume all" ? interrupted : interrupted.filter((_, i) => labels[i] === choice);
3034
- for (const run of toResume) {
3035
- try {
3036
- await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx);
3037
- ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info");
3038
- }
3039
- catch (err) {
3040
- ctx.ui.notify(`Cannot resume ${run.metadata.name}: ${err instanceof Error ? err.message : String(err)}`, "warning");
3041
- }
3042
- }
3043
- }
3044
- }
3045
- }
3046
- }
3047
- catch (error) {
3048
- await releaseSessionLease();
3049
- throw error;
3050
- }
3051
- });
3052
- pi.on("before_agent_start", (event, ctx) => {
3053
- if (!pi.getActiveTools().includes("workflow"))
3054
- return;
3055
- const roles = Object.entries(loadAgentDefinitions(ctx.cwd, undefined, projectTrusted(ctx))).filter(([, definition]) => definition.description);
3056
- if (!roles.length)
3057
- return;
3058
- const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
3059
- return { systemPrompt: `${event.systemPrompt}\n\n${content}` };
3060
- });
3061
- pi.registerTool({
3062
- name: "workflow",
3063
- label: WORKFLOW_TOOL_LABEL,
3064
- description: WORKFLOW_TOOL_DESCRIPTION,
3065
- promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
3066
- parameters: WORKFLOW_TOOL_PARAMETERS,
3067
- async execute(_id, params, signal, onUpdate, ctx) {
3068
- try {
3069
- const settingsPath = workflowSettingsPath();
3070
- const defaults = loadSettings(settingsPath);
3071
- if (!ctx.model)
3072
- throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
3073
- const settings = Object.freeze({ concurrency: params.concurrency ?? defaults.concurrency, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}) });
3074
- const budget = validateBudget(params.budget);
3075
- const rootModel = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
3076
- const rootModelName = `${rootModel.provider}/${rootModel.model}`;
3077
- const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
3078
- const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? [ctx.model]).map((model) => `${model.provider}/${model.id}`));
3079
- knownModels.add(rootModelName);
3080
- const availableModels = knownModels;
3081
- const rootTools = pi.getActiveTools().filter((name) => name !== "workflow" && name !== "workflow_respond" && name !== "workflow_stop" && name !== "workflow_catalog");
3082
- const trustedProject = projectTrusted(ctx);
3083
- if (typeof ctx.cwd === "string")
3084
- resolveAgentResourcePolicy(ctx.cwd, trustedProject, settingsPath);
3085
- const validated = validateWorkflowLaunchWithRegistry(params, { cwd: ctx.cwd, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
3086
- const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
3087
- await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
3088
- const runId = randomUUID();
3089
- const args = (params.args ?? null);
3090
- encoded(args);
3091
- const runController = new AbortController();
3092
- if (signal?.aborted)
3093
- runController.abort();
3094
- else
3095
- signal?.addEventListener("abort", () => { runController.abort(); }, { once: true });
3096
- const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
3097
- const variables = await resolveWorkflowVariables(runContext, runController, registry);
3098
- const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
3099
- const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]]));
3100
- const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
3101
- const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, defaults.modelAliases, knownModels, settingsPath)] : []; });
3102
- const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
3103
- const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}), ...(budget ? { budget } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
3104
- const budgetRuntime = new WorkflowBudgetRuntime(budget);
3105
- const initialBudget = budgetRuntime.snapshot();
3106
- await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
3107
- const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
3108
- const background = !params.foreground;
3109
- const providerPause = async () => { if (background)
3110
- deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
3111
- const executor = new WorkflowAgentExecutor({ cwd: ctx.cwd, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases: defaults.modelAliases ?? {}, settingsPath, agentDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(ctx.cwd, projectTrusted(ctx)), runContext }, createSession);
3112
- runs.set(runId, { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), budgetResolvers: new Map(), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) });
3113
- if (params.foreground && onUpdate)
3114
- onUpdate(workflowToolUpdate((await store.load()).run));
3115
- scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
3116
- const execution = runWorkflow(script, args, withWorkflowFunctions({ agent: async (prompt, options, agentSignal, identity) => {
3117
- await lifecycle.enter();
3118
- const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
3119
- const conversationLock = conversationId ? `${runId}:${conversationId}` : "";
3120
- try {
3121
- const path = conversationId ? conversationTurnPath(identity) : agentIdentityPath(identity);
3122
- const replayed = await store.replay(path);
3123
- if (replayed) {
3124
- if (conversationId) {
3125
- const conversation = await store.conversation(conversationId);
3126
- if (!conversation || conversation.head.turn < (identity.conversation?.turn ?? 0))
3127
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Completed conversation turn has no persisted head");
3128
- }
3129
- return replayed.value;
3130
- }
3131
- if (conversationId) {
3132
- if (conversationLocks.has(conversationLock))
3133
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation turns cannot overlap");
3134
- const conversation = await store.conversation(conversationId);
3135
- if (conversation ? conversation.head.turn + 1 !== identity.conversation?.turn : identity.conversation?.turn !== 1)
3136
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation turn does not continue its persisted head");
3137
- conversationLocks.add(conversationLock);
3138
- }
3139
- const worktree = agentWorktree(identity);
3140
- const cwd = worktree.worktreeOwner ? (await persistWorktree(store, checked.metadata, worktree.worktreeOwner)).cwd : ctx.cwd;
3141
- const role = typeof options.role === "string" ? options.role : undefined;
3142
- const model = typeof options.model === "string" ? options.model : undefined;
3143
- const thinking = parseThinking(options.thinking);
3144
- const requestedLabel = typeof options.label === "string" ? options.label : undefined;
3145
- const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: checked.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
3146
- const label = displayAgentName(requestedLabel, role, resolved.model);
3147
- const tools = resolved.tools;
3148
- const schema = object(options.outputSchema) ? options.outputSchema : undefined;
3149
- const spawned = scheduler.spawn(runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd, tools, ...worktree, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(schema ? { schema } : {}), ...(typeof options.retries === "number" ? { retries: options.retries } : {}), ...(positiveInteger(options.timeoutMs) || options.timeoutMs === null ? { timeoutMs: options.timeoutMs } : {}), ...(conversationId ? { conversation: { id: conversationId, turn: identity.conversation?.turn ?? 0 } } : {}), agentOptions: options, agentIdentity: identity });
3150
- const cancel = () => { scheduler.cancel(spawned.id); };
3151
- if (agentSignal.aborted)
3152
- cancel();
3153
- else
3154
- agentSignal.addEventListener("abort", cancel, { once: true });
3155
- const outcome = await spawned.result.finally(() => { agentSignal.removeEventListener("abort", cancel); });
3156
- if (!outcome.ok)
3157
- throw new WorkflowError(outcome.error.code, outcome.error.message);
3158
- await store.complete(path, outcome.value);
3159
- return outcome.value;
3160
- }
3161
- finally {
3162
- if (conversationLock)
3163
- conversationLocks.delete(conversationLock);
3164
- await lifecycle.leave();
3165
- }
3166
- }, checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined), phase: async (phase) => {
3167
- await lifecycle.enter();
3168
- try {
3169
- let previousPhase;
3170
- const persisted = await persistRunState(store, checked.metadata, (current) => { previousPhase = current.phase; return { ...current, phase }; });
3171
- await eventPublisher.phase(store, checked.metadata, previousPhase, phase);
3172
- runs.get(runId)?.update?.(workflowToolUpdate(persisted));
3173
- }
3174
- finally {
3175
- await lifecycle.leave();
3176
- }
3177
- }, log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
3178
- runs.get(runId).execution = execution;
3179
- await eventPublisher.runStarted(store, checked.metadata);
3180
- const finish = execution.result.then(async (value) => {
3181
- await scheduler.flush();
3182
- if (budgetRuntime.hardExhausted)
3183
- throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
3184
- const resultPath = await store.saveResult(value);
3185
- await lifecycle.terminal("completed", "completed");
3186
- await eventPublisher.runCompleted(store, checked.metadata, resultPath);
3187
- return { value, resultPath };
3188
- }).catch(async (error) => {
3189
- await scheduler.flush();
3190
- const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
3191
- if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
3192
- await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
3193
- await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
3194
- const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
3195
- await eventPublisher.runFailed(store, checked.metadata, typed, state);
3196
- throw typed;
3197
- });
3198
- runs.get(runId).completion = finish;
3199
- if (background) {
3200
- void finish.then(async ({ value, resultPath }) => {
3201
- deliver(pi, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
3202
- }, (error) => { deliverFailure(pi, checked.metadata.name, error); });
3203
- return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
3204
- }
3205
- const { value } = await finish;
3206
- const run = (await store.load()).run;
3207
- return { content: [{ type: "text", text: JSON.stringify(value) }], details: { runId, value, run } };
3208
- }
3209
- catch (error) {
3210
- throw mainAgentError(error);
3211
- }
3212
- },
3213
- renderCall(args) {
3214
- return textBlock(formatWorkflowPreview(args));
3215
- },
3216
- renderResult(result, { isPartial }, _theme, context) {
3217
- const details = result.details;
3218
- const state = context.state;
3219
- if (details?.run && isPartial && details.run.state === "running" && !state.workflowSpinner) {
3220
- state.workflowSpinner = setInterval(context.invalidate, 80);
3221
- state.workflowSpinner.unref();
3222
- }
3223
- else if ((!isPartial || details?.run?.state !== "running") && state.workflowSpinner) {
3224
- clearInterval(state.workflowSpinner);
3225
- delete state.workflowSpinner;
3226
- }
3227
- if (details?.run)
3228
- return workflowProgressBlock(details.run);
3229
- const content = result.content[0];
3230
- return textBlock(isPartial ? "Workflow starting..." : details?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
3231
- },
3232
- });
3233
- pi.registerCommand("workflow", {
3234
- description: "Inspect and control workflows for this Pi session",
3235
- handler: async (args, ctx) => {
3236
- const command = args.trim();
3237
- if (command === "doctor") {
3238
- const { doctor, doctorExitCode, formatDoctorReport } = await import("./doctor.js");
3239
- const report = await doctor({ cwd: ctx.cwd, activeTools: pi.getActiveTools().filter((tool) => tool !== "workflow" && tool !== "workflow_respond") });
3240
- ctx.ui.notify(formatDoctorReport(report), doctorExitCode(report) ? "warning" : "info");
3241
- return;
3242
- }
3243
- await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
3244
- const loadStores = async () => {
3245
- const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
3246
- const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
3247
- try {
3248
- return { store, loaded: await store.load() };
3249
- }
3250
- catch {
3251
- if (!await store.isComplete())
3252
- await store.delete(true).catch(() => undefined);
3253
- return undefined;
3254
- }
3255
- }));
3256
- return entries.filter((entry) => entry !== undefined);
3257
- };
3258
- let stores = await loadStores();
3259
- const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or proposal-id]. Use workflow_resume for budget patches.";
3260
- const setWorkflowStatus = (text) => {
3261
- const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
3262
- setStatus?.call(ctx.ui, "workflow-stop", text);
3263
- };
3264
- const runAction = async (actionCommand, keepContext, status = setWorkflowStatus) => {
3265
- const [action, runId, ...rest] = actionCommand.split(/\s+/);
3266
- try {
3267
- const run = runId ? runs.get(runId) : undefined;
3268
- const storedEntry = runId ? stores.find(({ store }) => store.runId === runId) : undefined;
3269
- const stored = storedEntry ? { store: storedEntry.store, loaded: await storedEntry.store.load() } : undefined;
3270
- if ((action === "approve" || action === "reject") && runId && rest.length) {
3271
- const accepted = await answerCheckpoint(runId, rest.join(" "), action === "approve", true);
3272
- ctx.ui.notify(accepted ? `${action === "approve" ? "Approved" : "Rejected"} checkpoint ${rest.join(" ")}.` : "Checkpoint is not awaiting a response.", accepted ? "info" : "warning");
3273
- return keepContext ? "dashboard" : "done";
3274
- }
3275
- if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
3276
- const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true);
3277
- ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
3278
- return keepContext ? "dashboard" : "done";
3279
- }
3280
- if (action === "delete" && stored) {
3281
- if (!["completed", "failed", "stopped"].includes(stored.loaded.run.state)) {
3282
- ctx.ui.notify("Stop the workflow before deleting it.", "warning");
3283
- return keepContext ? "dashboard" : "done";
3284
- }
3285
- if (!await ctx.ui.confirm("Delete workflow?", `Delete ${stored.loaded.run.workflowName} (${stored.store.runId}) and all owned artifacts? This cannot be undone.`))
3286
- return keepContext ? "dashboard" : "done";
3287
- await stored.store.delete(true);
3288
- runs.delete(stored.store.runId);
3289
- terminalRunStates.delete(stored.store.runId);
3290
- ctx.ui.notify(`Deleted workflow ${stored.store.runId}.`, "info");
3291
- return keepContext ? "picker" : "done";
3292
- }
3293
- if (action === "pause" && run) {
3294
- await run.lifecycle.pause();
3295
- ctx.ui.notify(`Paused workflow ${run.store.runId}.`, "info");
3296
- return keepContext ? "dashboard" : "done";
3297
- }
3298
- if (action === "resume" && run) {
3299
- if (run.lifecycle.state === "budget_exhausted") {
3300
- const patch = rest.length ? JSON.parse(rest.join(" ")) : undefined;
3301
- const result = await resumeWorkflowRun(run.store.runId, patch);
3302
- ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
3303
- }
3304
- else {
3305
- if (run.lifecycle.state === "interrupted")
3306
- await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
3307
- else {
3308
- if (run.lifecycle.state === "paused")
3309
- await refreshPausedRunAliases(run, ctx);
3310
- await run.lifecycle.resume();
3311
- }
3312
- ctx.ui.notify(`Resumed workflow ${run.store.runId}.`, "info");
3313
- }
3314
- return keepContext ? "dashboard" : "done";
3315
- }
3316
- if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
3317
- const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
3318
- if (input === undefined)
3319
- return keepContext ? "dashboard" : "done";
3320
- const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
3321
- ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
3322
- return keepContext ? "dashboard" : "done";
3323
- }
3324
- if (action === "stop" && run) {
3325
- const workflowName = stored?.loaded.run.workflowName ?? run.metadata.name;
3326
- if (keepContext && !await ctx.ui.confirm("Stop workflow?", `Stop workflow ${workflowName} (${run.store.runId})? This cannot be undone.`))
3327
- return "dashboard";
3328
- if (keepContext)
3329
- status(`Stopping workflow ${workflowName}...`);
3330
- await stopWorkflowRun(run.store.runId);
3331
- if (keepContext)
3332
- status(`Workflow ${run.store.runId} stopped.`);
3333
- ctx.ui.notify(`Stopped workflow ${run.store.runId}.`, "info");
3334
- return keepContext ? "dashboard" : "done";
3335
- }
3336
- if (keepContext && action && runId) {
3337
- ctx.ui.notify(`Cannot ${action} workflow ${runId}: the run is no longer available.`, "warning");
3338
- return "dashboard";
3339
- }
3340
- ctx.ui.notify(usage, "warning");
3341
- return "done";
3342
- }
3343
- catch (error) {
3344
- if (!keepContext)
3345
- throw error;
3346
- const message = error instanceof Error ? error.message : String(error);
3347
- if (action === "stop")
3348
- status(`Could not stop workflow ${runId ?? ""}: ${message}`);
3349
- ctx.ui.notify(`Cannot ${action ?? "workflow action"}${runId ? ` for ${runId}` : ""}: ${message}`, "warning");
3350
- return "dashboard";
3351
- }
3352
- };
3353
- const manageAliases = async () => {
3354
- const settingsPath = workflowSettingsPath();
3355
- const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
3356
- const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
3357
- const selectTarget = async () => {
3358
- const models = available();
3359
- const choice = await ctx.ui.select("Model alias target", [...models, "Manual model ID", "Back"]);
3360
- if (!choice || choice === "Back")
3361
- return undefined;
3362
- if (choice !== "Manual model ID")
3363
- return choice;
3364
- return (await ctx.ui.input("Manual model ID", "provider/model[:thinking]"))?.trim() || undefined;
3365
- };
3366
- const save = (aliases) => {
3367
- try {
3368
- saveModelAliases(settingsPath, aliases);
3369
- ctx.ui.notify(`Saved model aliases to ${settingsPath}.`, "info");
3370
- return true;
3371
- }
3372
- catch (error) {
3373
- ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
3374
- return false;
3375
- }
3376
- };
3377
- for (;;) {
3378
- let aliases;
3379
- try {
3380
- aliases = loadSettings(settingsPath).modelAliases ?? {};
3381
- }
3382
- catch (error) {
3383
- ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
3384
- return;
3385
- }
3386
- const names = Object.keys(aliases).sort();
3387
- const listing = names.length ? names.map((name) => `${name} = ${aliases[name] ?? ""}`).join("\n") : "(none)";
3388
- const options = ["Add alias", ...names.map((name) => `Edit ${name}`), ...names.map((name) => `Delete ${name}`), "Back"];
3389
- const choice = await ctx.ui.select(`Model aliases\n${listing}`, options);
3390
- if (!choice || choice === "Back")
3391
- return;
3392
- if (choice === "Add alias") {
3393
- const name = (await ctx.ui.input("Alias name", "reviewer-model"))?.trim();
3394
- if (!name)
3395
- continue;
3396
- if (Object.prototype.hasOwnProperty.call(aliases, name)) {
3397
- ctx.ui.notify(`Alias ${name} already exists; choose Edit ${name}.`, "warning");
3398
- continue;
3399
- }
3400
- const target = await selectTarget();
3401
- if (!target)
3402
- continue;
3403
- const next = { ...aliases, [name]: target };
3404
- try {
3405
- validateModelAliases(next, settingsPath);
3406
- }
3407
- catch (error) {
3408
- ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
3409
- continue;
3410
- }
3411
- const parsed = parseModelReference(target);
3412
- if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
3413
- ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
3414
- if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?"))
3415
- continue;
3416
- }
3417
- save(next);
3418
- continue;
3419
- }
3420
- const edit = /^Edit (.+)$/.exec(choice);
3421
- if (edit?.[1]) {
3422
- const target = await selectTarget();
3423
- if (!target)
3424
- continue;
3425
- const next = { ...aliases, [edit[1]]: target };
3426
- try {
3427
- validateModelAliases(next, settingsPath);
3428
- }
3429
- catch (error) {
3430
- ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
3431
- continue;
3432
- }
3433
- const parsed = parseModelReference(target);
3434
- if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
3435
- ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
3436
- if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?"))
3437
- continue;
3438
- }
3439
- save(next);
3440
- continue;
3441
- }
3442
- const deletion = /^Delete (.+)$/.exec(choice);
3443
- if (deletion?.[1] && await ctx.ui.confirm("Delete model alias?", `Delete ${deletion[1]}? Future workflow resumes using this alias may fail.`)) {
3444
- const next = Object.fromEntries(Object.entries(aliases).filter(([name]) => name !== deletion[1]));
3445
- save(next);
3446
- }
3447
- }
3448
- };
3449
- if (command === "model-aliases") {
3450
- if (!ctx.hasUI) {
3451
- ctx.ui.notify("Model alias management requires UI.", "warning");
3452
- return;
3453
- }
3454
- await manageAliases();
3455
- return;
3456
- }
3457
- if (!command) {
3458
- for (;;) {
3459
- if (!ctx.hasUI) {
3460
- if (!stores.length) {
3461
- ctx.ui.notify("No workflow runs in this session.", "info");
3462
- return;
3463
- }
3464
- const details = await Promise.all(stores.map(async ({ store, loaded }) => formatNavigatorRun(loaded, await store.awaitingCheckpoints(), await store.worktrees())));
3465
- ctx.ui.notify(details.join("\n\n"), "info");
3466
- return;
3467
- }
3468
- const sorted = navigatorAttentionSort(stores);
3469
- const labels = navigatorRunLabels(sorted);
3470
- const terminalStates = new Set(["completed", "failed", "stopped"]);
3471
- const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
3472
- const pickerOptions = [...labels, "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : [])];
3473
- const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
3474
- if (!runChoice || runChoice === "Close")
3475
- return;
3476
- if (runChoice === "Model aliases") {
3477
- await manageAliases();
3478
- stores = await loadStores();
3479
- continue;
3480
- }
3481
- if (runChoice === "Delete all completed") {
3482
- if (!await ctx.ui.confirm("Delete completed runs?", "Delete all completed workflow runs and their artifacts? This cannot be undone."))
3483
- continue;
3484
- for (const entry of sorted) {
3485
- if (entry.loaded.run.state === "completed") {
3486
- await entry.store.delete(true);
3487
- runs.delete(entry.store.runId);
3488
- terminalRunStates.delete(entry.store.runId);
3489
- }
3490
- }
3491
- ctx.ui.notify("Deleted all completed workflow runs.", "info");
3492
- stores = await loadStores();
3493
- continue;
3494
- }
3495
- const runIndex = labels.indexOf(runChoice);
3496
- if (runIndex < 0)
3497
- return;
3498
- const selected = sorted[runIndex];
3499
- if (!selected)
3500
- return;
3501
- const { store } = selected;
3502
- const copyArtifact = async (value, artifact) => {
3503
- try {
3504
- await clipboard(value);
3505
- ctx.ui.notify(`Copied ${artifact}.`, "info");
3506
- }
3507
- catch (error) {
3508
- ctx.ui.notify(`Failed to copy ${artifact}: ${error instanceof Error ? error.message : String(error)}`, "error");
3509
- }
3510
- };
3511
- const openTranscript = async (transcript) => {
3512
- try {
3513
- const entries = SessionManager.open(transcript).buildContextEntries();
3514
- if (ctx.mode !== "tui") {
3515
- ctx.ui.notify(`Transcript: ${transcript}`, "info");
3516
- return;
3517
- }
3518
- await ctx.ui.custom((tui, theme, keybindings, done) => {
3519
- let offset = 0;
3520
- let renderedLines = [];
3521
- const viewport = () => Math.max(1, tuiRows(tui) - 3);
3522
- const move = (delta) => { offset = Math.max(0, Math.min(Math.max(0, renderedLines.length - viewport()), offset + delta)); };
3523
- return {
3524
- render(width) {
3525
- renderedLines = transcriptLines(entries).flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
3526
- offset = Math.min(offset, Math.max(0, renderedLines.length - viewport()));
3527
- return [theme.fg("accent", "Native Pi transcript"), ...renderedLines.slice(offset, offset + viewport()), "", theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close")];
3528
- },
3529
- invalidate() { },
3530
- handleInput(data) {
3531
- if (keybindings.matches(data, "tui.select.up"))
3532
- move(-1);
3533
- else if (keybindings.matches(data, "tui.select.down"))
3534
- move(1);
3535
- else if (keybindings.matches(data, "tui.select.pageUp"))
3536
- move(-viewport());
3537
- else if (keybindings.matches(data, "tui.select.pageDown"))
3538
- move(viewport());
3539
- else if (keybindings.matches(data, "tui.editor.cursorLineStart"))
3540
- offset = 0;
3541
- else if (keybindings.matches(data, "tui.editor.cursorLineEnd"))
3542
- offset = Math.max(0, renderedLines.length - viewport());
3543
- else if (keybindings.matches(data, "tui.select.cancel"))
3544
- done(undefined);
3545
- tui.requestRender();
3546
- },
3547
- };
3548
- }, { overlay: true });
3549
- }
3550
- catch (error) {
3551
- ctx.ui.notify(`Cannot open transcript: ${error instanceof Error ? error.message : String(error)}`, "warning");
3552
- }
3553
- };
3554
- const loadDashboard = async () => {
3555
- const loaded = await store.load();
3556
- const checkpoints = await store.awaitingCheckpoints();
3557
- const worktrees = await store.worktrees();
3558
- const actions = new Map();
3559
- const copies = new Map();
3560
- const reviews = new Map();
3561
- const add = (label, value) => { actions.set(label, `${value} ${store.runId}`); };
3562
- const addCopy = (label, value, artifact) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
3563
- if (loaded.run.state === "running")
3564
- add("Pause", "pause");
3565
- if (["paused", "interrupted"].includes(loaded.run.state))
3566
- add("Resume", "resume");
3567
- if (loaded.run.state === "budget_exhausted") {
3568
- actions.set("Resume unchanged", `resume ${store.runId}`);
3569
- actions.set("Adjust budget", `adjust ${store.runId}`);
3570
- }
3571
- for (const decision of await store.pendingWorkflowDecisions()) {
3572
- const id = decision.proposalId.slice(0, 8);
3573
- actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
3574
- actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
3575
- }
3576
- if (!terminalStates.has(loaded.run.state))
3577
- add("Stop", "stop");
3578
- for (const cp of checkpoints) {
3579
- if (ctx.mode === "tui") {
3580
- const label = `Review ${cp.name}`;
3581
- actions.set(label, "review");
3582
- reviews.set(label, cp);
3583
- }
3584
- else {
3585
- actions.set(`Approve ${cp.name}`, `approve ${store.runId} ${cp.name}`);
3586
- actions.set(`Reject ${cp.name}`, `reject ${store.runId} ${cp.name}`);
3587
- }
3588
- }
3589
- if (ctx.mode !== "tui")
3590
- actions.set("Refresh", "refresh");
3591
- else
3592
- actions.set("View script", "view-script");
3593
- const transcripts = [...new Set([...loaded.run.agents.flatMap((agent) => (agent.attemptDetails ?? []).map((attempt) => attempt.sessionFile)), ...loaded.run.nativeSessions.map(({ sessionFile }) => sessionFile)])];
3594
- if (loaded.run.agents.length)
3595
- actions.set("Agents...", "agents");
3596
- if (!loaded.run.agents.length && ctx.mode === "tui" && transcripts.length)
3597
- actions.set("View transcript", "view-transcript");
3598
- if (!loaded.run.agents.length && transcripts.length)
3599
- actions.set("Transcript paths", "transcripts");
3600
- if (terminalStates.has(loaded.run.state))
3601
- add("Delete", "delete");
3602
- if (ctx.mode === "tui") {
3603
- addCopy("Copy run path", store.directory, "run path");
3604
- addCopy("Copy run ID", store.runId, "run ID");
3605
- }
3606
- return { dashboard: formatNavigatorDashboard(loaded.run, checkpoints, worktrees), actions, copies, reviews, transcripts, script: loaded.snapshot.script, agents: loaded.run.agents, worktrees };
3607
- };
3608
- const selectAgent = async (dashboard) => {
3609
- const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
3610
- const title = (agent) => {
3611
- const parents = [];
3612
- for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
3613
- const parent = byId.get(parentId);
3614
- if (!parent)
3615
- break;
3616
- parents.unshift(parent.label ?? parent.name);
3617
- }
3618
- return [...((agent.structuralPath ?? []).length ? [(agent.structuralPath ?? []).join(" > ")] : []), ...(agent.parentBreadcrumb ? [agent.parentBreadcrumb] : []), ...parents, agent.label ?? agent.name].join(" > ");
3619
- };
3620
- const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
3621
- const selectedLabel = await ctx.ui.select("Agents", [...labels, "Back"]);
3622
- const selectedIndex = selectedLabel ? labels.indexOf(selectedLabel) : -1;
3623
- const selected = selectedIndex >= 0 ? dashboard.agents[selectedIndex] : undefined;
3624
- if (!selected)
3625
- return;
3626
- const attempts = [...(selected.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
3627
- const worktree = selected.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === selected.worktreeOwner) : undefined;
3628
- const actions = [
3629
- ...(attempts.length ? ["View transcript", "Copy transcript path"] : []),
3630
- ...(worktree ? ["Copy branch", "Copy worktree path"] : []),
3631
- "Copy agent ID",
3632
- "Back",
3633
- ];
3634
- const chooseAttempt = async () => {
3635
- const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
3636
- const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Transcript attempts", [...choices, "Back"]);
3637
- const index = choice ? choices.indexOf(choice) : -1;
3638
- return index >= 0 ? attempts[index] : undefined;
3639
- };
3640
- for (;;) {
3641
- const action = await ctx.ui.select(title(selected), actions);
3642
- if (!action || action === "Back")
3643
- return;
3644
- if (action === "Copy agent ID") {
3645
- await copyArtifact(selected.id, "agent ID");
3646
- continue;
3647
- }
3648
- if (action === "Copy branch" && worktree) {
3649
- await copyArtifact(worktree.branch, "branch");
3650
- continue;
3651
- }
3652
- if (action === "Copy worktree path" && worktree) {
3653
- await copyArtifact(worktree.path, "worktree path");
3654
- continue;
3655
- }
3656
- if (action === "View transcript" || action === "Copy transcript path") {
3657
- const attempt = await chooseAttempt();
3658
- if (!attempt)
3659
- continue;
3660
- if (action === "Copy transcript path")
3661
- await copyArtifact(attempt.sessionFile, "transcript path");
3662
- else
3663
- await openTranscript(attempt.sessionFile);
3664
- }
3665
- }
3666
- };
3667
- for (;;) {
3668
- let view = await loadDashboard();
3669
- const actionChoice = ctx.mode === "tui"
3670
- ? await ctx.ui.custom((tui, theme, keybindings, done) => {
3671
- let options = [...view.actions.keys(), "Close"];
3672
- let selectedIndex = 0;
3673
- let dashboardOffset = 0;
3674
- let refreshing = false;
3675
- let disposed = false;
3676
- let stopRequested = false;
3677
- let stopStatus;
3678
- const terminalRows = () => Math.max(1, tuiRows(tui));
3679
- const keyLabels = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
3680
- const keyLabel = (binding, fallback) => {
3681
- const keys = keybindingKeys(keybindings, binding);
3682
- return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
3683
- };
3684
- const dashboardLayout = () => {
3685
- const rows = terminalRows();
3686
- const hintRows = rows >= 4 ? 1 : 0;
3687
- const separatorRows = rows >= 6 ? 1 : 0;
3688
- const available = Math.max(1, rows - hintRows - separatorRows);
3689
- const actionViewport = Math.min(options.length, Math.max(1, Math.floor(available / 2)));
3690
- return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
3691
- };
3692
- const updateDashboard = async (selectedOption) => {
3693
- const next = await loadDashboard();
3694
- if (disposed)
3695
- return;
3696
- view = next;
3697
- options = [...view.actions.keys(), "Close"];
3698
- selectedIndex = Math.max(0, options.indexOf(selectedOption ?? ""));
3699
- tui.requestRender();
3700
- };
3701
- const requestStop = () => {
3702
- if (stopRequested)
3703
- return;
3704
- stopRequested = true;
3705
- stopStatus = undefined;
3706
- setWorkflowStatus(undefined);
3707
- const selectedOption = options[selectedIndex];
3708
- void runAction(`stop ${store.runId}`, true, (status) => {
3709
- stopStatus = status;
3710
- setWorkflowStatus(status);
3711
- if (!disposed)
3712
- tui.requestRender();
3713
- }).then(() => updateDashboard(selectedOption)).catch((error) => {
3714
- if (disposed)
3715
- return;
3716
- stopStatus = `Could not stop workflow ${store.runId}: ${error instanceof Error ? error.message : String(error)}`;
3717
- tui.requestRender();
3718
- }).finally(() => {
3719
- stopRequested = false;
3720
- if (!disposed)
3721
- tui.requestRender();
3722
- });
3723
- };
3724
- const timer = setInterval(() => {
3725
- if (refreshing || stopRequested)
3726
- return;
3727
- refreshing = true;
3728
- const selectedOption = options[selectedIndex];
3729
- void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
3730
- }, 1000);
3731
- timer.unref();
3732
- return {
3733
- render(width) {
3734
- const dashboard = stopStatus ? `${view.dashboard}\n\n${stopStatus}` : view.dashboard;
3735
- const dashboardLines = truncateToVisualLines(theme.fg("accent", dashboard), Number.MAX_SAFE_INTEGER, width, 1).visualLines;
3736
- const actionLines = options.map((option, index) => truncateToVisualLines(`${index === selectedIndex ? "→ " : " "}${option}`, Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "");
3737
- const layout = dashboardLayout();
3738
- const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} navigate${dashboardLines.length > layout.dashboardViewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · ${keyLabel("tui.select.confirm", "enter")} select · ${keyLabel("tui.select.cancel", "esc")} close · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
3739
- const compact = [...dashboardLines, "", ...actionLines, "", hint];
3740
- if (compact.length <= layout.rows) {
3741
- dashboardOffset = 0;
3742
- return compact;
3743
- }
3744
- const maxOffset = Math.max(0, dashboardLines.length - layout.dashboardViewport);
3745
- dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
3746
- const actionStart = Math.min(Math.max(0, selectedIndex - layout.actionViewport + 1), Math.max(0, options.length - layout.actionViewport));
3747
- return [
3748
- ...dashboardLines.slice(dashboardOffset, dashboardOffset + layout.dashboardViewport),
3749
- ...(layout.separatorRows && layout.dashboardViewport ? [""] : []),
3750
- ...actionLines.slice(actionStart, actionStart + layout.actionViewport),
3751
- ...(layout.hintRows ? [hint] : []),
3752
- ];
3753
- },
3754
- invalidate() { },
3755
- handleInput(data) {
3756
- if (stopRequested)
3757
- return;
3758
- if (keybindings.matches(data, "tui.select.up"))
3759
- selectedIndex = (selectedIndex + options.length - 1) % options.length;
3760
- else if (keybindings.matches(data, "tui.select.down"))
3761
- selectedIndex = (selectedIndex + 1) % options.length;
3762
- else if (keybindings.matches(data, "tui.select.pageUp")) {
3763
- dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, dashboardLayout().dashboardViewport));
3764
- }
3765
- else if (keybindings.matches(data, "tui.select.pageDown")) {
3766
- dashboardOffset += Math.max(1, dashboardLayout().dashboardViewport);
3767
- }
3768
- else if (keybindings.matches(data, "tui.select.confirm")) {
3769
- if (options[selectedIndex] === "Stop")
3770
- requestStop();
3771
- else
3772
- done(options[selectedIndex]);
3773
- }
3774
- else if (keybindings.matches(data, "tui.select.cancel"))
3775
- done(undefined);
3776
- tui.requestRender();
3777
- },
3778
- dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
3779
- };
3780
- }, { overlay: true })
3781
- : await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
3782
- if (!actionChoice || actionChoice === "Close")
3783
- return;
3784
- if (actionChoice === "Agents...") {
3785
- await selectAgent(view);
3786
- continue;
3787
- }
3788
- if (actionChoice === "Refresh")
3789
- continue;
3790
- if (actionChoice === "View script") {
3791
- await ctx.ui.custom((tui, theme, keybindings, done) => {
3792
- const highlighted = highlightCode(view.script, "javascript");
3793
- let offset = 0;
3794
- let renderedLines = [];
3795
- const viewport = () => Math.max(1, tuiRows(tui) - 3);
3796
- const move = (delta) => {
3797
- const maxOffset = Math.max(0, renderedLines.length - viewport());
3798
- offset = Math.max(0, Math.min(maxOffset, offset + delta));
3799
- };
3800
- return {
3801
- render(width) {
3802
- renderedLines = highlighted.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
3803
- const maxOffset = Math.max(0, renderedLines.length - viewport());
3804
- offset = Math.min(offset, maxOffset);
3805
- return [
3806
- theme.fg("accent", "Workflow script"),
3807
- ...renderedLines.slice(offset, offset + viewport()),
3808
- "",
3809
- theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close"),
3810
- ];
3811
- },
3812
- invalidate() { },
3813
- handleInput(data) {
3814
- if (keybindings.matches(data, "tui.select.up"))
3815
- move(-1);
3816
- else if (keybindings.matches(data, "tui.select.down"))
3817
- move(1);
3818
- else if (keybindings.matches(data, "tui.select.pageUp"))
3819
- move(-viewport());
3820
- else if (keybindings.matches(data, "tui.select.pageDown"))
3821
- move(viewport());
3822
- else if (keybindings.matches(data, "tui.select.cancel"))
3823
- done(undefined);
3824
- tui.requestRender();
3825
- },
3826
- };
3827
- });
3828
- continue;
3829
- }
3830
- if (actionChoice === "View transcript") {
3831
- const transcript = await ctx.ui.select("Native Pi transcripts", [...view.transcripts, "Back"]);
3832
- if (transcript && transcript !== "Back")
3833
- await openTranscript(transcript);
3834
- continue;
3835
- }
3836
- if (actionChoice === "Transcript paths") {
3837
- const transcript = await ctx.ui.select("Native Pi transcript paths", [...view.transcripts, "Back"]);
3838
- if (transcript && transcript !== "Back") {
3839
- if (ctx.mode === "tui")
3840
- await copyArtifact(transcript, "transcript path");
3841
- else
3842
- ctx.ui.notify(transcript, "info");
3843
- }
3844
- continue;
3845
- }
3846
- const copy = view.copies.get(actionChoice);
3847
- if (copy) {
3848
- await copyArtifact(copy.value, copy.artifact);
3849
- continue;
3850
- }
3851
- if (actionChoice.startsWith("Review ")) {
3852
- const checkpoint = view.reviews.get(actionChoice);
3853
- if (!checkpoint)
3854
- continue;
3855
- const decision = await ctx.ui.custom((tui, theme, keybindings, done) => {
3856
- const options = ["Approve", "Reject", "Cancel"];
3857
- let selectedIndex = 0;
3858
- let offset = 0;
3859
- let renderedLines = [];
3860
- const layout = () => {
3861
- const rows = Math.max(1, tuiRows(tui));
3862
- const compactControls = rows < 4;
3863
- const titleRows = rows >= 5 ? 1 : 0;
3864
- const hintRows = rows >= 8 ? 1 : 0;
3865
- const separatorRows = rows >= 8 ? 2 : 0;
3866
- const controlRows = compactControls ? 1 : options.length;
3867
- const contentViewport = Math.max(0, rows - titleRows - hintRows - separatorRows - controlRows);
3868
- return { rows, compactControls, titleRows, hintRows, separatorRows, contentViewport };
3869
- };
3870
- const move = (delta) => {
3871
- const maxOffset = Math.max(0, renderedLines.length - layout().contentViewport);
3872
- offset = Math.max(0, Math.min(maxOffset, offset + delta));
3873
- };
3874
- return {
3875
- render(width) {
3876
- renderedLines = truncateToVisualLines(formatCheckpointReview(checkpoint), Number.MAX_SAFE_INTEGER, width, 0).visualLines;
3877
- const currentLayout = layout();
3878
- const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
3879
- offset = Math.min(offset, maxOffset);
3880
- const hint = truncateToVisualLines(theme.fg("dim", "↑↓/pgup/pgdn scroll · enter select · esc cancel"), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
3881
- const controls = currentLayout.compactControls
3882
- ? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
3883
- : options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
3884
- return [
3885
- ...(currentLayout.titleRows ? [theme.fg("accent", "Checkpoint review")] : []),
3886
- ...renderedLines.slice(offset, offset + currentLayout.contentViewport),
3887
- ...(currentLayout.separatorRows ? [""] : []),
3888
- ...controls,
3889
- ...(currentLayout.separatorRows ? [""] : []),
3890
- ...(currentLayout.hintRows ? [hint] : []),
3891
- ];
3892
- },
3893
- invalidate() { },
3894
- handleInput(data) {
3895
- if (keybindings.matches(data, "tui.select.up"))
3896
- selectedIndex = (selectedIndex + options.length - 1) % options.length;
3897
- else if (keybindings.matches(data, "tui.select.down"))
3898
- selectedIndex = (selectedIndex + 1) % options.length;
3899
- else if (keybindings.matches(data, "tui.select.pageUp"))
3900
- move(-layout().contentViewport);
3901
- else if (keybindings.matches(data, "tui.select.pageDown"))
3902
- move(layout().contentViewport);
3903
- else if (keybindings.matches(data, "tui.select.confirm"))
3904
- done(options[selectedIndex] === "Cancel" ? undefined : options[selectedIndex]);
3905
- else if (keybindings.matches(data, "tui.select.cancel"))
3906
- done(undefined);
3907
- tui.requestRender();
3908
- },
3909
- };
3910
- });
3911
- if (decision) {
3912
- const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
3913
- if (!accepted)
3914
- ctx.ui.notify("Checkpoint is not awaiting a response.", "warning");
3915
- }
3916
- continue;
3917
- }
3918
- const actionCommand = view.actions.get(actionChoice);
3919
- if (!actionCommand) {
3920
- ctx.ui.notify(`Cannot select workflow action: ${actionChoice}`, "warning");
3921
- continue;
3922
- }
3923
- const outcome = await runAction(actionCommand, true);
3924
- if (outcome === "picker") {
3925
- stores = await loadStores();
3926
- break;
3927
- }
3928
- }
3929
- }
3930
- }
3931
- await runAction(command, false);
3932
- },
3933
- });
3934
- pi.on("session_shutdown", async () => {
3935
- try {
3936
- await Promise.all([...runs.entries()].map(async ([runId, run]) => {
3937
- if (["completed", "failed", "stopped", "budget_exhausted"].includes(run.lifecycle.state)) {
3938
- await run.completion?.catch(() => undefined);
3939
- return;
3940
- }
3941
- if (!["completed", "failed", "stopped", "budget_exhausted"].includes(run.lifecycle.state)) {
3942
- try {
3943
- await run.lifecycle.terminal("interrupted");
3944
- }
3945
- catch (error) {
3946
- if (!["completed", "failed", "stopped", "budget_exhausted"].includes(run.lifecycle.state))
3947
- throw error;
3948
- }
3949
- run.abortController.abort();
3950
- run.execution?.cancel();
3951
- await scheduler.cancelRun(runId);
3952
- }
3953
- await run.completion?.catch(() => undefined);
3954
- }));
3955
- await scheduler.flush();
3956
- }
3957
- finally {
3958
- await releaseSessionLease();
3959
- resetWorkflowRegistry();
3960
- }
3961
- });
3962
- }
3963
- function displayAgentName(label, role, model) {
3964
- return label ?? role ?? model.model;
3965
- }
3966
- function modelSpec(value, fallback) {
3967
- try {
3968
- const parsed = parseModelReference(value);
3969
- return { ...parsed, ...(parsed.thinking || !fallback.thinking ? {} : { thinking: fallback.thinking }) };
3970
- }
3971
- catch {
3972
- return fallback;
3973
- }
3974
- }
1
+ export * from "./types.js";
2
+ export * from "./utils.js";
3
+ export * from "./budget.js";
4
+ export * from "./validation.js";
5
+ export * from "./registry.js";
6
+ export * from "./execution.js";
7
+ export * from "./host.js";
8
+ export { default } from "./host.js";
3975
9
  export { acquireSessionLease, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
3976
10
  export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
3977
11
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";