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.
Files changed (44) hide show
  1. package/README.md +18 -11
  2. package/dist/src/agent-execution.d.ts +4 -94
  3. package/dist/src/agent-execution.js +34 -208
  4. package/dist/src/budget.d.ts +38 -0
  5. package/dist/src/budget.js +160 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +60 -8
  8. package/dist/src/doctor-cleanup.d.ts +41 -0
  9. package/dist/src/doctor-cleanup.js +597 -0
  10. package/dist/src/doctor.d.ts +7 -2
  11. package/dist/src/doctor.js +127 -22
  12. package/dist/src/execution.d.ts +17 -0
  13. package/dist/src/execution.js +630 -0
  14. package/dist/src/host.d.ts +101 -0
  15. package/dist/src/host.js +3084 -0
  16. package/dist/src/index.d.ts +13 -634
  17. package/dist/src/index.js +10 -4432
  18. package/dist/src/persistence.d.ts +9 -19
  19. package/dist/src/persistence.js +175 -61
  20. package/dist/src/registry.d.ts +43 -0
  21. package/dist/src/registry.js +279 -0
  22. package/dist/src/session-inspector.js +5 -3
  23. package/dist/src/types.d.ts +692 -0
  24. package/dist/src/types.js +27 -0
  25. package/dist/src/utils.d.ts +32 -0
  26. package/dist/src/utils.js +168 -0
  27. package/dist/src/validation.d.ts +28 -0
  28. package/dist/src/validation.js +832 -0
  29. package/package.json +3 -2
  30. package/skills/pi-extensible-workflows/SKILL.md +69 -34
  31. package/src/agent-execution.ts +37 -189
  32. package/src/budget.ts +75 -0
  33. package/src/cli.ts +47 -7
  34. package/src/doctor-cleanup.ts +337 -0
  35. package/src/doctor.ts +117 -24
  36. package/src/execution.ts +540 -0
  37. package/src/host.ts +2598 -0
  38. package/src/index.ts +14 -3856
  39. package/src/persistence.ts +122 -37
  40. package/src/registry.ts +240 -0
  41. package/src/session-inspector.ts +5 -3
  42. package/src/types.ts +158 -0
  43. package/src/utils.ts +142 -0
  44. package/src/validation.ts +688 -0
package/src/types.ts ADDED
@@ -0,0 +1,158 @@
1
+ import type { AgentSessionEvent, CreateAgentSessionOptions, InlineExtension, SessionStats, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"] as const;
3
+ export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"] as const;
4
+ export const WORKFLOW_CALL_KINDS = ["agent", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"] as const;
5
+ export type WorkflowCallKind = (typeof WORKFLOW_CALL_KINDS)[number];
6
+ export const WORKFLOW_RUN_STARTED_EVENT = "workflow:run-started";
7
+ export const WORKFLOW_RUN_RESUMED_EVENT = "workflow:run-resumed";
8
+ export const WORKFLOW_RUN_STATE_CHANGED_EVENT = "workflow:run-state-changed";
9
+ export const WORKFLOW_RUN_COMPLETED_EVENT = "workflow:run-completed";
10
+ export const WORKFLOW_RUN_FAILED_EVENT = "workflow:run-failed";
11
+ export const WORKFLOW_AGENT_STATE_CHANGED_EVENT = "workflow:agent-state-changed";
12
+ export const WORKFLOW_PHASE_CHANGED_EVENT = "workflow:phase-changed";
13
+ export const WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT = "workflow:checkpoint-state-changed";
14
+ export const WORKFLOW_BUDGET_EVENT = "workflow:budget-event";
15
+ export const WORKFLOW_WORKTREE_CREATED_EVENT = "workflow:worktree-created";
16
+ export const ERROR_CODES = [
17
+ "CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
18
+ "RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
19
+ "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
20
+ ] as const;
21
+
22
+ export type RunState = (typeof RUN_STATES)[number];
23
+ export type AgentState = (typeof AGENT_STATES)[number];
24
+ export type WorkflowErrorCode = (typeof ERROR_CODES)[number];
25
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
26
+ export type JsonSchema = { [key: string]: JsonValue };
27
+ export interface AgentOptions {
28
+ label?: string;
29
+ model?: string;
30
+ thinking?: NonNullable<ModelSpec["thinking"]>;
31
+ tools?: string[];
32
+ role?: string;
33
+ outputSchema?: JsonSchema;
34
+ retries?: number;
35
+ timeoutMs?: number | null;
36
+ [key: string]: JsonValue;
37
+ }
38
+ export interface ShellOptions { timeoutMs?: number; env?: Record<string, string> }
39
+ export interface ShellResult { exitCode: number | null; stdout: string; stderr: string }
40
+ export type BudgetDimension = "tokens" | "costUsd" | "durationMs" | "agentLaunches";
41
+ export interface BudgetLimits { soft?: number; hard?: number }
42
+ export type WorkflowBudget = Partial<Record<BudgetDimension, BudgetLimits>>;
43
+ export type WorkflowBudgetPatch = Partial<Record<BudgetDimension, BudgetLimits | { soft?: number | null; hard?: number | null } | null>>;
44
+ export interface WorkflowBudgetUsage { tokens: number; costUsd: number; durationMs: number; agentLaunches: number }
45
+ export type BudgetEventType = "soft_crossed" | "hard_overrun" | "hard_exhausted" | "adjustment_requested" | "adjustment_approved" | "adjustment_rejected";
46
+ export interface BudgetEvent { type: BudgetEventType; budgetVersion: number; dimensions: readonly BudgetDimension[]; usage: WorkflowBudgetUsage; limits: WorkflowBudget; at: number; proposalId?: string; previous?: WorkflowBudget; proposed?: WorkflowBudget }
47
+ export interface BudgetApprovalRequest { kind: "budget"; proposalId: string; runId: string; consumed: WorkflowBudgetUsage; previous: WorkflowBudget; proposed: WorkflowBudget; budgetVersion: number }
48
+ export interface WorkflowErrorShape { code: WorkflowErrorCode; message: string }
49
+ export interface WorkflowEventBase { runId: string; sessionId: string; workflowName: string; cwd: string; runDirectory: string; timestamp: number }
50
+ export type WorkflowRunStartedEvent = WorkflowEventBase;
51
+ export type WorkflowRunResumedEvent = WorkflowEventBase;
52
+ export interface WorkflowRunStateChangedEvent extends WorkflowEventBase { previousState: RunState; state: RunState; reason?: string; errorCode?: WorkflowErrorCode }
53
+ export interface WorkflowRunCompletedEvent extends WorkflowEventBase { resultPath: string }
54
+ export interface WorkflowRunFailedEvent extends WorkflowEventBase { error: WorkflowErrorShape }
55
+ export interface WorkflowAgentStateChangedEvent extends WorkflowEventBase { agentId: string; displayLabel: string; role?: string; structuralPath: readonly string[]; parentId?: string; parentBreadcrumb?: string; worktreeOwner?: string; previousState?: AgentState; state: AgentState; attempt: number }
56
+ export interface WorkflowPhaseChangedEvent extends WorkflowEventBase { previousPhase?: string; phase: string }
57
+ export type WorkflowCheckpointState = "awaiting" | "approved" | "rejected";
58
+ export interface WorkflowCheckpointStateChangedEvent extends WorkflowEventBase { name: string; state: WorkflowCheckpointState }
59
+ export interface WorkflowBudgetEvent extends WorkflowEventBase { type: BudgetEventType; budgetVersion: number; dimensions: readonly BudgetDimension[]; usage: WorkflowBudgetUsage; limits: WorkflowBudget; proposalId?: string; previous?: WorkflowBudget; proposed?: WorkflowBudget }
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> }
63
+ export interface WorkflowMetadata { name: string; description?: string }
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> }
68
+ export interface AgentResourceExclusions { skills: readonly string[]; extensions: readonly 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[] }
70
+ export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
71
+ export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
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[] } }
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 }
74
+ export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
75
+ export interface WorkflowWorktreeReference { readonly path: string; readonly branch: string }
76
+ 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 }
77
+ export interface WorkflowRunEvent { type: string; message: string }
78
+ export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
79
+ export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
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[] }
81
+ export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
82
+ export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; disabledAgentResources?: AgentResourceExclusions }
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[] }
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 }
85
+ export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[] }; schemas: readonly JsonSchema[]; dynamicAgentRoles: boolean }
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 }
87
+ export interface WorkflowRunContext { cwd: string; sessionId: string; runId: string; workflow: Readonly<WorkflowMetadata>; args: JsonValue; signal: AbortSignal }
88
+ export interface WorkflowFunctionContext extends WorkflowOrchestrationContext { run: Readonly<WorkflowRunContext>; invoke: (name: string, input: Readonly<Record<string, JsonValue>>) => Promise<JsonValue> }
89
+ export type WorkflowWorktreeCallback = (reference: Readonly<WorkflowWorktreeReference>) => JsonValue | Promise<JsonValue>;
90
+ export interface WorkflowFunction { description: string; input: JsonSchema; output: JsonSchema; run: (input: Readonly<Record<string, JsonValue>>, context: Readonly<WorkflowFunctionContext>) => Promise<JsonValue> | JsonValue }
91
+ export interface WorkflowVariable { description: string; schema: JsonSchema; resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue }
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 }
127
+ export interface AgentSetupContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal }
128
+ export interface AgentSetupHook { priority?: number; setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void> }
129
+ export interface RegisteredAgentSetupHook { name: string; priority: number; setup: AgentSetupHook["setup"] }
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)[] }
133
+ export interface WorkflowJournal { get(path: string): JsonValue | undefined; put(path: string, value: JsonValue): void }
134
+ export class WorkflowError extends Error { constructor(public readonly code: WorkflowErrorCode, message: string) { super(message); this.name = "WorkflowError"; } }
135
+ export interface WorkflowFailureAgent { id: string; label?: string; role?: string; structuralPath: readonly string[]; attempt: number; sessionId?: string; sessionFile?: string }
136
+ export interface WorkflowSiblingAgent { id: string; label?: string; role?: string; structuralPath: readonly string[] }
137
+ export interface WorkflowFailureDiagnostics { runId: string; workflowName: string; state: RunState; failedAt: string | null; error: WorkflowErrorShape; failedAgent?: WorkflowFailureAgent; completedSiblingAgents?: readonly WorkflowSiblingAgent[]; completedSiblingPaths: readonly (readonly string[])[]; retry?: { sourceRunId: string; action: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[]; warning: string }; artifacts: { runDirectory: string; statePath: string; journalPath: string } }
138
+ export interface CheckpointInput { name: string; prompt: string; context: JsonValue }
139
+ export interface AgentIdentity { structuralPath: readonly string[]; callSite: string; occurrence: number; parentBreadcrumb?: string; worktreeOwner?: string }
140
+ export interface ShellIdentity { structuralPath: readonly string[]; callSite: string; occurrence: number; worktreeOwner?: string }
141
+ export interface WorkflowBridge { agent?: (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>; shell?: (command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>; checkpoint?: (input: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => boolean | Promise<boolean>; function?: (name: string, input: Readonly<Record<string, JsonValue>>, path: string, signal: AbortSignal, worktreeOwner?: string, structuralPath?: readonly string[]) => Promise<JsonValue>; worktree?: (owner: string, signal: AbortSignal) => Promise<Readonly<WorkflowWorktreeReference>>; functions?: Readonly<Record<string, { name: string }>>; variables?: Readonly<Record<string, JsonValue>>; phase?: (name: string) => void | Promise<void>; log?: (message: string) => void | Promise<void> }
142
+ export interface WorkflowExecution { result: Promise<JsonValue>; cancel: () => void }
143
+ export interface StaticWorkflowScope { kind: "parallel" | "pipeline"; name: string | null; key: string | null }
144
+ export type StaticWorkflowExecution = "parallel" | "sequential";
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[] }
146
+ export interface WorkflowCatalogFunction { name: string; version: string; headline: string; extensionDescription: string; description: string; input: JsonSchema; output: JsonSchema }
147
+ export interface WorkflowCatalogVariable { name: string; version: string; headline: string; extensionDescription: string; description: string; schema: JsonSchema }
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 }
152
+ export interface WorkflowCatalogIndexFunction { name: string; description: string; input: JsonSchema }
153
+ export interface WorkflowCatalogIndexVariable { name: string; description: string; schema: JsonSchema }
154
+ export interface WorkflowCatalogIndex { functions: readonly WorkflowCatalogIndexFunction[]; variables: readonly WorkflowCatalogIndexVariable[]; modelAliases?: Readonly<Record<string, string>>; modelAliasEntries?: readonly WorkflowCatalogModelAlias[]; settings?: WorkflowCatalogSettings }
155
+ export interface WorkflowCatalogError { error: { code: "NOT_FOUND"; name: string; message: string } }
156
+ export interface WorkflowValidationParameters { name?: string; description?: string; script?: string; workflow?: string; args?: unknown }
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 }
158
+ export interface ValidatedWorkflowLaunch { script: string; checked: PreflightResult; agentDefinitions: Readonly<Record<string, AgentDefinition>>; projectAgentDefinitions: Readonly<Record<string, AgentDefinition>>; roleNames: readonly string[]; functionName?: string }
package/src/utils.ts ADDED
@@ -0,0 +1,142 @@
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";
3
+
4
+ export function object(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
5
+ export { object as isObject };
6
+ export function jsonValue(value: unknown, seen = new Set<object>()): value is JsonValue {
7
+ if (value === null || typeof value === "boolean" || typeof value === "string") return true;
8
+ if (typeof value === "number") return Number.isFinite(value);
9
+ if (typeof value !== "object" || seen.has(value)) return false;
10
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
11
+ const keys = Reflect.ownKeys(value);
12
+ if (keys.some((key) => typeof key !== "string")) return false;
13
+ seen.add(value);
14
+ const valid = (Array.isArray(value) ? Array.from(value) : keys.map((key) => (value as Record<string, unknown>)[key as string])).every((item) => jsonValue(item, seen));
15
+ seen.delete(value);
16
+ return valid;
17
+ }
18
+ export function jsonObject(value: unknown): value is Record<string, JsonValue> { return jsonValue(value) && object(value); }
19
+ export function positiveInteger(value: unknown): value is number { return Number.isInteger(value) && (value as number) > 0; }
20
+ export function deepFreeze<T>(value: T): T {
21
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
22
+ Object.freeze(value);
23
+ for (const child of Object.values(value)) deepFreeze(child);
24
+ }
25
+ return value;
26
+ }
27
+ export function errorText(error: unknown): string { return error && typeof error === "object" && typeof (error as { message?: unknown }).message === "string" ? (error as { message: string }).message : error instanceof Error ? error.message : String(error); }
28
+ export function errorCode(error: unknown): WorkflowErrorCode | undefined {
29
+ if (error instanceof WorkflowError) return ERROR_CODES.includes(error.code) ? error.code : undefined;
30
+ if (!error || typeof error !== "object") return undefined;
31
+ const code = (error as { code?: unknown }).code;
32
+ return typeof code === "string" && ERROR_CODES.includes(code as WorkflowErrorCode) ? code as WorkflowErrorCode : undefined;
33
+ }
34
+ const WORKFLOW_AUTHORED_ERROR = Symbol("workflowAuthoredError");
35
+ export function markWorkflowAuthored(error: WorkflowError, authored = false): WorkflowError {
36
+ if (authored) Object.defineProperty(error, WORKFLOW_AUTHORED_ERROR, { value: true });
37
+ return error;
38
+ }
39
+ export function isWorkflowAuthored(error: unknown): boolean { return Boolean(error && typeof error === "object" && WORKFLOW_AUTHORED_ERROR in error); }
40
+ export function asWorkflowError(error: unknown): WorkflowError {
41
+ const code = errorCode(error);
42
+ return markWorkflowAuthored(error instanceof WorkflowError && code ? error : new WorkflowError(code ?? "INTERNAL_ERROR", errorText(error)), isWorkflowAuthored(error) || !code);
43
+ }
44
+ export function fail(code: WorkflowErrorCode, message: string): never { throw new WorkflowError(code, message); }
45
+
46
+ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
47
+ const MODEL_ALIAS_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
48
+ export function parseThinking(value: unknown): ModelSpec["thinking"] | undefined { return typeof value === "string" && THINKING_LEVELS.includes(value as (typeof THINKING_LEVELS)[number]) ? value as ModelSpec["thinking"] : undefined; }
49
+ export function parseModelReference(value: string): ModelSpec {
50
+ const match = /^([^/:\s]+)\/([^:\s]+)(?::([^:\s]+))?$/.exec(value);
51
+ if (!match?.[1] || !match[2]) fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
52
+ const thinking = match[3];
53
+ if (thinking && !THINKING_LEVELS.includes(thinking as (typeof THINKING_LEVELS)[number])) fail("UNKNOWN_MODEL", `Invalid thinking level: ${thinking}`);
54
+ return { provider: match[1], model: match[2], ...(thinking ? { thinking: thinking as NonNullable<ModelSpec["thinking"]> } : {}) };
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
+ }
70
+ export function modelAliasName(value: string, aliases: Readonly<Record<string, string>>): string | undefined {
71
+ const name = /^([^/:\s]+)(?::[^:\s]+)?$/.exec(value)?.[1];
72
+ return name && Object.prototype.hasOwnProperty.call(aliases, name) ? name : undefined;
73
+ }
74
+ export function validateModelAliases(value: unknown, settingsPath = "workflow settings"): Readonly<Record<string, string>> {
75
+ if (!object(value)) aliasError("modelAliases must be an object", settingsPath);
76
+ const aliases: Record<string, string> = {};
77
+ for (const [name, target] of Object.entries(value)) {
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);
80
+ aliases[name] = target;
81
+ }
82
+ for (const name of Object.keys(aliases)) {
83
+ try { resolveModelReference(name, aliases); } catch (error) { aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath, name); }
84
+ }
85
+ return Object.freeze(aliases);
86
+ }
87
+ export function unknownModel(value: string, target: string | undefined, settingsPath?: string): never {
88
+ const resolved = target ? ` resolved to ${target}` : "";
89
+ const path = settingsPath ? ` (settings: ${settingsPath})` : "";
90
+ fail("UNKNOWN_MODEL", `Unknown model${target ? " alias" : ""} ${value}${resolved}${path}`);
91
+ }
92
+ export function resolveModelReference(value: string, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
93
+ const resolveReference = (reference: string, chain: readonly string[]): ModelSpec => {
94
+ if (reference.includes("/")) return parseModelReference(reference);
95
+ const match = /^([^:\s]+)(?::([^:\s]+))?$/.exec(reference);
96
+ const thinking = match?.[2];
97
+ if (!match?.[1] || thinking && !THINKING_LEVELS.includes(thinking as (typeof THINKING_LEVELS)[number])) unknownModel(reference, undefined, settingsPath);
98
+ const alias = modelAliasName(reference, aliases);
99
+ if (alias) {
100
+ if (chain.includes(alias)) fail("UNKNOWN_MODEL", `Circular model alias: ${[...chain, alias].join(" -> ")}${settingsPath ? ` (settings: ${settingsPath})` : ""}`);
101
+ const parsed = resolveReference(aliases[alias] as string, [...chain, alias]);
102
+ return thinking ? { ...parsed, thinking: thinking as NonNullable<ModelSpec["thinking"]> } : parsed;
103
+ }
104
+ const candidates = [...(knownModels ?? [])].filter((model) => model.slice(model.indexOf("/") + 1) === match[1]);
105
+ if (candidates.length === 1) {
106
+ const parsed = parseModelReference(candidates[0] as string);
107
+ return thinking ? { ...parsed, thinking: thinking as NonNullable<ModelSpec["thinking"]> } : parsed;
108
+ }
109
+ unknownModel(reference, undefined, settingsPath);
110
+ };
111
+ return resolveReference(value, []);
112
+ }
113
+ export function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string {
114
+ const parsed = typeof value === "string" ? resolveModelReference(value, aliases, knownModels, settingsPath) : value;
115
+ return `${parsed.provider}/${parsed.model}`;
116
+ }
117
+ export function aliasDrift(previous: Readonly<Record<string, string>>, current: Readonly<Record<string, string>>): string[] {
118
+ return [...new Set([...Object.keys(previous), ...Object.keys(current)])].sort().flatMap((name) => previous[name] === current[name] ? [] : [`${name}: ${previous[name] ?? "(missing)"} -> ${current[name] ?? "(missing)"}`]);
119
+ }
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 ?? []) }; }
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 })); }
142
+ export function loadLaunchSnapshot(input: import("./types.js").LaunchSnapshot): Readonly<import("./types.js").LaunchSnapshot> { return deepFreeze(structuredClone(input)); }