@marcoscale98/pi-extensible-workflows 5.19.1-fork.1 → 6.2.1-fork.1
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/CHANGELOG.md +77 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +365 -127
- package/dist/src/background-widget.d.ts +0 -1
- package/dist/src/background-widget.js +4 -9
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +33 -37
- package/dist/src/execution.d.ts +2 -0
- package/dist/src/execution.js +5 -5
- package/dist/src/host-delivery.js +4 -8
- package/dist/src/host-navigator.js +5 -2
- package/dist/src/host-phases.d.ts +1 -0
- package/dist/src/host-recovery.d.ts +6 -6
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +1 -1
- package/dist/src/host-view.js +17 -15
- package/dist/src/host.js +62 -87
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1523 -1630
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +7 -6
- package/dist/src/paths.js +24 -15
- package/dist/src/persistence.d.ts +1 -0
- package/dist/src/persistence.js +1 -0
- package/dist/src/pi-runtime-runner.js +4 -1
- package/dist/src/registry.d.ts +9 -7
- package/dist/src/registry.js +28 -70
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +6 -3
- package/dist/src/settings.js +36 -47
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +0 -1
- package/dist/src/trajectory.js +2 -3
- package/dist/src/types.d.ts +100 -44
- package/dist/src/types.js +4 -7
- package/dist/src/utils.d.ts +3 -2
- package/dist/src/utils.js +20 -10
- package/dist/src/validation.d.ts +8 -2
- package/dist/src/validation.js +122 -86
- package/dist/starter/index.js +8 -9
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.js +8 -7
- package/dist/subagents/index.js +654 -814
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -3
- package/dist/subagents/src/contracts.js +2 -2
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +1 -2
- package/dist/subagents/src/manager.js +49 -70
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +0 -1
- package/dist/subagents/src/view.js +2 -16
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +8 -8
- package/dist/trajectory/index.js +487 -891
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +8 -8
- package/dist/trajectory/src/index.js +126 -86
- package/dist/trajectory/src/server.js +264 -25
- package/dist/trajectory/src/server.js.map +3 -3
- package/dist/trajectory/src/startup-mutex.d.ts +17 -0
- package/dist/trajectory/src/startup-mutex.js +172 -0
- package/examples/workflow-extension-template/README.md +4 -7
- package/examples/workflow-extension-template/extension.test.mjs +4 -8
- package/examples/workflow-extension-template/index.js +0 -3
- package/package.json +5 -5
- package/skills/pi-extensible-workflows/SKILL.md +10 -8
- package/src/agent-execution.ts +339 -127
- package/src/background-widget.ts +4 -10
- package/src/decoders.ts +31 -37
- package/src/execution.ts +5 -5
- package/src/host-delivery.ts +4 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +20 -26
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +18 -11
- package/src/host.ts +62 -113
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -12
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +27 -63
- package/src/runtime/agent-runner.ts +2 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +35 -47
- package/src/store.ts +18 -2
- package/src/trajectory.ts +2 -4
- package/src/types.ts +71 -30
- package/src/utils.ts +18 -9
- package/src/validation.ts +102 -79
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -7
- package/starter/roles/developer.md +0 -1
- package/starter/roles/oracle.md +0 -1
- package/starter/roles/researcher.md +0 -1
- package/starter/roles/reviewer.md +0 -1
- package/starter/roles/scout.md +0 -1
- package/subagents/README.md +8 -14
- package/subagents/src/contracts.ts +3 -4
- package/subagents/src/index.ts +2 -3
- package/subagents/src/manager.ts +45 -125
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +4 -14
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +8 -8
- package/trajectory/src/index.ts +106 -82
- package/trajectory/src/server.ts +44 -8
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -326
- package/dist/starter/roles/developer.md +0 -6
- package/dist/starter/roles/oracle.md +0 -18
- package/dist/starter/roles/researcher.md +0 -20
- package/dist/starter/roles/reviewer.md +0 -19
- package/dist/starter/roles/scout.md +0 -18
- package/examples/workflow-extension-template/roles/reviewer.md +0 -4
- package/src/roles.ts +0 -327
package/src/types.ts
CHANGED
|
@@ -27,29 +27,30 @@ export const WORKFLOW_BUDGET_EVENT = "workflow:budget-event";
|
|
|
27
27
|
export const WORKFLOW_WORKTREE_CREATED_EVENT = "workflow:worktree-created";
|
|
28
28
|
export const WORKFLOW_BLOCKED_EVENT = "workflow:blocked";
|
|
29
29
|
export const ERROR_CODES = [
|
|
30
|
-
"CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "
|
|
30
|
+
"CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT",
|
|
31
31
|
"RUN_OWNED", "RUN_NOT_FOUND", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "AGENT_RESULT_COLLECTED", "RESULT_INVALID",
|
|
32
32
|
"TOOL_FAILED", "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
|
|
33
33
|
] as const;
|
|
34
34
|
|
|
35
35
|
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
36
36
|
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
37
|
+
export const CONTEXT_FILE_SCOPES = ["global", "project", "cwd"] as const;
|
|
38
|
+
export type ContextFileScope = (typeof CONTEXT_FILE_SCOPES)[number];
|
|
39
|
+
export function isContextFileScope(value: unknown): value is ContextFileScope { return CONTEXT_FILE_SCOPES.some((scope) => scope === value); }
|
|
37
40
|
export type WorkflowErrorCode = (typeof ERROR_CODES)[number];
|
|
38
41
|
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
39
42
|
export type JsonSchema = { [key: string]: JsonValue };
|
|
40
43
|
type WorkflowSchema = JsonSchema | TSchema;
|
|
41
|
-
export function roleNameOf(value: unknown): string | undefined {
|
|
42
|
-
return typeof value === "string" ? value : undefined;
|
|
43
|
-
}
|
|
44
44
|
// Extra keys are reserved for extensions and forwarded as JSON to agentOptions; core options remain typed.
|
|
45
45
|
export interface AgentOptions<Schema extends TSchema = never> {
|
|
46
46
|
label?: string;
|
|
47
47
|
model?: string;
|
|
48
48
|
tools?: string[];
|
|
49
|
+
/** Exact tool names removed after preparation hooks; never widens the parent ceiling. */
|
|
50
|
+
excludeTools?: string[];
|
|
49
51
|
skills?: string[];
|
|
50
52
|
extensions?: string[];
|
|
51
53
|
contextFiles?: ContextFileScope[];
|
|
52
|
-
role?: string;
|
|
53
54
|
outputSchema?: JsonSchema | Schema;
|
|
54
55
|
retries?: number;
|
|
55
56
|
timeoutMs?: number | null;
|
|
@@ -80,7 +81,7 @@ export type WorkflowRunResumedEvent = WorkflowEventBase;
|
|
|
80
81
|
export interface WorkflowRunStateChangedEvent extends WorkflowEventBase { previousState: RunState; state: RunState; reason?: string; errorCode?: WorkflowErrorCode }
|
|
81
82
|
export interface WorkflowRunCompletedEvent extends WorkflowEventBase { resultPath: string }
|
|
82
83
|
export interface WorkflowRunFailedEvent extends WorkflowEventBase { error: WorkflowErrorShape }
|
|
83
|
-
export interface WorkflowAgentStateChangedEvent extends WorkflowEventBase { agentId: string; displayLabel: string;
|
|
84
|
+
export interface WorkflowAgentStateChangedEvent extends WorkflowEventBase { agentId: string; displayLabel: string; structuralPath: readonly string[]; parentId?: string; parentBreadcrumb?: string; worktreeOwner?: string; previousState?: AgentState; state: AgentState; attempt: number }
|
|
84
85
|
export interface WorkflowPhaseChangedEvent extends WorkflowEventBase { previousPhase?: string; phase: string }
|
|
85
86
|
export type WorkflowCheckpointState = "awaiting" | "approved" | "rejected";
|
|
86
87
|
export interface WorkflowCheckpointStateChangedEvent extends WorkflowEventBase { name: string; state: WorkflowCheckpointState }
|
|
@@ -92,8 +93,8 @@ export interface WorkflowMetadata { name: string; description?: string }
|
|
|
92
93
|
export interface HerdrExtensionSettings { enableFullyInspectableMode?: boolean }
|
|
93
94
|
export interface TrajectoryExtensionSettings { port?: number }
|
|
94
95
|
export interface WorkflowExtensionSettings { readonly [namespace: string]: JsonValue; readonly herdr?: Readonly<HerdrExtensionSettings>; readonly trajectory?: Readonly<TrajectoryExtensionSettings> }
|
|
95
|
-
export type WorkflowExtensionSettingsSource = "global" | "project" | "
|
|
96
|
-
export interface WorkflowExtensionSettingsValidatorContext { source: WorkflowExtensionSettingsSource; cwd: string; projectTrusted: boolean; settingsPath?: string
|
|
96
|
+
export type WorkflowExtensionSettingsSource = "global" | "project" | "effective";
|
|
97
|
+
export interface WorkflowExtensionSettingsValidatorContext { source: WorkflowExtensionSettingsSource; cwd: string; projectTrusted: boolean; settingsPath?: string }
|
|
97
98
|
export type WorkflowExtensionSettingsValidator = (settings: Readonly<WorkflowExtensionSettings>, context: Readonly<WorkflowExtensionSettingsValidatorContext>) => void;
|
|
98
99
|
export interface WorkflowSessionStartEvent { type: "session_start"; reason: "startup" | "reload" | "new" | "resume" | "fork"; previousSessionFile?: string; settings: Readonly<WorkflowExtensionSettings> }
|
|
99
100
|
export interface WorkflowRetentionSettings { olderThanDays?: number; maxTerminalRuns?: number }
|
|
@@ -101,14 +102,12 @@ export type CodemodeToolsSetting = "all" | "read-only" | "none";
|
|
|
101
102
|
export interface WorkflowSettings { concurrency: number; backgroundWidget?: boolean; codemodeTools?: CodemodeToolsSetting; modelAliases?: Readonly<Record<string, string>>; skills?: readonly string[]; extensions?: readonly string[]; extensionSettings?: Readonly<WorkflowExtensionSettings>; tools?: readonly string[]; retention?: Readonly<WorkflowRetentionSettings>; worktreePostCreateCommand?: readonly string[] }
|
|
102
103
|
export type WorkflowSettingsOverrides = Partial<Omit<WorkflowSettings, "backgroundWidget" | "codemodeTools">>;
|
|
103
104
|
export interface WorkflowSettingsSources { concurrency: string; modelAliases: string; skills?: string; extensions?: string; tools?: string; extensionSettings?: string; retention?: string; worktreePostCreateCommand?: string }
|
|
104
|
-
export interface WorkflowSettingsResolution { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: Readonly<WorkflowSettings>; project: Readonly<WorkflowSettingsOverrides>; effective: Readonly<WorkflowSettings>; sources: Readonly<WorkflowSettingsSources> }
|
|
105
|
+
export interface WorkflowSettingsResolution { selectorSources?: AgentResourceSelectorSources; globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: Readonly<WorkflowSettings>; project: Readonly<WorkflowSettingsOverrides>; effective: Readonly<WorkflowSettings>; sources: Readonly<WorkflowSettingsSources> }
|
|
105
106
|
export interface AgentResourceSelectors { skills?: readonly string[]; extensions?: readonly string[]; tools?: readonly string[] }
|
|
106
107
|
export interface AgentResourceSelectorSet { skills: readonly string[]; extensions: readonly string[]; tools?: readonly string[] }
|
|
107
|
-
export interface AgentResourceSelectorSources { global: AgentResourceSelectors; project: AgentResourceSelectors;
|
|
108
|
-
export const CONTEXT_FILE_SCOPES = ["global", "project", "cwd"] as const;
|
|
109
|
-
export type ContextFileScope = (typeof CONTEXT_FILE_SCOPES)[number];
|
|
110
|
-
export function isContextFileScope(value: unknown): value is ContextFileScope { return CONTEXT_FILE_SCOPES.some((scope) => scope === value); }
|
|
108
|
+
export interface AgentResourceSelectorSources { global: AgentResourceSelectors; project: AgentResourceSelectors; call?: AgentResourceSelectors }
|
|
111
109
|
export interface AgentResourcePolicy {
|
|
110
|
+
capabilities?: Readonly<{ tools: readonly string[]; skills: readonly string[]; extensions: readonly string[] }>;
|
|
112
111
|
globalSettingsPath: string;
|
|
113
112
|
projectSettingsPath: string;
|
|
114
113
|
projectTrusted: boolean;
|
|
@@ -122,6 +121,8 @@ export interface AgentResourcePolicy {
|
|
|
122
121
|
unmatchedExtensions: readonly string[];
|
|
123
122
|
unmatchedTools?: readonly string[];
|
|
124
123
|
selectorSources: AgentResourceSelectorSources;
|
|
124
|
+
/** Skills loaded by the parent session; ceiling for skills that extensions contribute through resources_discover. Absent for top-level agents. */
|
|
125
|
+
parentSkills?: readonly string[];
|
|
125
126
|
}
|
|
126
127
|
export interface AgentResourceInspection { selectors: AgentResourceSelectorSet; skills: readonly string[]; extensions: readonly string[]; tools: readonly string[]; unmatchedSkills: readonly string[]; unmatchedExtensions: readonly string[]; unmatchedTools: readonly string[]; selectorSources?: AgentResourceSelectorSources }
|
|
127
128
|
|
|
@@ -158,7 +159,6 @@ export interface AgentRecord {
|
|
|
158
159
|
handle?: string;
|
|
159
160
|
turn?: number;
|
|
160
161
|
continuity?: AgentContinuity;
|
|
161
|
-
role?: string;
|
|
162
162
|
requestedModel?: string;
|
|
163
163
|
model: ModelSpec;
|
|
164
164
|
tools: readonly string[];
|
|
@@ -208,11 +208,10 @@ export interface RunRecord {
|
|
|
208
208
|
events?: readonly WorkflowRunEvent[];
|
|
209
209
|
delivery?: WorkflowRunDelivery;
|
|
210
210
|
}
|
|
211
|
-
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION =
|
|
211
|
+
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 6;
|
|
212
212
|
export type WorkflowLaunchMode = "foreground" | "background";
|
|
213
|
-
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; skills?: readonly string[]; extensions?: readonly string[]; overrideSystemPrompt?: boolean; contextFiles?: readonly ContextFileScope[]; extensionSettings?: Readonly<WorkflowExtensionSettings> }
|
|
214
213
|
export interface LaunchSnapshot {
|
|
215
|
-
identityVersion
|
|
214
|
+
identityVersion: number;
|
|
216
215
|
launchMode?: WorkflowLaunchMode;
|
|
217
216
|
script: string;
|
|
218
217
|
args: JsonValue;
|
|
@@ -225,14 +224,18 @@ export interface LaunchSnapshot {
|
|
|
225
224
|
phases?: readonly string[];
|
|
226
225
|
models: readonly string[];
|
|
227
226
|
tools: readonly string[];
|
|
228
|
-
|
|
229
|
-
roles?: Readonly<Record<string, AgentDefinition>>;
|
|
230
|
-
projectRoles?: readonly string[];
|
|
227
|
+
agentConfigurations: Readonly<Record<string, PreparedAgentConfiguration>>;
|
|
231
228
|
schemas: readonly JsonSchema[];
|
|
232
229
|
}
|
|
233
|
-
export interface PreflightCapabilities { models: ReadonlySet<string>; tools: ReadonlySet<string>;
|
|
234
|
-
export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[];
|
|
235
|
-
export interface
|
|
230
|
+
export interface PreflightCapabilities { models: ReadonlySet<string>; tools: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; skipModelAvailability?: boolean }
|
|
231
|
+
export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; }; schemas: readonly JsonSchema[] }
|
|
232
|
+
export interface AgentHandleSendOptions<Schema extends TSchema = never> { outputSchema?: JsonSchema | Schema; timeoutMs?: number | null }
|
|
233
|
+
/** A persistent agent: each send continues the transcript of the previous completed turn. Sends must be awaited one at a time, in the scope that created the handle. */
|
|
234
|
+
export interface AgentHandle {
|
|
235
|
+
readonly name: string;
|
|
236
|
+
send: { <Schema extends TSchema>(prompt: string, options: Readonly<AgentHandleSendOptions<Schema> & { outputSchema: Schema }>): Promise<Static<Schema>>; (prompt: string, options?: Readonly<AgentHandleSendOptions>): Promise<JsonValue> };
|
|
237
|
+
}
|
|
238
|
+
export interface WorkflowOrchestrationContext { agent: { <Schema extends TSchema>(prompt: string, options: Readonly<AgentOptions<Schema> & { outputSchema: Schema }>): Promise<Static<Schema>>; (prompt: string, options?: Readonly<AgentOptions>): Promise<JsonValue>; create: (options: Readonly<AgentOptions & { name: string }>) => AgentHandle }; shell: (command: string, options?: ShellOptions) => Promise<ShellResult>; prompt: (template: string, values: Readonly<Record<string, JsonValue>>) => string; parallel: <Tasks extends ParallelTasks>(operationName: string, tasks: Tasks) => Promise<ParallelResult<Tasks>>; pipeline: <Items extends PipelineItems, Output extends JsonValue>(operationName: string, items: Items, stages: PipelineStages<Items[keyof Items], Output>) => Promise<PipelineResult<Items, Output>>; withWorktree: <Result extends JsonValue>(name: string, callback: WorkflowWorktreeCallback<Result>) => Promise<Result>; checkpoint: (input: CheckpointInput) => Promise<boolean>; phase: (name: string) => void; log: (message: string) => void }
|
|
236
239
|
export interface WorkflowRunContext { cwd: string; sessionId: string; runId: string; workflow: Readonly<WorkflowMetadata>; args: JsonValue; signal: AbortSignal }
|
|
237
240
|
export interface WorkflowFunctionContext extends WorkflowOrchestrationContext { run: Readonly<WorkflowRunContext>; settings: Readonly<WorkflowExtensionSettings>; invoke: (name: string, input: Readonly<Record<string, JsonValue>>, label?: string) => Promise<JsonValue> }
|
|
238
241
|
export type WorkflowWorktreeCallback<Result extends JsonValue = JsonValue> = (reference: Readonly<WorkflowWorktreeReference>) => Result | Promise<Result>;
|
|
@@ -267,6 +270,8 @@ export interface WorkflowAgentSession {
|
|
|
267
270
|
getState(): Readonly<WorkflowAgentSessionState>;
|
|
268
271
|
getSessionStats(): WorkflowAgentSessionStats;
|
|
269
272
|
getLastAssistant(): WorkflowAgentMessage | undefined;
|
|
273
|
+
/** Set by a transport that already delivered `PreparedAgentSession.turnControl` itself; otherwise the runner steers it before the first prompt. */
|
|
274
|
+
readonly turnControlDelivered?: boolean;
|
|
270
275
|
subscribe(listener: (event: WorkflowAgentSessionEvent) => void): () => void;
|
|
271
276
|
subscribeAsync?(listener: (event: WorkflowAgentSessionEvent) => void | Promise<void>): () => void;
|
|
272
277
|
prompt(text: string): Promise<WorkflowAgentTurnResult>;
|
|
@@ -280,7 +285,8 @@ export interface WorkflowAgentSession {
|
|
|
280
285
|
type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
|
|
281
286
|
type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
|
|
282
287
|
export interface SessionInput {
|
|
283
|
-
|
|
288
|
+
/** Read-only: setup hooks cannot move an agent, because Pi derives trust, settings and `.pi` resources from cwd. */
|
|
289
|
+
readonly cwd: string;
|
|
284
290
|
model: ModelSpec;
|
|
285
291
|
tools: SessionTools;
|
|
286
292
|
sessionLabel: string;
|
|
@@ -309,6 +315,8 @@ export interface PreparedAgentSession {
|
|
|
309
315
|
readonly sessionLabel: string;
|
|
310
316
|
readonly sessionPath?: string;
|
|
311
317
|
readonly initialPrompt?: string;
|
|
318
|
+
/** Dynamic per-attempt control text (retry state, budget). The runner steers it before the first prompt unless the session reports `turnControlDelivered`. */
|
|
319
|
+
readonly turnControl?: string;
|
|
312
320
|
readonly agentDir?: string;
|
|
313
321
|
readonly customTools?: readonly ToolDefinition[];
|
|
314
322
|
readonly resultTool?: ToolDefinition;
|
|
@@ -330,17 +338,50 @@ export interface AgentTransport { readonly id: string; createSession(prepared: R
|
|
|
330
338
|
export interface AgentSetup { prompt: string; options: AgentOptions; sessionInput: SessionInput; prepared: Readonly<PreparedAgentSession>; transport: AgentTransport }
|
|
331
339
|
export interface AgentSetupContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal; readonly tuiIndex?: number; readonly tuiLabel?: string; readonly mode?: AgentInspectionMode; readonly settings: Readonly<WorkflowExtensionSettings> }
|
|
332
340
|
|
|
341
|
+
export interface AgentPreparation {
|
|
342
|
+
label?: string;
|
|
343
|
+
model?: string;
|
|
344
|
+
tools: string[];
|
|
345
|
+
skills: string[];
|
|
346
|
+
extensions: string[];
|
|
347
|
+
contextFiles?: ContextFileScope[];
|
|
348
|
+
systemPrompt?: string;
|
|
349
|
+
systemPromptAppend: string;
|
|
350
|
+
settings: WorkflowExtensionSettings;
|
|
351
|
+
}
|
|
352
|
+
export interface AgentPreparationContext {
|
|
353
|
+
readonly options: Readonly<AgentOptions>;
|
|
354
|
+
readonly cwd: string;
|
|
355
|
+
/** Launch project root that owns project configuration; differs from `cwd` inside worktree scopes. Optional for older hosts. */
|
|
356
|
+
readonly projectCwd?: string;
|
|
357
|
+
readonly agentDir: string;
|
|
358
|
+
readonly projectTrusted: boolean;
|
|
359
|
+
/** `dynamicModelAliasNames` lists the `modelAliases` entries resolved by extensions rather than read from static settings. */
|
|
360
|
+
readonly defaults: Readonly<{ model: ModelSpec; modelAliases: Readonly<Record<string, string>>; dynamicModelAliasNames?: readonly string[]; selectorSources: AgentResourceSelectorSources; settings: Readonly<WorkflowExtensionSettings> }>;
|
|
361
|
+
readonly capabilities: Readonly<{ tools: readonly string[]; skills: readonly string[]; extensions: readonly string[] }>;
|
|
362
|
+
readonly knownModels: ReadonlySet<string>;
|
|
363
|
+
readonly availableModels: ReadonlySet<string>;
|
|
364
|
+
readonly signal: AbortSignal;
|
|
365
|
+
readonly mode: AgentInspectionMode;
|
|
366
|
+
}
|
|
367
|
+
export interface AgentPreparationHook { priority?: number; optionsSchema?: JsonSchema; prepare: (configuration: AgentPreparation, context: Readonly<AgentPreparationContext>) => void | Promise<void> }
|
|
368
|
+
/** `source` is the registering extension's module provenance, when it declared one. */
|
|
369
|
+
export interface RegisteredAgentPreparationHook { name: string; priority: number; optionsSchema?: JsonSchema; source?: string; prepare: AgentPreparationHook["prepare"] }
|
|
370
|
+
/**
|
|
371
|
+
* `excludeTools` holds the exact tool names the agent and its ancestors excluded, frozen with the configuration so setup and
|
|
372
|
+
* children apply them even when re-evaluated options change. It is absent only in configurations prepared before it existed, which cannot resume.
|
|
373
|
+
*/
|
|
374
|
+
export interface PreparedAgentConfiguration extends Omit<AgentPreparation, "model"> { model: ModelSpec; projectTrusted: boolean; excludeTools?: string[] }
|
|
333
375
|
export interface AgentSetupHook { priority?: number; setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void> }
|
|
334
376
|
export interface RegisteredAgentSetupHook { name: string; priority: number; setup: AgentSetupHook["setup"] }
|
|
335
377
|
export interface WorkflowExtensionMetadata { version: string; headline: string }
|
|
336
378
|
export interface WorkflowFunctionSource { readonly module: string; readonly export: string; readonly dependencies: readonly string[] }
|
|
337
|
-
export interface WorkflowRoleDirectoryRegistration { path: string; extension: WorkflowExtensionMetadata; readonly builtin?: true }
|
|
338
379
|
export interface AgentAttemptActionUi { notify(message: string, level?: "info" | "warning" | "error"): void; confirm(title: string, message: string): Promise<boolean>; select(title: string, options: readonly string[]): Promise<string | undefined>; input(title: string, placeholder?: string): Promise<string | undefined>; setWorkingMessage?(message?: string): void }
|
|
339
380
|
export interface StandaloneAgentRecord { readonly id: string; readonly name: string; readonly label?: string; readonly state: "running" | "completed" | "failed" | "stopped"; readonly structuralPath?: readonly string[] }
|
|
340
381
|
export interface StandaloneAgentAttemptActionContext { readonly agent: Readonly<StandaloneAgentRecord>; readonly attempt: Readonly<AgentAttemptSummary>; readonly session?: WorkflowAgentSessionReference; readonly liveSession?: WorkflowAgentSession; readonly prepared?: Readonly<PreparedAgentSession>; readonly handoff?: LiveSessionHandoff; readonly signal: AbortSignal; readonly ui: Readonly<AgentAttemptActionUi> }
|
|
341
382
|
export interface AgentAttemptActionContext { readonly run: Readonly<RunRecord>; readonly agent: Readonly<AgentRecord>; readonly attempt: Readonly<AgentAttemptSummary>; readonly session?: WorkflowAgentSessionReference; readonly liveSession?: WorkflowAgentSession; readonly prepared?: Readonly<PreparedAgentSession>; readonly handoff?: LiveSessionHandoff; readonly signal: AbortSignal; readonly ui: Readonly<AgentAttemptActionUi> }
|
|
342
383
|
export interface AgentAttemptAction { readonly label: string; visible(context: Readonly<AgentAttemptActionContext>): boolean; run(context: Readonly<AgentAttemptActionContext>): void | Promise<void>; visibleStandalone?(context: Readonly<StandaloneAgentAttemptActionContext>): boolean; runStandalone?(context: Readonly<StandaloneAgentAttemptActionContext>): void | Promise<void> }
|
|
343
|
-
export interface WorkflowExtension extends WorkflowExtensionMetadata { description?: string; source?: string; dependencies?: readonly string[]; validateSettings?: WorkflowExtensionSettingsValidator; functions?: Readonly<Record<string, WorkflowFunction>>; modelAliases?: Readonly<Record<string, WorkflowModelAlias>>;
|
|
384
|
+
export interface WorkflowExtension extends WorkflowExtensionMetadata { description?: string; source?: string; dependencies?: readonly string[]; validateSettings?: WorkflowExtensionSettingsValidator; functions?: Readonly<Record<string, WorkflowFunction>>; modelAliases?: Readonly<Record<string, WorkflowModelAlias>>; agentPreparationHooks?: Readonly<Record<string, AgentPreparationHook>>; agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>; agentAttemptActions?: Readonly<Record<string, AgentAttemptAction>> }
|
|
344
385
|
export interface WorkflowJournal { get(path: string): JsonValue | undefined; put(path: string, value: JsonValue): void }
|
|
345
386
|
// The brand keeps instanceof working across the bundled extension entries, which each inline their own copy of this class.
|
|
346
387
|
const WORKFLOW_ERROR_BRAND = Symbol.for("pi-extensible-workflows.workflow-error");
|
|
@@ -353,8 +394,8 @@ export class WorkflowError extends Error {
|
|
|
353
394
|
}
|
|
354
395
|
}
|
|
355
396
|
Object.defineProperty(WorkflowError, WORKFLOW_ERROR_BRAND, { value: true });
|
|
356
|
-
export interface WorkflowFailureAgent { id: string; label?: string;
|
|
357
|
-
export interface WorkflowSiblingAgent { id: string; label?: string;
|
|
397
|
+
export interface WorkflowFailureAgent { id: string; label?: string; structuralPath: readonly string[]; attempt: number; transport?: string; session?: WorkflowAgentSessionReference }
|
|
398
|
+
export interface WorkflowSiblingAgent { id: string; label?: string; structuralPath: readonly string[] }
|
|
358
399
|
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 } }
|
|
359
400
|
export interface CheckpointInput { name: string; prompt: string; context: JsonValue }
|
|
360
401
|
export interface FunctionIdentity { path: string; structuralPath: readonly string[]; occurrence: number; worktreeOwner?: string }
|
|
@@ -367,7 +408,7 @@ export interface WorkflowBridge { agent?: (prompt: string, options: Readonly<Rec
|
|
|
367
408
|
export interface WorkflowExecution { result: Promise<JsonValue>; cancel: () => void }
|
|
368
409
|
export interface StaticWorkflowScope { kind: "parallel" | "pipeline"; name: string | null; key: string | null }
|
|
369
410
|
export type StaticWorkflowExecution = "parallel" | "sequential";
|
|
370
|
-
export interface StaticWorkflowCall { kind: WorkflowCallKind; start: number; end: number; name: string | null; prompt: string | null; model: string | null; label?: string | null;
|
|
411
|
+
export interface StaticWorkflowCall { kind: WorkflowCallKind; start: number; end: number; name: string | null; prompt: string | null; model: string | null; label?: string | null; retries?: number | null; outputSchema?: JsonSchema | null; options?: Readonly<Record<string, JsonValue>> | null; optionKeys?: readonly string[]; execution?: StaticWorkflowExecution; structure?: readonly StaticWorkflowScope[] }
|
|
371
412
|
export interface WorkflowCatalogFunction { name: string; version: string; headline: string; description: string; input: JsonSchema; output: JsonSchema }
|
|
372
413
|
export interface WorkflowCatalogModelAlias { name: string; kind: "static" | "dynamic"; provenance: string; version?: string; headline?: string }
|
|
373
414
|
export interface WorkflowCatalogSettings { concurrency: number; backgroundWidget: boolean; modelAliases: Readonly<Record<string, string>>; skills: readonly string[]; extensions: readonly string[]; tools: readonly string[]; extensionSettings?: Readonly<WorkflowExtensionSettings>; worktreePostCreateCommand?: readonly string[]; globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; sources: WorkflowSettingsSources }
|
|
@@ -378,4 +419,4 @@ export interface WorkflowCatalogIndex { functions: readonly WorkflowCatalogIndex
|
|
|
378
419
|
export interface WorkflowCatalogError { error: { code: "NOT_FOUND"; name: string; message: string } }
|
|
379
420
|
export interface WorkflowValidationParameters { name: string; description?: string; script?: string; scriptPath?: string; args?: unknown }
|
|
380
421
|
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; extensionSettings?: Readonly<WorkflowExtensionSettings> }
|
|
381
|
-
export interface ValidatedWorkflowLaunch { script: string; checked: PreflightResult
|
|
422
|
+
export interface ValidatedWorkflowLaunch { script: string; checked: PreflightResult }
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION,
|
|
2
|
+
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, type JsonSchema, type JsonValue, type ModelSpec, type WorkflowErrorCode, type WorkflowExtensionSettings } from "./types.js";
|
|
3
3
|
import { Minimatch } from "minimatch";
|
|
4
|
+
import { THINKING_LEVELS, type ThinkingLevel } from "./types.js";
|
|
4
5
|
export class SerialLane {
|
|
5
6
|
#tail: Promise<void> = Promise.resolve();
|
|
6
7
|
run<T>(task: () => Promise<T>): Promise<T> {
|
|
@@ -92,11 +93,19 @@ export const WORKFLOW_EXTENSION_NAMESPACE = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
|
92
93
|
export function validWorkflowExtensionNamespace(value: string): boolean { return WORKFLOW_EXTENSION_NAMESPACE.test(value) && value !== "__proto__" && value !== "constructor" && value !== "prototype"; }
|
|
93
94
|
export function parseThinking(value: unknown): ModelSpec["thinking"] | undefined { return isThinkingLevel(value) ? value : undefined; }
|
|
94
95
|
export function parseModelReference(value: string): ModelSpec {
|
|
95
|
-
const match = /^([^/:\s]+)\/([
|
|
96
|
+
const match = /^([^/:\s]+)\/([^\s]+)$/.exec(value);
|
|
96
97
|
if (!match?.[1] || !match[2]) fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// The model id is everything after the first slash, so namespaced ids such as
|
|
99
|
+
// `deepseek/deepseek-v4.1-flash:nitro` survive intact. Only a trailing segment
|
|
100
|
+
// that names a thinking level is one.
|
|
101
|
+
const reference = match[2];
|
|
102
|
+
const separator = reference.lastIndexOf(":");
|
|
103
|
+
const suffix = separator === -1 ? undefined : reference.slice(separator + 1);
|
|
104
|
+
if (suffix === "") fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
|
|
105
|
+
const thinking = suffix !== undefined && isThinkingLevel(suffix) ? suffix : undefined;
|
|
106
|
+
const model = thinking === undefined ? reference : reference.slice(0, separator);
|
|
107
|
+
if (model === "") fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
|
|
108
|
+
return { provider: match[1], model, ...(thinking === undefined ? {} : { thinking }) };
|
|
100
109
|
}
|
|
101
110
|
export function assertModelThinking(value: string, path = "model"): void {
|
|
102
111
|
if (value.includes("/") && parseModelReference(value).thinking === undefined) fail("INVALID_METADATA", `${path} must be provider/model:thinking`);
|
|
@@ -163,6 +172,10 @@ export function resolveModelReference(value: string, aliases: Readonly<Record<st
|
|
|
163
172
|
};
|
|
164
173
|
return resolveReference(value, []);
|
|
165
174
|
}
|
|
175
|
+
export function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string {
|
|
176
|
+
const parsed = typeof value === "string" ? resolveModelReference(value, aliases, knownModels, settingsPath) : value;
|
|
177
|
+
return `${parsed.provider}/${parsed.model}`;
|
|
178
|
+
}
|
|
166
179
|
type ToolSource = { getActiveTools(): string[]; getAllTools?(): readonly { name: string; exposure?: string }[] };
|
|
167
180
|
/**
|
|
168
181
|
* Tools a session can reach, and so the ceiling for its agents: the declared tools plus the ones
|
|
@@ -181,10 +194,6 @@ export function physicalModel(spec: ModelSpec, aliases: Readonly<Record<string,
|
|
|
181
194
|
const target = resolveModelReference(spec.model, aliases, knownModels, settingsPath);
|
|
182
195
|
return spec.thinking === undefined ? target : { ...target, thinking: spec.thinking };
|
|
183
196
|
}
|
|
184
|
-
export function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string {
|
|
185
|
-
const parsed = typeof value === "string" ? resolveModelReference(value, aliases, knownModels, settingsPath) : value;
|
|
186
|
-
return `${parsed.provider}/${parsed.model}`;
|
|
187
|
-
}
|
|
188
197
|
export function aliasDrift(previous: Readonly<Record<string, string>>, current: Readonly<Record<string, string>>): string[] {
|
|
189
198
|
return [...new Set([...Object.keys(previous), ...Object.keys(current)])].sort().flatMap((name) => previous[name] === current[name] ? [] : [`${name}: ${previous[name] ?? "(missing)"} -> ${current[name] ?? "(missing)"}`]);
|
|
190
199
|
}
|
package/src/validation.ts
CHANGED
|
@@ -2,14 +2,11 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import * as acorn from "acorn";
|
|
4
4
|
import { Script } from "node:vm";
|
|
5
|
-
import type {
|
|
5
|
+
import type { CheckpointInput, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
6
6
|
import type { WorkflowRegistryApi } from "./registry.js";
|
|
7
|
-
import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue,
|
|
7
|
+
import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue, object, positiveInteger, resourcePatternHasMagic } from "./utils.js";
|
|
8
8
|
import { WORKFLOW_CALL_KINDS } from "./types.js";
|
|
9
9
|
export { validateSchema } from "./utils.js";
|
|
10
|
-
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
11
|
-
export { loadAgentDefinitions, loadProjectAgentDefinitions, parseRoleMarkdown, workflowRoleDirectories } from "./roles.js";
|
|
12
|
-
export type { WorkflowRoleDirectoryInput } from "./roles.js";
|
|
13
10
|
|
|
14
11
|
import { validateContextFileScopes, validateSelectorList } from "./settings.js";
|
|
15
12
|
export { DEFAULT_SETTINGS, loadCodemodeToolsSetting, loadSettings, loadSettingsOverrides, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateContextFileScopes, validateModelAliasAvailability, validateSelectorList, validateWorkflowExtensionSettings, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./settings.js";
|
|
@@ -21,22 +18,6 @@ export function validateCheckpoint(value: unknown): CheckpointInput {
|
|
|
21
18
|
return { name: value.name, prompt: value.prompt, context: value.context };
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
function validateRolePolicies(definitions: Readonly<Record<string, AgentDefinition>>, roles: readonly string[], availableModels: ReadonlySet<string>, aliases: Readonly<Record<string, string>> = {}, knownModels = availableModels, settingsPath?: string): void {
|
|
25
|
-
for (const role of roles) {
|
|
26
|
-
const definition = definitions[role];
|
|
27
|
-
if (!definition) continue;
|
|
28
|
-
if (definition.model !== undefined) {
|
|
29
|
-
const resolved = modelCapability(definition.model, aliases, knownModels, settingsPath);
|
|
30
|
-
if (!availableModels.has(resolved)) {
|
|
31
|
-
if (modelAliasName(definition.model, aliases)) unknownModel(definition.model, resolved, settingsPath);
|
|
32
|
-
fail("UNKNOWN_MODEL", `Unknown model for role ${role}: ${resolved}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
// Role tools absent from the session are tolerated at launch: the runtime resolve emits a
|
|
36
|
-
// warning and the agent runs without them. Doctor still reports unmatched patterns.
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
21
|
function validateWorkflowMetadata(value: unknown): WorkflowMetadata {
|
|
41
22
|
if (!object(value) || typeof value.name !== "string" || value.name.trim() === "") fail("INVALID_METADATA", "Workflow metadata requires a non-empty name");
|
|
42
23
|
if (value.description !== undefined && (typeof value.description !== "string" || value.description.trim() === "")) fail("INVALID_METADATA", "Workflow description must be a non-empty string when provided");
|
|
@@ -309,8 +290,9 @@ export function workflowPrompt(template: string, values: Readonly<Record<string,
|
|
|
309
290
|
});
|
|
310
291
|
}
|
|
311
292
|
|
|
312
|
-
const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "
|
|
313
|
-
function
|
|
293
|
+
const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "excludeTools", "skills", "extensions", "contextFiles", "outputSchema", "retries", "timeoutMs"]);
|
|
294
|
+
export function isCoreAgentOption(key: string): boolean { return AGENT_OPTION_KEYS.has(key); }
|
|
295
|
+
function validateAgentOption(key: string, value: unknown): void {
|
|
314
296
|
switch (key) {
|
|
315
297
|
case "label":
|
|
316
298
|
if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent label must be a non-empty string");
|
|
@@ -318,19 +300,19 @@ function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Rec
|
|
|
318
300
|
case "model":
|
|
319
301
|
if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent model must be a non-empty string");
|
|
320
302
|
assertModelThinking(value, "agent model");
|
|
321
|
-
if (aliases !== undefined) resolveModelReference(value, aliases, knownModels, settingsPath);
|
|
322
303
|
break;
|
|
323
304
|
case "tools":
|
|
324
305
|
case "skills":
|
|
325
306
|
case "extensions":
|
|
326
307
|
validateSelectorList(value, "agent options", key, "INVALID_METADATA", key !== "extensions");
|
|
327
308
|
break;
|
|
309
|
+
case "excludeTools":
|
|
310
|
+
if (!Array.isArray(value) || value.some((tool) => typeof tool !== "string" || !tool.trim() || tool !== tool.trim() || tool.startsWith("!") || resourcePatternHasMagic(tool))) fail("INVALID_METADATA", "agent excludeTools must be an array of exact tool names");
|
|
311
|
+
if (value.includes("workflow_result")) fail("INVALID_METADATA", "agent excludeTools cannot remove workflow_result");
|
|
312
|
+
break;
|
|
328
313
|
case "contextFiles":
|
|
329
314
|
validateContextFileScopes(value, "agent options");
|
|
330
315
|
break;
|
|
331
|
-
case "role":
|
|
332
|
-
if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent role must be a non-empty string");
|
|
333
|
-
break;
|
|
334
316
|
case "outputSchema":
|
|
335
317
|
validateSchema(value, "agent outputSchema");
|
|
336
318
|
break;
|
|
@@ -429,36 +411,101 @@ export function inspectWorkflowScript(script: string): StaticWorkflowCall[] {
|
|
|
429
411
|
if (value.known && jsonValue(value.value)) knownOptionEntries.push([key, value.value]);
|
|
430
412
|
}
|
|
431
413
|
const knownOptions: Record<string, JsonValue> = Object.fromEntries(knownOptionEntries);
|
|
432
|
-
const base = { ...placement, kind, start: call.start, end: call.end, name: null, prompt: staticString(first), model: staticString(propertyNode(options, "model")), label: staticString(propertyNode(options, "label"))
|
|
414
|
+
const base = { ...placement, kind, start: call.start, end: call.end, name: null, prompt: staticString(first), model: staticString(propertyNode(options, "model")), label: staticString(propertyNode(options, "label")) };
|
|
433
415
|
return { ...base, ...(retries.known && typeof retries.value === "number" ? { retries: retries.value } : {}), ...(staticOutputSchema === undefined ? {} : { outputSchema: staticOutputSchema }), ...(optionKeys.length ? { options: knownOptions, optionKeys } : {}) };
|
|
434
416
|
}
|
|
435
|
-
if (kind === "checkpoint") return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null
|
|
436
|
-
if (kind === "shell") return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null
|
|
437
|
-
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null
|
|
417
|
+
if (kind === "checkpoint") return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null };
|
|
418
|
+
if (kind === "shell") return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null };
|
|
419
|
+
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null };
|
|
438
420
|
});
|
|
439
421
|
}
|
|
440
422
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
423
|
+
/** Every option is a preparation input, because any hook may read it; one dynamic value leaves the whole call to runtime. */
|
|
424
|
+
function staticPreparationOptions(node: acorn.AnyNode | undefined, omitted?: string): Record<string, JsonValue> | undefined {
|
|
425
|
+
if (node?.type !== "ObjectExpression") return undefined;
|
|
426
|
+
const options = new Map<string, JsonValue>();
|
|
427
|
+
for (const property of node.properties) {
|
|
428
|
+
if (property.type === "SpreadElement" || property.computed) return undefined;
|
|
429
|
+
const key = propertyKeyName(property);
|
|
430
|
+
if (key === undefined) return undefined;
|
|
431
|
+
if (key === omitted) continue;
|
|
432
|
+
const value = staticValue(property.value);
|
|
433
|
+
// NOTE: an output schema only shapes the result, so a computed one (a const declared elsewhere) does not hide the call.
|
|
434
|
+
if (key === "outputSchema" && !value.known) continue;
|
|
435
|
+
if (!value.known || !jsonValue(value.value)) return undefined;
|
|
436
|
+
options.set(key, value.value);
|
|
446
437
|
}
|
|
438
|
+
return Object.fromEntries(options);
|
|
447
439
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
440
|
+
/**
|
|
441
|
+
* A handle prepares its first turn with the create options merged with that send's options, so create options are complete
|
|
442
|
+
* only when every use of the handle provably passes no send options: `agent.create(...).send(prompt)` directly, or a
|
|
443
|
+
* `const` handle whose every reference is `handle.send(prompt)`. Computed access, destructuring, aliases or any other
|
|
444
|
+
* use leave the handle to runtime.
|
|
445
|
+
*/
|
|
446
|
+
function handlesWithCompleteOptions(program: acorn.Program): acorn.CallExpression[] {
|
|
447
|
+
const parents = new Map<acorn.AnyNode, acorn.AnyNode>();
|
|
448
|
+
const references = new Map<string, acorn.AnyNode[]>();
|
|
449
|
+
const visit = (node: acorn.AnyNode): void => {
|
|
450
|
+
if (node.type === "Identifier") references.set(node.name, [...(references.get(node.name) ?? []), node]);
|
|
451
|
+
for (const child of astChildren(node)) { parents.set(child, node); visit(child); }
|
|
452
|
+
};
|
|
453
|
+
visit(program);
|
|
454
|
+
const plainSend = (member: acorn.AnyNode | undefined): boolean => {
|
|
455
|
+
const call = member ? parents.get(member) : undefined;
|
|
456
|
+
return member?.type === "MemberExpression" && !member.computed && member.property.type === "Identifier" && member.property.name === "send"
|
|
457
|
+
&& call?.type === "CallExpression" && call.callee === member && call.arguments.length <= 1 && call.arguments.every((argument) => argument.type !== "SpreadElement");
|
|
458
|
+
};
|
|
459
|
+
return agentCreateCalls(program).filter((create) => {
|
|
460
|
+
const parent = parents.get(create);
|
|
461
|
+
if (parent?.type === "MemberExpression" && parent.object === create) return plainSend(parent);
|
|
462
|
+
const declaration = parent ? parents.get(parent) : undefined;
|
|
463
|
+
if (parent?.type !== "VariableDeclarator" || parent.init !== create || parent.id.type !== "Identifier" || declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") return false;
|
|
464
|
+
const name = parent.id.name;
|
|
465
|
+
return (references.get(name) ?? []).every((reference) => reference === parent.id || (parents.get(reference)?.type === "MemberExpression" && (parents.get(reference) as acorn.MemberExpression).object === reference && plainSend(parents.get(reference))));
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Distinct, completely static option objects of agent(...) and agent.create(...) calls, so launch can inspect them
|
|
470
|
+
* before any effect. Calls with any dynamic option stay runtime-checked, and so do handles whose sends may add options.
|
|
471
|
+
*/
|
|
472
|
+
export function staticAgentPreparationOptions(script: string, registered: Readonly<Record<string, { run: (...args: never[]) => unknown }>> = {}): Array<Readonly<Record<string, JsonValue>>> {
|
|
473
|
+
const program = parseWorkflow(script);
|
|
474
|
+
const sources = [program, ...registeredFunctionPrograms(program, registered)];
|
|
475
|
+
const distinct = new Map<string, Readonly<Record<string, JsonValue>>>();
|
|
476
|
+
for (const source of sources) for (const options of programAgentOptions(source)) distinct.set(JSON.stringify(options), deepFreeze(options));
|
|
477
|
+
return [...distinct.values()];
|
|
478
|
+
}
|
|
479
|
+
/** Programs of the registered functions the script calls by name, so their agents are inspected before launch like inline ones. */
|
|
480
|
+
function registeredFunctionPrograms(program: acorn.Program, registered: Readonly<Record<string, { run: (...args: never[]) => unknown }>>): acorn.Program[] {
|
|
481
|
+
const called = new Set<string>();
|
|
482
|
+
const visit = (node: acorn.AnyNode): void => {
|
|
483
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && Object.prototype.hasOwnProperty.call(registered, node.callee.name)) called.add(node.callee.name);
|
|
484
|
+
for (const child of astChildren(node)) visit(child);
|
|
485
|
+
};
|
|
486
|
+
visit(program);
|
|
487
|
+
const programs: acorn.Program[] = [];
|
|
488
|
+
for (const name of called) {
|
|
489
|
+
const source = registered[name]?.run.toString() ?? "";
|
|
490
|
+
// A method prints as `async run(...) {}`, which only parses inside an object literal.
|
|
491
|
+
for (const wrapped of [`(${source})`, `({${source}})`]) {
|
|
492
|
+
try { programs.push(acorn.parse(wrapped, { ecmaVersion: "latest", sourceType: "module" })); break; } catch { /* try the next form; an unparsable function stays runtime-checked */ }
|
|
459
493
|
}
|
|
460
494
|
}
|
|
461
|
-
return
|
|
495
|
+
return programs;
|
|
496
|
+
}
|
|
497
|
+
function programAgentOptions(program: acorn.Program): Array<Readonly<Record<string, JsonValue>>> {
|
|
498
|
+
const calls = workflowCalls(program).filter((call) => call.callee.name === "agent").map((call) => call.arguments.some((argument) => argument.type === "SpreadElement") ? undefined : call.arguments.length < 2 ? {} : staticPreparationOptions(callArgument(call, 1)));
|
|
499
|
+
const handles = handlesWithCompleteOptions(program).map((call) => staticPreparationOptions(callArgument(call, 0), "name"));
|
|
500
|
+
return [...calls, ...handles].filter((options): options is Record<string, JsonValue> => options !== undefined);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function validateStaticAgentOptions(node: acorn.AnyNode | undefined): void {
|
|
504
|
+
if (node?.type !== "ObjectExpression") return;
|
|
505
|
+
for (const key of AGENT_OPTION_KEYS) {
|
|
506
|
+
const value = staticValue(propertyNode(node, key));
|
|
507
|
+
if (value.known) validateAgentOption(key, value.value);
|
|
508
|
+
}
|
|
462
509
|
}
|
|
463
510
|
function validateStaticShellOptions(call: WorkflowCall): void {
|
|
464
511
|
if (call.arguments.some((argument) => argument.type === "SpreadElement")) return;
|
|
@@ -495,7 +542,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
495
542
|
const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
|
|
496
543
|
for (const call of calls) {
|
|
497
544
|
const operation = call.callee.name;
|
|
498
|
-
if (operation === "agent") validateStaticAgentOptions(call.arguments[1]
|
|
545
|
+
if (operation === "agent") validateStaticAgentOptions(call.arguments[1]);
|
|
499
546
|
if (operation === "withWorktree") validateStaticWithWorktree(call, compatibility);
|
|
500
547
|
if (operation === "shell") validateStaticShellOptions(call);
|
|
501
548
|
if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement")) continue;
|
|
@@ -506,11 +553,10 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
506
553
|
const handleOptions = agentCreateCalls(program).map((call) => {
|
|
507
554
|
const options = call.arguments.length === 1 ? callArgument(call, 0) : undefined;
|
|
508
555
|
if (options?.type !== "ObjectExpression" || !literalString(propertyNode(options, "name"))?.trim()) fail("INVALID_METADATA", "agent.create requires an options object with a stable explicit name");
|
|
509
|
-
validateStaticAgentOptions(options
|
|
556
|
+
validateStaticAgentOptions(options);
|
|
510
557
|
return options;
|
|
511
558
|
});
|
|
512
559
|
const agentOptions = [...calls.filter((call) => call.callee.name === "agent").map((call) => callArgument(call, 1)), ...handleOptions];
|
|
513
|
-
const dynamicAgentRoles = agentOptions.some((options) => hasDynamicAgentRole(options));
|
|
514
560
|
const staticSchemas: JsonSchema[] = [];
|
|
515
561
|
for (const options of agentOptions) {
|
|
516
562
|
const value = staticValue(propertyNode(options, "outputSchema"));
|
|
@@ -520,25 +566,16 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
520
566
|
staticSchemas.push(schema);
|
|
521
567
|
}
|
|
522
568
|
checkedSchemas.push(...staticSchemas);
|
|
523
|
-
const
|
|
524
|
-
const models = modelRefs.map(({ resolved }) => resolved);
|
|
569
|
+
const models: string[] = [];
|
|
525
570
|
const tools = agentOptions.flatMap((options) => {
|
|
526
571
|
const value = propertyNode(options, "tools");
|
|
527
572
|
return value?.type === "ArrayExpression" ? value.elements.flatMap((element) => { const tool = element && element.type !== "SpreadElement" ? literalString(element) : undefined; return tool === undefined ? [] : [tool]; }) : [];
|
|
528
573
|
});
|
|
529
|
-
const agentTypes = agentOptions.flatMap((options) => { const value = staticString(propertyNode(options, "role")); return value === null ? [] : [value]; });
|
|
530
574
|
for (const pattern of tools) {
|
|
531
575
|
const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
532
576
|
if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !capabilities.tools.has(body)) fail("UNKNOWN_TOOL", `Unknown tool: ${body}`);
|
|
533
577
|
}
|
|
534
|
-
|
|
535
|
-
if (missingModel) {
|
|
536
|
-
if (modelAliasName(missingModel.requested, capabilities.modelAliases ?? {})) unknownModel(missingModel.requested, missingModel.resolved, capabilities.settingsPath);
|
|
537
|
-
fail("UNKNOWN_MODEL", `Unknown model: ${missingModel.resolved}`);
|
|
538
|
-
}
|
|
539
|
-
const missingType = agentTypes.find((type) => !capabilities.agentTypes.has(type));
|
|
540
|
-
if (missingType) fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
|
|
541
|
-
return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas), dynamicAgentRoles });
|
|
578
|
+
return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools }), schemas: deepFreeze(checkedSchemas) });
|
|
542
579
|
}
|
|
543
580
|
|
|
544
581
|
export function validateWorkflowLaunch(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry?: WorkflowRegistryApi): ValidatedWorkflowLaunch {
|
|
@@ -562,23 +599,9 @@ export function validateWorkflowLaunchWithRegistry(params: WorkflowValidationPar
|
|
|
562
599
|
const script = typeof params.script === "string" && params.script.trim() ? params.script : fileScript ?? "";
|
|
563
600
|
if (!script) fail("INVALID_SYNTAX", "Provide script or scriptPath");
|
|
564
601
|
const metadata = validateWorkflowMetadata({ name: explicitName, ...(typeof params.description === "string" ? { description: params.description } : {}) });
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const aliases = context.modelAliases ?? {};
|
|
569
|
-
const knownModels = context.knownModels ?? context.availableModels;
|
|
570
|
-
const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, agentTypes: new Set(Object.keys(agentDefinitions)), modelAliases: aliases, knownModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
|
|
571
|
-
const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
|
|
572
|
-
validateRolePolicies(agentDefinitions, roleNames, context.availableModels, aliases, knownModels, context.settingsPath);
|
|
573
|
-
if (registry?.validateExtensionSettings) {
|
|
574
|
-
const validate = registry.validateExtensionSettings.bind(registry);
|
|
575
|
-
const validatorContext = (source: "effective" | "role", role?: string) => ({ source, cwd: context.cwd, projectTrusted: context.projectTrusted, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}), ...(role === undefined ? {} : { role }) });
|
|
576
|
-
validate(context.extensionSettings, validatorContext("effective"));
|
|
577
|
-
for (const role of roleNames) {
|
|
578
|
-
validate(mergeWorkflowExtensionSettings(context.extensionSettings, agentDefinitions[role]?.extensionSettings), validatorContext("role", role));
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
|
|
602
|
+
const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, modelAliases: context.modelAliases ?? {}, knownModels: context.knownModels ?? context.availableModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
|
|
603
|
+
registry?.validateExtensionSettings(context.extensionSettings, { source: "effective", cwd: context.cwd, projectTrusted: context.projectTrusted, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) });
|
|
604
|
+
return { script, checked };
|
|
582
605
|
}
|
|
583
606
|
|
|
584
607
|
export { createLaunchSnapshot, loadLaunchSnapshot } from "./utils.js";
|
package/starter/index.ts
CHANGED