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/dist/src/index.d.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/dist/src/index.js
CHANGED
|
@@ -5,7 +5,9 @@ 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 { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
11
12
|
export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
|
13
|
+
export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
|
|
@@ -39,7 +39,9 @@ export interface BorrowedWorktreeBinding {
|
|
|
39
39
|
owner: string;
|
|
40
40
|
}
|
|
41
41
|
export declare function projectStorageKey(cwd: string): string;
|
|
42
|
+
export declare function projectSessionsDirectory(cwd: string, home?: string): string;
|
|
42
43
|
export declare function runsDirectory(cwd: string, sessionId: string, home?: string): string;
|
|
44
|
+
export declare function hasLiveSessionLease(cwd: string, sessionId: string, home?: string): Promise<boolean>;
|
|
43
45
|
export declare class SessionLease {
|
|
44
46
|
#private;
|
|
45
47
|
readonly path: string;
|
|
@@ -112,6 +114,7 @@ export declare class RunStore {
|
|
|
112
114
|
sourceRunId: string;
|
|
113
115
|
owner: string;
|
|
114
116
|
}>;
|
|
117
|
+
validateDeletionWorktrees(): Promise<void>;
|
|
115
118
|
validateBorrowedWorktrees(): Promise<void>;
|
|
116
119
|
validateNamedWorktrees(): Promise<void>;
|
|
117
120
|
ownsWorktree(owner: string): Promise<boolean>;
|
package/dist/src/persistence.js
CHANGED
|
@@ -18,8 +18,11 @@ export function projectStorageKey(cwd) {
|
|
|
18
18
|
const slug = safePart(basename(exact)) || "root";
|
|
19
19
|
return `${slug}-${createHash("sha256").update(exact).digest("hex").slice(0, 12)}`;
|
|
20
20
|
}
|
|
21
|
+
export function projectSessionsDirectory(cwd, home = homedir()) {
|
|
22
|
+
return join(home, ".pi", "workflows", "projects", projectStorageKey(cwd), "sessions");
|
|
23
|
+
}
|
|
21
24
|
export function runsDirectory(cwd, sessionId, home = homedir()) {
|
|
22
|
-
return join(
|
|
25
|
+
return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
|
|
23
26
|
}
|
|
24
27
|
const SESSION_OWNER_FILE = "owner.json";
|
|
25
28
|
const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
|
|
@@ -43,6 +46,24 @@ async function processAlive(pid, startedAt) {
|
|
|
43
46
|
}
|
|
44
47
|
return true;
|
|
45
48
|
}
|
|
49
|
+
export async function hasLiveSessionLease(cwd, sessionId, home = homedir()) {
|
|
50
|
+
const path = join(runsDirectory(cwd, sessionId, home), SESSION_OWNER_FILE);
|
|
51
|
+
let owner;
|
|
52
|
+
try {
|
|
53
|
+
owner = JSON.parse(await readFile(path, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === "ENOENT")
|
|
57
|
+
return false;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (!owner || typeof owner !== "object" || Array.isArray(owner))
|
|
61
|
+
throw new WorkflowError("RUN_OWNED", `Pi session ${sessionId} has an invalid ownership lease`);
|
|
62
|
+
const candidate = owner;
|
|
63
|
+
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))
|
|
64
|
+
throw new WorkflowError("RUN_OWNED", `Pi session ${sessionId} has an invalid ownership lease`);
|
|
65
|
+
return processAlive(candidate.pid, candidate.startedAt);
|
|
66
|
+
}
|
|
46
67
|
function sameOwner(left, right) {
|
|
47
68
|
if (!left || typeof left !== "object" || !right || typeof right !== "object")
|
|
48
69
|
return false;
|
|
@@ -587,6 +608,33 @@ export class RunStore {
|
|
|
587
608
|
throw new WorkflowError("WORKTREE_FAILED", `Missing named worktree ${name}`);
|
|
588
609
|
return resolved;
|
|
589
610
|
}
|
|
611
|
+
async validateDeletionWorktrees() {
|
|
612
|
+
try {
|
|
613
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
614
|
+
if (!Array.isArray(records))
|
|
615
|
+
throw new Error("Worktree records are invalid");
|
|
616
|
+
const owners = new Set();
|
|
617
|
+
const paths = new Set();
|
|
618
|
+
records.forEach((record) => {
|
|
619
|
+
if (!record || typeof record !== "object" || typeof record.owner !== "string")
|
|
620
|
+
throw new Error("Invalid worktree record");
|
|
621
|
+
const owner = record.owner;
|
|
622
|
+
if (owners.has(owner))
|
|
623
|
+
throw new Error(`Duplicate worktree record for ${owner}`);
|
|
624
|
+
owners.add(owner);
|
|
625
|
+
const reference = this.structuralWorktree(owner, record);
|
|
626
|
+
paths.add(resolve(reference.path));
|
|
627
|
+
});
|
|
628
|
+
const entries = await readdir(join(this.directory, "worktrees"), { withFileTypes: true }).catch((error) => { if (error.code === "ENOENT")
|
|
629
|
+
return []; throw error; });
|
|
630
|
+
for (const entry of entries)
|
|
631
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !paths.has(resolve(join(this.directory, "worktrees", entry.name))))
|
|
632
|
+
throw new Error(`Unrecorded worktree artifact: ${join(this.directory, "worktrees", entry.name)}`);
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
590
638
|
async validateBorrowedWorktrees() {
|
|
591
639
|
try {
|
|
592
640
|
const loaded = await this.load();
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowVariable } from "./types.js";
|
|
1
|
+
import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
|
|
2
2
|
export declare class WorkflowRegistry {
|
|
3
3
|
#private;
|
|
4
4
|
get frozen(): boolean;
|
|
@@ -6,9 +6,9 @@ export declare class WorkflowRegistry {
|
|
|
6
6
|
register(extension: WorkflowExtension): void;
|
|
7
7
|
function(name: string): WorkflowFunction;
|
|
8
8
|
functions(): Readonly<Record<string, WorkflowFunction>>;
|
|
9
|
-
catalog(): WorkflowCatalog;
|
|
10
|
-
catalogIndex(): WorkflowCatalogIndex;
|
|
11
|
-
catalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError;
|
|
9
|
+
catalog(context?: WorkflowCatalogContext): WorkflowCatalog;
|
|
10
|
+
catalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex;
|
|
11
|
+
catalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | WorkflowCatalogError;
|
|
12
12
|
globals(): Readonly<Record<string, {
|
|
13
13
|
name: string;
|
|
14
14
|
}>>;
|
|
@@ -18,14 +18,26 @@ export declare class WorkflowRegistry {
|
|
|
18
18
|
variable: WorkflowVariable;
|
|
19
19
|
}[];
|
|
20
20
|
agentSetupHooks(): readonly RegisteredAgentSetupHook[];
|
|
21
|
+
roleDirectories(): readonly string[];
|
|
22
|
+
roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
|
|
23
|
+
modelAliases(): readonly {
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
headline: string;
|
|
27
|
+
extensionDescription: string;
|
|
28
|
+
resolve: WorkflowModelAlias["resolve"];
|
|
29
|
+
}[];
|
|
30
|
+
resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed?: ReadonlySet<string>): Promise<Readonly<Record<string, string>>>;
|
|
21
31
|
}
|
|
22
|
-
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "agentSetupHooks">;
|
|
32
|
+
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "roleDirectories" | "roleDirectoryRegistrations">;
|
|
23
33
|
export declare function resetWorkflowRegistry(): void;
|
|
24
34
|
export declare function beginWorkflowExtensionLoading(): void;
|
|
25
35
|
export declare function loadingRegistry(): WorkflowRegistryApi;
|
|
26
36
|
export declare function registerWorkflowExtension(extension: WorkflowExtension): void;
|
|
27
|
-
export declare function workflowCatalog(): WorkflowCatalog;
|
|
28
|
-
export declare function workflowCatalogIndex(): WorkflowCatalogIndex;
|
|
29
|
-
export declare function workflowCatalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError;
|
|
37
|
+
export declare function workflowCatalog(context?: WorkflowCatalogContext): WorkflowCatalog;
|
|
38
|
+
export declare function workflowCatalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex;
|
|
39
|
+
export declare function workflowCatalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | WorkflowCatalogError;
|
|
30
40
|
export declare function registeredWorkflowFunctions(): Readonly<Record<string, WorkflowFunction>>;
|
|
31
|
-
export
|
|
41
|
+
export declare function registeredWorkflowRoleDirectories(): readonly string[];
|
|
42
|
+
export declare function registeredWorkflowRoleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
|
|
43
|
+
export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowCatalogVariable, WorkflowRoleDirectoryRegistration } from "./types.js";
|
package/dist/src/registry.js
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
1
3
|
import { Value } from "typebox/value";
|
|
2
4
|
import { deepFreeze, fail, jsonValue, object } from "./utils.js";
|
|
3
|
-
import { loadSettings, validateSchema } from "./validation.js";
|
|
5
|
+
import { loadSettings, resolveWorkflowSettings, validateSchema } from "./validation.js";
|
|
4
6
|
const RESERVED_GLOBALS = new Set(["agent", "shell", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
|
|
5
7
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
6
8
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
9
|
+
function normalizeRoleDirectory(value) {
|
|
10
|
+
try {
|
|
11
|
+
if (value instanceof URL) {
|
|
12
|
+
if (value.protocol !== "file:")
|
|
13
|
+
fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
|
|
14
|
+
return fileURLToPath(value);
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
if (value.startsWith("file:"))
|
|
18
|
+
return fileURLToPath(value);
|
|
19
|
+
if (isAbsolute(value))
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
fail("INVALID_METADATA", `Invalid workflow role directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
25
|
+
}
|
|
26
|
+
fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
|
|
27
|
+
}
|
|
7
28
|
export class WorkflowRegistry {
|
|
8
29
|
#extensions = new Set();
|
|
9
30
|
#globals = new Map();
|
|
10
31
|
#hooks = new Map();
|
|
32
|
+
#roleDirectories = new Map();
|
|
33
|
+
#modelAliases = new Map();
|
|
11
34
|
#frozen = false;
|
|
12
35
|
get frozen() { return this.#frozen; }
|
|
13
36
|
freeze() { this.#frozen = true; }
|
|
@@ -16,13 +39,18 @@ export class WorkflowRegistry {
|
|
|
16
39
|
fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
17
40
|
if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
|
|
18
41
|
fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
|
|
19
|
-
if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "agentSetupHooks"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
|
|
42
|
+
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())
|
|
20
43
|
fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
|
|
21
44
|
const functions = extension.functions ?? {};
|
|
22
45
|
const variables = extension.variables ?? {};
|
|
46
|
+
const modelAliases = extension.modelAliases ?? {};
|
|
23
47
|
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
|
|
49
|
+
if (!Array.isArray(roleDirectoryValues))
|
|
50
|
+
fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
|
|
51
|
+
const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
|
|
52
|
+
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))
|
|
53
|
+
fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, or role directories");
|
|
26
54
|
const names = [...Object.keys(functions), ...Object.keys(variables)];
|
|
27
55
|
if (new Set(names).size !== names.length)
|
|
28
56
|
fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
@@ -47,16 +75,29 @@ export class WorkflowRegistry {
|
|
|
47
75
|
fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
|
|
48
76
|
validateSchema(variable.schema, `${name} schema`);
|
|
49
77
|
}
|
|
78
|
+
for (const [name, alias] of Object.entries(modelAliases)) {
|
|
79
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name))
|
|
80
|
+
fail("INVALID_METADATA", `Invalid model alias name: ${name}`);
|
|
81
|
+
if (!object(alias) || Object.keys(alias).some((key) => key !== "resolve") || typeof alias.resolve !== "function")
|
|
82
|
+
fail("INVALID_METADATA", `Invalid model alias resolver: ${name}`);
|
|
83
|
+
if (this.#modelAliases.has(name))
|
|
84
|
+
fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
|
|
85
|
+
}
|
|
50
86
|
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
51
87
|
if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority)))
|
|
52
88
|
fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
|
|
53
89
|
if (this.#hooks.has(name))
|
|
54
90
|
fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
55
91
|
}
|
|
56
|
-
const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
|
|
92
|
+
const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, ...(roleDirectories.length ? { roleDirectories } : {}) });
|
|
57
93
|
this.#extensions.add(stored);
|
|
94
|
+
for (const directory of roleDirectories)
|
|
95
|
+
if (!this.#roleDirectories.has(directory))
|
|
96
|
+
this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline, description: extension.description } }));
|
|
58
97
|
for (const name of names)
|
|
59
98
|
this.#globals.set(name, name);
|
|
99
|
+
for (const [name, alias] of Object.entries(modelAliases))
|
|
100
|
+
this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, resolve: alias.resolve });
|
|
60
101
|
for (const [name, hook] of Object.entries(agentSetupHooks))
|
|
61
102
|
this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
62
103
|
}
|
|
@@ -71,7 +112,7 @@ export class WorkflowRegistry {
|
|
|
71
112
|
functions() {
|
|
72
113
|
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
|
|
73
114
|
}
|
|
74
|
-
catalog() {
|
|
115
|
+
catalog(context) {
|
|
75
116
|
const functions = [];
|
|
76
117
|
const variables = [];
|
|
77
118
|
for (const extension of this.#extensions) {
|
|
@@ -81,28 +122,53 @@ export class WorkflowRegistry {
|
|
|
81
122
|
variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
|
|
82
123
|
}
|
|
83
124
|
let aliases;
|
|
125
|
+
let settings;
|
|
126
|
+
let source = "global settings";
|
|
84
127
|
try {
|
|
85
|
-
|
|
128
|
+
const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : undefined;
|
|
129
|
+
aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
|
|
130
|
+
if (resolved) {
|
|
131
|
+
source = resolved.projectTrusted && resolved.sources.modelAliases === resolved.projectSettingsPath ? "trusted project settings" : "global settings";
|
|
132
|
+
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 };
|
|
133
|
+
}
|
|
86
134
|
}
|
|
87
135
|
catch {
|
|
88
136
|
aliases = undefined;
|
|
89
137
|
}
|
|
138
|
+
const staticEntries = Object.keys(aliases ?? {}).map((name) => ({ name, kind: "static", provenance: source }));
|
|
139
|
+
const dynamicEntries = [...this.#modelAliases.values()].map(({ name, version, headline, extensionDescription }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline, extensionDescription }));
|
|
140
|
+
const modelAliasEntries = [...staticEntries, ...dynamicEntries].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind) || left.provenance.localeCompare(right.provenance));
|
|
90
141
|
const sort = (left, right) => left.name.localeCompare(right.name);
|
|
91
|
-
|
|
142
|
+
const catalog = { functions: functions.sort(sort), variables: variables.sort(sort), ...(modelAliasEntries.length ? { modelAliasEntries } : {}) };
|
|
143
|
+
if (aliases && Object.keys(aliases).length)
|
|
144
|
+
Object.defineProperty(catalog, "modelAliases", { value: Object.freeze(structuredClone(aliases)), enumerable: false });
|
|
145
|
+
if (settings) {
|
|
146
|
+
const publicSettings = { ...settings, modelAliases: Object.keys(aliases ?? {}).length ? {} : settings.modelAliases };
|
|
147
|
+
Object.defineProperty(catalog, "settings", { value: deepFreeze(publicSettings), enumerable: true });
|
|
148
|
+
}
|
|
149
|
+
return deepFreeze(catalog);
|
|
92
150
|
}
|
|
93
|
-
catalogIndex() {
|
|
94
|
-
const catalog = this.catalog();
|
|
95
|
-
|
|
151
|
+
catalogIndex(context) {
|
|
152
|
+
const catalog = this.catalog(context);
|
|
153
|
+
const index = {
|
|
96
154
|
functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
|
|
97
155
|
variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
|
|
98
|
-
...(catalog.
|
|
99
|
-
}
|
|
156
|
+
...(catalog.modelAliasEntries ? { modelAliasEntries: structuredClone(catalog.modelAliasEntries) } : {}),
|
|
157
|
+
};
|
|
158
|
+
if (catalog.modelAliases)
|
|
159
|
+
Object.defineProperty(index, "modelAliases", { value: Object.freeze(structuredClone(catalog.modelAliases)), enumerable: false });
|
|
160
|
+
if (catalog.settings)
|
|
161
|
+
Object.defineProperty(index, "settings", { value: deepFreeze(structuredClone(catalog.settings)), enumerable: true });
|
|
162
|
+
return deepFreeze(index);
|
|
100
163
|
}
|
|
101
|
-
catalogDetail(name) {
|
|
102
|
-
const catalog = this.catalog();
|
|
164
|
+
catalogDetail(name, context) {
|
|
165
|
+
const catalog = this.catalog(context);
|
|
103
166
|
const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
|
|
104
167
|
if (entry)
|
|
105
168
|
return entry;
|
|
169
|
+
const alias = catalog.modelAliasEntries?.find((candidate) => candidate.name === name);
|
|
170
|
+
if (alias)
|
|
171
|
+
return alias;
|
|
106
172
|
return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
|
|
107
173
|
}
|
|
108
174
|
globals() {
|
|
@@ -131,6 +197,38 @@ export class WorkflowRegistry {
|
|
|
131
197
|
agentSetupHooks() {
|
|
132
198
|
return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
133
199
|
}
|
|
200
|
+
roleDirectories() {
|
|
201
|
+
return [...this.#roleDirectories.keys()];
|
|
202
|
+
}
|
|
203
|
+
roleDirectoryRegistrations() {
|
|
204
|
+
return [...this.#roleDirectories.values()];
|
|
205
|
+
}
|
|
206
|
+
modelAliases() { return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name)); }
|
|
207
|
+
async resolveModelAliases(context, shadowed = new Set()) {
|
|
208
|
+
const resolved = {};
|
|
209
|
+
const isAborted = () => context.signal.aborted;
|
|
210
|
+
for (const alias of this.modelAliases()) {
|
|
211
|
+
if (shadowed.has(alias.name))
|
|
212
|
+
continue;
|
|
213
|
+
if (isAborted())
|
|
214
|
+
fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
|
|
215
|
+
let target;
|
|
216
|
+
try {
|
|
217
|
+
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 }));
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (isAborted() || error instanceof Error && error.name === "AbortError" || typeof error === "object" && error !== null && !Array.isArray(error) && error.code === "CANCELLED")
|
|
221
|
+
fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
|
|
222
|
+
fail("CONFIG_ERROR", `Model alias resolver failed for ${alias.name} (${alias.headline}): ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
}
|
|
224
|
+
if (isAborted())
|
|
225
|
+
fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
|
|
226
|
+
if (typeof target !== "string" || !target.trim())
|
|
227
|
+
fail("CONFIG_ERROR", `Model alias resolver returned an invalid target for ${alias.name} (${alias.headline})`);
|
|
228
|
+
resolved[alias.name] = target.trim();
|
|
229
|
+
}
|
|
230
|
+
return Object.freeze(resolved);
|
|
231
|
+
}
|
|
134
232
|
}
|
|
135
233
|
const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
|
|
136
234
|
const globalRegistry = globalThis;
|
|
@@ -141,12 +239,16 @@ function createWorkflowRegistryApi(registry) {
|
|
|
141
239
|
register: (extension) => { registry.register(extension); },
|
|
142
240
|
function: (name) => registry.function(name),
|
|
143
241
|
functions: () => registry.functions(),
|
|
144
|
-
catalog: () => registry.catalog(),
|
|
145
|
-
catalogIndex: () => registry.catalogIndex(),
|
|
146
|
-
catalogDetail: (name) => registry.catalogDetail(name),
|
|
242
|
+
catalog: (context) => registry.catalog(context),
|
|
243
|
+
catalogIndex: (context) => registry.catalogIndex(context),
|
|
244
|
+
catalogDetail: (name, context) => registry.catalogDetail(name, context),
|
|
147
245
|
globals: () => registry.globals(),
|
|
148
246
|
invokeFunction: (...args) => registry.invokeFunction(...args),
|
|
149
247
|
variables: () => registry.variables(),
|
|
248
|
+
modelAliases: () => registry.modelAliases(),
|
|
249
|
+
resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
|
|
250
|
+
roleDirectories: () => registry.roleDirectories(),
|
|
251
|
+
roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
|
|
150
252
|
agentSetupHooks: () => registry.agentSetupHooks(),
|
|
151
253
|
};
|
|
152
254
|
}
|
|
@@ -163,7 +265,15 @@ export function beginWorkflowExtensionLoading() {
|
|
|
163
265
|
export function loadingRegistry() { return workflowRegistryHost().api; }
|
|
164
266
|
beginWorkflowExtensionLoading();
|
|
165
267
|
export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
|
|
166
|
-
export function workflowCatalog() { return loadingRegistry().catalog(); }
|
|
167
|
-
export function workflowCatalogIndex() { return loadingRegistry().catalogIndex(); }
|
|
168
|
-
export function workflowCatalogDetail(name) { return loadingRegistry().catalogDetail(name); }
|
|
268
|
+
export function workflowCatalog(context) { return loadingRegistry().catalog(context); }
|
|
269
|
+
export function workflowCatalogIndex(context) { return loadingRegistry().catalogIndex(context); }
|
|
270
|
+
export function workflowCatalogDetail(name, context) { return loadingRegistry().catalogDetail(name, context); }
|
|
169
271
|
export function registeredWorkflowFunctions() { return loadingRegistry().functions(); }
|
|
272
|
+
export function registeredWorkflowRoleDirectories() {
|
|
273
|
+
const directories = loadingRegistry().roleDirectories;
|
|
274
|
+
return typeof directories === "function" ? directories() : [];
|
|
275
|
+
}
|
|
276
|
+
export function registeredWorkflowRoleDirectoryRegistrations() {
|
|
277
|
+
const registrations = loadingRegistry().roleDirectoryRegistrations;
|
|
278
|
+
return typeof registrations === "function" ? registrations() : [];
|
|
279
|
+
}
|
|
@@ -296,8 +296,10 @@ function detailLines(workflow) {
|
|
|
296
296
|
`Hooks: ${attempt.setup.hookNames.join(", ") || "(none)"}`,
|
|
297
297
|
`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}`,
|
|
298
298
|
...(attempt.setup.disabledAgentResources ? [
|
|
299
|
-
`
|
|
300
|
-
`
|
|
299
|
+
`Configured skill patterns: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
|
|
300
|
+
`Excluded skills: ${attempt.setup.disabledAgentResources.excludedSkills?.join(", ") || "(none)"}`,
|
|
301
|
+
`Configured extension patterns: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
|
|
302
|
+
`Excluded extensions: ${attempt.setup.disabledAgentResources.excludedExtensions?.join(", ") || "(none)"}`,
|
|
301
303
|
`Unmatched skills: ${attempt.setup.disabledAgentResources.unmatchedSkills.join(", ") || "(none)"}`,
|
|
302
304
|
`Unmatched extensions: ${attempt.setup.disabledAgentResources.unmatchedExtensions.join(", ") || "(none)"}`,
|
|
303
305
|
] : []),
|