pi-extensible-workflows 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +2 -31
  2. package/dist/src/agent-execution.d.ts +67 -9
  3. package/dist/src/agent-execution.js +385 -141
  4. package/dist/src/bundles.d.ts +53 -0
  5. package/dist/src/bundles.js +457 -0
  6. package/dist/src/cli.d.ts +3 -1
  7. package/dist/src/cli.js +156 -22
  8. package/dist/src/doctor-cleanup.js +68 -31
  9. package/dist/src/eval-capture-extension.js +16 -1
  10. package/dist/src/execution.d.ts +1 -1
  11. package/dist/src/execution.js +29 -13
  12. package/dist/src/host.d.ts +12 -9
  13. package/dist/src/host.js +525 -195
  14. package/dist/src/index.d.ts +5 -4
  15. package/dist/src/index.js +3 -2
  16. package/dist/src/persistence.d.ts +43 -8
  17. package/dist/src/persistence.js +54 -0
  18. package/dist/src/registry.d.ts +3 -2
  19. package/dist/src/registry.js +16 -4
  20. package/dist/src/session-inspector.d.ts +12 -2
  21. package/dist/src/session-inspector.js +50 -24
  22. package/dist/src/types.d.ts +141 -60
  23. package/dist/src/types.js +1 -0
  24. package/dist/src/validation.js +28 -7
  25. package/dist/src/workflow-artifacts.d.ts +1 -0
  26. package/dist/src/workflow-artifacts.js +1 -0
  27. package/dist/src/workflow-evals.d.ts +7 -1
  28. package/dist/src/workflow-evals.js +23 -3
  29. package/evals/cases/recovery-completed-worktree.yaml +17 -0
  30. package/evals/cases/recovery-failed-run.yaml +14 -0
  31. package/package.json +1 -1
  32. package/skills/pi-extensible-workflows/SKILL.md +13 -5
  33. package/src/agent-execution.ts +302 -107
  34. package/src/bundles.ts +471 -0
  35. package/src/cli.ts +130 -22
  36. package/src/doctor-cleanup.ts +38 -4
  37. package/src/eval-capture-extension.ts +15 -1
  38. package/src/execution.ts +27 -15
  39. package/src/host.ts +454 -175
  40. package/src/index.ts +5 -4
  41. package/src/persistence.ts +58 -4
  42. package/src/registry.ts +14 -5
  43. package/src/session-inspector.ts +49 -26
  44. package/src/types.ts +55 -30
  45. package/src/validation.ts +19 -6
  46. package/src/workflow-artifacts.ts +1 -0
  47. package/src/workflow-evals.ts +24 -3
package/src/index.ts CHANGED
@@ -6,11 +6,12 @@ export * from "./registry.js";
6
6
  export * from "./execution.js";
7
7
  export * from "./host.js";
8
8
  export * from "./workflow-artifacts.js";
9
+ export * from "./bundles.js";
9
10
  export { default } from "./host.js";
10
- export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
11
- export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
12
- export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
13
- export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./agent-execution.js";
11
+ export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
12
+ export type { AwaitingCheckpoint, CompletedOperation, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, RunSummary, RunSummaryAgent, RunSummaryArtifacts, WorktreeReference } from "./persistence.js";
13
+ export { FairAgentScheduler, WorkflowAgentExecutor, createLocalWorkflowAgentSession, localAgentTransport } from "./agent-execution.js";
14
+ export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, AgentTransport, AgentTransportContext, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult } from "./agent-execution.js";
14
15
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
15
16
  export type { DoctorDiagnostic, DoctorFunction, DoctorOptions, DoctorPiState, DoctorReport, DoctorRole, DoctorSeverity, DoctorTrust } from "./doctor.js";
16
17
  export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
@@ -5,19 +5,32 @@ import { access, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFi
5
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { promisify } from "node:util";
8
- import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./types.js";
8
+ import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowBudgetUsage, WorkflowRunEvent } from "./types.js";
9
9
  import type { OwnershipRecord } from "./agent-execution.js";
10
10
  import { WorkflowError } from "./types.js";
11
11
  import { loadLaunchSnapshot } from "./utils.js";
12
12
 
13
- export interface NativeSessionReference { sessionId: string; sessionFile: string }
14
13
  export interface EffectiveSystemPrompt { sessionId: string; attempt: number; turn: number; sha256: string; prompt: string }
15
- export interface PersistedRun extends RunRecord { nativeSessions: readonly NativeSessionReference[] }
14
+ export type PersistedRun = RunRecord;
15
+ type LoadedPersistedRun = PersistedRun;
16
+ export interface RunSummaryAgent { id: string; name: string; label?: string; state: string; role?: string; attempts: number }
17
+ export interface RunSummaryArtifacts { runDirectory: string; statePath: string; journalPath: string; snapshotPath: string; workflowPath: string; resultPath: string; summaryPath: string }
18
+ export interface RunSummary { schemaVersion: 1; runId: string; sessionId: string; workflowName: string; state: RunRecord["state"]; createdAt: string; updatedAt: string; terminalAt?: string; usage: WorkflowBudgetUsage; agents: readonly RunSummaryAgent[]; error?: RunRecord["error"]; failedAt?: string; replayablePaths: readonly string[]; incompletePaths: readonly string[]; artifacts: RunSummaryArtifacts }
16
19
  export interface CompletedOperation { path: string; value: JsonValue }
17
20
  export interface AwaitingCheckpoint { path: string; name: string; prompt: string; context: JsonValue }
18
21
  export type PendingWorkflowDecision = BudgetApprovalRequest
19
22
  export type PersistedOwnershipNode = OwnershipRecord
20
23
  type Journal = { completed: Record<string, CompletedOperation>; awaiting?: Record<string, AwaitingCheckpoint>; decisions?: Record<string, PendingWorkflowDecision> };
24
+ const TERMINAL_SUMMARY_STATES = new Set(["completed", "failed", "stopped"]);
25
+ const EMPTY_USAGE: WorkflowBudgetUsage = { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 };
26
+ function summaryArtifacts(directory: string): RunSummaryArtifacts { return { runDirectory: directory, statePath: join(directory, "state.json"), journalPath: join(directory, "journal.json"), snapshotPath: join(directory, "snapshot.json"), workflowPath: join(directory, "workflow.js"), resultPath: join(directory, "result.json"), summaryPath: join(directory, "summary.json") }; }
27
+ function summaryFromRun(run: PersistedRun, directory: string, journal: Journal, previous: Partial<RunSummary> | undefined, fallbackCreatedAt: string, now = new Date().toISOString()): RunSummary {
28
+ const createdAt = typeof previous?.createdAt === "string" ? previous.createdAt : fallbackCreatedAt;
29
+ const failedAt = run.failedAt ?? run.error?.failedAt;
30
+ const replayablePaths = [...new Set([...(run.retry?.completedPaths ?? []), ...Object.keys(journal.completed)])];
31
+ const incompletePaths = [...new Set([...(run.retry?.incompletePaths ?? []), ...(failedAt ? [failedAt] : [])])];
32
+ return { schemaVersion: 1, runId: run.id, sessionId: run.sessionId, workflowName: run.workflowName, state: run.state, createdAt, updatedAt: now, ...(previous?.terminalAt || TERMINAL_SUMMARY_STATES.has(run.state) ? { terminalAt: previous?.terminalAt ?? now } : {}), usage: { ...EMPTY_USAGE, ...(run.usage ?? {}) }, agents: run.agents.map(({ id, name, label, state, role, attempts }) => ({ id, name, ...(label ? { label } : {}), state, ...(role ? { role } : {}), attempts })), ...(run.error ? { error: run.error } : {}), ...(failedAt ? { failedAt } : {}), replayablePaths, incompletePaths, artifacts: summaryArtifacts(directory) };
33
+ }
21
34
  export interface WorktreeReference { owner: string; path: string; branch: string; cwd: string; base: string }
22
35
  export interface BorrowedWorktreeBinding { name: string; sourceRunId: string; owner: string }
23
36
 
@@ -41,6 +54,15 @@ export function projectSessionsDirectory(cwd: string, home = homedir()): string
41
54
  export function runsDirectory(cwd: string, sessionId: string, home = homedir()): string {
42
55
  return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
43
56
  }
57
+ export async function listPersistedSessionIds(cwd: string, home = homedir()): Promise<string[]> {
58
+ try {
59
+ const entries = await readdir(projectSessionsDirectory(cwd, home), { withFileTypes: true });
60
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map(({ name }) => name);
61
+ } catch (error) {
62
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
63
+ throw error;
64
+ }
65
+ }
44
66
 
45
67
  const SESSION_OWNER_FILE = "owner.json";
46
68
  const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
@@ -197,6 +219,7 @@ export class RunStore {
197
219
  private journalWrite: Promise<void> = Promise.resolve();
198
220
  // ponytail: serializes one RunStore instance; cross-process run sharing remains unsupported.
199
221
  private stateWrite: Promise<void> = Promise.resolve();
222
+ private summaryWrite: Promise<void> = Promise.resolve();
200
223
  private worktreeWrite: Promise<void> = Promise.resolve();
201
224
  private borrowedWorktreeWrite: Promise<void> = Promise.resolve();
202
225
  private snapshotWrite: Promise<void> = Promise.resolve();
@@ -222,29 +245,58 @@ export class RunStore {
222
245
  await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
223
246
  await atomicJson(join(temporary, "state.json"), run);
224
247
  await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
248
+ await atomicJson(join(temporary, "summary.json"), summaryFromRun(run, this.directory, { completed: {} }, undefined, new Date().toISOString()));
225
249
  await rename(temporary, this.directory);
226
250
  } catch (error) {
227
251
  await rm(temporary, { recursive: true, force: true });
228
252
  throw error;
229
253
  }
230
254
  }
255
+ private async refreshSummary(): Promise<void> {
256
+ const write = this.summaryWrite.then(async () => {
257
+ const run = await json<PersistedRun>(join(this.directory, "state.json"));
258
+ const journal = await json<Journal>(join(this.directory, "journal.json"));
259
+ const previous = await json<Partial<RunSummary>>(join(this.directory, "summary.json")).catch(() => undefined);
260
+ const fallbackCreatedAt = await stat(join(this.directory, "state.json")).then((value) => new Date(value.mtimeMs).toISOString());
261
+ await atomicJson(join(this.directory, "summary.json"), summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt));
262
+ });
263
+ this.summaryWrite = write.catch(() => undefined);
264
+ await write;
265
+ }
266
+ private refreshSummaryBestEffort(): void { void this.refreshSummary().catch(() => undefined); }
231
267
 
232
268
  async isComplete(): Promise<boolean> {
233
269
  try { await Promise.all([access(join(this.directory, "snapshot.json")), access(join(this.directory, "journal.json")), access(join(this.directory, "ownership.json")), access(join(this.directory, "state.json"))]); return true; }
234
270
  catch { return false; }
235
271
  }
236
272
 
237
- async load(): Promise<{ run: PersistedRun; snapshot: Readonly<LaunchSnapshot> }> {
273
+ async load(): Promise<{ run: LoadedPersistedRun; snapshot: Readonly<LaunchSnapshot> }> {
238
274
  await this.stateWrite;
239
275
  const run = await json<PersistedRun>(join(this.directory, "state.json"));
240
276
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId) throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run belongs to another cwd or Pi session");
277
+ const persisted = run as unknown as Record<string, unknown>;
278
+ if (!Array.isArray(persisted.agentSessions) || Object.hasOwn(persisted, "nativeSessions")) throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run uses an unsupported agent session format");
241
279
  return { run, snapshot: loadLaunchSnapshot(await json<LaunchSnapshot>(join(this.directory, "snapshot.json"))) };
242
280
  }
281
+ async loadSummary(): Promise<RunSummary> {
282
+ await this.stateWrite;
283
+ await this.journalWrite;
284
+ await this.summaryWrite;
285
+ const run = await json<PersistedRun>(join(this.directory, "state.json"));
286
+ const journal = await json<Journal>(join(this.directory, "journal.json"));
287
+ const previous = await json<RunSummary>(join(this.directory, "summary.json")).catch(() => undefined);
288
+ const [stateStat, journalStat] = await Promise.all([stat(join(this.directory, "state.json")), stat(join(this.directory, "journal.json"))]);
289
+ const fallbackCreatedAt = new Date(stateStat.mtimeMs).toISOString();
290
+ const previousUpdatedAt = previous?.updatedAt === undefined ? Number.NaN : Date.parse(previous.updatedAt);
291
+ const updatedAt = new Date(Math.max(stateStat.mtimeMs, journalStat.mtimeMs, Number.isNaN(previousUpdatedAt) ? 0 : previousUpdatedAt)).toISOString();
292
+ return summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt, updatedAt);
293
+ }
243
294
 
244
295
  async saveState(run: PersistedRun): Promise<void> {
245
296
  const write = this.stateWrite.then(async () => {
246
297
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId) throw new WorkflowError("INTERNAL_ERROR", "Run identity does not match its session-scoped store");
247
298
  await atomicJson(join(this.directory, "state.json"), run);
299
+ this.refreshSummaryBestEffort();
248
300
  });
249
301
  this.stateWrite = write.catch(() => undefined);
250
302
  await write;
@@ -258,6 +310,7 @@ export class RunStore {
258
310
  result = await update(current);
259
311
  if (resolve(result.cwd) !== this.cwd || result.sessionId !== this.sessionId || result.id !== this.runId) throw new WorkflowError("INTERNAL_ERROR", "Run identity does not match its session-scoped store");
260
312
  await atomicJson(join(this.directory, "state.json"), result);
313
+ this.refreshSummaryBestEffort();
261
314
  });
262
315
  this.stateWrite = write.catch(() => undefined);
263
316
  await write;
@@ -308,6 +361,7 @@ export class RunStore {
308
361
  journal.awaiting ??= {};
309
362
  result = await update(journal);
310
363
  await atomicJson(journalPath, journal);
364
+ this.refreshSummaryBestEffort();
311
365
  });
312
366
  this.journalWrite = write.catch(() => undefined);
313
367
  await write;
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 {
@@ -5,7 +5,7 @@ import { createInterface } from "node:readline/promises";
5
5
  import { stdin, stdout } from "node:process";
6
6
  import { highlightCode, initTheme, SessionManager, truncateToVisualLines, type SessionEntry, type SessionInfo } from "@earendil-works/pi-coding-agent";
7
7
  import { formatBudgetStatus, inspectWorkflowScript, type AgentSetupSummary, type ModelSpec, type StaticWorkflowCall } from "./index.js";
8
- import { listRunIds, RunStore, type PersistedRun } from "./persistence.js";
8
+ import { listPersistedSessionIds, listRunIds, RunStore, type PersistedRun, type RunSummary } from "./persistence.js";
9
9
 
10
10
  export interface ModelUsage { model: string; cost: number }
11
11
  export interface AttemptReport { attempt: number; prompt: string; model: string; thinking?: ModelSpec["thinking"]; cost: number; models: readonly ModelUsage[]; error?: string; setup?: AgentSetupSummary }
@@ -13,6 +13,8 @@ export interface AgentReport { name: string; label?: string; state: string; role
13
13
  export interface WorkflowReport { name: string; description?: string; status: string; runId?: string; script?: string; calls: readonly StaticWorkflowCall[]; parseError?: string; cost: number; models: readonly ModelUsage[]; agents: readonly AgentReport[]; budget?: PersistedRun["budget"]; budgetVersion?: number; usage?: PersistedRun["usage"]; budgetEvents?: PersistedRun["budgetEvents"]; events?: readonly { type: string; message: string }[] }
14
14
  export interface SessionReport { id: string; cwd: string; path: string; cost: number; models: readonly ModelUsage[]; workflows: readonly WorkflowReport[]; totalCost: number; totalModels: readonly ModelUsage[] }
15
15
  export interface InspectorViewState { view: "list" | "detail" | "script"; selected: number; scroll: number }
16
+ export type InspectMode = "tui" | "json" | "summary";
17
+ export interface PersistedSessionSummary { schemaVersion: 1; cwd: string; sessionId: string; runs: readonly RunSummary[] }
16
18
 
17
19
  type TranscriptSummary = { prompt?: string; cost: number; models: readonly ModelUsage[]; model?: string; thinking?: ModelSpec["thinking"] };
18
20
  type ToolResult = { toolCallId: string; isError: boolean; content: unknown; details?: unknown };
@@ -117,6 +119,12 @@ function readTranscript(path: string): TranscriptSummary | undefined {
117
119
  return summary.model === undefined ? undefined : summary;
118
120
  } catch { return undefined; }
119
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
+ }
120
128
 
121
129
  function resultRunId(result: ToolResult | undefined): string | undefined {
122
130
  if (!result) return undefined;
@@ -155,33 +163,18 @@ async function agentReport(agent: PersistedRun["agents"][number]): Promise<Agent
155
163
  const fallbackThinking = agent.model.thinking;
156
164
  const attempts: AttemptReport[] = [];
157
165
  for (const attempt of agent.attemptDetails ?? []) {
158
- const log = readTranscript(attempt.sessionFile);
166
+ const setup = attempt.setup;
167
+ const path = attemptTranscriptPath(attempt);
168
+ const log = path ? readTranscript(path) : undefined;
159
169
  if (log) {
160
- const model = log.model ?? fallbackModel;
170
+ const model = log.model ?? `${setup.model.provider}/${setup.model.model}`;
161
171
  const cost = log.cost;
162
- attempts.push({
163
- attempt: attempt.attempt,
164
- prompt: log.prompt ?? "(transcript unavailable)",
165
- model,
166
- ...(log.thinking !== undefined ? { thinking: log.thinking } : {}),
167
- cost,
168
- models: log.models.length ? log.models : [{ model, cost }],
169
- ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
170
- ...(attempt.setup ? { setup: attempt.setup } : {}),
171
- });
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 });
172
173
  continue;
173
174
  }
175
+ const model = `${setup.model.provider}/${setup.model.model}`;
174
176
  const cost = attempt.accounting.cost;
175
- attempts.push({
176
- attempt: attempt.attempt,
177
- prompt: "(transcript unavailable)",
178
- model: fallbackModel,
179
- ...(fallbackThinking !== undefined ? { thinking: fallbackThinking } : {}),
180
- cost,
181
- models: [{ model: fallbackModel, cost }],
182
- ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
183
- ...(attempt.setup ? { setup: attempt.setup } : {}),
184
- });
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 });
185
178
  }
186
179
  if (!attempts.length) {
187
180
  const cost = agent.accounting?.cost ?? 0;
@@ -334,7 +327,26 @@ export function renderInspector(report: SessionReport, state: InspectorViewState
334
327
  const scroll = Math.max(0, Math.min(state.scroll, Math.max(0, fitted.length - room)));
335
328
  return [...header, ...fitted.slice(scroll, scroll + room), ...footer].slice(0, height);
336
329
  }
337
-
330
+ export async function loadPersistedSessionSummary(cwd: string, sessionId: string, home = homedir(), failedOnly = false): Promise<PersistedSessionSummary> {
331
+ const runs: RunSummary[] = [];
332
+ for (const runId of (await listRunIds(cwd, sessionId, home)).sort()) {
333
+ try {
334
+ const summary = await new RunStore(cwd, sessionId, runId, home).loadSummary();
335
+ if (!failedOnly || summary.state === "failed") runs.push(summary);
336
+ } catch { /* Ignore corrupt or concurrently removed runs. */ }
337
+ }
338
+ return { schemaVersion: 1, cwd, sessionId, runs };
339
+ }
340
+ export async function loadPersistedSummaries(cwd: string, sessionId: string | undefined, home = homedir(), failedOnly = false): Promise<readonly PersistedSessionSummary[]> {
341
+ const sessionIds = sessionId ? [sessionId] : (await listPersistedSessionIds(cwd, home)).sort();
342
+ const sessions = await Promise.all(sessionIds.map((id) => loadPersistedSessionSummary(cwd, id, home, failedOnly)));
343
+ return sessionId || !failedOnly ? sessions : sessions.filter((session) => session.runs.length > 0);
344
+ }
345
+ export function formatPersistedRunSummary(summary: RunSummary, sessionId = summary.sessionId): string {
346
+ const counts = summary.agents.reduce<Record<string, number>>((result, agent) => { result[agent.state] = (result[agent.state] ?? 0) + 1; return result; }, {});
347
+ const status = Object.entries(counts).map(([state, count]) => `${state}=${String(count)}`).join(", ") || "agents=0";
348
+ return `${sessionId} ${summary.runId} ${summary.workflowName} ${summary.state} ${status} updated=${summary.updatedAt}`;
349
+ }
338
350
  function nextState(current: InspectorViewState, key: string, workflowCount: number): InspectorViewState {
339
351
  if (current.view === "list") {
340
352
  if (key === "up") return { ...current, selected: Math.max(0, current.selected - 1) };
@@ -385,8 +397,19 @@ async function askSessionId(): Promise<string> {
385
397
  export async function resolveSession(query: string, sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR): Promise<SessionInfo> {
386
398
  return matchSession(query, await SessionManager.listAll(sessionDir));
387
399
  }
388
-
389
- export async function runSessionInspector(sessionId?: string): Promise<void> {
400
+ export async function runSessionInspector(sessionId?: string, mode: InspectMode = "tui", cwd = process.cwd(), home = homedir(), write: (text: string) => void = (text) => { stdout.write(text); }, failedOnly = false): Promise<void> {
401
+ if (mode !== "tui") {
402
+ const sessions = await loadPersistedSummaries(cwd, sessionId?.trim() || undefined, home, failedOnly);
403
+ if (mode === "json") {
404
+ const value = sessionId?.trim() ? sessions[0] : undefined;
405
+ write(`${JSON.stringify(value ?? { schemaVersion: 1, cwd, sessions })}\n`);
406
+ return;
407
+ }
408
+ const emptyLabel = failedOnly ? "no failed runs" : "no persisted runs";
409
+ const lines = sessions.flatMap((session) => session.runs.length ? session.runs.map((run) => formatPersistedRunSummary(run, session.sessionId)) : [`${session.sessionId} (${emptyLabel})`]);
410
+ write(`${lines.length ? lines.join("\n") : failedOnly ? "No failed workflow runs." : "No persisted workflow runs."}\n`);
411
+ return;
412
+ }
390
413
  const query = sessionId?.trim() || await askSessionId();
391
414
  if (!query) throw new Error("Session ID is required.");
392
415
  const session = await resolveSession(query);
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;
@@ -45,7 +45,7 @@ export interface WorkflowBudgetUsage { tokens: number; costUsd: number; duration
45
45
  export type BudgetEventType = "soft_crossed" | "hard_overrun" | "hard_exhausted" | "adjustment_requested" | "adjustment_approved" | "adjustment_rejected";
46
46
  export interface BudgetEvent { type: BudgetEventType; budgetVersion: number; dimensions: readonly BudgetDimension[]; usage: WorkflowBudgetUsage; limits: WorkflowBudget; at: number; proposalId?: string; previous?: WorkflowBudget; proposed?: WorkflowBudget }
47
47
  export interface BudgetApprovalRequest { kind: "budget"; proposalId: string; runId: string; consumed: WorkflowBudgetUsage; previous: WorkflowBudget; proposed: WorkflowBudget; budgetVersion: number }
48
- export interface WorkflowErrorShape { code: WorkflowErrorCode; message: string }
48
+ export interface WorkflowErrorShape { code: WorkflowErrorCode; message: string; failedAt?: string }
49
49
  export interface WorkflowEventBase { runId: string; sessionId: string; workflowName: string; cwd: string; runDirectory: string; timestamp: number }
50
50
  export type WorkflowRunStartedEvent = WorkflowEventBase;
51
51
  export type WorkflowRunResumedEvent = WorkflowEventBase;
@@ -66,21 +66,27 @@ 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
+ export const WORKFLOW_AGENT_STALL_THRESHOLD_MS = 10 * 60 * 1000;
71
72
  export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
72
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[] } }
73
- export interface AgentAttemptSummary { attempt: number; sessionId: string; sessionFile: string; error?: { code: string; message: string }; accounting: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; setup?: AgentSetupSummary }
74
+ export interface AgentAttemptError { code: string; message: string }
75
+ export interface AgentAttemptSummary { attempt: number; transport: string; session?: WorkflowAgentSessionReference; setup: AgentSetupSummary; error?: AgentAttemptError; accounting: AgentAccounting }
74
76
  export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
75
77
  export interface WorkflowWorktreeReference { readonly path: string; readonly branch: string }
76
- export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; 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 }
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 }
79
+ export type WorkflowDeliveryMode = "foreground" | "background";
80
+ export type WorkflowDeliveryStatus = "attached" | "pending" | "delivered";
81
+ export interface WorkflowRunDelivery { mode: WorkflowDeliveryMode; state: WorkflowDeliveryStatus; toolCallId?: string }
77
82
  export interface WorkflowRunEvent { type: string; message: string }
78
83
  export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
79
84
  export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
80
- export interface RunRecord { id: string; workflowName: string; cwd: string; sessionId: string; state: RunState; parentRunId?: string; retry?: WorkflowRetryProvenance; phase?: string; phaseHistory?: readonly WorkflowPhaseRecord[]; agents: readonly AgentRecord[]; error?: WorkflowErrorShape; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[] }
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 }
81
86
  export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
82
- export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; disabledAgentResources?: AgentResourceExclusions }
83
- export interface LaunchSnapshot { identityVersion?: number; launchKind?: "inline" | "function"; functionName?: string; script: string; args: JsonValue; metadata: WorkflowMetadata; settings: WorkflowSettings; settingsSources?: WorkflowSettingsSources; budget?: WorkflowBudget; settingsPath?: string; modelAliases?: Readonly<Record<string, string>>; phases?: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[]; roles?: Readonly<Record<string, AgentDefinition>>; projectRoles?: readonly string[]; schemas: readonly JsonSchema[] }
87
+ export type WorkflowLaunchMode = "foreground" | "background";
88
+ export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; overrideSystemPrompt?: boolean; disabledAgentResources?: AgentResourceExclusions }
89
+ export interface LaunchSnapshot { identityVersion?: number; launchKind?: "inline" | "function"; functionName?: string; launchMode?: WorkflowLaunchMode; script: string; args: JsonValue; metadata: WorkflowMetadata; settings: WorkflowSettings; settingsSources?: WorkflowSettingsSources; budget?: WorkflowBudget; settingsPath?: string; modelAliases?: Readonly<Record<string, string>>; phases?: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[]; roles?: Readonly<Record<string, AgentDefinition>>; projectRoles?: readonly string[]; schemas: readonly JsonSchema[] }
84
90
  export interface PreflightCapabilities { models: ReadonlySet<string>; tools: ReadonlySet<string>; agentTypes: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; skipModelAvailability?: boolean }
85
91
  export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[] }; schemas: readonly JsonSchema[]; dynamicAgentRoles: boolean }
86
92
  export interface WorkflowOrchestrationContext { agent: (prompt: string, options?: Readonly<AgentOptions>) => Promise<JsonValue>; shell: (command: string, options?: ShellOptions) => Promise<ShellResult>; prompt: (template: string, values: Readonly<Record<string, JsonValue>>) => string; parallel: (...args: readonly unknown[]) => Promise<JsonValue>; pipeline: (...args: readonly unknown[]) => Promise<JsonValue>; withWorktree: (name: string, callback: WorkflowWorktreeCallback) => Promise<JsonValue>; checkpoint: (...args: readonly unknown[]) => Promise<boolean>; phase: (name: string) => void; log: (message: string) => void }
@@ -89,23 +95,21 @@ export interface WorkflowFunctionContext extends WorkflowOrchestrationContext {
89
95
  export type WorkflowWorktreeCallback = (reference: Readonly<WorkflowWorktreeReference>) => JsonValue | Promise<JsonValue>;
90
96
  export interface WorkflowFunction { description: string; input: JsonSchema; output: JsonSchema; run: (input: Readonly<Record<string, JsonValue>>, context: Readonly<WorkflowFunctionContext>) => Promise<JsonValue> | JsonValue }
91
97
  export interface WorkflowVariable { description: string; schema: JsonSchema; resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue }
92
- type NativeAgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
93
- type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
94
- export interface NativeSession {
95
- readonly sessionId: string;
96
- readonly sessionFile: string | undefined;
97
- readonly messages: readonly NativeAgentMessage[];
98
- getSessionStats(): NativeSessionStats;
99
- readonly systemPrompt?: string;
100
- readonly model?: { provider: string; model?: string; id?: string };
101
- readonly agent?: { state: { tools: readonly { name: string }[] } };
102
- getLeafId?: () => string | null;
103
- getToolDefinitions?: () => unknown;
104
- subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
105
- prompt(text: string): Promise<void>;
106
- steer?(text: string): Promise<void>;
107
- abort?(): Promise<void>;
108
- dispose(): void;
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>;
109
113
  }
110
114
  type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
111
115
  type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
@@ -117,22 +121,43 @@ export interface SessionInput {
117
121
  agentDir?: string;
118
122
  customTools?: SessionCustomTools;
119
123
  resultTool?: ToolDefinition;
124
+ systemPrompt?: string;
120
125
  systemPromptAppend?: string;
121
126
  extensionFactories?: InlineExtension[];
127
+ additionalSkillPaths?: readonly string[];
122
128
  resourcePolicy?: AgentResourcePolicy;
123
129
  options?: AgentOptions;
124
130
  }
125
- export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
126
- 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 }
127
149
  export interface AgentSetupContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal }
128
150
  export interface AgentSetupHook { priority?: number; setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void> }
129
151
  export interface RegisteredAgentSetupHook { name: string; priority: number; setup: AgentSetupHook["setup"] }
130
152
  export interface WorkflowExtensionMetadata { version: string; headline: string; description: string }
131
153
  export interface WorkflowRoleDirectoryRegistration { path: string; extension: WorkflowExtensionMetadata }
132
- export interface WorkflowExtension extends WorkflowExtensionMetadata { functions?: Readonly<Record<string, WorkflowFunction>>; variables?: Readonly<Record<string, WorkflowVariable>>; modelAliases?: Readonly<Record<string, WorkflowModelAlias>>; agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>; roleDirectories?: readonly (string | URL)[] }
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)[] }
133
158
  export interface WorkflowJournal { get(path: string): JsonValue | undefined; put(path: string, value: JsonValue): void }
134
159
  export class WorkflowError extends Error { constructor(public readonly code: WorkflowErrorCode, message: string) { super(message); this.name = "WorkflowError"; } }
135
- export interface WorkflowFailureAgent { id: string; label?: string; role?: string; structuralPath: readonly string[]; attempt: number; sessionId?: string; sessionFile?: string }
160
+ export interface WorkflowFailureAgent { id: string; label?: string; role?: string; structuralPath: readonly string[]; attempt: number; transport?: string; session?: WorkflowAgentSessionReference }
136
161
  export interface WorkflowSiblingAgent { id: string; label?: string; role?: string; structuralPath: readonly string[] }
137
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 } }
138
163
  export interface CheckpointInput { name: string; prompt: string; context: JsonValue }
@@ -153,6 +178,6 @@ export interface WorkflowCatalogIndexFunction { name: string; description: strin
153
178
  export interface WorkflowCatalogIndexVariable { name: string; description: string; schema: JsonSchema }
154
179
  export interface WorkflowCatalogIndex { functions: readonly WorkflowCatalogIndexFunction[]; variables: readonly WorkflowCatalogIndexVariable[]; modelAliases?: Readonly<Record<string, string>>; modelAliasEntries?: readonly WorkflowCatalogModelAlias[]; settings?: WorkflowCatalogSettings }
155
180
  export interface WorkflowCatalogError { error: { code: "NOT_FOUND"; name: string; message: string } }
156
- export interface WorkflowValidationParameters { name?: string; description?: string; script?: string; workflow?: string; args?: unknown }
181
+ export interface WorkflowValidationParameters { name?: string; description?: string; script?: string; scriptPath?: string; workflow?: string; args?: unknown }
157
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 }
158
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
@@ -141,7 +141,7 @@ export function parseRoleMarkdown(content: string, strict = false, rolePath?: st
141
141
  if (end < 0) return { prompt: content };
142
142
  const meta: Record<string, string> = {};
143
143
  for (const line of content.slice(4, end).split("\n")) {
144
- const match = /^(model|thinking|tools|description)\s*:\s*(.+)$/.exec(line.trim());
144
+ const match = /^(model|thinking|tools|description|overrideSystemPrompt|override_system_prompt|is_system_prompt)\s*:\s*(.+)$/.exec(line.trim());
145
145
  if (match?.[1] && match[2]) meta[match[1]] = match[2].trim();
146
146
  }
147
147
  const tools = meta.tools ? meta.tools.replace(/^\[|\]$/g, "").split(",").map((tool) => tool.trim().replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "")).filter(Boolean) : undefined;
@@ -152,6 +152,8 @@ export function parseRoleMarkdown(content: string, strict = false, rolePath?: st
152
152
  if (meta.description) definition.description = meta.description.replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
153
153
  if (thinking) definition.thinking = thinking as NonNullable<AgentDefinition["thinking"]>;
154
154
  if (tools) definition.tools = tools;
155
+ const overrideSystemPrompt = meta.overrideSystemPrompt ?? meta.override_system_prompt ?? meta.is_system_prompt;
156
+ if (overrideSystemPrompt) definition.overrideSystemPrompt = overrideSystemPrompt === "true";
155
157
  return definition;
156
158
  }
157
159
  const normalized = content.replace(/\r\n?/g, "\n");
@@ -161,12 +163,14 @@ export function parseRoleMarkdown(content: string, strict = false, rolePath?: st
161
163
  catch (error) { fail("INVALID_METADATA", `Invalid role frontmatter: ${errorText(error)}`); }
162
164
  if (!object(parsed.frontmatter)) fail("INVALID_METADATA", "Role frontmatter must be an object");
163
165
  const { model, thinking, tools, description, disabledAgentResources } = parsed.frontmatter;
166
+ const overrideSystemPrompt = parsed.frontmatter.overrideSystemPrompt ?? parsed.frontmatter.override_system_prompt ?? parsed.frontmatter.is_system_prompt;
164
167
  if (model !== undefined && (typeof model !== "string" || model.trim() === "")) fail("INVALID_METADATA", "Role model must be a non-empty string");
165
168
  if (thinking !== undefined && (typeof thinking !== "string" || !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinking))) fail("INVALID_METADATA", `Invalid role thinking level: ${typeof thinking === "string" ? thinking : typeof thinking}`);
166
169
  if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description))) fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
170
+ if (overrideSystemPrompt !== undefined && typeof overrideSystemPrompt !== "boolean") fail("INVALID_METADATA", "Role overrideSystemPrompt must be a boolean");
167
171
  if (tools !== undefined && (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string" || tool.trim() === ""))) fail("INVALID_METADATA", "Role tools must be an array of non-empty strings");
168
172
  const normalizedResources = validateAgentResourceExclusions(disabledAgentResources, rolePath ?? "<role>", "INVALID_METADATA");
169
- return { prompt: parsed.body, ...(typeof description === "string" ? { description: description.trim() } : {}), ...(typeof model === "string" ? { model: model.trim() } : {}), ...(typeof thinking === "string" ? { thinking: thinking as NonNullable<AgentDefinition["thinking"]> } : {}), ...(Array.isArray(tools) ? { tools: tools.map((tool) => (tool as string).trim()) } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
173
+ return { prompt: parsed.body, ...(typeof description === "string" ? { description: description.trim() } : {}), ...(typeof model === "string" ? { model: model.trim() } : {}), ...(typeof thinking === "string" ? { thinking: thinking as NonNullable<AgentDefinition["thinking"]> } : {}), ...(Array.isArray(tools) ? { tools: tools.map((tool) => (tool as string).trim()) } : {}), ...(typeof overrideSystemPrompt === "boolean" ? { overrideSystemPrompt } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
170
174
  }
171
175
 
172
176
  const ROLE_DIRECTORY = "pi-extensible-workflows";
@@ -678,16 +682,25 @@ export function validateWorkflowLaunch(params: WorkflowValidationParameters, con
678
682
  export function validateWorkflowLaunchWithRegistry(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry?: WorkflowRegistryApi): ValidatedWorkflowLaunch {
679
683
  if (Object.prototype.hasOwnProperty.call(params, "maxAgentLaunches")) fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
680
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
+ }
681
693
  const functionName = typeof params.workflow === "string" ? params.workflow : undefined;
682
- if (functionName !== undefined && params.name !== undefined) fail("INVALID_METADATA", "Registered function launches do not accept name; workflow is the run name");
683
- 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 ?? "";
684
697
  if (functionName === undefined && !workflowName) fail("INVALID_METADATA", "Inline workflow launches require a non-empty name");
685
698
  const fn = functionName === undefined ? undefined : registry?.function(functionName);
686
699
  if (functionName !== undefined && !registry) fail("MISSING_WORKFLOW", `Registered function is unavailable: ${functionName}`);
687
700
  const args = params.args === undefined ? null : params.args;
688
701
  if (functionName !== undefined && fn && (!object(args) || !jsonValue(args) || !Value.Check(fn.input, args))) fail("RESULT_INVALID", `Invalid input for ${functionName}`);
689
- const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
690
- 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");
691
704
  const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
692
705
  const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
693
706
  const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};