pi-extensible-workflows 3.0.0 → 3.2.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 +15 -17
- package/dist/src/agent-execution.d.ts +4 -80
- package/dist/src/agent-execution.js +20 -10
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +48 -2
- 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.js +13 -4
- package/dist/src/host.d.ts +83 -4
- package/dist/src/host.js +1246 -410
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +3 -1
- package/dist/src/persistence.d.ts +3 -0
- package/dist/src/persistence.js +49 -1
- package/dist/src/registry.d.ts +21 -9
- package/dist/src/registry.js +131 -21
- package/dist/src/session-inspector.js +4 -2
- package/dist/src/types.d.ts +135 -7
- package/dist/src/utils.d.ts +6 -0
- package/dist/src/utils.js +45 -5
- package/dist/src/validation.d.ts +6 -2
- package/dist/src/validation.js +157 -31
- package/dist/src/workflow-artifacts.d.ts +13 -0
- package/dist/src/workflow-artifacts.js +39 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +5 -3
- package/skills/pi-extensible-workflows/SKILL.md +45 -18
- package/src/agent-execution.ts +23 -37
- package/src/cli.ts +33 -2
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/execution.ts +13 -4
- package/src/host.ts +1039 -366
- package/src/index.ts +5 -2
- package/src/persistence.ts +35 -1
- package/src/registry.ts +108 -25
- package/src/session-inspector.ts +4 -2
- package/src/types.ts +53 -8
- package/src/utils.ts +39 -5
- package/src/validation.ts +130 -31
- package/src/workflow-artifacts.ts +34 -0
package/src/index.ts
CHANGED
|
@@ -5,10 +5,13 @@ export * from "./validation.js";
|
|
|
5
5
|
export * from "./registry.js";
|
|
6
6
|
export * from "./execution.js";
|
|
7
7
|
export * from "./host.js";
|
|
8
|
+
export * from "./workflow-artifacts.js";
|
|
8
9
|
export { default } from "./host.js";
|
|
9
|
-
export { acquireSessionLease, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|
|
10
|
+
export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|
|
10
11
|
export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
11
12
|
export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
12
|
-
export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, SessionInput } from "./agent-execution.js";
|
|
13
|
+
export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./agent-execution.js";
|
|
13
14
|
export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
|
14
15
|
export type { DoctorDiagnostic, DoctorFunction, DoctorOptions, DoctorPiState, DoctorReport, DoctorRole, DoctorSeverity, DoctorTrust } from "./doctor.js";
|
|
16
|
+
export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
|
|
17
|
+
export type { CleanupFailure, CleanupRunResult, CleanupSessionReport, DoctorCleanupOptions, DoctorCleanupReport } from "./doctor-cleanup.js";
|
package/src/persistence.ts
CHANGED
|
@@ -35,8 +35,11 @@ export function projectStorageKey(cwd: string): string {
|
|
|
35
35
|
return `${slug}-${createHash("sha256").update(exact).digest("hex").slice(0, 12)}`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function projectSessionsDirectory(cwd: string, home = homedir()): string {
|
|
39
|
+
return join(home, ".pi", "workflows", "projects", projectStorageKey(cwd), "sessions");
|
|
40
|
+
}
|
|
38
41
|
export function runsDirectory(cwd: string, sessionId: string, home = homedir()): string {
|
|
39
|
-
return join(
|
|
42
|
+
return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
const SESSION_OWNER_FILE = "owner.json";
|
|
@@ -52,6 +55,16 @@ async function processAlive(pid: number, startedAt?: number): Promise<boolean> {
|
|
|
52
55
|
}
|
|
53
56
|
return true;
|
|
54
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
|
+
}
|
|
55
68
|
|
|
56
69
|
function sameOwner(left: unknown, right: unknown): boolean {
|
|
57
70
|
if (!left || typeof left !== "object" || !right || typeof right !== "object") return false;
|
|
@@ -528,6 +541,27 @@ export class RunStore {
|
|
|
528
541
|
if (!resolved) throw new WorkflowError("WORKTREE_FAILED", `Missing named worktree ${name}`);
|
|
529
542
|
return resolved;
|
|
530
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
|
+
|
|
531
565
|
|
|
532
566
|
async validateBorrowedWorktrees(): Promise<void> {
|
|
533
567
|
try {
|
package/src/registry.ts
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
1
3
|
import { Value } from "typebox/value";
|
|
2
|
-
import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowVariable } from "./types.js";
|
|
4
|
+
import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
|
|
3
5
|
import { deepFreeze, fail, jsonValue, object } from "./utils.js";
|
|
4
|
-
import { loadSettings, validateSchema } from "./validation.js";
|
|
6
|
+
import { loadSettings, resolveWorkflowSettings, validateSchema } from "./validation.js";
|
|
5
7
|
|
|
6
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"]);
|
|
7
9
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
8
10
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
9
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
|
+
}
|
|
10
27
|
export class WorkflowRegistry {
|
|
11
28
|
readonly #extensions = new Set<Readonly<WorkflowExtension>>();
|
|
12
29
|
readonly #globals = new Map<string, string>();
|
|
13
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"] }>();
|
|
14
33
|
#frozen = false;
|
|
15
34
|
|
|
16
35
|
get frozen(): boolean { return this.#frozen; }
|
|
@@ -19,11 +38,15 @@ export class WorkflowRegistry {
|
|
|
19
38
|
register(extension: WorkflowExtension): void {
|
|
20
39
|
if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
21
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");
|
|
22
|
-
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()) fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
|
|
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");
|
|
23
42
|
const functions = extension.functions ?? {};
|
|
24
43
|
const variables = extension.variables ?? {};
|
|
44
|
+
const modelAliases = extension.modelAliases ?? {};
|
|
25
45
|
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
26
|
-
|
|
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");
|
|
27
50
|
const names = [...Object.keys(functions), ...Object.keys(variables)];
|
|
28
51
|
if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
29
52
|
for (const name of names) {
|
|
@@ -41,13 +64,20 @@ export class WorkflowRegistry {
|
|
|
41
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}`);
|
|
42
65
|
validateSchema(variable.schema, `${name} schema`);
|
|
43
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
|
+
}
|
|
44
72
|
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
45
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}`);
|
|
46
74
|
if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
47
75
|
}
|
|
48
|
-
const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
|
|
76
|
+
const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, ...(roleDirectories.length ? { roleDirectories } : {}) });
|
|
49
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 } }));
|
|
50
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 });
|
|
51
81
|
for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
52
82
|
}
|
|
53
83
|
|
|
@@ -62,7 +92,7 @@ export class WorkflowRegistry {
|
|
|
62
92
|
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
|
|
63
93
|
}
|
|
64
94
|
|
|
65
|
-
catalog(): WorkflowCatalog {
|
|
95
|
+
catalog(context?: WorkflowCatalogContext): WorkflowCatalog {
|
|
66
96
|
const functions: WorkflowCatalogFunction[] = [];
|
|
67
97
|
const variables: WorkflowCatalogVariable[] = [];
|
|
68
98
|
for (const extension of this.#extensions) {
|
|
@@ -70,27 +100,43 @@ export class WorkflowRegistry {
|
|
|
70
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) });
|
|
71
101
|
}
|
|
72
102
|
let aliases: Readonly<Record<string, string>> | undefined;
|
|
73
|
-
|
|
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));
|
|
74
113
|
const sort = (left: { name: string }, right: { name: string }) => left.name.localeCompare(right.name);
|
|
75
|
-
|
|
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);
|
|
76
118
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
return deepFreeze({
|
|
119
|
+
catalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex {
|
|
120
|
+
const catalog = this.catalog(context);
|
|
121
|
+
const index: WorkflowCatalogIndex = {
|
|
81
122
|
functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
|
|
82
123
|
variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
|
|
83
|
-
...(catalog.
|
|
84
|
-
}
|
|
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);
|
|
85
129
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const catalog = this.catalog();
|
|
130
|
+
catalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | WorkflowCatalogError {
|
|
131
|
+
const catalog = this.catalog(context);
|
|
89
132
|
const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
|
|
90
133
|
if (entry) return entry;
|
|
134
|
+
const alias = catalog.modelAliasEntries?.find((candidate) => candidate.name === name);
|
|
135
|
+
if (alias) return alias;
|
|
91
136
|
return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
|
|
92
137
|
}
|
|
93
138
|
|
|
139
|
+
|
|
94
140
|
globals(): Readonly<Record<string, { name: string }>> {
|
|
95
141
|
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.keys(extension.functions ?? {}).map((name) => [name, { name }]))));
|
|
96
142
|
}
|
|
@@ -116,8 +162,33 @@ export class WorkflowRegistry {
|
|
|
116
162
|
agentSetupHooks(): readonly RegisteredAgentSetupHook[] {
|
|
117
163
|
return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
118
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
|
+
}
|
|
119
190
|
}
|
|
120
|
-
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "agentSetupHooks">;
|
|
191
|
+
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "roleDirectories" | "roleDirectoryRegistrations">;
|
|
121
192
|
interface WorkflowRegistryHost { api: WorkflowRegistryApi }
|
|
122
193
|
const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
|
|
123
194
|
const globalRegistry = globalThis as typeof globalThis & Record<symbol, WorkflowRegistryHost | undefined>;
|
|
@@ -128,12 +199,16 @@ function createWorkflowRegistryApi(registry: WorkflowRegistry): WorkflowRegistry
|
|
|
128
199
|
register: (extension) => { registry.register(extension); },
|
|
129
200
|
function: (name) => registry.function(name),
|
|
130
201
|
functions: () => registry.functions(),
|
|
131
|
-
catalog: () => registry.catalog(),
|
|
132
|
-
catalogIndex: () => registry.catalogIndex(),
|
|
133
|
-
catalogDetail: (name) => registry.catalogDetail(name),
|
|
202
|
+
catalog: (context) => registry.catalog(context),
|
|
203
|
+
catalogIndex: (context) => registry.catalogIndex(context),
|
|
204
|
+
catalogDetail: (name, context) => registry.catalogDetail(name, context),
|
|
134
205
|
globals: () => registry.globals(),
|
|
135
206
|
invokeFunction: (...args) => registry.invokeFunction(...args),
|
|
136
207
|
variables: () => registry.variables(),
|
|
208
|
+
modelAliases: () => registry.modelAliases(),
|
|
209
|
+
resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
|
|
210
|
+
roleDirectories: () => registry.roleDirectories(),
|
|
211
|
+
roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
|
|
137
212
|
agentSetupHooks: () => registry.agentSetupHooks(),
|
|
138
213
|
};
|
|
139
214
|
}
|
|
@@ -149,9 +224,17 @@ export function beginWorkflowExtensionLoading(): void {
|
|
|
149
224
|
export function loadingRegistry(): WorkflowRegistryApi { return workflowRegistryHost().api; }
|
|
150
225
|
beginWorkflowExtensionLoading();
|
|
151
226
|
export function registerWorkflowExtension(extension: WorkflowExtension): void { loadingRegistry().register(extension); }
|
|
152
|
-
export function workflowCatalog(): WorkflowCatalog { return loadingRegistry().catalog(); }
|
|
153
|
-
export function workflowCatalogIndex(): WorkflowCatalogIndex { return loadingRegistry().catalogIndex(); }
|
|
154
|
-
export function workflowCatalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError { return loadingRegistry().catalogDetail(name); }
|
|
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); }
|
|
155
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
|
+
}
|
|
156
239
|
|
|
157
|
-
export type { WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogVariable } from "./types.js";
|
|
240
|
+
export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowCatalogVariable, WorkflowRoleDirectoryRegistration } from "./types.js";
|
package/src/session-inspector.ts
CHANGED
|
@@ -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
|
] : []),
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentSessionEvent, CreateAgentSessionOptions, InlineExtension, SessionStats, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"] as const;
|
|
2
3
|
export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"] as const;
|
|
3
4
|
export const WORKFLOW_CALL_KINDS = ["agent", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"] as const;
|
|
@@ -57,24 +58,29 @@ export type WorkflowCheckpointState = "awaiting" | "approved" | "rejected";
|
|
|
57
58
|
export interface WorkflowCheckpointStateChangedEvent extends WorkflowEventBase { name: string; state: WorkflowCheckpointState }
|
|
58
59
|
export interface WorkflowBudgetEvent extends WorkflowEventBase { type: BudgetEventType; budgetVersion: number; dimensions: readonly BudgetDimension[]; usage: WorkflowBudgetUsage; limits: WorkflowBudget; proposalId?: string; previous?: WorkflowBudget; proposed?: WorkflowBudget }
|
|
59
60
|
export interface ModelSpec { provider: string; model: string; thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" }
|
|
61
|
+
export interface WorkflowModelAliasResolverContext { cwd: string; projectTrusted: boolean; rootModel: ModelSpec; knownModels: ReadonlySet<string>; availableModels: ReadonlySet<string>; signal: AbortSignal }
|
|
62
|
+
export interface WorkflowModelAlias { resolve: (context: Readonly<WorkflowModelAliasResolverContext>) => string | Promise<string> }
|
|
60
63
|
export interface WorkflowMetadata { name: string; description?: string }
|
|
61
64
|
export interface WorkflowSettings { concurrency: number; modelAliases?: Readonly<Record<string, string>>; disabledAgentResources?: Readonly<AgentResourceExclusions> }
|
|
65
|
+
export interface WorkflowSettingsOverrides { concurrency?: number; modelAliases?: Readonly<Record<string, string>>; disabledAgentResources?: Readonly<AgentResourceExclusions> }
|
|
66
|
+
export interface WorkflowSettingsSources { concurrency: string; modelAliases: string; disabledAgentResources: string }
|
|
67
|
+
export interface WorkflowSettingsResolution { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: Readonly<WorkflowSettings>; project: Readonly<WorkflowSettingsOverrides>; effective: Readonly<WorkflowSettings>; sources: Readonly<WorkflowSettingsSources> }
|
|
62
68
|
export interface AgentResourceExclusions { skills: readonly string[]; extensions: readonly string[] }
|
|
63
|
-
export interface AgentResourcePolicy { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: AgentResourceExclusions; project: AgentResourceExclusions; effective: AgentResourceExclusions; unmatchedSkills: string[]; unmatchedExtensions: string[] }
|
|
69
|
+
export interface AgentResourcePolicy { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: AgentResourceExclusions; project: AgentResourceExclusions; effective: AgentResourceExclusions; unmatchedSkills: string[]; unmatchedExtensions: string[]; excludedSkills?: string[]; excludedExtensions?: string[] }
|
|
64
70
|
export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
|
|
65
71
|
export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
|
|
66
|
-
export interface AgentSetupSummary { hookNames: readonly string[]; model: ModelSpec; tools: readonly string[]; cwd: string; disabledAgentResources?: { skills: readonly string[]; extensions: readonly string[]; unmatchedSkills: readonly string[]; unmatchedExtensions: readonly string[] } }
|
|
72
|
+
export interface AgentSetupSummary { hookNames: readonly string[]; model: ModelSpec; tools: readonly string[]; cwd: string; disabledAgentResources?: { skills: readonly string[]; extensions: readonly string[]; excludedSkills?: readonly string[]; excludedExtensions?: readonly string[]; unmatchedSkills: readonly string[]; unmatchedExtensions: readonly string[] } }
|
|
67
73
|
export interface AgentAttemptSummary { attempt: number; sessionId: string; sessionFile: string; error?: { code: string; message: string }; accounting: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; setup?: AgentSetupSummary }
|
|
68
74
|
export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
|
|
69
75
|
export interface WorkflowWorktreeReference { readonly path: string; readonly branch: string }
|
|
70
|
-
export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined }
|
|
76
|
+
export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; resultPath?: string; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined }
|
|
71
77
|
export interface WorkflowRunEvent { type: string; message: string }
|
|
72
78
|
export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
|
|
73
79
|
export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
|
|
74
80
|
export interface RunRecord { id: string; workflowName: string; cwd: string; sessionId: string; state: RunState; parentRunId?: string; retry?: WorkflowRetryProvenance; phase?: string; phaseHistory?: readonly WorkflowPhaseRecord[]; agents: readonly AgentRecord[]; error?: WorkflowErrorShape; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[] }
|
|
75
81
|
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
|
|
76
82
|
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; disabledAgentResources?: AgentResourceExclusions }
|
|
77
|
-
export interface LaunchSnapshot { identityVersion?: number; launchKind?: "inline" | "function"; functionName?: string; script: string; args: JsonValue; metadata: WorkflowMetadata; settings: WorkflowSettings; budget?: WorkflowBudget; settingsPath?: string; modelAliases?: Readonly<Record<string, string>>; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[]; roles?: Readonly<Record<string, AgentDefinition>>; projectRoles?: readonly string[]; schemas: readonly JsonSchema[] }
|
|
83
|
+
export interface LaunchSnapshot { identityVersion?: number; launchKind?: "inline" | "function"; functionName?: string; script: string; args: JsonValue; metadata: WorkflowMetadata; settings: WorkflowSettings; settingsSources?: WorkflowSettingsSources; budget?: WorkflowBudget; settingsPath?: string; modelAliases?: Readonly<Record<string, string>>; phases?: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[]; roles?: Readonly<Record<string, AgentDefinition>>; projectRoles?: readonly string[]; schemas: readonly JsonSchema[] }
|
|
78
84
|
export interface PreflightCapabilities { models: ReadonlySet<string>; tools: ReadonlySet<string>; agentTypes: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; skipModelAvailability?: boolean }
|
|
79
85
|
export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[] }; schemas: readonly JsonSchema[]; dynamicAgentRoles: boolean }
|
|
80
86
|
export interface WorkflowOrchestrationContext { agent: (prompt: string, options?: Readonly<AgentOptions>) => Promise<JsonValue>; shell: (command: string, options?: ShellOptions) => Promise<ShellResult>; prompt: (template: string, values: Readonly<Record<string, JsonValue>>) => string; parallel: (...args: readonly unknown[]) => Promise<JsonValue>; pipeline: (...args: readonly unknown[]) => Promise<JsonValue>; withWorktree: (name: string, callback: WorkflowWorktreeCallback) => Promise<JsonValue>; checkpoint: (...args: readonly unknown[]) => Promise<boolean>; phase: (name: string) => void; log: (message: string) => void }
|
|
@@ -83,11 +89,47 @@ export interface WorkflowFunctionContext extends WorkflowOrchestrationContext {
|
|
|
83
89
|
export type WorkflowWorktreeCallback = (reference: Readonly<WorkflowWorktreeReference>) => JsonValue | Promise<JsonValue>;
|
|
84
90
|
export interface WorkflowFunction { description: string; input: JsonSchema; output: JsonSchema; run: (input: Readonly<Record<string, JsonValue>>, context: Readonly<WorkflowFunctionContext>) => Promise<JsonValue> | JsonValue }
|
|
85
91
|
export interface WorkflowVariable { description: string; schema: JsonSchema; resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue }
|
|
86
|
-
|
|
92
|
+
type NativeAgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
|
|
93
|
+
type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
|
|
94
|
+
export interface NativeSession {
|
|
95
|
+
readonly sessionId: string;
|
|
96
|
+
readonly sessionFile: string | undefined;
|
|
97
|
+
readonly messages: readonly NativeAgentMessage[];
|
|
98
|
+
getSessionStats(): NativeSessionStats;
|
|
99
|
+
readonly systemPrompt?: string;
|
|
100
|
+
readonly model?: { provider: string; model?: string; id?: string };
|
|
101
|
+
readonly agent?: { state: { tools: readonly { name: string }[] } };
|
|
102
|
+
getLeafId?: () => string | null;
|
|
103
|
+
getToolDefinitions?: () => unknown;
|
|
104
|
+
subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
|
|
105
|
+
prompt(text: string): Promise<void>;
|
|
106
|
+
steer?(text: string): Promise<void>;
|
|
107
|
+
abort?(): Promise<void>;
|
|
108
|
+
dispose(): void;
|
|
109
|
+
}
|
|
110
|
+
type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
|
|
111
|
+
type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
|
|
112
|
+
export interface SessionInput {
|
|
113
|
+
cwd: string;
|
|
114
|
+
model: ModelSpec;
|
|
115
|
+
tools: SessionTools;
|
|
116
|
+
sessionLabel: string;
|
|
117
|
+
agentDir?: string;
|
|
118
|
+
customTools?: SessionCustomTools;
|
|
119
|
+
resultTool?: ToolDefinition;
|
|
120
|
+
systemPromptAppend?: string;
|
|
121
|
+
extensionFactories?: InlineExtension[];
|
|
122
|
+
resourcePolicy?: AgentResourcePolicy;
|
|
123
|
+
options?: AgentOptions;
|
|
124
|
+
}
|
|
125
|
+
export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
|
|
126
|
+
export interface AgentSetup { prompt: string; options: AgentOptions; sessionInput: SessionInput; createSession: SessionFactory }
|
|
87
127
|
export interface AgentSetupContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal }
|
|
88
128
|
export interface AgentSetupHook { priority?: number; setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void> }
|
|
89
129
|
export interface RegisteredAgentSetupHook { name: string; priority: number; setup: AgentSetupHook["setup"] }
|
|
90
|
-
export interface
|
|
130
|
+
export interface WorkflowExtensionMetadata { version: string; headline: string; description: string }
|
|
131
|
+
export interface WorkflowRoleDirectoryRegistration { path: string; extension: WorkflowExtensionMetadata }
|
|
132
|
+
export interface WorkflowExtension extends WorkflowExtensionMetadata { functions?: Readonly<Record<string, WorkflowFunction>>; variables?: Readonly<Record<string, WorkflowVariable>>; modelAliases?: Readonly<Record<string, WorkflowModelAlias>>; agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>; roleDirectories?: readonly (string | URL)[] }
|
|
91
133
|
export interface WorkflowJournal { get(path: string): JsonValue | undefined; put(path: string, value: JsonValue): void }
|
|
92
134
|
export class WorkflowError extends Error { constructor(public readonly code: WorkflowErrorCode, message: string) { super(message); this.name = "WorkflowError"; } }
|
|
93
135
|
export interface WorkflowFailureAgent { id: string; label?: string; role?: string; structuralPath: readonly string[]; attempt: number; sessionId?: string; sessionFile?: string }
|
|
@@ -103,10 +145,13 @@ export type StaticWorkflowExecution = "parallel" | "sequential";
|
|
|
103
145
|
export interface StaticWorkflowCall { kind: WorkflowCallKind; start: number; end: number; name: string | null; prompt: string | null; model: string | null; label?: string | null; role: string | null; retries?: number | null; outputSchema?: JsonSchema | null; options?: Readonly<Record<string, JsonValue>> | null; optionKeys?: readonly string[]; execution?: StaticWorkflowExecution; structure?: readonly StaticWorkflowScope[] }
|
|
104
146
|
export interface WorkflowCatalogFunction { name: string; version: string; headline: string; extensionDescription: string; description: string; input: JsonSchema; output: JsonSchema }
|
|
105
147
|
export interface WorkflowCatalogVariable { name: string; version: string; headline: string; extensionDescription: string; description: string; schema: JsonSchema }
|
|
106
|
-
export interface
|
|
148
|
+
export interface WorkflowCatalogModelAlias { name: string; kind: "static" | "dynamic"; provenance: string; version?: string; headline?: string; extensionDescription?: string }
|
|
149
|
+
export interface WorkflowCatalogSettings { concurrency: number; modelAliases: Readonly<Record<string, string>>; disabledAgentResources: AgentResourceExclusions; globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; sources: WorkflowSettingsSources }
|
|
150
|
+
export interface WorkflowCatalogContext { cwd: string; projectTrusted: boolean; globalSettingsPath?: string }
|
|
151
|
+
export interface WorkflowCatalog { functions: readonly WorkflowCatalogFunction[]; variables: readonly WorkflowCatalogVariable[]; modelAliases?: Readonly<Record<string, string>>; modelAliasEntries?: readonly WorkflowCatalogModelAlias[]; settings?: WorkflowCatalogSettings }
|
|
107
152
|
export interface WorkflowCatalogIndexFunction { name: string; description: string; input: JsonSchema }
|
|
108
153
|
export interface WorkflowCatalogIndexVariable { name: string; description: string; schema: JsonSchema }
|
|
109
|
-
export interface WorkflowCatalogIndex { functions: readonly WorkflowCatalogIndexFunction[]; variables: readonly WorkflowCatalogIndexVariable[]; modelAliases?: Readonly<Record<string, string
|
|
154
|
+
export interface WorkflowCatalogIndex { functions: readonly WorkflowCatalogIndexFunction[]; variables: readonly WorkflowCatalogIndexVariable[]; modelAliases?: Readonly<Record<string, string>>; modelAliasEntries?: readonly WorkflowCatalogModelAlias[]; settings?: WorkflowCatalogSettings }
|
|
110
155
|
export interface WorkflowCatalogError { error: { code: "NOT_FOUND"; name: string; message: string } }
|
|
111
156
|
export interface WorkflowValidationParameters { name?: string; description?: string; script?: string; workflow?: string; args?: unknown }
|
|
112
157
|
export interface WorkflowValidationContext { cwd: string; projectTrusted: boolean; availableModels: ReadonlySet<string>; rootTools: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; agentDir?: string }
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, type AgentResourceExclusions, type JsonValue, type ModelSpec, type WorkflowErrorCode } from "./types.js";
|
|
2
|
+
import { Minimatch } from "minimatch";
|
|
2
3
|
|
|
3
4
|
export function object(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
4
5
|
export { object as isObject };
|
|
@@ -52,7 +53,20 @@ export function parseModelReference(value: string): ModelSpec {
|
|
|
52
53
|
if (thinking && !THINKING_LEVELS.includes(thinking as (typeof THINKING_LEVELS)[number])) fail("UNKNOWN_MODEL", `Invalid thinking level: ${thinking}`);
|
|
53
54
|
return { provider: match[1], model: match[2], ...(thinking ? { thinking: thinking as NonNullable<ModelSpec["thinking"]> } : {}) };
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
const MODEL_ALIAS_ERROR_NAME = Symbol("modelAliasErrorName");
|
|
57
|
+
type ModelAliasError = WorkflowError & { [MODEL_ALIAS_ERROR_NAME]?: string };
|
|
58
|
+
function aliasError(message: string, settingsPath: string, name?: string): never {
|
|
59
|
+
const error = new WorkflowError("CONFIG_ERROR", `${message} (settings: ${settingsPath})`);
|
|
60
|
+
if (name) Object.defineProperty(error, MODEL_ALIAS_ERROR_NAME, { value: name, configurable: true });
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
export function annotateModelAliasError(error: unknown, name: string): unknown {
|
|
64
|
+
if (error instanceof WorkflowError) Object.defineProperty(error, MODEL_ALIAS_ERROR_NAME, { value: name, configurable: true });
|
|
65
|
+
return error;
|
|
66
|
+
}
|
|
67
|
+
export function modelAliasErrorName(error: unknown): string | undefined {
|
|
68
|
+
return error instanceof WorkflowError ? (error as ModelAliasError)[MODEL_ALIAS_ERROR_NAME] : undefined;
|
|
69
|
+
}
|
|
56
70
|
export function modelAliasName(value: string, aliases: Readonly<Record<string, string>>): string | undefined {
|
|
57
71
|
const name = /^([^/:\s]+)(?::[^:\s]+)?$/.exec(value)?.[1];
|
|
58
72
|
return name && Object.prototype.hasOwnProperty.call(aliases, name) ? name : undefined;
|
|
@@ -61,12 +75,12 @@ export function validateModelAliases(value: unknown, settingsPath = "workflow se
|
|
|
61
75
|
if (!object(value)) aliasError("modelAliases must be an object", settingsPath);
|
|
62
76
|
const aliases: Record<string, string> = {};
|
|
63
77
|
for (const [name, target] of Object.entries(value)) {
|
|
64
|
-
if (!MODEL_ALIAS_NAME.test(name)) aliasError(`Invalid model alias name: ${name}`, settingsPath);
|
|
65
|
-
if (typeof target !== "string" || !target.trim()) aliasError(`Invalid model alias target for ${name}`, settingsPath);
|
|
78
|
+
if (!MODEL_ALIAS_NAME.test(name)) aliasError(`Invalid model alias name: ${name}`, settingsPath, name);
|
|
79
|
+
if (typeof target !== "string" || !target.trim()) aliasError(`Invalid model alias target for ${name}`, settingsPath, name);
|
|
66
80
|
aliases[name] = target;
|
|
67
81
|
}
|
|
68
82
|
for (const name of Object.keys(aliases)) {
|
|
69
|
-
try { resolveModelReference(name, aliases); } catch (error) { aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath); }
|
|
83
|
+
try { resolveModelReference(name, aliases); } catch (error) { aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath, name); }
|
|
70
84
|
}
|
|
71
85
|
return Object.freeze(aliases);
|
|
72
86
|
}
|
|
@@ -103,6 +117,26 @@ export function modelCapability(value: string | ModelSpec, aliases?: Readonly<Re
|
|
|
103
117
|
export function aliasDrift(previous: Readonly<Record<string, string>>, current: Readonly<Record<string, string>>): string[] {
|
|
104
118
|
return [...new Set([...Object.keys(previous), ...Object.keys(current)])].sort().flatMap((name) => previous[name] === current[name] ? [] : [`${name}: ${previous[name] ?? "(missing)"} -> ${current[name] ?? "(missing)"}`]);
|
|
105
119
|
}
|
|
106
|
-
|
|
120
|
+
const RESOURCE_PATTERN_OPTIONS = { dot: true, nonegate: true, nocomment: true } as const;
|
|
121
|
+
function resourcePatternBody(pattern: string): string { return pattern.startsWith("!") ? pattern.slice(1) : pattern; }
|
|
122
|
+
function resourcePatternPath(value: string): string { return value.replaceAll("\\", "/"); }
|
|
123
|
+
export function validateResourcePattern(pattern: string): void {
|
|
124
|
+
const body = resourcePatternBody(pattern);
|
|
125
|
+
if (!body) throw new Error(`Empty minimatch pattern ${JSON.stringify(pattern)}`);
|
|
126
|
+
const matcher = new Minimatch(resourcePatternPath(body), RESOURCE_PATTERN_OPTIONS);
|
|
127
|
+
if (matcher.makeRe() === false) throw new Error(`Invalid minimatch pattern ${JSON.stringify(pattern)}`);
|
|
128
|
+
}
|
|
129
|
+
export function resourcePatternMatches(resource: string, pattern: string): boolean { return new Minimatch(resourcePatternPath(resourcePatternBody(pattern)), RESOURCE_PATTERN_OPTIONS).match(resourcePatternPath(resource)); }
|
|
130
|
+
export function disabledResources(patterns: readonly string[], resources: readonly string[]): string[] {
|
|
131
|
+
const disabled = new Set<string>();
|
|
132
|
+
for (const resource of resources) {
|
|
133
|
+
let excluded = false;
|
|
134
|
+
for (const pattern of patterns) if (resourcePatternMatches(resource, pattern)) excluded = !pattern.startsWith("!");
|
|
135
|
+
if (excluded) disabled.add(resource);
|
|
136
|
+
}
|
|
137
|
+
return [...disabled];
|
|
138
|
+
}
|
|
139
|
+
export function unmatchedResourcePatterns(patterns: readonly string[], resources: readonly string[]): string[] { return patterns.filter((pattern) => !resources.some((resource) => resourcePatternMatches(resource, pattern))); }
|
|
140
|
+
export function mergeAgentResourceExclusions(...values: (AgentResourceExclusions | undefined)[]): AgentResourceExclusions { return { skills: values.flatMap((value) => value?.skills ?? []), extensions: values.flatMap((value) => value?.extensions ?? []) }; }
|
|
107
141
|
export function createLaunchSnapshot(input: Omit<import("./types.js").LaunchSnapshot, "identityVersion"> & { identityVersion?: number }): Readonly<import("./types.js").LaunchSnapshot> { return deepFreeze(structuredClone({ ...input, launchKind: input.launchKind ?? (input.functionName ? "function" : "inline"), identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION })); }
|
|
108
142
|
export function loadLaunchSnapshot(input: import("./types.js").LaunchSnapshot): Readonly<import("./types.js").LaunchSnapshot> { return deepFreeze(structuredClone(input)); }
|