pi-extensible-workflows 2.0.0 → 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.
@@ -1,4 +1,4 @@
1
- import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./index.js";
1
+ import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./types.js";
2
2
  import type { OwnershipRecord } from "./agent-execution.js";
3
3
  export interface NativeSessionReference {
4
4
  sessionId: string;
@@ -11,20 +11,6 @@ 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
- }
28
14
  export interface PersistedRun extends RunRecord {
29
15
  nativeSessions: readonly NativeSessionReference[];
30
16
  }
@@ -80,7 +66,6 @@ export declare class RunStore {
80
66
  private snapshotWrite;
81
67
  private launchSnapshotWrite;
82
68
  private systemPromptWrite;
83
- private conversationWrite;
84
69
  constructor(cwd: string, sessionId: string, runId: string, home?: string);
85
70
  create(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>): Promise<void>;
86
71
  isComplete(): Promise<boolean>;
@@ -97,12 +82,11 @@ export declare class RunStore {
97
82
  systemPromptPath(): string;
98
83
  recordSystemPrompt(entry: Omit<EffectiveSystemPrompt, "sha256">): Promise<void>;
99
84
  systemPrompts(): Promise<readonly EffectiveSystemPrompt[]>;
100
- conversationPath(): string;
101
- conversation(id: string): Promise<PersistedConversation | undefined>;
102
- saveConversation(conversation: PersistedConversation): Promise<void>;
103
85
  private updateJournal;
104
86
  complete(path: string, value: JsonValue): Promise<void>;
105
87
  replay(path: string): Promise<CompletedOperation | undefined>;
88
+ replayableOperations(): Promise<readonly CompletedOperation[]>;
89
+ private replayableOperationsFrom;
106
90
  awaitCheckpoint(checkpoint: AwaitingCheckpoint): Promise<boolean | undefined>;
107
91
  awaitingCheckpoints(): Promise<readonly AwaitingCheckpoint[]>;
108
92
  requestWorkflowDecision(request: PendingWorkflowDecision): Promise<void>;
@@ -119,6 +103,7 @@ export declare class RunStore {
119
103
  private borrowedWorktree;
120
104
  private sourceRun;
121
105
  validateParentRun(parentRunId: string): Promise<void>;
106
+ validateRetrySource(): Promise<void>;
122
107
  private ownedWorktree;
123
108
  private resolveBorrowedWorktree;
124
109
  private findNamedWorktree;
@@ -128,6 +113,7 @@ export declare class RunStore {
128
113
  owner: string;
129
114
  }>;
130
115
  validateBorrowedWorktrees(): Promise<void>;
116
+ validateNamedWorktrees(): Promise<void>;
131
117
  ownsWorktree(owner: string): Promise<boolean>;
132
118
  private cleanupMarker;
133
119
  private cleanupOrphanWorktrees;
@@ -137,6 +123,7 @@ export declare class RunStore {
137
123
  private bindBorrowedWorktree;
138
124
  snapshotWorktree(owner: string): Promise<string>;
139
125
  worktrees(): Promise<readonly WorktreeReference[]>;
126
+ validNamedWorktrees(): Promise<readonly string[]>;
140
127
  changedWorktrees(): Promise<readonly WorktreeReference[]>;
141
128
  saveResult(value: JsonValue): Promise<string>;
142
129
  delete(confirmed: boolean): Promise<void>;
@@ -5,13 +5,8 @@ import { access, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFi
5
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { promisify } from "node:util";
8
- import { loadLaunchSnapshot, WorkflowError } from "./index.js";
9
- function isConversationArtifact(value) {
10
- if (!value || typeof value !== "object" || Array.isArray(value))
11
- return false;
12
- const artifact = value;
13
- return artifact.version === 1 && Boolean(artifact.conversations) && typeof artifact.conversations === "object" && !Array.isArray(artifact.conversations);
14
- }
8
+ import { WorkflowError } from "./types.js";
9
+ import { loadLaunchSnapshot } from "./utils.js";
15
10
  const execute = promisify(execFile);
16
11
  const gitIdentity = {
17
12
  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",
@@ -226,7 +221,6 @@ export class RunStore {
226
221
  launchSnapshotWrite = Promise.resolve();
227
222
  // ponytail: the session lease prevents concurrent RunStore writers for one run.
228
223
  systemPromptWrite = Promise.resolve();
229
- conversationWrite = Promise.resolve();
230
224
  constructor(cwd, sessionId, runId, home = homedir()) {
231
225
  this.cwd = cwd;
232
226
  this.sessionId = sessionId;
@@ -250,7 +244,6 @@ export class RunStore {
250
244
  await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
251
245
  await atomicJson(join(temporary, "state.json"), run);
252
246
  await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
253
- await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
254
247
  await rename(temporary, this.directory);
255
248
  }
256
249
  catch (error) {
@@ -329,54 +322,6 @@ export class RunStore {
329
322
  return (await json(this.systemPromptPath()).catch((error) => { if (error.code === "ENOENT")
330
323
  return { version: 1, entries: [] }; throw error; })).entries;
331
324
  }
332
- conversationPath() { return join(this.directory, "conversations.json"); }
333
- async conversation(id) {
334
- await this.conversationWrite;
335
- let artifact;
336
- try {
337
- const raw = await json(this.conversationPath());
338
- if (!isConversationArtifact(raw))
339
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
340
- artifact = raw;
341
- }
342
- catch (error) {
343
- if (error.code === "ENOENT")
344
- return undefined;
345
- if (error instanceof WorkflowError)
346
- throw error;
347
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
348
- }
349
- return artifact.conversations[id];
350
- }
351
- async saveConversation(conversation) {
352
- const write = this.conversationWrite.then(async () => {
353
- const path = this.conversationPath();
354
- let artifact;
355
- try {
356
- const raw = await json(path);
357
- if (!isConversationArtifact(raw))
358
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
359
- artifact = raw;
360
- }
361
- catch (error) {
362
- if (error.code === "ENOENT")
363
- artifact = { version: 1, conversations: {} };
364
- else if (error instanceof WorkflowError)
365
- throw error;
366
- else
367
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
368
- }
369
- const previous = artifact.conversations[conversation.id];
370
- if (previous && previous.head.turn + 1 !== conversation.head.turn)
371
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation head is not the previous turn: ${conversation.id}`);
372
- if (!previous && conversation.head.turn !== 1)
373
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation must start at turn one: ${conversation.id}`);
374
- artifact.conversations[conversation.id] = structuredClone(conversation);
375
- await atomicJson(path, artifact);
376
- });
377
- this.conversationWrite = write.catch(() => undefined);
378
- await write;
379
- }
380
325
  async updateJournal(update) {
381
326
  let result;
382
327
  const write = this.journalWrite.then(async () => {
@@ -398,10 +343,34 @@ export class RunStore {
398
343
  });
399
344
  }
400
345
  async replay(path) {
346
+ const operations = await this.replayableOperations();
347
+ return operations.find((operation) => operation.path === path);
348
+ }
349
+ async replayableOperations() {
350
+ return this.replayableOperationsFrom(new Set());
351
+ }
352
+ async replayableOperationsFrom(seen) {
353
+ if (seen.has(this.runId))
354
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance contains a cycle");
355
+ const nextSeen = new Set(seen);
356
+ nextSeen.add(this.runId);
401
357
  await this.journalWrite;
402
- return (await json(join(this.directory, "journal.json"))).completed[path];
358
+ const loaded = await this.load();
359
+ const operations = new Map();
360
+ if (loaded.run.retry?.sourceRunId) {
361
+ const source = await this.sourceRun(loaded.run.retry.sourceRunId);
362
+ for (const operation of await source.replayableOperationsFrom(nextSeen))
363
+ operations.set(operation.path, operation);
364
+ }
365
+ const journal = await json(join(this.directory, "journal.json"));
366
+ for (const operation of Object.values(journal.completed))
367
+ operations.set(operation.path, operation);
368
+ return [...operations.values()].map((operation) => structuredClone(operation));
403
369
  }
404
370
  async awaitCheckpoint(checkpoint) {
371
+ const replayed = await this.replay(checkpoint.path);
372
+ if (replayed)
373
+ return replayed.value;
405
374
  return this.updateJournal((journal) => {
406
375
  const completed = journal.completed[checkpoint.path];
407
376
  if (completed)
@@ -523,6 +492,36 @@ export class RunStore {
523
492
  }
524
493
  }
525
494
  async validateParentRun(parentRunId) { await this.sourceRun(parentRunId); }
495
+ async validateRetrySource() {
496
+ const validate = async (current, seen) => {
497
+ if (seen.has(current.runId))
498
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance contains a cycle");
499
+ const nextSeen = new Set(seen);
500
+ nextSeen.add(current.runId);
501
+ const loaded = await current.load();
502
+ const retry = loaded.run.retry;
503
+ if (!retry)
504
+ return;
505
+ if (typeof retry.sourceRunId !== "string" || !retry.sourceRunId || retry.sourceRunId === current.runId || typeof retry.lineageRootRunId !== "string" || !retry.lineageRootRunId || !Array.isArray(retry.completedPaths) || retry.completedPaths.some((path) => typeof path !== "string") || !Array.isArray(retry.incompletePaths) || retry.incompletePaths.some((path) => typeof path !== "string") || !Array.isArray(retry.namedWorktrees) || retry.namedWorktrees.some((name) => typeof name !== "string"))
506
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance is incomplete");
507
+ const source = await current.sourceRun(retry.sourceRunId);
508
+ const sourceRun = (await source.load()).run;
509
+ if (loaded.run.parentRunId !== retry.sourceRunId)
510
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry parent run does not match its source run");
511
+ if (sourceRun.state !== "failed")
512
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Retry source run ${retry.sourceRunId} is not failed`);
513
+ const expectedLineageRoot = sourceRun.retry?.lineageRootRunId ?? sourceRun.id;
514
+ if (retry.lineageRootRunId !== expectedLineageRoot)
515
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry lineage root does not match its source run");
516
+ await validate(source, nextSeen);
517
+ };
518
+ try {
519
+ await validate(this, new Set());
520
+ }
521
+ catch (error) {
522
+ throw error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE" ? error : new WorkflowError("RESUME_INCOMPATIBLE", error instanceof Error ? error.message : String(error));
523
+ }
524
+ }
526
525
  async ownedWorktree(owner, cwd) {
527
526
  const records = await json(join(this.directory, "worktrees.json"));
528
527
  const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
@@ -567,8 +566,13 @@ export class RunStore {
567
566
  }
568
567
  const records = await json(join(this.directory, "worktrees.json"));
569
568
  const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
570
- if (matches.length === 0)
571
- return undefined;
569
+ if (matches.length === 0) {
570
+ const loaded = await this.load();
571
+ if (loaded.run.parentRunId === undefined)
572
+ return undefined;
573
+ const parent = await this.sourceRun(loaded.run.parentRunId);
574
+ return parent.findNamedWorktree(name, nextSeen);
575
+ }
572
576
  try {
573
577
  const reference = await this.ownedWorktree(owner);
574
578
  return { reference, sourceRunId: this.runId, owner };
@@ -595,6 +599,19 @@ export class RunStore {
595
599
  throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
596
600
  }
597
601
  }
602
+ async validateNamedWorktrees() {
603
+ try {
604
+ const records = await json(join(this.directory, "worktrees.json"));
605
+ for (const record of records) {
606
+ const owner = record && typeof record === "object" && typeof record.owner === "string" ? record.owner : undefined;
607
+ if (owner && this.worktreeName(owner))
608
+ await this.validateWorktree(owner);
609
+ }
610
+ }
611
+ catch (error) {
612
+ throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
613
+ }
614
+ }
598
615
  async ownsWorktree(owner) {
599
616
  const records = await json(join(this.directory, "worktrees.json"));
600
617
  return records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner).length === 1;
@@ -662,6 +679,8 @@ export class RunStore {
662
679
  return resolved.reference;
663
680
  }
664
681
  }
682
+ if (name && Array.isArray(loaded.run.retry?.namedWorktrees) && loaded.run.retry.namedWorktrees.includes(name))
683
+ throw new WorkflowError("WORKTREE_FAILED", `Missing inherited named worktree ${name}`);
665
684
  const existing = records.find((record) => record.owner === owner);
666
685
  if (existing)
667
686
  return this.validateWorktree(owner);
@@ -775,6 +794,53 @@ export class RunStore {
775
794
  const borrowed = await Promise.all(bindings.map(async (binding) => (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference));
776
795
  return [...owned.filter((record) => record !== undefined), ...borrowed];
777
796
  }
797
+ async validNamedWorktrees() {
798
+ const load = async (path) => {
799
+ try {
800
+ return await json(path);
801
+ }
802
+ catch (error) {
803
+ if (error.code === "ENOENT")
804
+ return [];
805
+ throw error;
806
+ }
807
+ };
808
+ const names = new Set();
809
+ const rawRecords = await load(join(this.directory, "worktrees.json"));
810
+ let bindings;
811
+ try {
812
+ bindings = await this.borrowedWorktreeRecords();
813
+ }
814
+ catch (error) {
815
+ if (error instanceof WorkflowError && error.code === "WORKTREE_FAILED")
816
+ return [];
817
+ throw error;
818
+ }
819
+ const boundOwners = new Set(bindings.map((binding) => binding.owner));
820
+ for (const record of Array.isArray(rawRecords) ? rawRecords : []) {
821
+ if (!record || typeof record !== "object")
822
+ continue;
823
+ const owner = record.owner;
824
+ if (typeof owner !== "string")
825
+ continue;
826
+ const name = this.worktreeName(owner);
827
+ if (!name || owner !== this.namedWorktreeOwner(name) || boundOwners.has(owner))
828
+ continue;
829
+ try {
830
+ await this.ownedWorktree(owner);
831
+ names.add(name);
832
+ }
833
+ catch { /* Do not advertise stale or invalid records. */ }
834
+ }
835
+ for (const binding of bindings) {
836
+ try {
837
+ await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
838
+ names.add(binding.name);
839
+ }
840
+ catch { /* Do not advertise stale inherited records. */ }
841
+ }
842
+ return [...names];
843
+ }
778
844
  async changedWorktrees() {
779
845
  const changed = [];
780
846
  for (const valid of await this.worktrees()) {
@@ -0,0 +1,31 @@
1
+ import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowVariable } from "./types.js";
2
+ export declare class WorkflowRegistry {
3
+ #private;
4
+ get frozen(): boolean;
5
+ freeze(): void;
6
+ register(extension: WorkflowExtension): void;
7
+ function(name: string): WorkflowFunction;
8
+ functions(): Readonly<Record<string, WorkflowFunction>>;
9
+ catalog(): WorkflowCatalog;
10
+ catalogIndex(): WorkflowCatalogIndex;
11
+ catalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError;
12
+ globals(): Readonly<Record<string, {
13
+ name: string;
14
+ }>>;
15
+ invokeFunction(name: string, input: unknown, context: Readonly<WorkflowFunctionContext>, path: string, journal: WorkflowJournal): Promise<JsonValue>;
16
+ variables(): readonly {
17
+ name: string;
18
+ variable: WorkflowVariable;
19
+ }[];
20
+ agentSetupHooks(): readonly RegisteredAgentSetupHook[];
21
+ }
22
+ export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "agentSetupHooks">;
23
+ export declare function resetWorkflowRegistry(): void;
24
+ export declare function beginWorkflowExtensionLoading(): void;
25
+ export declare function loadingRegistry(): WorkflowRegistryApi;
26
+ export declare function registerWorkflowExtension(extension: WorkflowExtension): void;
27
+ export declare function workflowCatalog(): WorkflowCatalog;
28
+ export declare function workflowCatalogIndex(): WorkflowCatalogIndex;
29
+ export declare function workflowCatalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError;
30
+ export declare function registeredWorkflowFunctions(): Readonly<Record<string, WorkflowFunction>>;
31
+ export type { WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogVariable } from "./types.js";
@@ -0,0 +1,169 @@
1
+ import { Value } from "typebox/value";
2
+ import { deepFreeze, fail, jsonValue, object } from "./utils.js";
3
+ import { loadSettings, validateSchema } from "./validation.js";
4
+ const RESERVED_GLOBALS = new Set(["agent", "shell", "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"]);
5
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
6
+ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
7
+ export class WorkflowRegistry {
8
+ #extensions = new Set();
9
+ #globals = new Map();
10
+ #hooks = new Map();
11
+ #frozen = false;
12
+ get frozen() { return this.#frozen; }
13
+ freeze() { this.#frozen = true; }
14
+ register(extension) {
15
+ if (this.#frozen)
16
+ fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
17
+ if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
18
+ fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
19
+ if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "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())
20
+ fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
21
+ const functions = extension.functions ?? {};
22
+ const variables = extension.variables ?? {};
23
+ const agentSetupHooks = extension.agentSetupHooks ?? {};
24
+ if (!object(functions) || !object(variables) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(agentSetupHooks).length === 0))
25
+ fail("INVALID_METADATA", "Workflow extensions require functions, variables, or agent setup hooks");
26
+ const names = [...Object.keys(functions), ...Object.keys(variables)];
27
+ if (new Set(names).size !== names.length)
28
+ fail("GLOBAL_COLLISION", "Global name collision inside extension");
29
+ for (const name of names) {
30
+ if (!IDENTIFIER.test(name) || name.startsWith("__pi_extensible_workflows_"))
31
+ fail("INVALID_METADATA", `Invalid global name: ${name}`);
32
+ if (RESERVED_GLOBALS.has(name))
33
+ fail("GLOBAL_COLLISION", `Global name is reserved: ${name}`);
34
+ if (this.#globals.has(name))
35
+ fail("GLOBAL_COLLISION", `Global name is already registered: ${name}`);
36
+ }
37
+ for (const [name, fn] of Object.entries(functions)) {
38
+ 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")
39
+ fail("INVALID_METADATA", `Invalid workflow function: ${name}`);
40
+ validateSchema(fn.input, `${name} input`);
41
+ validateSchema(fn.output, `${name} output`);
42
+ if (fn.input.type !== "object")
43
+ fail("INVALID_SCHEMA", `${name} input must describe one object`);
44
+ }
45
+ for (const [name, variable] of Object.entries(variables)) {
46
+ if (!object(variable) || Object.keys(variable).some((key) => !["description", "schema", "resolve"].includes(key)) || typeof variable.description !== "string" || !variable.description.trim() || typeof variable.resolve !== "function")
47
+ fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
48
+ validateSchema(variable.schema, `${name} schema`);
49
+ }
50
+ for (const [name, hook] of Object.entries(agentSetupHooks)) {
51
+ 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)))
52
+ fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
53
+ if (this.#hooks.has(name))
54
+ fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
55
+ }
56
+ const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
57
+ this.#extensions.add(stored);
58
+ for (const name of names)
59
+ this.#globals.set(name, name);
60
+ for (const [name, hook] of Object.entries(agentSetupHooks))
61
+ this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
62
+ }
63
+ function(name) {
64
+ if (!IDENTIFIER.test(name))
65
+ fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
66
+ const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
67
+ if (!fn)
68
+ fail("MISSING_WORKFLOW", `Registered function is unavailable: ${name}; the separate registered-workflow format was removed`);
69
+ return fn;
70
+ }
71
+ functions() {
72
+ return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
73
+ }
74
+ catalog() {
75
+ const functions = [];
76
+ const variables = [];
77
+ for (const extension of this.#extensions) {
78
+ for (const [name, fn] of Object.entries(extension.functions ?? {}))
79
+ functions.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: fn.description, input: structuredClone(fn.input), output: structuredClone(fn.output) });
80
+ for (const [name, variable] of Object.entries(extension.variables ?? {}))
81
+ variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
82
+ }
83
+ let aliases;
84
+ try {
85
+ aliases = loadSettings().modelAliases;
86
+ }
87
+ catch {
88
+ aliases = undefined;
89
+ }
90
+ const sort = (left, right) => left.name.localeCompare(right.name);
91
+ return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort), ...(aliases ? { modelAliases: structuredClone(aliases) } : {}) });
92
+ }
93
+ catalogIndex() {
94
+ const catalog = this.catalog();
95
+ return deepFreeze({
96
+ functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
97
+ variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
98
+ ...(catalog.modelAliases ? { modelAliases: structuredClone(catalog.modelAliases) } : {}),
99
+ });
100
+ }
101
+ catalogDetail(name) {
102
+ const catalog = this.catalog();
103
+ const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
104
+ if (entry)
105
+ return entry;
106
+ return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
107
+ }
108
+ globals() {
109
+ return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.keys(extension.functions ?? {}).map((name) => [name, { name }]))));
110
+ }
111
+ async invokeFunction(name, input, context, path, journal) {
112
+ const fn = this.function(name);
113
+ if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input))
114
+ fail("RESULT_INVALID", `Invalid input for ${name}`);
115
+ const replayed = journal.get(path);
116
+ if (replayed !== undefined) {
117
+ if (!jsonValue(replayed) || !Value.Check(fn.output, replayed))
118
+ fail("RESULT_INVALID", `Invalid replay for ${name}`);
119
+ return structuredClone(replayed);
120
+ }
121
+ const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, invoke: context.invoke, agent: context.agent, shell: context.shell, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
122
+ if (!jsonValue(result) || !Value.Check(fn.output, result))
123
+ fail("RESULT_INVALID", `Invalid output from ${name}`);
124
+ const stored = structuredClone(result);
125
+ journal.put(path, stored);
126
+ return structuredClone(stored);
127
+ }
128
+ variables() {
129
+ return [...this.#extensions].flatMap((extension) => Object.entries(extension.variables ?? {}).map(([name, variable]) => ({ name, variable })));
130
+ }
131
+ agentSetupHooks() {
132
+ return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
133
+ }
134
+ }
135
+ const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
136
+ const globalRegistry = globalThis;
137
+ function createWorkflowRegistryApi(registry) {
138
+ return {
139
+ get frozen() { return registry.frozen; },
140
+ freeze: () => { registry.freeze(); },
141
+ register: (extension) => { registry.register(extension); },
142
+ function: (name) => registry.function(name),
143
+ functions: () => registry.functions(),
144
+ catalog: () => registry.catalog(),
145
+ catalogIndex: () => registry.catalogIndex(),
146
+ catalogDetail: (name) => registry.catalogDetail(name),
147
+ globals: () => registry.globals(),
148
+ invokeFunction: (...args) => registry.invokeFunction(...args),
149
+ variables: () => registry.variables(),
150
+ agentSetupHooks: () => registry.agentSetupHooks(),
151
+ };
152
+ }
153
+ function workflowRegistryHost() {
154
+ return globalRegistry[WORKFLOW_REGISTRY_KEY] ??= { api: createWorkflowRegistryApi(new WorkflowRegistry()) };
155
+ }
156
+ export function resetWorkflowRegistry() {
157
+ workflowRegistryHost().api = createWorkflowRegistryApi(new WorkflowRegistry());
158
+ }
159
+ export function beginWorkflowExtensionLoading() {
160
+ if (workflowRegistryHost().api.frozen)
161
+ resetWorkflowRegistry();
162
+ }
163
+ export function loadingRegistry() { return workflowRegistryHost().api; }
164
+ beginWorkflowExtensionLoading();
165
+ export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
166
+ export function workflowCatalog() { return loadingRegistry().catalog(); }
167
+ export function workflowCatalogIndex() { return loadingRegistry().catalogIndex(); }
168
+ export function workflowCatalogDetail(name) { return loadingRegistry().catalogDetail(name); }
169
+ export function registeredWorkflowFunctions() { return loadingRegistry().functions(); }
@@ -223,7 +223,7 @@ export async function loadSessionReport(path, home = homedir()) {
223
223
  const args = call.arguments;
224
224
  const agents = loaded ? await Promise.all(loaded.run.agents.map(agentReport)) : [];
225
225
  const models = mergedModels(agents.flatMap(({ attempts }) => attempts.map(({ models: attemptModels }) => attemptModels)));
226
- const name = typeof args.name === "string" ? args.name : typeof args.workflow === "string" ? args.workflow : loaded?.run.workflowName ?? "workflow";
226
+ const name = loaded?.run.workflowName ?? (typeof args.workflow === "string" ? args.workflow : typeof args.name === "string" ? args.name : "workflow");
227
227
  const description = typeof args.description === "string" ? args.description : loaded?.snapshot.metadata.description;
228
228
  const script = typeof args.script === "string" && args.script.trim() ? args.script : loaded?.snapshot.script;
229
229
  let staticCalls = [];