pi-extensible-workflows 0.3.0 → 1.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.
@@ -1,4 +1,4 @@
1
- import type { JsonValue, LaunchSnapshot, RunRecord } from "./index.js";
1
+ import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./index.js";
2
2
  import type { OwnershipRecord } from "./agent-execution.js";
3
3
  export interface NativeSessionReference {
4
4
  sessionId: string;
@@ -11,6 +11,20 @@ export interface EffectiveSystemPrompt {
11
11
  sha256: string;
12
12
  prompt: string;
13
13
  }
14
+ export interface ConversationHead {
15
+ turn: number;
16
+ sessionId: string;
17
+ sessionFile: string;
18
+ leafId: string;
19
+ systemPrompt: string;
20
+ systemPromptSha256: string;
21
+ toolDefinitionsSha256: string;
22
+ }
23
+ export interface PersistedConversation {
24
+ id: string;
25
+ policy: JsonValue;
26
+ head: ConversationHead;
27
+ }
14
28
  export interface PersistedRun extends RunRecord {
15
29
  nativeSessions: readonly NativeSessionReference[];
16
30
  }
@@ -24,6 +38,7 @@ export interface AwaitingCheckpoint {
24
38
  prompt: string;
25
39
  context: JsonValue;
26
40
  }
41
+ export type PendingWorkflowDecision = BudgetApprovalRequest;
27
42
  export type PersistedOwnershipNode = OwnershipRecord;
28
43
  export interface WorktreeReference {
29
44
  owner: string;
@@ -54,7 +69,9 @@ export declare class RunStore {
54
69
  private stateWrite;
55
70
  private worktreeWrite;
56
71
  private snapshotWrite;
72
+ private launchSnapshotWrite;
57
73
  private systemPromptWrite;
74
+ private conversationWrite;
58
75
  constructor(cwd: string, sessionId: string, runId: string, home?: string);
59
76
  create(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>): Promise<void>;
60
77
  isComplete(): Promise<boolean>;
@@ -64,16 +81,24 @@ export declare class RunStore {
64
81
  }>;
65
82
  saveState(run: PersistedRun): Promise<void>;
66
83
  updateState(update: (run: PersistedRun) => PersistedRun | Promise<PersistedRun>): Promise<PersistedRun>;
84
+ saveSnapshot(snapshot: Readonly<LaunchSnapshot>): Promise<void>;
85
+ appendEvent(event: WorkflowRunEvent): Promise<void>;
67
86
  saveOwnership(nodes: readonly PersistedOwnershipNode[]): Promise<void>;
68
87
  loadOwnership(): Promise<readonly PersistedOwnershipNode[]>;
69
88
  systemPromptPath(): string;
70
89
  recordSystemPrompt(entry: Omit<EffectiveSystemPrompt, "sha256">): Promise<void>;
71
90
  systemPrompts(): Promise<readonly EffectiveSystemPrompt[]>;
91
+ conversationPath(): string;
92
+ conversation(id: string): Promise<PersistedConversation | undefined>;
93
+ saveConversation(conversation: PersistedConversation): Promise<void>;
72
94
  private updateJournal;
73
95
  complete(path: string, value: JsonValue): Promise<void>;
74
96
  replay(path: string): Promise<CompletedOperation | undefined>;
75
97
  awaitCheckpoint(checkpoint: AwaitingCheckpoint): Promise<boolean | undefined>;
76
98
  awaitingCheckpoints(): Promise<readonly AwaitingCheckpoint[]>;
99
+ requestWorkflowDecision(request: PendingWorkflowDecision): Promise<void>;
100
+ pendingWorkflowDecisions(): Promise<readonly PendingWorkflowDecision[]>;
101
+ answerWorkflowDecision(proposalId: string, approved: boolean): Promise<PendingWorkflowDecision | undefined>;
77
102
  answerCheckpoint(name: string, approved: boolean): Promise<AwaitingCheckpoint | undefined>;
78
103
  private expectedWorktree;
79
104
  private markerPath;
@@ -5,6 +5,12 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { promisify } from "node:util";
7
7
  import { loadLaunchSnapshot, WorkflowError } from "./index.js";
8
+ function isConversationArtifact(value) {
9
+ if (!value || typeof value !== "object" || Array.isArray(value))
10
+ return false;
11
+ const artifact = value;
12
+ return artifact.version === 1 && Boolean(artifact.conversations) && typeof artifact.conversations === "object" && !Array.isArray(artifact.conversations);
13
+ }
8
14
  const execute = promisify(execFile);
9
15
  const gitIdentity = {
10
16
  GIT_AUTHOR_NAME: "pi-extensible-workflows", GIT_AUTHOR_EMAIL: "pi-extensible-workflows@localhost", GIT_COMMITTER_NAME: "pi-extensible-workflows", GIT_COMMITTER_EMAIL: "pi-extensible-workflows@localhost",
@@ -192,8 +198,10 @@ export class RunStore {
192
198
  stateWrite = Promise.resolve();
193
199
  worktreeWrite = Promise.resolve();
194
200
  snapshotWrite = Promise.resolve();
201
+ launchSnapshotWrite = Promise.resolve();
195
202
  // ponytail: the session lease prevents concurrent RunStore writers for one run.
196
203
  systemPromptWrite = Promise.resolve();
204
+ conversationWrite = Promise.resolve();
197
205
  constructor(cwd, sessionId, runId, home = homedir()) {
198
206
  this.cwd = cwd;
199
207
  this.sessionId = sessionId;
@@ -208,12 +216,14 @@ export class RunStore {
208
216
  await mkdir(dirname(this.directory), { recursive: true, mode: 0o700 });
209
217
  await mkdir(temporary, { mode: 0o700 });
210
218
  try {
219
+ await writeFile(join(temporary, "workflow.js"), snapshot.script, { encoding: "utf8", mode: 0o600 });
211
220
  await atomicJson(join(temporary, "snapshot.json"), snapshot);
212
- await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {} });
221
+ await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {}, decisions: {} });
213
222
  await atomicJson(join(temporary, "ownership.json"), []);
214
223
  await atomicJson(join(temporary, "worktrees.json"), []);
215
224
  await atomicJson(join(temporary, "state.json"), run);
216
225
  await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
226
+ await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
217
227
  await rename(temporary, this.directory);
218
228
  }
219
229
  catch (error) {
@@ -261,6 +271,14 @@ export class RunStore {
261
271
  await write;
262
272
  return result;
263
273
  }
274
+ async saveSnapshot(snapshot) {
275
+ const write = this.launchSnapshotWrite.then(() => atomicJson(join(this.directory, "snapshot.json"), snapshot));
276
+ this.launchSnapshotWrite = write.catch(() => undefined);
277
+ await write;
278
+ }
279
+ async appendEvent(event) {
280
+ await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.message === event.message) ? [] : [event])] }));
281
+ }
264
282
  async saveOwnership(nodes) {
265
283
  await atomicJson(join(this.directory, "ownership.json"), nodes);
266
284
  }
@@ -284,6 +302,54 @@ export class RunStore {
284
302
  return (await json(this.systemPromptPath()).catch((error) => { if (error.code === "ENOENT")
285
303
  return { version: 1, entries: [] }; throw error; })).entries;
286
304
  }
305
+ conversationPath() { return join(this.directory, "conversations.json"); }
306
+ async conversation(id) {
307
+ await this.conversationWrite;
308
+ let artifact;
309
+ try {
310
+ const raw = await json(this.conversationPath());
311
+ if (!isConversationArtifact(raw))
312
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
313
+ artifact = raw;
314
+ }
315
+ catch (error) {
316
+ if (error.code === "ENOENT")
317
+ return undefined;
318
+ if (error instanceof WorkflowError)
319
+ throw error;
320
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
321
+ }
322
+ return artifact.conversations[id];
323
+ }
324
+ async saveConversation(conversation) {
325
+ const write = this.conversationWrite.then(async () => {
326
+ const path = this.conversationPath();
327
+ let artifact;
328
+ try {
329
+ const raw = await json(path);
330
+ if (!isConversationArtifact(raw))
331
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
332
+ artifact = raw;
333
+ }
334
+ catch (error) {
335
+ if (error.code === "ENOENT")
336
+ artifact = { version: 1, conversations: {} };
337
+ else if (error instanceof WorkflowError)
338
+ throw error;
339
+ else
340
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
341
+ }
342
+ const previous = artifact.conversations[conversation.id];
343
+ if (previous && previous.head.turn + 1 !== conversation.head.turn)
344
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation head is not the previous turn: ${conversation.id}`);
345
+ if (!previous && conversation.head.turn !== 1)
346
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation must start at turn one: ${conversation.id}`);
347
+ artifact.conversations[conversation.id] = structuredClone(conversation);
348
+ await atomicJson(path, artifact);
349
+ });
350
+ this.conversationWrite = write.catch(() => undefined);
351
+ await write;
352
+ }
287
353
  async updateJournal(update) {
288
354
  let result;
289
355
  const write = this.journalWrite.then(async () => {
@@ -322,13 +388,31 @@ export class RunStore {
322
388
  const journal = await json(join(this.directory, "journal.json"));
323
389
  return Object.values(journal.awaiting ?? {});
324
390
  }
391
+ async requestWorkflowDecision(request) {
392
+ await this.updateJournal((journal) => { journal.decisions ??= {}; journal.decisions[request.proposalId] = request; });
393
+ }
394
+ async pendingWorkflowDecisions() {
395
+ await this.journalWrite;
396
+ const journal = await json(join(this.directory, "journal.json"));
397
+ return Object.values(journal.decisions ?? {});
398
+ }
399
+ async answerWorkflowDecision(proposalId, approved) {
400
+ return this.updateJournal((journal) => {
401
+ const request = journal.decisions?.[proposalId];
402
+ if (!request)
403
+ return undefined;
404
+ journal.completed[`decision/${proposalId}`] = { path: `decision/${proposalId}`, value: approved };
405
+ delete journal.decisions?.[proposalId];
406
+ return request;
407
+ });
408
+ }
325
409
  async answerCheckpoint(name, approved) {
326
410
  return this.updateJournal((journal) => {
327
- const checkpoint = Object.values(journal.awaiting).find((item) => item.name === name);
411
+ const checkpoint = Object.values(journal.awaiting ?? {}).find((item) => item.name === name);
328
412
  if (!checkpoint || journal.completed[checkpoint.path])
329
413
  return undefined;
330
414
  journal.completed[checkpoint.path] = { path: checkpoint.path, value: approved };
331
- journal.awaiting = Object.fromEntries(Object.entries(journal.awaiting).filter(([path]) => path !== checkpoint.path));
415
+ journal.awaiting = Object.fromEntries(Object.entries(journal.awaiting ?? {}).filter(([path]) => path !== checkpoint.path));
332
416
  return checkpoint;
333
417
  });
334
418
  }
@@ -461,9 +545,19 @@ export class RunStore {
461
545
  try {
462
546
  const write = this.snapshotWrite.then(async () => {
463
547
  const record = await this.worktree(owner);
464
- await git(record.path, ["add", "-A"]);
465
- if ((await git(record.path, ["status", "--porcelain"])).trim())
466
- await git(record.path, ["commit", "-m", "pi-extensible-workflows runtime snapshot"], gitIdentity);
548
+ for (let attempt = 0; attempt < 3; attempt += 1) {
549
+ await git(record.path, ["add", "-A"]);
550
+ if (!(await git(record.path, ["status", "--porcelain"])).trim())
551
+ break;
552
+ try {
553
+ await git(record.path, ["commit", "-m", "pi-extensible-workflows runtime snapshot"], gitIdentity);
554
+ break;
555
+ }
556
+ catch (error) {
557
+ if (attempt === 2)
558
+ throw error;
559
+ }
560
+ }
467
561
  return (await git(record.path, ["rev-parse", "HEAD"])).trim();
468
562
  });
469
563
  this.snapshotWrite = write.then(() => undefined, () => undefined);
@@ -1,5 +1,6 @@
1
1
  import { type SessionEntry, type SessionInfo } from "@earendil-works/pi-coding-agent";
2
- import { type ModelSpec, type StaticWorkflowCall } from "./index.js";
2
+ import { type AgentSetupSummary, type ModelSpec, type StaticWorkflowCall } from "./index.js";
3
+ import { type PersistedRun } from "./persistence.js";
3
4
  export interface ModelUsage {
4
5
  model: string;
5
6
  cost: number;
@@ -12,16 +13,19 @@ export interface AttemptReport {
12
13
  cost: number;
13
14
  models: readonly ModelUsage[];
14
15
  error?: string;
16
+ setup?: AgentSetupSummary;
15
17
  }
16
18
  export interface AgentReport {
17
19
  name: string;
18
20
  label?: string;
19
21
  state: string;
20
22
  role?: string;
23
+ requestedModel?: string;
21
24
  model: string;
22
25
  thinking?: ModelSpec["thinking"];
23
26
  cost: number;
24
27
  attempts: readonly AttemptReport[];
28
+ setup?: AgentSetupSummary;
25
29
  }
26
30
  export interface WorkflowReport {
27
31
  name: string;
@@ -34,6 +38,14 @@ export interface WorkflowReport {
34
38
  cost: number;
35
39
  models: readonly ModelUsage[];
36
40
  agents: readonly AgentReport[];
41
+ budget?: PersistedRun["budget"];
42
+ budgetVersion?: number;
43
+ usage?: PersistedRun["usage"];
44
+ budgetEvents?: PersistedRun["budgetEvents"];
45
+ events?: readonly {
46
+ type: string;
47
+ message: string;
48
+ }[];
37
49
  }
38
50
  export interface SessionReport {
39
51
  id: string;
@@ -4,7 +4,7 @@ import { emitKeypressEvents } from "node:readline";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { stdin, stdout } from "node:process";
6
6
  import { highlightCode, initTheme, SessionManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
7
- import { inspectWorkflowScript } from "./index.js";
7
+ import { formatBudgetStatus, inspectWorkflowScript } from "./index.js";
8
8
  import { listRunIds, RunStore } from "./persistence.js";
9
9
  function text(content) {
10
10
  if (typeof content === "string")
@@ -170,6 +170,7 @@ async function agentReport(agent) {
170
170
  cost,
171
171
  models: log.models.length ? log.models : [{ model, cost }],
172
172
  ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
173
+ ...(attempt.setup ? { setup: attempt.setup } : {}),
173
174
  });
174
175
  continue;
175
176
  }
@@ -182,6 +183,7 @@ async function agentReport(agent) {
182
183
  cost,
183
184
  models: [{ model: fallbackModel, cost }],
184
185
  ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
186
+ ...(attempt.setup ? { setup: attempt.setup } : {}),
185
187
  });
186
188
  }
187
189
  if (!attempts.length) {
@@ -189,7 +191,7 @@ async function agentReport(agent) {
189
191
  attempts.push({ attempt: 1, prompt: "(transcript unavailable)", model: fallbackModel, ...(fallbackThinking !== undefined ? { thinking: fallbackThinking } : {}), cost, models: [{ model: fallbackModel, cost }] });
190
192
  }
191
193
  const latest = attempts[attempts.length - 1];
192
- return { name: agent.name, ...(agent.label ? { label: agent.label } : {}), state: agent.state, ...(agent.role ? { role: agent.role } : {}), model: latest?.model ?? fallbackModel, ...(latest?.thinking !== undefined ? { thinking: latest.thinking } : {}), cost: attempts.reduce((sum, attempt) => sum + attempt.cost, 0), attempts };
194
+ return { name: agent.name, ...(agent.label ? { label: agent.label } : {}), state: agent.state, ...(agent.role ? { role: agent.role } : {}), ...(agent.requestedModel ? { requestedModel: agent.requestedModel } : {}), model: latest?.model ?? fallbackModel, ...(latest?.thinking !== undefined ? { thinking: latest.thinking } : {}), cost: attempts.reduce((sum, attempt) => sum + attempt.cost, 0), attempts, ...(latest?.setup ? { setup: latest.setup } : {}) };
193
195
  }
194
196
  export function matchSession(query, sessions) {
195
197
  const exact = sessions.filter(({ id }) => id === query);
@@ -242,6 +244,11 @@ export async function loadSessionReport(path, home = homedir()) {
242
244
  cost: agents.reduce((sum, agent) => sum + agent.cost, 0),
243
245
  models,
244
246
  agents,
247
+ ...(loaded?.run.budget ? { budget: loaded.run.budget } : {}),
248
+ ...(loaded?.run.budgetVersion !== undefined ? { budgetVersion: loaded.run.budgetVersion } : {}),
249
+ ...(loaded?.run.usage ? { usage: loaded.run.usage } : {}),
250
+ ...(loaded?.run.budgetEvents ? { budgetEvents: loaded.run.budgetEvents } : {}),
251
+ ...(loaded?.run.events?.length ? { events: loaded.run.events } : {})
245
252
  });
246
253
  }
247
254
  const workflowCost = workflows.reduce((sum, workflow) => sum + workflow.cost, 0);
@@ -263,7 +270,9 @@ function detailLines(workflow) {
263
270
  style(ansi.bold + ansi.cyan, workflow.name),
264
271
  `${workflow.status} · ${money(workflow.cost)}${workflow.runId ? ` · ${workflow.runId}` : ""}`,
265
272
  workflow.description ?? "",
273
+ ...(workflow.events?.length ? ["", style(ansi.bold, "Run events"), ...workflow.events.map((event) => `${event.type}: ${event.message}`)] : []),
266
274
  "",
275
+ ...(workflow.budget ? formatBudgetStatus({ budget: workflow.budget, ...(workflow.budgetVersion !== undefined ? { budgetVersion: workflow.budgetVersion } : {}), ...(workflow.usage ? { usage: workflow.usage } : {}), ...(workflow.budgetEvents ? { budgetEvents: workflow.budgetEvents } : {}) }).map((line) => `Budget ${line}`) : []),
267
276
  style(ansi.bold, "Models"),
268
277
  modelSummary(workflow.models),
269
278
  "",
@@ -278,9 +287,18 @@ function detailLines(workflow) {
278
287
  if (!workflow.agents.length)
279
288
  lines.push("(no agent run was persisted)");
280
289
  for (const agent of workflow.agents) {
281
- lines.push("", style(agent.state === "completed" ? ansi.green : agent.state === "failed" ? ansi.red : ansi.yellow, `${agent.label ?? agent.name} [${agent.state}]`), `${agent.role ? `role=${agent.role} · ` : ""}${agent.model}${agent.thinking !== undefined ? `:${agent.thinking}` : ""} · ${money(agent.cost)}`);
290
+ lines.push("", style(agent.state === "completed" ? ansi.green : agent.state === "failed" ? ansi.red : ansi.yellow, `${agent.label ?? agent.name} [${agent.state}]`), `${agent.role ? `role=${agent.role} · ` : ""}${agent.requestedModel ? `requested=${agent.requestedModel} · ` : ""}${agent.model}${agent.thinking !== undefined ? `:${agent.thinking}` : ""} · ${money(agent.cost)}`);
282
291
  for (const attempt of agent.attempts) {
283
- lines.push(`Attempt ${String(attempt.attempt)} · ${attempt.model}${attempt.thinking !== undefined ? `:${attempt.thinking}` : ""} · ${money(attempt.cost)}${attempt.error ? ` · ${attempt.error}` : ""}`, `Prompt: ${attempt.prompt}`);
292
+ lines.push(`Attempt ${String(attempt.attempt)} · ${attempt.model}${attempt.thinking !== undefined ? `:${attempt.thinking}` : ""} · ${money(attempt.cost)}${attempt.error ? ` · ${attempt.error}` : ""}`, `Prompt: ${attempt.prompt}`, ...(attempt.setup ? [
293
+ `Hooks: ${attempt.setup.hookNames.join(", ") || "(none)"}`,
294
+ `Effective: model=${attempt.setup.model.provider}/${attempt.setup.model.model}${attempt.setup.model.thinking ? `:${attempt.setup.model.thinking}` : ""} tools=${attempt.setup.tools.join(",") || "(none)"} cwd=${attempt.setup.cwd}`,
295
+ ...(attempt.setup.disabledAgentResources ? [
296
+ `Disabled skills: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
297
+ `Disabled extensions: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
298
+ `Unmatched skills: ${attempt.setup.disabledAgentResources.unmatchedSkills.join(", ") || "(none)"}`,
299
+ `Unmatched extensions: ${attempt.setup.disabledAgentResources.unmatchedExtensions.join(", ") || "(none)"}`,
300
+ ] : []),
301
+ ] : []));
284
302
  }
285
303
  }
286
304
  return lines.filter((line, index) => line || index !== 2);
@@ -14,7 +14,7 @@ export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "ba
14
14
  const EVAL_MODEL_TOKEN = "$EVAL_MODEL";
15
15
  const semantic = (description) => [{ id: "intent", description }];
16
16
  const JSON_RESULT_TYPES = ["null", "boolean", "number", "integer", "string", "array", "object"];
17
- const WORKFLOW_CALL_KINDS = ["agent", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
17
+ const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
18
18
  const AGENT_OPTION_NAMES = ["role", "model", "thinking", "tools", "retries"];
19
19
  const expectationKeys = ["firstSignificantAction", "firstTool", "firstBatchToolSequence", "parentToolSequence", "workflowCallCount", "requiredOperations", "forbiddenOperations", "requiredRoles", "minimumAgentCalls", "requireOutputSchema", "expectedResults", "agentPolicies", "requiredAgentOrder", "requiredAgentStructures", "requiredDataFlow"];
20
20
  const caseKeys = ["id", "prompt", "timeoutMs", "maxCost", "expectations", "expectedWorkflowCalls", "semanticCriteria"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensible-workflows",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "description": "Deterministic multi-agent workflow orchestration for Pi",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/vekexasia/pi-extensible-workflows.git" },
@@ -34,10 +34,11 @@ return agent(
34
34
 
35
35
  To pass structured input from the main agent, include `args`:
36
36
  ```json
37
- { "workflow": "namespace.workflowName", "args": { "issue": 42 } }
37
+ { "workflow": "workflowName", "args": { "issue": 42 } }
38
38
  ```
39
39
  Inside the workflow, read `args.issue`; omitted `args` is `null`.
40
- If `workflow_catalog` is available, call it once before creating the first workflow for a task. Use the returned global functions, variables, and registered workflows as needed for the rest of that task. Do not try to reinvent already exposed functions.
40
+ Use `workflow_stop` with the exact run ID to stop an active background run from the current Pi session.
41
+ If `workflow_catalog` is available, call it once before creating the first workflow for a task. Use the returned global functions, variables, registered workflows, and configured model aliases as needed for the rest of that task. Alias targets are catalog metadata, not an availability probe. Do not try to reinvent already exposed functions.
41
42
 
42
43
  Pass downstream only needed results. Workflow JavaScript has no imports, filesystem, network, process, or timers; delegate such work to agents with the required tools.
43
44
 
@@ -46,7 +47,7 @@ Pass downstream only needed results. Workflow JavaScript has no imports, filesys
46
47
  ```typescript
47
48
  interface AgentOptions {
48
49
  label?: string; // optional non-empty display name
49
- model?: `${provider}/${model}` | `${provider}/${model}:${thinking}`;
50
+ model?: string; // configured alias or provider/model[:thinking]
50
51
  thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
51
52
  role?: string; // one of the available workflow roles
52
53
  tools?: string[]; // [] = no tools; omitted uses role or launch tools
@@ -56,7 +57,9 @@ interface AgentOptions {
56
57
  }
57
58
  ```
58
59
 
59
- Agent calls are unnamed. Direct `agent(...)` calls receive hidden source call-site identity; aliases are unsupported. Calls from one source call site must not race outside `parallel` or `pipeline`, whose structural keys keep replay deterministic.
60
+ Extensions may define additional JSON-compatible agent option keys such as `advisor: true`. Core-owned keys still use the validation and role constraints above; extension options are passed to setup hooks and native setup but are not inherited by child agents.
61
+
62
+ Agent calls are unnamed. Direct `agent(...)` calls receive hidden source call-site identity; JavaScript aliases for workflow calls are unsupported. Calls from one source call site must not race outside `parallel` or `pipeline`, whose structural keys keep replay deterministic.
60
63
 
61
64
  ## Shared worktree scope
62
65
 
@@ -101,7 +104,8 @@ Registered extension functions receive `withWorktree` in their context, so they
101
104
  - Preserve item metadata in workflow code between pipeline stages instead of requiring agents to echo it through `outputSchema`.
102
105
  - Repeated work uses a JavaScript loop; each direct `agent(...)` call receives deterministic call-site and occurrence identity.
103
106
  - Runs default to background; set tool-call `foreground: true` when asked to wait.
104
- - Omit `maxAgentLaunches` unless an explicit total launch budget is required.
107
+ - Add `budget` only when the run needs aggregate token, cost, duration, or launch limits; each dimension accepts optional `soft` and `hard` values. Soft crossings request wrap-up, while hard exhaustion blocks further budgeted work.
108
+ - A `budget_exhausted` run is resumable through `workflow_resume`. Omitted patch values stay unchanged, explicit `null` removes a limit, and any relaxation requires an exact human-approved proposal through `workflow_respond`.
105
109
  - `parallel()` and `pipeline()` return keyed bare values. Await results before use.
106
110
  - Interpolate results with `prompt("...{value}", { value })`; placeholders in plain strings stay literal.
107
111
  - Use `outputSchema` only when another phase must compare, aggregate, or validate the result. Never add it to a final agent whose prose is returned directly. Keep only fields the consumer needs, and avoid repeating the same evidence in multiple schemas.