pi-extensible-workflows 3.3.0 → 3.4.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/src/registry.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { isAbsolute } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { Value } from "typebox/value";
4
- import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
4
+ import type { AgentAttemptAction, JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
5
5
  import { deepFreeze, fail, jsonValue, object } from "./utils.js";
6
6
  import { loadSettings, resolveWorkflowSettings, validateSchema } from "./validation.js";
7
7
 
@@ -28,6 +28,7 @@ export class WorkflowRegistry {
28
28
  readonly #extensions = new Set<Readonly<WorkflowExtension>>();
29
29
  readonly #globals = new Map<string, string>();
30
30
  readonly #hooks = new Map<string, RegisteredAgentSetupHook>();
31
+ readonly #agentAttemptActions = new Map<string, AgentAttemptAction>();
31
32
  readonly #roleDirectories = new Map<string, WorkflowRoleDirectoryRegistration>();
32
33
  readonly #modelAliases = new Map<string, { name: string; version: string; headline: string; extensionDescription: string; resolve: WorkflowModelAlias["resolve"] }>();
33
34
  #frozen = false;
@@ -38,15 +39,16 @@ export class WorkflowRegistry {
38
39
  register(extension: WorkflowExtension): void {
39
40
  if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
40
41
  if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows")) fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
41
- if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "modelAliases", "agentSetupHooks", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim()) fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
42
+ if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "modelAliases", "agentSetupHooks", "agentAttemptActions", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim()) fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
42
43
  const functions = extension.functions ?? {};
43
44
  const variables = extension.variables ?? {};
44
45
  const modelAliases = extension.modelAliases ?? {};
45
46
  const agentSetupHooks = extension.agentSetupHooks ?? {};
47
+ const agentAttemptActions = extension.agentAttemptActions ?? {};
46
48
  const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
47
49
  if (!Array.isArray(roleDirectoryValues)) fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
48
50
  const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
49
- if (!object(functions) || !object(variables) || !object(modelAliases) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && roleDirectories.length === 0)) fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, or role directories");
51
+ if (!object(functions) || !object(variables) || !object(modelAliases) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && roleDirectories.length === 0)) fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, agent attempt actions, or role directories");
50
52
  const names = [...Object.keys(functions), ...Object.keys(variables)];
51
53
  if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
52
54
  for (const name of names) {
@@ -73,12 +75,17 @@ export class WorkflowRegistry {
73
75
  if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority))) fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
74
76
  if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
75
77
  }
76
- const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, ...(roleDirectories.length ? { roleDirectories } : {}) });
78
+ for (const [name, action] of Object.entries(agentAttemptActions)) {
79
+ if (!IDENTIFIER.test(name) || !object(action) || Object.keys(action).some((key) => !["label", "visible", "run"].includes(key)) || typeof action.label !== "string" || !action.label.trim() || typeof action.visible !== "function" || typeof action.run !== "function") fail("INVALID_METADATA", `Invalid agent attempt action: ${name}`);
80
+ if (this.#agentAttemptActions.has(name)) fail("DUPLICATE_NAME", `Agent attempt action already registered: ${name}`);
81
+ }
82
+ const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, agentAttemptActions, ...(roleDirectories.length ? { roleDirectories } : {}) });
77
83
  this.#extensions.add(stored);
78
84
  for (const directory of roleDirectories) if (!this.#roleDirectories.has(directory)) this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline, description: extension.description } }));
79
85
  for (const name of names) this.#globals.set(name, name);
80
86
  for (const [name, alias] of Object.entries(modelAliases)) this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, resolve: alias.resolve });
81
87
  for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
88
+ for (const [name, action] of Object.entries(agentAttemptActions)) this.#agentAttemptActions.set(name, action);
82
89
  }
83
90
 
84
91
  function(name: string): WorkflowFunction {
@@ -162,6 +169,7 @@ export class WorkflowRegistry {
162
169
  agentSetupHooks(): readonly RegisteredAgentSetupHook[] {
163
170
  return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
164
171
  }
172
+ agentAttemptActions(): Readonly<Record<string, AgentAttemptAction>> { return Object.freeze(Object.fromEntries(this.#agentAttemptActions.entries())); }
165
173
  roleDirectories(): readonly string[] {
166
174
  return [...this.#roleDirectories.keys()];
167
175
  }
@@ -188,7 +196,7 @@ export class WorkflowRegistry {
188
196
  return Object.freeze(resolved);
189
197
  }
190
198
  }
191
- export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "roleDirectories" | "roleDirectoryRegistrations">;
199
+ export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "agentAttemptActions" | "roleDirectories" | "roleDirectoryRegistrations">;
192
200
  interface WorkflowRegistryHost { api: WorkflowRegistryApi }
193
201
  const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
194
202
  const globalRegistry = globalThis as typeof globalThis & Record<symbol, WorkflowRegistryHost | undefined>;
@@ -210,6 +218,7 @@ function createWorkflowRegistryApi(registry: WorkflowRegistry): WorkflowRegistry
210
218
  roleDirectories: () => registry.roleDirectories(),
211
219
  roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
212
220
  agentSetupHooks: () => registry.agentSetupHooks(),
221
+ agentAttemptActions: () => registry.agentAttemptActions(),
213
222
  };
214
223
  }
215
224
  function workflowRegistryHost(): WorkflowRegistryHost {
@@ -119,6 +119,12 @@ function readTranscript(path: string): TranscriptSummary | undefined {
119
119
  return summary.model === undefined ? undefined : summary;
120
120
  } catch { return undefined; }
121
121
  }
122
+ function attemptTranscriptPath(attempt: NonNullable<PersistedRun["agents"][number]["attemptDetails"]>[number]): string | undefined {
123
+ const session = attempt.session;
124
+ if (!session || session.transport !== "local" || typeof session.locator !== "object" || session.locator === null || Array.isArray(session.locator)) return undefined;
125
+ const path = session.locator.sessionFile;
126
+ return typeof path === "string" && path ? path : undefined;
127
+ }
122
128
 
123
129
  function resultRunId(result: ToolResult | undefined): string | undefined {
124
130
  if (!result) return undefined;
@@ -157,33 +163,18 @@ async function agentReport(agent: PersistedRun["agents"][number]): Promise<Agent
157
163
  const fallbackThinking = agent.model.thinking;
158
164
  const attempts: AttemptReport[] = [];
159
165
  for (const attempt of agent.attemptDetails ?? []) {
160
- const log = readTranscript(attempt.sessionFile);
166
+ const setup = attempt.setup;
167
+ const path = attemptTranscriptPath(attempt);
168
+ const log = path ? readTranscript(path) : undefined;
161
169
  if (log) {
162
- const model = log.model ?? fallbackModel;
170
+ const model = log.model ?? `${setup.model.provider}/${setup.model.model}`;
163
171
  const cost = log.cost;
164
- attempts.push({
165
- attempt: attempt.attempt,
166
- prompt: log.prompt ?? "(transcript unavailable)",
167
- model,
168
- ...(log.thinking !== undefined ? { thinking: log.thinking } : {}),
169
- cost,
170
- models: log.models.length ? log.models : [{ model, cost }],
171
- ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
172
- ...(attempt.setup ? { setup: attempt.setup } : {}),
173
- });
172
+ attempts.push({ attempt: attempt.attempt, prompt: log.prompt ?? "(transcript unavailable)", model, ...(log.thinking !== undefined ? { thinking: log.thinking } : {}), cost, models: log.models.length ? log.models : [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
174
173
  continue;
175
174
  }
175
+ const model = `${setup.model.provider}/${setup.model.model}`;
176
176
  const cost = attempt.accounting.cost;
177
- attempts.push({
178
- attempt: attempt.attempt,
179
- prompt: "(transcript unavailable)",
180
- model: fallbackModel,
181
- ...(fallbackThinking !== undefined ? { thinking: fallbackThinking } : {}),
182
- cost,
183
- models: [{ model: fallbackModel, cost }],
184
- ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
185
- ...(attempt.setup ? { setup: attempt.setup } : {}),
186
- });
177
+ attempts.push({ attempt: attempt.attempt, prompt: "(transcript unavailable)", model, ...(setup.model.thinking !== undefined ? { thinking: setup.model.thinking } : {}), cost, models: [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
187
178
  }
188
179
  if (!attempts.length) {
189
180
  const cost = agent.accounting?.cost ?? 0;
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentSessionEvent, CreateAgentSessionOptions, InlineExtension, SessionStats, ToolDefinition } from "@earendil-works/pi-coding-agent";
1
+ import type { CreateAgentSessionOptions, InlineExtension, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"] as const;
3
3
  export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"] as const;
4
4
  export const WORKFLOW_CALL_KINDS = ["agent", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"] as const;
@@ -66,22 +66,23 @@ export interface WorkflowSettingsOverrides { concurrency?: number; modelAliases?
66
66
  export interface WorkflowSettingsSources { concurrency: string; modelAliases: string; disabledAgentResources: string }
67
67
  export interface WorkflowSettingsResolution { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: Readonly<WorkflowSettings>; project: Readonly<WorkflowSettingsOverrides>; effective: Readonly<WorkflowSettings>; sources: Readonly<WorkflowSettingsSources> }
68
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[] }
69
+ export interface AgentResourcePolicy { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: AgentResourceExclusions; project: AgentResourceExclusions; effective: AgentResourceExclusions; unmatchedSkills: readonly string[]; unmatchedExtensions: readonly string[]; excludedSkills?: readonly string[]; excludedExtensions?: readonly string[] }
70
70
  export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
71
71
  export const WORKFLOW_AGENT_STALL_THRESHOLD_MS = 10 * 60 * 1000;
72
72
  export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
73
73
  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[] } }
74
- 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 AgentAttemptError { code: string; message: string }
75
+ export interface AgentAttemptSummary { attempt: number; transport: string; session?: WorkflowAgentSessionReference; setup: AgentSetupSummary; error?: AgentAttemptError; accounting: AgentAccounting }
75
76
  export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
76
77
  export interface WorkflowWorktreeReference { readonly path: string; readonly branch: string }
77
- export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; resultPath?: string; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined; lastEventAt?: number }
78
+ export interface AgentRecord { systemPrompt?: string; prompt?: string; id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; resultPath?: string; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined; lastEventAt?: number }
78
79
  export type WorkflowDeliveryMode = "foreground" | "background";
79
80
  export type WorkflowDeliveryStatus = "attached" | "pending" | "delivered";
80
81
  export interface WorkflowRunDelivery { mode: WorkflowDeliveryMode; state: WorkflowDeliveryStatus; toolCallId?: string }
81
82
  export interface WorkflowRunEvent { type: string; message: string }
82
83
  export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
83
84
  export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
84
- 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[]; activeShells?: number; error?: WorkflowErrorShape; failedAt?: string; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[]; delivery?: WorkflowRunDelivery }
85
+ export interface RunRecord { id: string; workflowName: string; cwd: string; sessionId: string; state: RunState; agentSessions: readonly WorkflowAgentSessionReference[]; parentRunId?: string; retry?: WorkflowRetryProvenance; phase?: string; phaseHistory?: readonly WorkflowPhaseRecord[]; agents: readonly AgentRecord[]; activeShells?: number; error?: WorkflowErrorShape; failedAt?: string; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[]; delivery?: WorkflowRunDelivery }
85
86
  export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
86
87
  export type WorkflowLaunchMode = "foreground" | "background";
87
88
  export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; overrideSystemPrompt?: boolean; disabledAgentResources?: AgentResourceExclusions }
@@ -94,23 +95,21 @@ export interface WorkflowFunctionContext extends WorkflowOrchestrationContext {
94
95
  export type WorkflowWorktreeCallback = (reference: Readonly<WorkflowWorktreeReference>) => JsonValue | Promise<JsonValue>;
95
96
  export interface WorkflowFunction { description: string; input: JsonSchema; output: JsonSchema; run: (input: Readonly<Record<string, JsonValue>>, context: Readonly<WorkflowFunctionContext>) => Promise<JsonValue> | JsonValue }
96
97
  export interface WorkflowVariable { description: string; schema: JsonSchema; resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue }
97
- type NativeAgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
98
- type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
99
- export interface NativeSession {
100
- readonly sessionId: string;
101
- readonly sessionFile: string | undefined;
102
- readonly messages: readonly NativeAgentMessage[];
103
- getSessionStats(): NativeSessionStats;
104
- readonly systemPrompt?: string;
105
- readonly model?: { provider: string; model?: string; id?: string };
106
- readonly agent?: { state: { tools: readonly { name: string }[] } };
107
- getLeafId?: () => string | null;
108
- getToolDefinitions?: () => unknown;
109
- subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
110
- prompt(text: string): Promise<void>;
111
- steer?(text: string): Promise<void>;
112
- abort?(): Promise<void>;
113
- dispose(): void;
98
+ export interface WorkflowAgentSessionReference { readonly transport: string; readonly sessionId: string; readonly locator?: JsonValue }
99
+ export interface WorkflowAgentSessionStats { readonly tokens: { readonly input: number; readonly output: number; readonly cacheRead: number; readonly cacheWrite: number; readonly total: number }; readonly cost: number }
100
+ export interface WorkflowAgentMessage { readonly role: string; readonly content?: unknown; readonly stopReason?: string; readonly errorMessage?: string; readonly usage?: { readonly input: number; readonly output: number; readonly cacheRead: number; readonly cacheWrite: number; readonly cost: { readonly total: number } } }
101
+ export interface WorkflowAgentSessionState { readonly model: ModelSpec; readonly thinking?: ModelSpec["thinking"]; readonly tools: readonly string[]; readonly systemPrompt?: string }
102
+ export interface WorkflowAgentSessionEvent { readonly type: string; readonly state?: Readonly<WorkflowAgentSessionState>; readonly message?: WorkflowAgentMessage; readonly assistantMessageEvent?: { readonly type: string }; readonly toolCallId?: string; readonly toolName?: string; readonly isError?: boolean }
103
+ export interface WorkflowAgentTurnResult { readonly assistant?: WorkflowAgentMessage }
104
+ export interface WorkflowAgentSession {
105
+ readonly reference: WorkflowAgentSessionReference;
106
+ getState(): Readonly<WorkflowAgentSessionState>;
107
+ getSessionStats(): WorkflowAgentSessionStats;
108
+ subscribe(listener: (event: WorkflowAgentSessionEvent) => void): () => void;
109
+ prompt(text: string): Promise<WorkflowAgentTurnResult>;
110
+ steer(text: string): Promise<void>;
111
+ abort(): Promise<void>;
112
+ dispose(): Promise<void>;
114
113
  }
115
114
  type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
116
115
  type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
@@ -129,17 +128,36 @@ export interface SessionInput {
129
128
  resourcePolicy?: AgentResourcePolicy;
130
129
  options?: AgentOptions;
131
130
  }
132
- export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
133
- export interface AgentSetup { prompt: string; options: AgentOptions; sessionInput: SessionInput; createSession: SessionFactory }
131
+ export interface PreparedAgentSession {
132
+ readonly cwd: string;
133
+ readonly model: ModelSpec;
134
+ readonly tools: readonly string[];
135
+ readonly sessionLabel: string;
136
+ readonly agentDir?: string;
137
+ readonly customTools?: readonly ToolDefinition[];
138
+ readonly resultTool?: ToolDefinition;
139
+ readonly options?: Readonly<Record<string, JsonValue>>;
140
+ readonly systemPrompt?: string;
141
+ readonly systemPromptAppend?: string;
142
+ readonly extensionFactories?: readonly InlineExtension[];
143
+ readonly additionalSkillPaths?: readonly string[];
144
+ readonly resourcePolicy?: Readonly<AgentResourcePolicy>;
145
+ }
146
+ export interface AgentTransportContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal }
147
+ export interface AgentTransport { readonly id: string; createSession(prepared: Readonly<PreparedAgentSession>, context: Readonly<AgentTransportContext>): Promise<WorkflowAgentSession> }
148
+ export interface AgentSetup { prompt: string; options: AgentOptions; sessionInput: SessionInput; prepared: Readonly<PreparedAgentSession>; transport: AgentTransport }
134
149
  export interface AgentSetupContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal }
135
150
  export interface AgentSetupHook { priority?: number; setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void> }
136
151
  export interface RegisteredAgentSetupHook { name: string; priority: number; setup: AgentSetupHook["setup"] }
137
152
  export interface WorkflowExtensionMetadata { version: string; headline: string; description: string }
138
153
  export interface WorkflowRoleDirectoryRegistration { path: string; extension: WorkflowExtensionMetadata }
139
- 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)[] }
154
+ 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> }
155
+ export interface AgentAttemptActionContext { readonly run: Readonly<RunRecord>; readonly agent: Readonly<AgentRecord>; readonly attempt: Readonly<AgentAttemptSummary>; readonly session?: WorkflowAgentSessionReference; readonly liveSession?: WorkflowAgentSession; readonly signal: AbortSignal; readonly ui: Readonly<AgentAttemptActionUi> }
156
+ export interface AgentAttemptAction { readonly label: string; visible(context: Readonly<AgentAttemptActionContext>): boolean; run(context: Readonly<AgentAttemptActionContext>): void | Promise<void> }
157
+ 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>>; agentAttemptActions?: Readonly<Record<string, AgentAttemptAction>>; roleDirectories?: readonly (string | URL)[] }
140
158
  export interface WorkflowJournal { get(path: string): JsonValue | undefined; put(path: string, value: JsonValue): void }
141
159
  export class WorkflowError extends Error { constructor(public readonly code: WorkflowErrorCode, message: string) { super(message); this.name = "WorkflowError"; } }
142
- export interface WorkflowFailureAgent { id: string; label?: string; role?: string; structuralPath: readonly string[]; attempt: number; sessionId?: string; sessionFile?: string }
160
+ export interface WorkflowFailureAgent { id: string; label?: string; role?: string; structuralPath: readonly string[]; attempt: number; transport?: string; session?: WorkflowAgentSessionReference }
143
161
  export interface WorkflowSiblingAgent { id: string; label?: string; role?: string; structuralPath: readonly string[] }
144
162
  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 } }
145
163
  export interface CheckpointInput { name: string; prompt: string; context: JsonValue }
@@ -160,6 +178,6 @@ export interface WorkflowCatalogIndexFunction { name: string; description: strin
160
178
  export interface WorkflowCatalogIndexVariable { name: string; description: string; schema: JsonSchema }
161
179
  export interface WorkflowCatalogIndex { functions: readonly WorkflowCatalogIndexFunction[]; variables: readonly WorkflowCatalogIndexVariable[]; modelAliases?: Readonly<Record<string, string>>; modelAliasEntries?: readonly WorkflowCatalogModelAlias[]; settings?: WorkflowCatalogSettings }
162
180
  export interface WorkflowCatalogError { error: { code: "NOT_FOUND"; name: string; message: string } }
163
- export interface WorkflowValidationParameters { name?: string; description?: string; script?: string; workflow?: string; args?: unknown }
181
+ export interface WorkflowValidationParameters { name?: string; description?: string; script?: string; scriptPath?: string; workflow?: string; args?: unknown }
164
182
  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 }
165
183
  export interface ValidatedWorkflowLaunch { script: string; checked: PreflightResult; agentDefinitions: Readonly<Record<string, AgentDefinition>>; projectAgentDefinitions: Readonly<Record<string, AgentDefinition>>; roleNames: readonly string[]; functionName?: string }
package/src/validation.ts CHANGED
@@ -682,16 +682,25 @@ export function validateWorkflowLaunch(params: WorkflowValidationParameters, con
682
682
  export function validateWorkflowLaunchWithRegistry(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry?: WorkflowRegistryApi): ValidatedWorkflowLaunch {
683
683
  if (Object.prototype.hasOwnProperty.call(params, "maxAgentLaunches")) fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
684
684
  if (params.script !== undefined && params.workflow !== undefined) fail("INVALID_METADATA", "Provide either script or workflow, not both");
685
+ if (params.scriptPath !== undefined && (typeof params.scriptPath !== "string" || !params.scriptPath.trim())) fail("INVALID_METADATA", "scriptPath must be a non-empty path");
686
+ if ((params.script !== undefined || params.workflow !== undefined) && params.scriptPath !== undefined) fail("INVALID_METADATA", "Provide either script, scriptPath, or workflow, not more than one");
687
+ const scriptPath = typeof params.scriptPath === "string" ? params.scriptPath.trim() : undefined;
688
+ let fileScript: string | undefined;
689
+ if (scriptPath !== undefined) {
690
+ try { fileScript = readFileSync(resolve(context.cwd, scriptPath), "utf8"); }
691
+ catch (error) { fail("INVALID_SYNTAX", `Cannot read workflow script file ${scriptPath}: ${errorText(error)}`); }
692
+ }
685
693
  const functionName = typeof params.workflow === "string" ? params.workflow : undefined;
686
- if (functionName !== undefined && params.name !== undefined) fail("INVALID_METADATA", "Registered function launches do not accept name; workflow is the run name");
687
- const workflowName = functionName ?? (typeof params.name === "string" ? params.name.trim() : "");
694
+ const explicitName = params.name === undefined ? undefined : typeof params.name === "string" ? params.name.trim() : "";
695
+ if (params.name !== undefined && !explicitName) fail("INVALID_METADATA", "Workflow name must be non-empty when provided");
696
+ const workflowName = explicitName ?? functionName ?? "";
688
697
  if (functionName === undefined && !workflowName) fail("INVALID_METADATA", "Inline workflow launches require a non-empty name");
689
698
  const fn = functionName === undefined ? undefined : registry?.function(functionName);
690
699
  if (functionName !== undefined && !registry) fail("MISSING_WORKFLOW", `Registered function is unavailable: ${functionName}`);
691
700
  const args = params.args === undefined ? null : params.args;
692
701
  if (functionName !== undefined && fn && (!object(args) || !jsonValue(args) || !Value.Check(fn.input, args))) fail("RESULT_INVALID", `Invalid input for ${functionName}`);
693
- const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
694
- if (!script) fail("INVALID_SYNTAX", "Provide script or registered function");
702
+ const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : fileScript ?? "";
703
+ if (!script) fail("INVALID_SYNTAX", "Provide script, scriptPath, or registered function");
695
704
  const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
696
705
  const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
697
706
  const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
@@ -8,6 +8,7 @@ export interface WorkflowArtifact { extension: ".js" | ".json" | ".md"; content:
8
8
  export type WorkflowTui = { stop(): void; start(): void; requestRender(force?: boolean): void };
9
9
 
10
10
  export function workflowScriptArtifact(script: string): WorkflowArtifact { return { extension: ".js", content: script }; }
11
+ export function workflowPromptArtifact(prompt: string): WorkflowArtifact { return { extension: ".md", content: prompt }; }
11
12
  export function workflowResultArtifact(value: JsonValue): WorkflowArtifact { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
12
13
 
13
14
  async function spawnWorkflowEditor(command: string, path: string): Promise<number | null> {