pi-extensible-workflows 1.0.1 → 2.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 +7 -2
- package/dist/src/agent-execution.d.ts +13 -0
- package/dist/src/agent-execution.js +130 -33
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +530 -6
- package/dist/src/doctor.d.ts +4 -4
- package/dist/src/doctor.js +9 -29
- package/dist/src/herdr.d.ts +12 -0
- package/dist/src/herdr.js +74 -0
- package/dist/src/index.d.ts +101 -24
- package/dist/src/index.js +669 -212
- package/dist/src/persistence.d.ts +26 -0
- package/dist/src/persistence.js +186 -12
- package/dist/src/session-inspector.d.ts +1 -0
- package/dist/src/session-inspector.js +3 -0
- package/dist/src/workflow-evals.js +2 -1
- package/package.json +4 -3
- package/skills/pi-extensible-workflows/SKILL.md +48 -64
- package/src/agent-execution.ts +95 -20
- package/src/cli.ts +418 -6
- package/src/doctor.ts +13 -32
- package/src/herdr.ts +73 -0
- package/src/index.ts +595 -193
- package/src/persistence.ts +169 -12
- package/src/session-inspector.ts +4 -0
- package/src/workflow-evals.ts +2 -1
|
@@ -47,6 +47,11 @@ export interface WorktreeReference {
|
|
|
47
47
|
cwd: string;
|
|
48
48
|
base: string;
|
|
49
49
|
}
|
|
50
|
+
export interface BorrowedWorktreeBinding {
|
|
51
|
+
name: string;
|
|
52
|
+
sourceRunId: string;
|
|
53
|
+
owner: string;
|
|
54
|
+
}
|
|
50
55
|
export declare function projectStorageKey(cwd: string): string;
|
|
51
56
|
export declare function runsDirectory(cwd: string, sessionId: string, home?: string): string;
|
|
52
57
|
export declare class SessionLease {
|
|
@@ -66,10 +71,12 @@ export declare class RunStore {
|
|
|
66
71
|
readonly cwd: string;
|
|
67
72
|
readonly sessionId: string;
|
|
68
73
|
readonly runId: string;
|
|
74
|
+
readonly home: string;
|
|
69
75
|
readonly directory: string;
|
|
70
76
|
private journalWrite;
|
|
71
77
|
private stateWrite;
|
|
72
78
|
private worktreeWrite;
|
|
79
|
+
private borrowedWorktreeWrite;
|
|
73
80
|
private snapshotWrite;
|
|
74
81
|
private launchSnapshotWrite;
|
|
75
82
|
private systemPromptWrite;
|
|
@@ -104,11 +111,30 @@ export declare class RunStore {
|
|
|
104
111
|
answerCheckpoint(name: string, approved: boolean): Promise<AwaitingCheckpoint | undefined>;
|
|
105
112
|
private expectedWorktree;
|
|
106
113
|
private markerPath;
|
|
114
|
+
private namedWorktreeOwner;
|
|
115
|
+
private worktreeName;
|
|
107
116
|
private structuralWorktree;
|
|
117
|
+
private borrowedWorktreeRecords;
|
|
118
|
+
borrowedWorktrees(): Promise<readonly BorrowedWorktreeBinding[]>;
|
|
119
|
+
private borrowedWorktree;
|
|
120
|
+
private sourceRun;
|
|
121
|
+
validateParentRun(parentRunId: string): Promise<void>;
|
|
122
|
+
private ownedWorktree;
|
|
123
|
+
private resolveBorrowedWorktree;
|
|
124
|
+
private findNamedWorktree;
|
|
125
|
+
resolveNamedWorktree(name: string, seen?: Set<string>): Promise<{
|
|
126
|
+
reference: WorktreeReference;
|
|
127
|
+
sourceRunId: string;
|
|
128
|
+
owner: string;
|
|
129
|
+
}>;
|
|
130
|
+
validateBorrowedWorktrees(): Promise<void>;
|
|
131
|
+
ownsWorktree(owner: string): Promise<boolean>;
|
|
108
132
|
private cleanupMarker;
|
|
109
133
|
private cleanupOrphanWorktrees;
|
|
110
134
|
validateWorktree(owner: string, cwd?: string): Promise<WorktreeReference>;
|
|
111
135
|
worktree(owner: string): Promise<WorktreeReference>;
|
|
136
|
+
private resolveNamedWorktreeFromParent;
|
|
137
|
+
private bindBorrowedWorktree;
|
|
112
138
|
snapshotWorktree(owner: string): Promise<string>;
|
|
113
139
|
worktrees(): Promise<readonly WorktreeReference[]>;
|
|
114
140
|
changedWorktrees(): Promise<readonly WorktreeReference[]>;
|
package/dist/src/persistence.js
CHANGED
|
@@ -215,11 +215,13 @@ export class RunStore {
|
|
|
215
215
|
cwd;
|
|
216
216
|
sessionId;
|
|
217
217
|
runId;
|
|
218
|
+
home;
|
|
218
219
|
directory;
|
|
219
220
|
journalWrite = Promise.resolve();
|
|
220
221
|
// ponytail: serializes one RunStore instance; cross-process run sharing remains unsupported.
|
|
221
222
|
stateWrite = Promise.resolve();
|
|
222
223
|
worktreeWrite = Promise.resolve();
|
|
224
|
+
borrowedWorktreeWrite = Promise.resolve();
|
|
223
225
|
snapshotWrite = Promise.resolve();
|
|
224
226
|
launchSnapshotWrite = Promise.resolve();
|
|
225
227
|
// ponytail: the session lease prevents concurrent RunStore writers for one run.
|
|
@@ -229,6 +231,7 @@ export class RunStore {
|
|
|
229
231
|
this.cwd = cwd;
|
|
230
232
|
this.sessionId = sessionId;
|
|
231
233
|
this.runId = runId;
|
|
234
|
+
this.home = home;
|
|
232
235
|
this.cwd = resolve(cwd);
|
|
233
236
|
this.directory = join(runsDirectory(this.cwd, sessionId, home), safePart(runId));
|
|
234
237
|
}
|
|
@@ -244,6 +247,7 @@ export class RunStore {
|
|
|
244
247
|
await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {}, decisions: {} });
|
|
245
248
|
await atomicJson(join(temporary, "ownership.json"), []);
|
|
246
249
|
await atomicJson(join(temporary, "worktrees.json"), []);
|
|
250
|
+
await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
|
|
247
251
|
await atomicJson(join(temporary, "state.json"), run);
|
|
248
252
|
await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
|
|
249
253
|
await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
|
|
@@ -447,6 +451,26 @@ export class RunStore {
|
|
|
447
451
|
const key = createHash("sha256").update(`${this.sessionId}\0${this.runId}\0${owner}`).digest("hex").slice(0, 16);
|
|
448
452
|
return join(this.directory, `worktree-${key}.creating`);
|
|
449
453
|
}
|
|
454
|
+
namedWorktreeOwner(name) {
|
|
455
|
+
if (!name.trim())
|
|
456
|
+
throw new WorkflowError("WORKTREE_FAILED", "Named worktree names must be non-empty");
|
|
457
|
+
return structuralPath("worktree", "named", name.trim());
|
|
458
|
+
}
|
|
459
|
+
worktreeName(owner) {
|
|
460
|
+
const prefix = `${structuralPath("worktree", "named")}/`;
|
|
461
|
+
if (!owner.startsWith(prefix))
|
|
462
|
+
return undefined;
|
|
463
|
+
const encoded = owner.slice(prefix.length);
|
|
464
|
+
if (!encoded || encoded.includes("/"))
|
|
465
|
+
return undefined;
|
|
466
|
+
try {
|
|
467
|
+
const name = decodeURIComponent(encoded);
|
|
468
|
+
return name.trim() ? name : undefined;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
450
474
|
structuralWorktree(owner, record) {
|
|
451
475
|
if (!record || typeof record !== "object")
|
|
452
476
|
throw new Error(`Invalid worktree record for ${owner}`);
|
|
@@ -458,6 +482,123 @@ export class RunStore {
|
|
|
458
482
|
throw new Error(`Invalid worktree record for ${owner}`);
|
|
459
483
|
return candidate;
|
|
460
484
|
}
|
|
485
|
+
async borrowedWorktreeRecords(wait = true) {
|
|
486
|
+
if (wait)
|
|
487
|
+
await this.borrowedWorktreeWrite;
|
|
488
|
+
const records = await json(join(this.directory, "borrowed-worktrees.json")).catch((error) => { if (error.code === "ENOENT")
|
|
489
|
+
return []; throw error; });
|
|
490
|
+
if (!Array.isArray(records))
|
|
491
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree bindings are invalid");
|
|
492
|
+
const seen = new Set();
|
|
493
|
+
return records.map((record) => {
|
|
494
|
+
if (!record || typeof record !== "object")
|
|
495
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree binding is invalid");
|
|
496
|
+
const candidate = record;
|
|
497
|
+
if (typeof candidate.name !== "string" || !candidate.name.trim() || candidate.name !== candidate.name.trim() || typeof candidate.sourceRunId !== "string" || !candidate.sourceRunId || typeof candidate.owner !== "string" || candidate.owner !== this.namedWorktreeOwner(candidate.name))
|
|
498
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree binding is invalid");
|
|
499
|
+
if (seen.has(candidate.name))
|
|
500
|
+
throw new WorkflowError("WORKTREE_FAILED", `Duplicate borrowed worktree binding for ${candidate.name}`);
|
|
501
|
+
seen.add(candidate.name);
|
|
502
|
+
return { name: candidate.name, sourceRunId: candidate.sourceRunId, owner: candidate.owner };
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async borrowedWorktrees() { return this.borrowedWorktreeRecords(); }
|
|
506
|
+
async borrowedWorktree(name) {
|
|
507
|
+
return (await this.borrowedWorktreeRecords()).find((binding) => binding.name === name);
|
|
508
|
+
}
|
|
509
|
+
async sourceRun(sourceRunId) {
|
|
510
|
+
if (!sourceRunId || sourceRunId === this.runId)
|
|
511
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree source run is invalid");
|
|
512
|
+
const source = new RunStore(this.cwd, this.sessionId, sourceRunId, this.home);
|
|
513
|
+
try {
|
|
514
|
+
const loaded = await source.load();
|
|
515
|
+
if (!["completed", "failed", "stopped"].includes(loaded.run.state))
|
|
516
|
+
throw new Error(`Source run ${sourceRunId} is not terminal`);
|
|
517
|
+
return source;
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
if (error instanceof WorkflowError && error.code === "WORKTREE_FAILED")
|
|
521
|
+
throw error;
|
|
522
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async validateParentRun(parentRunId) { await this.sourceRun(parentRunId); }
|
|
526
|
+
async ownedWorktree(owner, cwd) {
|
|
527
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
528
|
+
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
|
|
529
|
+
if (matches.length !== 1)
|
|
530
|
+
throw new Error(`Missing or duplicate worktree record for ${owner}`);
|
|
531
|
+
const record = this.structuralWorktree(owner, matches[0]);
|
|
532
|
+
if (cwd !== undefined && resolve(cwd) !== resolve(record.cwd))
|
|
533
|
+
throw new Error(`Invalid worktree record for ${owner}`);
|
|
534
|
+
await access(record.cwd);
|
|
535
|
+
return record;
|
|
536
|
+
}
|
|
537
|
+
async resolveBorrowedWorktree(binding, seen) {
|
|
538
|
+
try {
|
|
539
|
+
const source = await this.sourceRun(binding.sourceRunId);
|
|
540
|
+
const resolved = await source.findNamedWorktree(binding.name, seen);
|
|
541
|
+
if (!resolved)
|
|
542
|
+
throw new Error(`Missing named worktree ${binding.name} in source run ${binding.sourceRunId}`);
|
|
543
|
+
if (resolved.owner !== binding.owner)
|
|
544
|
+
throw new Error(`Borrowed worktree binding does not match source owner for ${binding.name}`);
|
|
545
|
+
return resolved;
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async findNamedWorktree(name, seen = new Set()) {
|
|
552
|
+
const owner = this.namedWorktreeOwner(name);
|
|
553
|
+
if (seen.has(this.runId))
|
|
554
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree bindings contain a cycle");
|
|
555
|
+
const nextSeen = new Set(seen);
|
|
556
|
+
nextSeen.add(this.runId);
|
|
557
|
+
const binding = await this.borrowedWorktree(name);
|
|
558
|
+
if (binding) {
|
|
559
|
+
const loaded = await this.load();
|
|
560
|
+
if (loaded.run.parentRunId === undefined)
|
|
561
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree ${name} has no parent run`);
|
|
562
|
+
const parent = await this.sourceRun(loaded.run.parentRunId);
|
|
563
|
+
const resolved = await parent.findNamedWorktree(name, nextSeen);
|
|
564
|
+
if (!resolved || resolved.sourceRunId !== binding.sourceRunId || resolved.owner !== binding.owner)
|
|
565
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree binding for ${name} is not inherited from its parent run`);
|
|
566
|
+
return resolved;
|
|
567
|
+
}
|
|
568
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
569
|
+
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
|
|
570
|
+
if (matches.length === 0)
|
|
571
|
+
return undefined;
|
|
572
|
+
try {
|
|
573
|
+
const reference = await this.ownedWorktree(owner);
|
|
574
|
+
return { reference, sourceRunId: this.runId, owner };
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async resolveNamedWorktree(name, seen = new Set()) {
|
|
581
|
+
const resolved = await this.findNamedWorktree(name, seen);
|
|
582
|
+
if (!resolved)
|
|
583
|
+
throw new WorkflowError("WORKTREE_FAILED", `Missing named worktree ${name}`);
|
|
584
|
+
return resolved;
|
|
585
|
+
}
|
|
586
|
+
async validateBorrowedWorktrees() {
|
|
587
|
+
try {
|
|
588
|
+
const loaded = await this.load();
|
|
589
|
+
if (loaded.run.parentRunId !== undefined)
|
|
590
|
+
await this.validateParentRun(loaded.run.parentRunId);
|
|
591
|
+
for (const binding of await this.borrowedWorktreeRecords())
|
|
592
|
+
await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async ownsWorktree(owner) {
|
|
599
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
600
|
+
return records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner).length === 1;
|
|
601
|
+
}
|
|
461
602
|
async cleanupMarker(markerPath) {
|
|
462
603
|
let marker;
|
|
463
604
|
try {
|
|
@@ -490,15 +631,15 @@ export class RunStore {
|
|
|
490
631
|
async validateWorktree(owner, cwd) {
|
|
491
632
|
try {
|
|
492
633
|
await this.load();
|
|
493
|
-
const
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return
|
|
634
|
+
const name = this.worktreeName(owner);
|
|
635
|
+
const binding = name ? await this.borrowedWorktree(name) : undefined;
|
|
636
|
+
if (binding) {
|
|
637
|
+
const resolved = await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
|
|
638
|
+
if (cwd !== undefined && resolve(cwd) !== resolve(resolved.reference.cwd))
|
|
639
|
+
throw new Error(`Invalid worktree record for ${owner}`);
|
|
640
|
+
return resolved.reference;
|
|
641
|
+
}
|
|
642
|
+
return await this.ownedWorktree(owner, cwd);
|
|
502
643
|
}
|
|
503
644
|
catch (error) {
|
|
504
645
|
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
@@ -506,10 +647,21 @@ export class RunStore {
|
|
|
506
647
|
}
|
|
507
648
|
async worktree(owner) {
|
|
508
649
|
const write = this.worktreeWrite.then(async () => {
|
|
509
|
-
await this.load();
|
|
650
|
+
const loaded = await this.load();
|
|
510
651
|
const recordsPath = join(this.directory, "worktrees.json");
|
|
511
652
|
let records = await json(recordsPath).catch((error) => { if (error.code === "ENOENT")
|
|
512
653
|
return []; throw error; });
|
|
654
|
+
const name = this.worktreeName(owner);
|
|
655
|
+
const binding = name ? await this.borrowedWorktree(name) : undefined;
|
|
656
|
+
if (binding)
|
|
657
|
+
return (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference;
|
|
658
|
+
if (name && loaded.run.parentRunId !== undefined) {
|
|
659
|
+
const resolved = await this.resolveNamedWorktreeFromParent(name, loaded.run.parentRunId);
|
|
660
|
+
if (resolved) {
|
|
661
|
+
await this.bindBorrowedWorktree({ name, sourceRunId: resolved.sourceRunId, owner: resolved.owner });
|
|
662
|
+
return resolved.reference;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
513
665
|
const existing = records.find((record) => record.owner === owner);
|
|
514
666
|
if (existing)
|
|
515
667
|
return this.validateWorktree(owner);
|
|
@@ -564,6 +716,25 @@ export class RunStore {
|
|
|
564
716
|
this.worktreeWrite = write.then(() => undefined, () => undefined);
|
|
565
717
|
return write;
|
|
566
718
|
}
|
|
719
|
+
async resolveNamedWorktreeFromParent(name, parentRunId) {
|
|
720
|
+
const source = await this.sourceRun(parentRunId);
|
|
721
|
+
return source.findNamedWorktree(name, new Set([this.runId]));
|
|
722
|
+
}
|
|
723
|
+
async bindBorrowedWorktree(binding) {
|
|
724
|
+
const write = this.borrowedWorktreeWrite.then(async () => {
|
|
725
|
+
const records = [...await this.borrowedWorktreeRecords(false)];
|
|
726
|
+
const existing = records.find((candidate) => candidate.name === binding.name);
|
|
727
|
+
if (existing) {
|
|
728
|
+
if (JSON.stringify(existing) !== JSON.stringify(binding))
|
|
729
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree binding for ${binding.name} changed`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
records.push(binding);
|
|
733
|
+
await atomicJson(join(this.directory, "borrowed-worktrees.json"), records);
|
|
734
|
+
});
|
|
735
|
+
this.borrowedWorktreeWrite = write.then(() => undefined, () => undefined);
|
|
736
|
+
await write;
|
|
737
|
+
}
|
|
567
738
|
async snapshotWorktree(owner) {
|
|
568
739
|
try {
|
|
569
740
|
const write = this.snapshotWrite.then(async () => {
|
|
@@ -593,13 +764,16 @@ export class RunStore {
|
|
|
593
764
|
async worktrees() {
|
|
594
765
|
const records = await json(join(this.directory, "worktrees.json")).catch((error) => { if (error.code === "ENOENT")
|
|
595
766
|
return []; throw error; });
|
|
596
|
-
const
|
|
767
|
+
const bindings = await this.borrowedWorktreeRecords();
|
|
768
|
+
const boundOwners = new Set(bindings.map((binding) => binding.owner));
|
|
769
|
+
const owned = await Promise.all(records.filter((record) => !boundOwners.has(record.owner)).map(async (record) => { try {
|
|
597
770
|
return await this.validateWorktree(record.owner);
|
|
598
771
|
}
|
|
599
772
|
catch {
|
|
600
773
|
return undefined;
|
|
601
774
|
} }));
|
|
602
|
-
|
|
775
|
+
const borrowed = await Promise.all(bindings.map(async (binding) => (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference));
|
|
776
|
+
return [...owned.filter((record) => record !== undefined), ...borrowed];
|
|
603
777
|
}
|
|
604
778
|
async changedWorktrees() {
|
|
605
779
|
const changed = [];
|
|
@@ -63,6 +63,7 @@ export interface InspectorViewState {
|
|
|
63
63
|
scroll: number;
|
|
64
64
|
}
|
|
65
65
|
export declare function transcriptLines(entries: readonly SessionEntry[]): string[];
|
|
66
|
+
export declare function transcriptFileLines(path: string): string[];
|
|
66
67
|
export declare function matchSession(query: string, sessions: readonly SessionInfo[]): SessionInfo;
|
|
67
68
|
export declare function loadSessionReport(path: string, home?: string): Promise<SessionReport>;
|
|
68
69
|
export declare function renderInspector(report: SessionReport, state: InspectorViewState, width?: number, height?: number, highlighter?: (script: string) => string[]): string[];
|
|
@@ -45,6 +45,9 @@ export function transcriptLines(entries) {
|
|
|
45
45
|
return index ? ["", ...lines] : lines;
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
+
export function transcriptFileLines(path) {
|
|
49
|
+
return transcriptLines(SessionManager.open(path).buildContextEntries());
|
|
50
|
+
}
|
|
48
51
|
function mergedModels(groups) {
|
|
49
52
|
const totals = new Map();
|
|
50
53
|
for (const group of groups)
|
|
@@ -707,6 +707,7 @@ export async function replayWorkflowScript(script, args = null, signal) {
|
|
|
707
707
|
active -= 1;
|
|
708
708
|
}
|
|
709
709
|
},
|
|
710
|
+
worktree: async () => ({ path: "/worktrees/eval", branch: "eval-branch" }),
|
|
710
711
|
phase: (name) => { phases.push(name); },
|
|
711
712
|
log: (message) => { logs.push(message); },
|
|
712
713
|
}, signal);
|
|
@@ -860,7 +861,7 @@ function semanticJudgePrompt(evalCase, calls, cwd, home) {
|
|
|
860
861
|
return [];
|
|
861
862
|
} }));
|
|
862
863
|
const roleText = [...usedRoles].map((role) => `${role}: ${roles[role]?.description ?? "no description"}`).join("\n") || "none";
|
|
863
|
-
const docs = "agent(prompt, options) delegates; parallel(name, tasks) runs independent tasks concurrently; pipeline(name, items, stages) applies ordered stages; prompt(template, data) carries values into prompts. A role owns model/thinking/tools policy.";
|
|
864
|
+
const docs = "agent(prompt, options) delegates; shell(command, options) runs a deterministic host command and returns exitCode/stdout/stderr; parallel(name, tasks) runs independent tasks concurrently; pipeline(name, items, stages) applies ordered stages; prompt(template, data) carries values into prompts. A role owns model/thinking/tools policy.";
|
|
864
865
|
return `Judge whether the captured workflow design satisfies each criterion. Do not execute it. Return only JSON: {"criteria":[{"id":"criterion id","pass":true,"evidence":"specific script evidence"}]}.\n\nOriginal request:\n${evalCase.prompt}\n\nCriteria:\n${JSON.stringify(evalCase.semanticCriteria ?? [])}\n\nDSL:\n${docs}\n\nRelevant roles:\n${roleText}\n\nCaptured workflow script(s):\n${calls.map((call, index) => `--- ${String(index)} ---\n${call.script ?? "<missing>"}`).join("\n")}`;
|
|
865
866
|
}
|
|
866
867
|
async function runSemanticJudge(input, calls, cwd, home, sessionDir, maxCost) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.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": {
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
"build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/src/cli.js",
|
|
32
32
|
"inspect": "node dist/src/cli.js inspect",
|
|
33
33
|
"lint": "eslint .",
|
|
34
|
-
"test": "npm run build &&
|
|
35
|
-
"
|
|
34
|
+
"test": "npm run build && TEST_FILES='dist/test/*.test.js' npm run test:run",
|
|
35
|
+
"test:run": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; TMPDIR=\"$tmp\" node --test $TEST_FILES",
|
|
36
|
+
"acceptance": "npm run build && TEST_FILES='dist/test/runtime-acceptance.test.js' npm run test:run",
|
|
36
37
|
"docs:check": "node scripts/check-docs.mjs",
|
|
37
38
|
"check": "npm run lint && npm test && npm run docs:check",
|
|
38
39
|
"evals": "npm run build && node dist/src/workflow-evals.js",
|
|
@@ -4,87 +4,80 @@ description: Use when the task is complex enough to require multiple subagents o
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# pi-extensible-workflows
|
|
7
|
-
|
|
8
|
-
Use `workflow` exclusively for genuinely multi-agent orchestration. For one agent, use ordinary tools or `Agent` directly. Do not wrap a single agent in a workflow; define distinct responsibilities and keep the result flow explicit.
|
|
7
|
+
Use `workflow` only for genuinely multi-agent orchestration; one agent uses ordinary tools or `Agent` directly. Give phases distinct responsibilities and keep result flow explicit.
|
|
9
8
|
|
|
10
9
|
## Pattern
|
|
11
|
-
|
|
12
10
|
```js
|
|
13
|
-
const reportSchema = {
|
|
14
|
-
type: "object",
|
|
15
|
-
properties: {
|
|
16
|
-
summary: { type: "string" },
|
|
17
|
-
findings: { type: "array", items: { type: "string" } },
|
|
18
|
-
},
|
|
19
|
-
required: ["summary", "findings"],
|
|
20
|
-
additionalProperties: false,
|
|
21
|
-
};
|
|
11
|
+
const reportSchema = { type: "object", properties: { summary: { type: "string" }, findings: { type: "array", items: { type: "string" } } }, required: ["summary", "findings"], additionalProperties: false };
|
|
22
12
|
|
|
23
13
|
const reports = await parallel("research", {
|
|
24
14
|
first: () => agent("Research the first target.", { role: "scout", outputSchema: reportSchema }),
|
|
25
15
|
second: () => agent("Research the second target.", { role: "scout", outputSchema: reportSchema }),
|
|
26
16
|
});
|
|
27
17
|
|
|
28
|
-
// Add a downstream agent only when synthesis or independent review is a real phase.
|
|
29
18
|
return agent(
|
|
30
19
|
prompt("Review these reports:\n\n{reports}", { reports }),
|
|
31
20
|
{ role: "reviewer", outputSchema: reportSchema },
|
|
32
21
|
);
|
|
33
22
|
```
|
|
34
23
|
|
|
35
|
-
|
|
24
|
+
Pass structured input from the main agent with `args`:
|
|
36
25
|
```json
|
|
37
26
|
{ "workflow": "workflowName", "args": { "issue": 42 } }
|
|
38
27
|
```
|
|
39
|
-
Inside the workflow, read `args.issue
|
|
40
|
-
|
|
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.
|
|
28
|
+
Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground results retain their value and completed `runId`, while background launches return `runId` immediately. A terminal `parentRunId` reuses matching named `withWorktree` scopes; unnamed or missing names create new worktrees.
|
|
29
|
+
If `workflow_catalog` is available, call it once before creating the first workflow for a task. The name-less result is a compact index with launch-ready input schemas, descriptions, variables, and configured model aliases. Use those inputs to launch registered functions directly with `{ "workflow": "name", "args": { ... } }`; their input and output schemas are enforced. Request full detail with `{ "name": "name" }` only when composing a function programmatically or inspecting its output contract and extension metadata; do not load full definitions unconditionally. Alias targets are catalog metadata, not an availability probe. Do not try to reinvent already exposed functions.
|
|
42
30
|
|
|
43
|
-
|
|
31
|
+
Workflow JavaScript has no imports, filesystem, network, process, or timers. Delegate that work to agents. `shell(command, options)` is the trusted host RPC for deterministic gates: it inherits the workflow or active-worktree cwd, merges string `env` overrides, and returns `{ exitCode, stdout, stderr }`; nonzero exits are results, but launch failures and timeouts fail with `SHELL_FAILED`.
|
|
44
32
|
|
|
45
|
-
|
|
33
|
+
Use a bounded verification loop when command output controls the gate:
|
|
34
|
+
```js
|
|
35
|
+
return withWorktree("fix-tests", async ({ path, branch }) => {
|
|
36
|
+
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
37
|
+
const tests = await shell("yarn test", { env: { CI: "1" } });
|
|
38
|
+
if (tests.exitCode === 0) return { path, branch, tests };
|
|
39
|
+
await agent(prompt("Fix these failures:\n\n{output}", { output: tests.stderr || tests.stdout }));
|
|
40
|
+
}
|
|
41
|
+
return { path, branch, tests: await shell("yarn test", { env: { CI: "1" } }) };
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
Shell results are journaled only after process exit and RPC validation. A host crash after side effects but before journaling can rerun the command on resume; use `shell()` mainly for verification and bounded gates, not exactly-once mutations.
|
|
46
45
|
|
|
46
|
+
## `agent()` options
|
|
47
47
|
```typescript
|
|
48
48
|
interface AgentOptions {
|
|
49
|
-
label?: string;
|
|
50
|
-
|
|
51
|
-
thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
52
|
-
role?: string; // one of the available workflow roles
|
|
53
|
-
tools?: string[]; // [] = no tools; omitted uses role or launch tools
|
|
54
|
-
outputSchema?: JsonSchema;
|
|
55
|
-
retries?: number; // non-negative; use for safe, repeatable work
|
|
56
|
-
timeoutMs?: number | null; // positive milliseconds; null means unlimited
|
|
49
|
+
label?: string; model?: string; role?: string; tools?: string[];
|
|
50
|
+
thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; outputSchema?: JsonSchema; retries?: number; timeoutMs?: number | null;
|
|
57
51
|
}
|
|
58
52
|
```
|
|
59
53
|
|
|
60
|
-
Extensions may
|
|
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.
|
|
54
|
+
Extensions may add JSON-compatible agent options such as `advisor: true`; core keys retain validation and role constraints. Extension options go to setup hooks/native setup and are not inherited by child agents.
|
|
63
55
|
|
|
64
|
-
|
|
56
|
+
Agent calls are unnamed. Direct calls receive hidden source call-site identity; aliases are unsupported, and calls from one source site must not race outside `parallel` or `pipeline`, whose structural keys make replay deterministic.
|
|
65
57
|
|
|
66
|
-
|
|
58
|
+
## Persistent conversations
|
|
59
|
+
Use `conversation(name, options)` when several agent turns should share a persisted transcript and build on one another. Unlike independent `agent(prompt, options)` calls, the returned handle continues from the last successful turn:
|
|
67
60
|
|
|
68
61
|
```js
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
62
|
+
const handle = conversation("developer", { role: "developer" });
|
|
63
|
+
const findings = await handle.run("Inspect the implementation.");
|
|
64
|
+
const fix = await handle.run("Now propose the smallest fix.");
|
|
65
|
+
return { findings, fix };
|
|
73
66
|
```
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`parallel()` tasks may call any workflow function, not only `agent()`:
|
|
68
|
+
Await each `handle.run(prompt, turnOptions)` call before starting the next one; conversation turns must be sequential and cannot overlap. Conversation creation accepts the same execution-policy options as `agent()`. `timeoutMs` and `retries` passed to `run()` are turn-local, so a failed turn does not advance the persisted conversation head.
|
|
78
69
|
|
|
70
|
+
## Worktrees
|
|
71
|
+
Use `withWorktree(callback)` or `withWorktree(name, callback)` for top-level agents that collaborate in one worktree:
|
|
79
72
|
```js
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
const result = await withWorktree("issue", async ({ path, branch }) => {
|
|
74
|
+
const report = await agent("Implement the issue");
|
|
75
|
+
return { path, branch, report };
|
|
83
76
|
});
|
|
84
77
|
```
|
|
78
|
+
Entering the scope materializes its worktree before the callback. The callback receives a frozen reference containing only the real string `path` and `branch`; callbacks may ignore the argument, and their bare return value is preserved. Concurrent agents share mutable files, so assign non-conflicting work or coordinate explicitly.
|
|
85
79
|
|
|
86
|
-
Use separate named scopes when
|
|
87
|
-
|
|
80
|
+
Branches may call any workflow function, not only `agent()`. Use separate named scopes when parallel branches need isolated worktrees:
|
|
88
81
|
```js
|
|
89
82
|
const results = await parallel("implementation", {
|
|
90
83
|
api: () => withWorktree("api", () => agent("Implement the API")),
|
|
@@ -92,26 +85,17 @@ const results = await parallel("implementation", {
|
|
|
92
85
|
});
|
|
93
86
|
```
|
|
94
87
|
|
|
95
|
-
Registered extension functions receive `withWorktree` in
|
|
96
|
-
```ts
|
|
97
|
-
const report = await context.invoke("reviewRepository", { focus: "security" });
|
|
98
|
-
```
|
|
99
|
-
Their public inputs and outputs must remain JSON; callbacks cannot cross the extension-function boundary.
|
|
88
|
+
Registered extension functions receive `withWorktree` in context and can compose other registered functions with `context.invoke("reviewRepository", { focus: "security" })`. Their public inputs and outputs remain JSON; callbacks cannot cross the extension boundary.
|
|
100
89
|
|
|
101
90
|
## Rules
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
- Use `
|
|
105
|
-
-
|
|
106
|
-
- Use
|
|
107
|
-
- Call shapes are `parallel(operationName, tasksRecord)` and `pipeline(operationName, itemsRecord, stagesRecord)`; object keys are stable task, item, and stage names.
|
|
108
|
-
- Preserve item metadata in workflow code between pipeline stages instead of requiring agents to echo it through `outputSchema`.
|
|
109
|
-
- Repeated work uses a JavaScript loop; each direct `agent(...)` call receives deterministic call-site and occurrence identity.
|
|
91
|
+
- Use `log(messageString)` for brief operator status.
|
|
92
|
+
- A role owns execution policy: with `role`, do not set `model`, `thinking`, or `tools`; only task options such as `outputSchema`, retries, timeout, or a `withWorktree` scope may accompany it.
|
|
93
|
+
- Use `parallel()` for independent tasks with different flows and `pipeline()` when every keyed item follows the same ordered stages; do not duplicate identical chains in `parallel()`. Signatures are `parallel(operationName, tasksRecord)` and `pipeline(operationName, itemsRecord, stagesRecord)`; keys are stable task, item, and stage names.
|
|
94
|
+
- Preserve item metadata in workflow code between pipeline stages instead of making agents echo it through `outputSchema`.
|
|
95
|
+
- Use a JavaScript loop for repeated work; each direct `agent(...)` call gets deterministic call-site and occurrence identity.
|
|
110
96
|
- Runs default to background; set tool-call `foreground: true` when asked to wait.
|
|
111
|
-
- Add `budget` only
|
|
112
|
-
-
|
|
113
|
-
- `parallel()` and `pipeline()` return keyed bare values
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
- With `outputSchema`, agents must call `workflow_result`; one repair prompt is built in. Omit `retries` unless an additional retry is justified and the work is idempotent.
|
|
117
|
-
- Do not add "persona" specs to the prompt for agents. Just define the task.
|
|
97
|
+
- Add `budget` only for aggregate limits. Valid dimensions are exactly `tokens`, `costUsd`, `durationMs`, and `agentLaunches`; each is `{ soft?: number, hard?: number }` with `soft < hard`.
|
|
98
|
+
- `budget_exhausted` runs resume through `workflow_resume`: omitted patch values stay unchanged, `null` removes a limit, and tightening resumes directly. Relaxation stores the exact proposal and returns `{ state: "awaiting_approval", proposalId }`; `workflow_respond` must answer that ID. Rejection leaves the run exhausted; approval applies the budget and cold-resumes it.
|
|
99
|
+
- `parallel()` and `pipeline()` return keyed bare values; await them before use. Interpolate results with `prompt("...{value}", { value })`; placeholders in plain strings remain literal.
|
|
100
|
+
- Use `outputSchema` only when another phase compares, aggregates, or validates a result, never for final prose. Keep only consumer-needed fields and avoid repeated evidence. Agents with it must call `workflow_result`; one repair prompt is built in. Omit `retries` unless an extra retry is justified and work is idempotent.
|
|
101
|
+
- Do not add persona specifications to agent prompts; define the task directly.
|