pi-extensible-workflows 2.0.0 → 3.1.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 +18 -11
- package/dist/src/agent-execution.d.ts +4 -94
- package/dist/src/agent-execution.js +34 -208
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +160 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +60 -8
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/execution.d.ts +17 -0
- package/dist/src/execution.js +630 -0
- package/dist/src/host.d.ts +101 -0
- package/dist/src/host.js +3084 -0
- package/dist/src/index.d.ts +13 -634
- package/dist/src/index.js +10 -4432
- package/dist/src/persistence.d.ts +9 -19
- package/dist/src/persistence.js +175 -61
- package/dist/src/registry.d.ts +43 -0
- package/dist/src/registry.js +279 -0
- package/dist/src/session-inspector.js +5 -3
- package/dist/src/types.d.ts +692 -0
- package/dist/src/types.js +27 -0
- package/dist/src/utils.d.ts +32 -0
- package/dist/src/utils.js +168 -0
- package/dist/src/validation.d.ts +28 -0
- package/dist/src/validation.js +832 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +69 -34
- package/src/agent-execution.ts +37 -189
- package/src/budget.ts +75 -0
- package/src/cli.ts +47 -7
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/execution.ts +540 -0
- package/src/host.ts +2598 -0
- package/src/index.ts +14 -3856
- package/src/persistence.ts +122 -37
- package/src/registry.ts +240 -0
- package/src/session-inspector.ts +5 -3
- package/src/types.ts +158 -0
- package/src/utils.ts +142 -0
- package/src/validation.ts +688 -0
package/src/persistence.ts
CHANGED
|
@@ -5,20 +5,13 @@ 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 type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./
|
|
8
|
+
import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./types.js";
|
|
9
9
|
import type { OwnershipRecord } from "./agent-execution.js";
|
|
10
|
-
import {
|
|
10
|
+
import { WorkflowError } from "./types.js";
|
|
11
|
+
import { loadLaunchSnapshot } from "./utils.js";
|
|
11
12
|
|
|
12
13
|
export interface NativeSessionReference { sessionId: string; sessionFile: string }
|
|
13
14
|
export interface EffectiveSystemPrompt { sessionId: string; attempt: number; turn: number; sha256: string; prompt: string }
|
|
14
|
-
export interface ConversationHead { turn: number; sessionId: string; sessionFile: string; leafId: string; systemPrompt: string; systemPromptSha256: string; toolDefinitionsSha256: string }
|
|
15
|
-
export interface PersistedConversation { id: string; policy: JsonValue; head: ConversationHead }
|
|
16
|
-
type ConversationArtifact = { version: 1; conversations: Record<string, PersistedConversation> };
|
|
17
|
-
function isConversationArtifact(value: unknown): value is ConversationArtifact {
|
|
18
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
19
|
-
const artifact = value as { version?: unknown; conversations?: unknown };
|
|
20
|
-
return artifact.version === 1 && Boolean(artifact.conversations) && typeof artifact.conversations === "object" && !Array.isArray(artifact.conversations);
|
|
21
|
-
}
|
|
22
15
|
export interface PersistedRun extends RunRecord { nativeSessions: readonly NativeSessionReference[] }
|
|
23
16
|
export interface CompletedOperation { path: string; value: JsonValue }
|
|
24
17
|
export interface AwaitingCheckpoint { path: string; name: string; prompt: string; context: JsonValue }
|
|
@@ -42,8 +35,11 @@ export function projectStorageKey(cwd: string): string {
|
|
|
42
35
|
return `${slug}-${createHash("sha256").update(exact).digest("hex").slice(0, 12)}`;
|
|
43
36
|
}
|
|
44
37
|
|
|
38
|
+
export function projectSessionsDirectory(cwd: string, home = homedir()): string {
|
|
39
|
+
return join(home, ".pi", "workflows", "projects", projectStorageKey(cwd), "sessions");
|
|
40
|
+
}
|
|
45
41
|
export function runsDirectory(cwd: string, sessionId: string, home = homedir()): string {
|
|
46
|
-
return join(
|
|
42
|
+
return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
const SESSION_OWNER_FILE = "owner.json";
|
|
@@ -59,6 +55,16 @@ async function processAlive(pid: number, startedAt?: number): Promise<boolean> {
|
|
|
59
55
|
}
|
|
60
56
|
return true;
|
|
61
57
|
}
|
|
58
|
+
export async function hasLiveSessionLease(cwd: string, sessionId: string, home = homedir()): Promise<boolean> {
|
|
59
|
+
const path = join(runsDirectory(cwd, sessionId, home), SESSION_OWNER_FILE);
|
|
60
|
+
let owner: unknown;
|
|
61
|
+
try { owner = JSON.parse(await readFile(path, "utf8")); }
|
|
62
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; throw error; }
|
|
63
|
+
if (!owner || typeof owner !== "object" || Array.isArray(owner)) throw new WorkflowError("RUN_OWNED", `Pi session ${sessionId} has an invalid ownership lease`);
|
|
64
|
+
const candidate = owner as Partial<SessionOwner>;
|
|
65
|
+
if (typeof candidate.pid !== "number" || !Number.isInteger(candidate.pid) || candidate.pid < 1 || typeof candidate.token !== "string" || !candidate.token || typeof candidate.startedAt !== "number" || !Number.isFinite(candidate.startedAt)) throw new WorkflowError("RUN_OWNED", `Pi session ${sessionId} has an invalid ownership lease`);
|
|
66
|
+
return processAlive(candidate.pid, candidate.startedAt);
|
|
67
|
+
}
|
|
62
68
|
|
|
63
69
|
function sameOwner(left: unknown, right: unknown): boolean {
|
|
64
70
|
if (!left || typeof left !== "object" || !right || typeof right !== "object") return false;
|
|
@@ -197,7 +203,6 @@ export class RunStore {
|
|
|
197
203
|
private launchSnapshotWrite: Promise<void> = Promise.resolve();
|
|
198
204
|
// ponytail: the session lease prevents concurrent RunStore writers for one run.
|
|
199
205
|
private systemPromptWrite: Promise<void> = Promise.resolve();
|
|
200
|
-
private conversationWrite: Promise<void> = Promise.resolve();
|
|
201
206
|
constructor(readonly cwd: string, readonly sessionId: string, readonly runId: string, readonly home = homedir()) {
|
|
202
207
|
this.cwd = resolve(cwd);
|
|
203
208
|
this.directory = join(runsDirectory(this.cwd, sessionId, home), safePart(runId));
|
|
@@ -217,7 +222,6 @@ export class RunStore {
|
|
|
217
222
|
await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
|
|
218
223
|
await atomicJson(join(temporary, "state.json"), run);
|
|
219
224
|
await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
|
|
220
|
-
await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
|
|
221
225
|
await rename(temporary, this.directory);
|
|
222
226
|
} catch (error) {
|
|
223
227
|
await rm(temporary, { recursive: true, force: true });
|
|
@@ -296,27 +300,6 @@ export class RunStore {
|
|
|
296
300
|
return (await json<{ version: 1; entries: EffectiveSystemPrompt[] }>(this.systemPromptPath()).catch((error: unknown) => { if ((error as NodeJS.ErrnoException).code === "ENOENT") return { version: 1 as const, entries: [] }; throw error; })).entries;
|
|
297
301
|
}
|
|
298
302
|
|
|
299
|
-
conversationPath(): string { return join(this.directory, "conversations.json"); }
|
|
300
|
-
async conversation(id: string): Promise<PersistedConversation | undefined> {
|
|
301
|
-
await this.conversationWrite;
|
|
302
|
-
let artifact: ConversationArtifact;
|
|
303
|
-
try { const raw = await json<unknown>(this.conversationPath()); if (!isConversationArtifact(raw)) throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt"); artifact = raw; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; if (error instanceof WorkflowError) throw error; throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`); }
|
|
304
|
-
return artifact.conversations[id];
|
|
305
|
-
}
|
|
306
|
-
async saveConversation(conversation: PersistedConversation): Promise<void> {
|
|
307
|
-
const write = this.conversationWrite.then(async () => {
|
|
308
|
-
const path = this.conversationPath();
|
|
309
|
-
let artifact: ConversationArtifact;
|
|
310
|
-
try { const raw = await json<unknown>(path); if (!isConversationArtifact(raw)) throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt"); artifact = raw; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") artifact = { version: 1, conversations: {} }; else if (error instanceof WorkflowError) throw error; else throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`); }
|
|
311
|
-
const previous = artifact.conversations[conversation.id];
|
|
312
|
-
if (previous && previous.head.turn + 1 !== conversation.head.turn) throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation head is not the previous turn: ${conversation.id}`);
|
|
313
|
-
if (!previous && conversation.head.turn !== 1) throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation must start at turn one: ${conversation.id}`);
|
|
314
|
-
artifact.conversations[conversation.id] = structuredClone(conversation);
|
|
315
|
-
await atomicJson(path, artifact);
|
|
316
|
-
});
|
|
317
|
-
this.conversationWrite = write.catch(() => undefined);
|
|
318
|
-
await write;
|
|
319
|
-
}
|
|
320
303
|
private async updateJournal<T>(update: (journal: Journal) => T | Promise<T>): Promise<T> {
|
|
321
304
|
let result!: T;
|
|
322
305
|
const write = this.journalWrite.then(async () => {
|
|
@@ -339,11 +322,33 @@ export class RunStore {
|
|
|
339
322
|
}
|
|
340
323
|
|
|
341
324
|
async replay(path: string): Promise<CompletedOperation | undefined> {
|
|
325
|
+
const operations = await this.replayableOperations();
|
|
326
|
+
return operations.find((operation) => operation.path === path);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async replayableOperations(): Promise<readonly CompletedOperation[]> {
|
|
330
|
+
return this.replayableOperationsFrom(new Set());
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private async replayableOperationsFrom(seen: Set<string>): Promise<readonly CompletedOperation[]> {
|
|
334
|
+
if (seen.has(this.runId)) throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance contains a cycle");
|
|
335
|
+
const nextSeen = new Set(seen);
|
|
336
|
+
nextSeen.add(this.runId);
|
|
342
337
|
await this.journalWrite;
|
|
343
|
-
|
|
338
|
+
const loaded = await this.load();
|
|
339
|
+
const operations = new Map<string, CompletedOperation>();
|
|
340
|
+
if (loaded.run.retry?.sourceRunId) {
|
|
341
|
+
const source = await this.sourceRun(loaded.run.retry.sourceRunId);
|
|
342
|
+
for (const operation of await source.replayableOperationsFrom(nextSeen)) operations.set(operation.path, operation);
|
|
343
|
+
}
|
|
344
|
+
const journal = await json<Journal>(join(this.directory, "journal.json"));
|
|
345
|
+
for (const operation of Object.values(journal.completed)) operations.set(operation.path, operation);
|
|
346
|
+
return [...operations.values()].map((operation) => structuredClone(operation));
|
|
344
347
|
}
|
|
345
348
|
|
|
346
349
|
async awaitCheckpoint(checkpoint: AwaitingCheckpoint): Promise<boolean | undefined> {
|
|
350
|
+
const replayed = await this.replay(checkpoint.path);
|
|
351
|
+
if (replayed) return replayed.value as boolean;
|
|
347
352
|
return this.updateJournal((journal) => {
|
|
348
353
|
const completed = journal.completed[checkpoint.path];
|
|
349
354
|
if (completed) return completed.value as boolean;
|
|
@@ -458,6 +463,26 @@ export class RunStore {
|
|
|
458
463
|
}
|
|
459
464
|
|
|
460
465
|
async validateParentRun(parentRunId: string): Promise<void> { await this.sourceRun(parentRunId); }
|
|
466
|
+
async validateRetrySource(): Promise<void> {
|
|
467
|
+
const validate = async (current: RunStore, seen: Set<string>): Promise<void> => {
|
|
468
|
+
if (seen.has(current.runId)) throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance contains a cycle");
|
|
469
|
+
const nextSeen = new Set(seen);
|
|
470
|
+
nextSeen.add(current.runId);
|
|
471
|
+
const loaded = await current.load();
|
|
472
|
+
const retry = loaded.run.retry;
|
|
473
|
+
if (!retry) return;
|
|
474
|
+
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")) throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance is incomplete");
|
|
475
|
+
const source = await current.sourceRun(retry.sourceRunId);
|
|
476
|
+
const sourceRun = (await source.load()).run;
|
|
477
|
+
if (loaded.run.parentRunId !== retry.sourceRunId) throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry parent run does not match its source run");
|
|
478
|
+
if (sourceRun.state !== "failed") throw new WorkflowError("RESUME_INCOMPATIBLE", `Retry source run ${retry.sourceRunId} is not failed`);
|
|
479
|
+
const expectedLineageRoot = sourceRun.retry?.lineageRootRunId ?? sourceRun.id;
|
|
480
|
+
if (retry.lineageRootRunId !== expectedLineageRoot) throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry lineage root does not match its source run");
|
|
481
|
+
await validate(source, nextSeen);
|
|
482
|
+
};
|
|
483
|
+
try { await validate(this, new Set()); }
|
|
484
|
+
catch (error) { throw error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE" ? error : new WorkflowError("RESUME_INCOMPATIBLE", error instanceof Error ? error.message : String(error)); }
|
|
485
|
+
}
|
|
461
486
|
|
|
462
487
|
private async ownedWorktree(owner: string, cwd?: string): Promise<WorktreeReference> {
|
|
463
488
|
const records = await json<unknown[]>(join(this.directory, "worktrees.json"));
|
|
@@ -497,7 +522,12 @@ export class RunStore {
|
|
|
497
522
|
}
|
|
498
523
|
const records = await json<unknown[]>(join(this.directory, "worktrees.json"));
|
|
499
524
|
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && (candidate as Partial<WorktreeReference>).owner === owner);
|
|
500
|
-
if (matches.length === 0)
|
|
525
|
+
if (matches.length === 0) {
|
|
526
|
+
const loaded = await this.load();
|
|
527
|
+
if (loaded.run.parentRunId === undefined) return undefined;
|
|
528
|
+
const parent = await this.sourceRun(loaded.run.parentRunId);
|
|
529
|
+
return parent.findNamedWorktree(name, nextSeen);
|
|
530
|
+
}
|
|
501
531
|
try {
|
|
502
532
|
const reference = await this.ownedWorktree(owner);
|
|
503
533
|
return { reference, sourceRunId: this.runId, owner };
|
|
@@ -511,6 +541,27 @@ export class RunStore {
|
|
|
511
541
|
if (!resolved) throw new WorkflowError("WORKTREE_FAILED", `Missing named worktree ${name}`);
|
|
512
542
|
return resolved;
|
|
513
543
|
}
|
|
544
|
+
async validateDeletionWorktrees(): Promise<void> {
|
|
545
|
+
try {
|
|
546
|
+
const records = await json<unknown[]>(join(this.directory, "worktrees.json"));
|
|
547
|
+
if (!Array.isArray(records)) throw new Error("Worktree records are invalid");
|
|
548
|
+
const owners = new Set<string>();
|
|
549
|
+
const paths = new Set<string>();
|
|
550
|
+
records.forEach((record) => {
|
|
551
|
+
if (!record || typeof record !== "object" || typeof (record as Partial<WorktreeReference>).owner !== "string") throw new Error("Invalid worktree record");
|
|
552
|
+
const owner = (record as Partial<WorktreeReference>).owner as string;
|
|
553
|
+
if (owners.has(owner)) throw new Error(`Duplicate worktree record for ${owner}`);
|
|
554
|
+
owners.add(owner);
|
|
555
|
+
const reference = this.structuralWorktree(owner, record);
|
|
556
|
+
paths.add(resolve(reference.path));
|
|
557
|
+
});
|
|
558
|
+
const entries = await readdir(join(this.directory, "worktrees"), { withFileTypes: true }).catch((error: unknown) => { if ((error as NodeJS.ErrnoException).code === "ENOENT") return [] as import("node:fs").Dirent[]; throw error; });
|
|
559
|
+
for (const entry of entries) if (!entry.isDirectory() || entry.isSymbolicLink() || !paths.has(resolve(join(this.directory, "worktrees", entry.name)))) throw new Error(`Unrecorded worktree artifact: ${join(this.directory, "worktrees", entry.name)}`);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
514
565
|
|
|
515
566
|
async validateBorrowedWorktrees(): Promise<void> {
|
|
516
567
|
try {
|
|
@@ -521,6 +572,17 @@ export class RunStore {
|
|
|
521
572
|
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
522
573
|
}
|
|
523
574
|
}
|
|
575
|
+
async validateNamedWorktrees(): Promise<void> {
|
|
576
|
+
try {
|
|
577
|
+
const records = await json<unknown[]>(join(this.directory, "worktrees.json"));
|
|
578
|
+
for (const record of records) {
|
|
579
|
+
const owner = record && typeof record === "object" && typeof (record as Partial<WorktreeReference>).owner === "string" ? (record as Partial<WorktreeReference>).owner : undefined;
|
|
580
|
+
if (owner && this.worktreeName(owner)) await this.validateWorktree(owner);
|
|
581
|
+
}
|
|
582
|
+
} catch (error) {
|
|
583
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
524
586
|
|
|
525
587
|
async ownsWorktree(owner: string): Promise<boolean> {
|
|
526
588
|
const records = await json<unknown[]>(join(this.directory, "worktrees.json"));
|
|
@@ -579,6 +641,7 @@ export class RunStore {
|
|
|
579
641
|
return resolved.reference;
|
|
580
642
|
}
|
|
581
643
|
}
|
|
644
|
+
if (name && Array.isArray(loaded.run.retry?.namedWorktrees) && loaded.run.retry.namedWorktrees.includes(name)) throw new WorkflowError("WORKTREE_FAILED", `Missing inherited named worktree ${name}`);
|
|
582
645
|
const existing = records.find((record) => record.owner === owner);
|
|
583
646
|
if (existing) return this.validateWorktree(owner);
|
|
584
647
|
const { path, branch } = this.expectedWorktree(owner);
|
|
@@ -673,7 +736,29 @@ export class RunStore {
|
|
|
673
736
|
const borrowed = await Promise.all(bindings.map(async (binding) => (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference));
|
|
674
737
|
return [...owned.filter((record): record is WorktreeReference => record !== undefined), ...borrowed];
|
|
675
738
|
}
|
|
676
|
-
|
|
739
|
+
async validNamedWorktrees(): Promise<readonly string[]> {
|
|
740
|
+
const load = async (path: string): Promise<unknown> => {
|
|
741
|
+
try { return await json<unknown>(path); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; throw error; }
|
|
742
|
+
};
|
|
743
|
+
const names = new Set<string>();
|
|
744
|
+
const rawRecords = await load(join(this.directory, "worktrees.json"));
|
|
745
|
+
let bindings: readonly BorrowedWorktreeBinding[];
|
|
746
|
+
try { bindings = await this.borrowedWorktreeRecords(); }
|
|
747
|
+
catch (error) { if (error instanceof WorkflowError && error.code === "WORKTREE_FAILED") return []; throw error; }
|
|
748
|
+
const boundOwners = new Set(bindings.map((binding) => binding.owner));
|
|
749
|
+
for (const record of Array.isArray(rawRecords) ? rawRecords : []) {
|
|
750
|
+
if (!record || typeof record !== "object") continue;
|
|
751
|
+
const owner = (record as Partial<WorktreeReference>).owner;
|
|
752
|
+
if (typeof owner !== "string") continue;
|
|
753
|
+
const name = this.worktreeName(owner);
|
|
754
|
+
if (!name || owner !== this.namedWorktreeOwner(name) || boundOwners.has(owner)) continue;
|
|
755
|
+
try { await this.ownedWorktree(owner); names.add(name); } catch { /* Do not advertise stale or invalid records. */ }
|
|
756
|
+
}
|
|
757
|
+
for (const binding of bindings) {
|
|
758
|
+
try { await this.resolveBorrowedWorktree(binding, new Set([this.runId])); names.add(binding.name); } catch { /* Do not advertise stale inherited records. */ }
|
|
759
|
+
}
|
|
760
|
+
return [...names];
|
|
761
|
+
}
|
|
677
762
|
async changedWorktrees(): Promise<readonly WorktreeReference[]> {
|
|
678
763
|
const changed: WorktreeReference[] = [];
|
|
679
764
|
for (const valid of await this.worktrees()) {
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
|
|
5
|
+
import { deepFreeze, fail, jsonValue, object } from "./utils.js";
|
|
6
|
+
import { loadSettings, resolveWorkflowSettings, validateSchema } from "./validation.js";
|
|
7
|
+
|
|
8
|
+
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"]);
|
|
9
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
10
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
11
|
+
|
|
12
|
+
function normalizeRoleDirectory(value: unknown): string {
|
|
13
|
+
try {
|
|
14
|
+
if (value instanceof URL) {
|
|
15
|
+
if (value.protocol !== "file:") fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
|
|
16
|
+
return fileURLToPath(value);
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
if (value.startsWith("file:")) return fileURLToPath(value);
|
|
20
|
+
if (isAbsolute(value)) return value;
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
fail("INVALID_METADATA", `Invalid workflow role directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
24
|
+
}
|
|
25
|
+
fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
|
|
26
|
+
}
|
|
27
|
+
export class WorkflowRegistry {
|
|
28
|
+
readonly #extensions = new Set<Readonly<WorkflowExtension>>();
|
|
29
|
+
readonly #globals = new Map<string, string>();
|
|
30
|
+
readonly #hooks = new Map<string, RegisteredAgentSetupHook>();
|
|
31
|
+
readonly #roleDirectories = new Map<string, WorkflowRoleDirectoryRegistration>();
|
|
32
|
+
readonly #modelAliases = new Map<string, { name: string; version: string; headline: string; extensionDescription: string; resolve: WorkflowModelAlias["resolve"] }>();
|
|
33
|
+
#frozen = false;
|
|
34
|
+
|
|
35
|
+
get frozen(): boolean { return this.#frozen; }
|
|
36
|
+
freeze(): void { this.#frozen = true; }
|
|
37
|
+
|
|
38
|
+
register(extension: WorkflowExtension): void {
|
|
39
|
+
if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
40
|
+
if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows")) fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
|
|
41
|
+
if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "modelAliases", "agentSetupHooks", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim()) fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
|
|
42
|
+
const functions = extension.functions ?? {};
|
|
43
|
+
const variables = extension.variables ?? {};
|
|
44
|
+
const modelAliases = extension.modelAliases ?? {};
|
|
45
|
+
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
46
|
+
const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
|
|
47
|
+
if (!Array.isArray(roleDirectoryValues)) fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
|
|
48
|
+
const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
|
|
49
|
+
if (!object(functions) || !object(variables) || !object(modelAliases) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && roleDirectories.length === 0)) fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, or role directories");
|
|
50
|
+
const names = [...Object.keys(functions), ...Object.keys(variables)];
|
|
51
|
+
if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
if (!IDENTIFIER.test(name) || name.startsWith("__pi_extensible_workflows_")) fail("INVALID_METADATA", `Invalid global name: ${name}`);
|
|
54
|
+
if (RESERVED_GLOBALS.has(name)) fail("GLOBAL_COLLISION", `Global name is reserved: ${name}`);
|
|
55
|
+
if (this.#globals.has(name)) fail("GLOBAL_COLLISION", `Global name is already registered: ${name}`);
|
|
56
|
+
}
|
|
57
|
+
for (const [name, fn] of Object.entries(functions)) {
|
|
58
|
+
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") fail("INVALID_METADATA", `Invalid workflow function: ${name}`);
|
|
59
|
+
validateSchema(fn.input, `${name} input`);
|
|
60
|
+
validateSchema(fn.output, `${name} output`);
|
|
61
|
+
if (fn.input.type !== "object") fail("INVALID_SCHEMA", `${name} input must describe one object`);
|
|
62
|
+
}
|
|
63
|
+
for (const [name, variable] of Object.entries(variables)) {
|
|
64
|
+
if (!object(variable) || Object.keys(variable).some((key) => !["description", "schema", "resolve"].includes(key)) || typeof variable.description !== "string" || !variable.description.trim() || typeof variable.resolve !== "function") fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
|
|
65
|
+
validateSchema(variable.schema, `${name} schema`);
|
|
66
|
+
}
|
|
67
|
+
for (const [name, alias] of Object.entries(modelAliases)) {
|
|
68
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) fail("INVALID_METADATA", `Invalid model alias name: ${name}`);
|
|
69
|
+
if (!object(alias) || Object.keys(alias).some((key) => key !== "resolve") || typeof alias.resolve !== "function") fail("INVALID_METADATA", `Invalid model alias resolver: ${name}`);
|
|
70
|
+
if (this.#modelAliases.has(name)) fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
|
|
71
|
+
}
|
|
72
|
+
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
73
|
+
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))) fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
|
|
74
|
+
if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
75
|
+
}
|
|
76
|
+
const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, ...(roleDirectories.length ? { roleDirectories } : {}) });
|
|
77
|
+
this.#extensions.add(stored);
|
|
78
|
+
for (const directory of roleDirectories) if (!this.#roleDirectories.has(directory)) this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline, description: extension.description } }));
|
|
79
|
+
for (const name of names) this.#globals.set(name, name);
|
|
80
|
+
for (const [name, alias] of Object.entries(modelAliases)) this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, resolve: alias.resolve });
|
|
81
|
+
for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function(name: string): WorkflowFunction {
|
|
85
|
+
if (!IDENTIFIER.test(name)) fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
|
|
86
|
+
const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
|
|
87
|
+
if (!fn) fail("MISSING_WORKFLOW", `Registered function is unavailable: ${name}; the separate registered-workflow format was removed`);
|
|
88
|
+
return fn;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
functions(): Readonly<Record<string, WorkflowFunction>> {
|
|
92
|
+
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
catalog(context?: WorkflowCatalogContext): WorkflowCatalog {
|
|
96
|
+
const functions: WorkflowCatalogFunction[] = [];
|
|
97
|
+
const variables: WorkflowCatalogVariable[] = [];
|
|
98
|
+
for (const extension of this.#extensions) {
|
|
99
|
+
for (const [name, fn] of Object.entries(extension.functions ?? {})) functions.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: fn.description, input: structuredClone(fn.input), output: structuredClone(fn.output) });
|
|
100
|
+
for (const [name, variable] of Object.entries(extension.variables ?? {})) variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
|
|
101
|
+
}
|
|
102
|
+
let aliases: Readonly<Record<string, string>> | undefined;
|
|
103
|
+
let settings: WorkflowCatalog["settings"];
|
|
104
|
+
let source = "global settings";
|
|
105
|
+
try {
|
|
106
|
+
const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : undefined;
|
|
107
|
+
aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
|
|
108
|
+
if (resolved) { source = resolved.projectTrusted && resolved.sources.modelAliases === resolved.projectSettingsPath ? "trusted project settings" : "global settings"; settings = { concurrency: resolved.effective.concurrency, modelAliases: resolved.effective.modelAliases ?? {}, disabledAgentResources: resolved.effective.disabledAgentResources ?? { skills: [], extensions: [] }, globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted: resolved.projectTrusted, sources: resolved.sources }; }
|
|
109
|
+
} catch { aliases = undefined; }
|
|
110
|
+
const staticEntries: WorkflowCatalogModelAlias[] = Object.keys(aliases ?? {}).map((name) => ({ name, kind: "static", provenance: source }));
|
|
111
|
+
const dynamicEntries: WorkflowCatalogModelAlias[] = [...this.#modelAliases.values()].map(({ name, version, headline, extensionDescription }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline, extensionDescription }));
|
|
112
|
+
const modelAliasEntries = [...staticEntries, ...dynamicEntries].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind) || left.provenance.localeCompare(right.provenance));
|
|
113
|
+
const sort = (left: { name: string }, right: { name: string }) => left.name.localeCompare(right.name);
|
|
114
|
+
const catalog: WorkflowCatalog = { functions: functions.sort(sort), variables: variables.sort(sort), ...(modelAliasEntries.length ? { modelAliasEntries } : {}) };
|
|
115
|
+
if (aliases && Object.keys(aliases).length) Object.defineProperty(catalog, "modelAliases", { value: Object.freeze(structuredClone(aliases)), enumerable: false });
|
|
116
|
+
if (settings) { const publicSettings = { ...settings, modelAliases: Object.keys(aliases ?? {}).length ? {} : settings.modelAliases }; Object.defineProperty(catalog, "settings", { value: deepFreeze(publicSettings), enumerable: true }); }
|
|
117
|
+
return deepFreeze(catalog);
|
|
118
|
+
}
|
|
119
|
+
catalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex {
|
|
120
|
+
const catalog = this.catalog(context);
|
|
121
|
+
const index: WorkflowCatalogIndex = {
|
|
122
|
+
functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
|
|
123
|
+
variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
|
|
124
|
+
...(catalog.modelAliasEntries ? { modelAliasEntries: structuredClone(catalog.modelAliasEntries) } : {}),
|
|
125
|
+
};
|
|
126
|
+
if (catalog.modelAliases) Object.defineProperty(index, "modelAliases", { value: Object.freeze(structuredClone(catalog.modelAliases)), enumerable: false });
|
|
127
|
+
if (catalog.settings) Object.defineProperty(index, "settings", { value: deepFreeze(structuredClone(catalog.settings)), enumerable: true });
|
|
128
|
+
return deepFreeze(index);
|
|
129
|
+
}
|
|
130
|
+
catalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | WorkflowCatalogError {
|
|
131
|
+
const catalog = this.catalog(context);
|
|
132
|
+
const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
|
|
133
|
+
if (entry) return entry;
|
|
134
|
+
const alias = catalog.modelAliasEntries?.find((candidate) => candidate.name === name);
|
|
135
|
+
if (alias) return alias;
|
|
136
|
+
return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
globals(): Readonly<Record<string, { name: string }>> {
|
|
141
|
+
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.keys(extension.functions ?? {}).map((name) => [name, { name }]))));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async invokeFunction(name: string, input: unknown, context: Readonly<WorkflowFunctionContext>, path: string, journal: WorkflowJournal): Promise<JsonValue> {
|
|
145
|
+
const fn = this.function(name);
|
|
146
|
+
if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input)) fail("RESULT_INVALID", `Invalid input for ${name}`);
|
|
147
|
+
const replayed = journal.get(path);
|
|
148
|
+
if (replayed !== undefined) {
|
|
149
|
+
if (!jsonValue(replayed) || !Value.Check(fn.output, replayed)) fail("RESULT_INVALID", `Invalid replay for ${name}`);
|
|
150
|
+
return structuredClone(replayed);
|
|
151
|
+
}
|
|
152
|
+
const result: unknown = 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 }));
|
|
153
|
+
if (!jsonValue(result) || !Value.Check(fn.output, result)) fail("RESULT_INVALID", `Invalid output from ${name}`);
|
|
154
|
+
const stored = structuredClone(result);
|
|
155
|
+
journal.put(path, stored);
|
|
156
|
+
return structuredClone(stored);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
variables(): readonly { name: string; variable: WorkflowVariable }[] {
|
|
160
|
+
return [...this.#extensions].flatMap((extension) => Object.entries(extension.variables ?? {}).map(([name, variable]) => ({ name, variable })));
|
|
161
|
+
}
|
|
162
|
+
agentSetupHooks(): readonly RegisteredAgentSetupHook[] {
|
|
163
|
+
return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
164
|
+
}
|
|
165
|
+
roleDirectories(): readonly string[] {
|
|
166
|
+
return [...this.#roleDirectories.keys()];
|
|
167
|
+
}
|
|
168
|
+
roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[] {
|
|
169
|
+
return [...this.#roleDirectories.values()];
|
|
170
|
+
}
|
|
171
|
+
modelAliases(): readonly { name: string; version: string; headline: string; extensionDescription: string; resolve: WorkflowModelAlias["resolve"] }[] { return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name)); }
|
|
172
|
+
async resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed: ReadonlySet<string> = new Set()): Promise<Readonly<Record<string, string>>> {
|
|
173
|
+
const resolved: Record<string, string> = {};
|
|
174
|
+
const isAborted = (): boolean => context.signal.aborted;
|
|
175
|
+
for (const alias of this.modelAliases()) {
|
|
176
|
+
if (shadowed.has(alias.name)) continue;
|
|
177
|
+
if (isAborted()) fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
|
|
178
|
+
let target: unknown;
|
|
179
|
+
try { target = await alias.resolve(Object.freeze({ cwd: context.cwd, projectTrusted: context.projectTrusted, rootModel: Object.freeze({ ...context.rootModel }), knownModels: new Set(context.knownModels), availableModels: new Set(context.availableModels), signal: context.signal })); }
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (isAborted() || error instanceof Error && error.name === "AbortError" || typeof error === "object" && error !== null && !Array.isArray(error) && (error as { code?: unknown }).code === "CANCELLED") fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
|
|
182
|
+
fail("CONFIG_ERROR", `Model alias resolver failed for ${alias.name} (${alias.headline}): ${error instanceof Error ? error.message : String(error)}`);
|
|
183
|
+
}
|
|
184
|
+
if (isAborted()) fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
|
|
185
|
+
if (typeof target !== "string" || !target.trim()) fail("CONFIG_ERROR", `Model alias resolver returned an invalid target for ${alias.name} (${alias.headline})`);
|
|
186
|
+
resolved[alias.name] = target.trim();
|
|
187
|
+
}
|
|
188
|
+
return Object.freeze(resolved);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "roleDirectories" | "roleDirectoryRegistrations">;
|
|
192
|
+
interface WorkflowRegistryHost { api: WorkflowRegistryApi }
|
|
193
|
+
const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
|
|
194
|
+
const globalRegistry = globalThis as typeof globalThis & Record<symbol, WorkflowRegistryHost | undefined>;
|
|
195
|
+
function createWorkflowRegistryApi(registry: WorkflowRegistry): WorkflowRegistryApi {
|
|
196
|
+
return {
|
|
197
|
+
get frozen() { return registry.frozen; },
|
|
198
|
+
freeze: () => { registry.freeze(); },
|
|
199
|
+
register: (extension) => { registry.register(extension); },
|
|
200
|
+
function: (name) => registry.function(name),
|
|
201
|
+
functions: () => registry.functions(),
|
|
202
|
+
catalog: (context) => registry.catalog(context),
|
|
203
|
+
catalogIndex: (context) => registry.catalogIndex(context),
|
|
204
|
+
catalogDetail: (name, context) => registry.catalogDetail(name, context),
|
|
205
|
+
globals: () => registry.globals(),
|
|
206
|
+
invokeFunction: (...args) => registry.invokeFunction(...args),
|
|
207
|
+
variables: () => registry.variables(),
|
|
208
|
+
modelAliases: () => registry.modelAliases(),
|
|
209
|
+
resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
|
|
210
|
+
roleDirectories: () => registry.roleDirectories(),
|
|
211
|
+
roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
|
|
212
|
+
agentSetupHooks: () => registry.agentSetupHooks(),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function workflowRegistryHost(): WorkflowRegistryHost {
|
|
216
|
+
return globalRegistry[WORKFLOW_REGISTRY_KEY] ??= { api: createWorkflowRegistryApi(new WorkflowRegistry()) };
|
|
217
|
+
}
|
|
218
|
+
export function resetWorkflowRegistry(): void {
|
|
219
|
+
workflowRegistryHost().api = createWorkflowRegistryApi(new WorkflowRegistry());
|
|
220
|
+
}
|
|
221
|
+
export function beginWorkflowExtensionLoading(): void {
|
|
222
|
+
if (workflowRegistryHost().api.frozen) resetWorkflowRegistry();
|
|
223
|
+
}
|
|
224
|
+
export function loadingRegistry(): WorkflowRegistryApi { return workflowRegistryHost().api; }
|
|
225
|
+
beginWorkflowExtensionLoading();
|
|
226
|
+
export function registerWorkflowExtension(extension: WorkflowExtension): void { loadingRegistry().register(extension); }
|
|
227
|
+
export function workflowCatalog(context?: WorkflowCatalogContext): WorkflowCatalog { return loadingRegistry().catalog(context); }
|
|
228
|
+
export function workflowCatalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex { return loadingRegistry().catalogIndex(context); }
|
|
229
|
+
export function workflowCatalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | WorkflowCatalogError { return loadingRegistry().catalogDetail(name, context); }
|
|
230
|
+
export function registeredWorkflowFunctions(): Readonly<Record<string, WorkflowFunction>> { return loadingRegistry().functions(); }
|
|
231
|
+
export function registeredWorkflowRoleDirectories(): readonly string[] {
|
|
232
|
+
const directories = loadingRegistry().roleDirectories;
|
|
233
|
+
return typeof directories === "function" ? directories() : [];
|
|
234
|
+
}
|
|
235
|
+
export function registeredWorkflowRoleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[] {
|
|
236
|
+
const registrations = loadingRegistry().roleDirectoryRegistrations;
|
|
237
|
+
return typeof registrations === "function" ? registrations() : [];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowCatalogVariable, WorkflowRoleDirectoryRegistration } from "./types.js";
|
package/src/session-inspector.ts
CHANGED
|
@@ -215,7 +215,7 @@ export async function loadSessionReport(path: string, home = homedir()): Promise
|
|
|
215
215
|
const args = call.arguments;
|
|
216
216
|
const agents = loaded ? await Promise.all(loaded.run.agents.map(agentReport)) : [];
|
|
217
217
|
const models = mergedModels(agents.flatMap(({ attempts }) => attempts.map(({ models: attemptModels }) => attemptModels)));
|
|
218
|
-
const name = typeof args.
|
|
218
|
+
const name = loaded?.run.workflowName ?? (typeof args.workflow === "string" ? args.workflow : typeof args.name === "string" ? args.name : "workflow");
|
|
219
219
|
const description = typeof args.description === "string" ? args.description : loaded?.snapshot.metadata.description;
|
|
220
220
|
const script = typeof args.script === "string" && args.script.trim() ? args.script : loaded?.snapshot.script;
|
|
221
221
|
let staticCalls: StaticWorkflowCall[] = [];
|
|
@@ -286,8 +286,10 @@ function detailLines(workflow: WorkflowReport): string[] {
|
|
|
286
286
|
`Hooks: ${attempt.setup.hookNames.join(", ") || "(none)"}`,
|
|
287
287
|
`Effective: model=${attempt.setup.model.provider}/${attempt.setup.model.model}${attempt.setup.model.thinking ? `:${attempt.setup.model.thinking}` : ""} tools=${attempt.setup.tools.join(",") || "(none)"} cwd=${attempt.setup.cwd}`,
|
|
288
288
|
...(attempt.setup.disabledAgentResources ? [
|
|
289
|
-
`
|
|
290
|
-
`
|
|
289
|
+
`Configured skill patterns: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
|
|
290
|
+
`Excluded skills: ${attempt.setup.disabledAgentResources.excludedSkills?.join(", ") || "(none)"}`,
|
|
291
|
+
`Configured extension patterns: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
|
|
292
|
+
`Excluded extensions: ${attempt.setup.disabledAgentResources.excludedExtensions?.join(", ") || "(none)"}`,
|
|
291
293
|
`Unmatched skills: ${attempt.setup.disabledAgentResources.unmatchedSkills.join(", ") || "(none)"}`,
|
|
292
294
|
`Unmatched extensions: ${attempt.setup.disabledAgentResources.unmatchedExtensions.join(", ") || "(none)"}`,
|
|
293
295
|
] : []),
|