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.
- package/README.md +3 -1
- package/dist/src/agent-execution.d.ts +1 -15
- package/dist/src/agent-execution.js +15 -199
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +160 -0
- package/dist/src/cli.js +13 -7
- package/dist/src/execution.d.ts +17 -0
- package/dist/src/execution.js +630 -0
- package/dist/src/host.d.ts +62 -0
- package/dist/src/host.js +2696 -0
- package/dist/src/index.d.ts +10 -633
- package/dist/src/index.js +8 -4431
- package/dist/src/persistence.d.ts +6 -19
- package/dist/src/persistence.js +126 -60
- package/dist/src/registry.d.ts +31 -0
- package/dist/src/registry.js +169 -0
- package/dist/src/session-inspector.js +1 -1
- package/dist/src/types.d.ts +565 -0
- package/dist/src/types.js +27 -0
- package/dist/src/utils.d.ts +26 -0
- package/dist/src/utils.js +128 -0
- package/dist/src/validation.d.ts +24 -0
- package/dist/src/validation.js +740 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +28 -27
- package/src/agent-execution.ts +17 -155
- package/src/budget.ts +75 -0
- package/src/cli.ts +15 -6
- package/src/execution.ts +540 -0
- package/src/host.ts +2265 -0
- package/src/index.ts +11 -3855
- package/src/persistence.ts +87 -36
- package/src/registry.ts +157 -0
- package/src/session-inspector.ts +1 -1
- package/src/types.ts +113 -0
- package/src/utils.ts +108 -0
- package/src/validation.ts +616 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./
|
|
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>;
|
package/dist/src/persistence.js
CHANGED
|
@@ -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 {
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 = [];
|