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
@@ -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";
package/dist/src/index.js CHANGED
@@ -6,8 +6,9 @@ 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 { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
11
+ export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
12
+ export { FairAgentScheduler, WorkflowAgentExecutor, createLocalWorkflowAgentSession, localAgentTransport } from "./agent-execution.js";
12
13
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
13
14
  export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
@@ -1,9 +1,5 @@
1
- import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./types.js";
1
+ import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowBudgetUsage, WorkflowRunEvent } from "./types.js";
2
2
  import type { OwnershipRecord } from "./agent-execution.js";
3
- export interface NativeSessionReference {
4
- sessionId: string;
5
- sessionFile: string;
6
- }
7
3
  export interface EffectiveSystemPrompt {
8
4
  sessionId: string;
9
5
  attempt: number;
@@ -11,8 +7,41 @@ export interface EffectiveSystemPrompt {
11
7
  sha256: string;
12
8
  prompt: string;
13
9
  }
14
- export interface PersistedRun extends RunRecord {
15
- nativeSessions: readonly NativeSessionReference[];
10
+ export type PersistedRun = RunRecord;
11
+ type LoadedPersistedRun = PersistedRun;
12
+ export interface RunSummaryAgent {
13
+ id: string;
14
+ name: string;
15
+ label?: string;
16
+ state: string;
17
+ role?: string;
18
+ attempts: number;
19
+ }
20
+ export interface RunSummaryArtifacts {
21
+ runDirectory: string;
22
+ statePath: string;
23
+ journalPath: string;
24
+ snapshotPath: string;
25
+ workflowPath: string;
26
+ resultPath: string;
27
+ summaryPath: string;
28
+ }
29
+ export interface RunSummary {
30
+ schemaVersion: 1;
31
+ runId: string;
32
+ sessionId: string;
33
+ workflowName: string;
34
+ state: RunRecord["state"];
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ terminalAt?: string;
38
+ usage: WorkflowBudgetUsage;
39
+ agents: readonly RunSummaryAgent[];
40
+ error?: RunRecord["error"];
41
+ failedAt?: string;
42
+ replayablePaths: readonly string[];
43
+ incompletePaths: readonly string[];
44
+ artifacts: RunSummaryArtifacts;
16
45
  }
17
46
  export interface CompletedOperation {
18
47
  path: string;
@@ -41,6 +70,7 @@ export interface BorrowedWorktreeBinding {
41
70
  export declare function projectStorageKey(cwd: string): string;
42
71
  export declare function projectSessionsDirectory(cwd: string, home?: string): string;
43
72
  export declare function runsDirectory(cwd: string, sessionId: string, home?: string): string;
73
+ export declare function listPersistedSessionIds(cwd: string, home?: string): Promise<string[]>;
44
74
  export declare function hasLiveSessionLease(cwd: string, sessionId: string, home?: string): Promise<boolean>;
45
75
  export declare class SessionLease {
46
76
  #private;
@@ -63,6 +93,7 @@ export declare class RunStore {
63
93
  readonly directory: string;
64
94
  private journalWrite;
65
95
  private stateWrite;
96
+ private summaryWrite;
66
97
  private worktreeWrite;
67
98
  private borrowedWorktreeWrite;
68
99
  private snapshotWrite;
@@ -70,11 +101,14 @@ export declare class RunStore {
70
101
  private systemPromptWrite;
71
102
  constructor(cwd: string, sessionId: string, runId: string, home?: string);
72
103
  create(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>): Promise<void>;
104
+ private refreshSummary;
105
+ private refreshSummaryBestEffort;
73
106
  isComplete(): Promise<boolean>;
74
107
  load(): Promise<{
75
- run: PersistedRun;
108
+ run: LoadedPersistedRun;
76
109
  snapshot: Readonly<LaunchSnapshot>;
77
110
  }>;
111
+ loadSummary(): Promise<RunSummary>;
78
112
  saveState(run: PersistedRun): Promise<void>;
79
113
  updateState(update: (run: PersistedRun) => PersistedRun | Promise<PersistedRun>): Promise<PersistedRun>;
80
114
  saveSnapshot(snapshot: Readonly<LaunchSnapshot>): Promise<void>;
@@ -131,3 +165,4 @@ export declare class RunStore {
131
165
  saveResult(value: JsonValue): Promise<string>;
132
166
  delete(confirmed: boolean): Promise<void>;
133
167
  }
168
+ export {};
@@ -7,6 +7,16 @@ import { homedir } from "node:os";
7
7
  import { promisify } from "node:util";
8
8
  import { WorkflowError } from "./types.js";
9
9
  import { loadLaunchSnapshot } from "./utils.js";
10
+ const TERMINAL_SUMMARY_STATES = new Set(["completed", "failed", "stopped"]);
11
+ const EMPTY_USAGE = { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 };
12
+ function summaryArtifacts(directory) { 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") }; }
13
+ function summaryFromRun(run, directory, journal, previous, fallbackCreatedAt, now = new Date().toISOString()) {
14
+ const createdAt = typeof previous?.createdAt === "string" ? previous.createdAt : fallbackCreatedAt;
15
+ const failedAt = run.failedAt ?? run.error?.failedAt;
16
+ const replayablePaths = [...new Set([...(run.retry?.completedPaths ?? []), ...Object.keys(journal.completed)])];
17
+ const incompletePaths = [...new Set([...(run.retry?.incompletePaths ?? []), ...(failedAt ? [failedAt] : [])])];
18
+ 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) };
19
+ }
10
20
  const execute = promisify(execFile);
11
21
  const gitIdentity = {
12
22
  GIT_AUTHOR_NAME: "pi-extensible-workflows", GIT_AUTHOR_EMAIL: "pi-extensible-workflows@localhost", GIT_COMMITTER_NAME: "pi-extensible-workflows", GIT_COMMITTER_EMAIL: "pi-extensible-workflows@localhost",
@@ -24,6 +34,17 @@ export function projectSessionsDirectory(cwd, home = homedir()) {
24
34
  export function runsDirectory(cwd, sessionId, home = homedir()) {
25
35
  return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
26
36
  }
37
+ export async function listPersistedSessionIds(cwd, home = homedir()) {
38
+ try {
39
+ const entries = await readdir(projectSessionsDirectory(cwd, home), { withFileTypes: true });
40
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map(({ name }) => name);
41
+ }
42
+ catch (error) {
43
+ if (error.code === "ENOENT")
44
+ return [];
45
+ throw error;
46
+ }
47
+ }
27
48
  const SESSION_OWNER_FILE = "owner.json";
28
49
  const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
29
50
  const RUN_CREATE_TEMP = /^\.([a-zA-Z0-9._-]+)\.(\d+)\.[0-9a-f-]+\.tmp$/;
@@ -236,6 +257,7 @@ export class RunStore {
236
257
  journalWrite = Promise.resolve();
237
258
  // ponytail: serializes one RunStore instance; cross-process run sharing remains unsupported.
238
259
  stateWrite = Promise.resolve();
260
+ summaryWrite = Promise.resolve();
239
261
  worktreeWrite = Promise.resolve();
240
262
  borrowedWorktreeWrite = Promise.resolve();
241
263
  snapshotWrite = Promise.resolve();
@@ -265,6 +287,7 @@ export class RunStore {
265
287
  await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
266
288
  await atomicJson(join(temporary, "state.json"), run);
267
289
  await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
290
+ await atomicJson(join(temporary, "summary.json"), summaryFromRun(run, this.directory, { completed: {} }, undefined, new Date().toISOString()));
268
291
  await rename(temporary, this.directory);
269
292
  }
270
293
  catch (error) {
@@ -272,6 +295,18 @@ export class RunStore {
272
295
  throw error;
273
296
  }
274
297
  }
298
+ async refreshSummary() {
299
+ const write = this.summaryWrite.then(async () => {
300
+ const run = await json(join(this.directory, "state.json"));
301
+ const journal = await json(join(this.directory, "journal.json"));
302
+ const previous = await json(join(this.directory, "summary.json")).catch(() => undefined);
303
+ const fallbackCreatedAt = await stat(join(this.directory, "state.json")).then((value) => new Date(value.mtimeMs).toISOString());
304
+ await atomicJson(join(this.directory, "summary.json"), summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt));
305
+ });
306
+ this.summaryWrite = write.catch(() => undefined);
307
+ await write;
308
+ }
309
+ refreshSummaryBestEffort() { void this.refreshSummary().catch(() => undefined); }
275
310
  async isComplete() {
276
311
  try {
277
312
  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"))]);
@@ -286,13 +321,30 @@ export class RunStore {
286
321
  const run = await json(join(this.directory, "state.json"));
287
322
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId)
288
323
  throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run belongs to another cwd or Pi session");
324
+ const persisted = run;
325
+ if (!Array.isArray(persisted.agentSessions) || Object.hasOwn(persisted, "nativeSessions"))
326
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run uses an unsupported agent session format");
289
327
  return { run, snapshot: loadLaunchSnapshot(await json(join(this.directory, "snapshot.json"))) };
290
328
  }
329
+ async loadSummary() {
330
+ await this.stateWrite;
331
+ await this.journalWrite;
332
+ await this.summaryWrite;
333
+ const run = await json(join(this.directory, "state.json"));
334
+ const journal = await json(join(this.directory, "journal.json"));
335
+ const previous = await json(join(this.directory, "summary.json")).catch(() => undefined);
336
+ const [stateStat, journalStat] = await Promise.all([stat(join(this.directory, "state.json")), stat(join(this.directory, "journal.json"))]);
337
+ const fallbackCreatedAt = new Date(stateStat.mtimeMs).toISOString();
338
+ const previousUpdatedAt = previous?.updatedAt === undefined ? Number.NaN : Date.parse(previous.updatedAt);
339
+ const updatedAt = new Date(Math.max(stateStat.mtimeMs, journalStat.mtimeMs, Number.isNaN(previousUpdatedAt) ? 0 : previousUpdatedAt)).toISOString();
340
+ return summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt, updatedAt);
341
+ }
291
342
  async saveState(run) {
292
343
  const write = this.stateWrite.then(async () => {
293
344
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId)
294
345
  throw new WorkflowError("INTERNAL_ERROR", "Run identity does not match its session-scoped store");
295
346
  await atomicJson(join(this.directory, "state.json"), run);
347
+ this.refreshSummaryBestEffort();
296
348
  });
297
349
  this.stateWrite = write.catch(() => undefined);
298
350
  await write;
@@ -307,6 +359,7 @@ export class RunStore {
307
359
  if (resolve(result.cwd) !== this.cwd || result.sessionId !== this.sessionId || result.id !== this.runId)
308
360
  throw new WorkflowError("INTERNAL_ERROR", "Run identity does not match its session-scoped store");
309
361
  await atomicJson(join(this.directory, "state.json"), result);
362
+ this.refreshSummaryBestEffort();
310
363
  });
311
364
  this.stateWrite = write.catch(() => undefined);
312
365
  await write;
@@ -351,6 +404,7 @@ export class RunStore {
351
404
  journal.awaiting ??= {};
352
405
  result = await update(journal);
353
406
  await atomicJson(journalPath, journal);
407
+ this.refreshSummaryBestEffort();
354
408
  });
355
409
  this.journalWrite = write.catch(() => undefined);
356
410
  await write;
@@ -1,4 +1,4 @@
1
- import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
1
+ import type { AgentAttemptAction, JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
2
2
  export declare class WorkflowRegistry {
3
3
  #private;
4
4
  get frozen(): boolean;
@@ -18,6 +18,7 @@ export declare class WorkflowRegistry {
18
18
  variable: WorkflowVariable;
19
19
  }[];
20
20
  agentSetupHooks(): readonly RegisteredAgentSetupHook[];
21
+ agentAttemptActions(): Readonly<Record<string, AgentAttemptAction>>;
21
22
  roleDirectories(): readonly string[];
22
23
  roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
23
24
  modelAliases(): readonly {
@@ -29,7 +30,7 @@ export declare class WorkflowRegistry {
29
30
  }[];
30
31
  resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed?: ReadonlySet<string>): Promise<Readonly<Record<string, string>>>;
31
32
  }
32
- export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "roleDirectories" | "roleDirectoryRegistrations">;
33
+ export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "agentAttemptActions" | "roleDirectories" | "roleDirectoryRegistrations">;
33
34
  export declare function resetWorkflowRegistry(): void;
34
35
  export declare function beginWorkflowExtensionLoading(): void;
35
36
  export declare function loadingRegistry(): WorkflowRegistryApi;
@@ -29,6 +29,7 @@ export class WorkflowRegistry {
29
29
  #extensions = new Set();
30
30
  #globals = new Map();
31
31
  #hooks = new Map();
32
+ #agentAttemptActions = new Map();
32
33
  #roleDirectories = new Map();
33
34
  #modelAliases = new Map();
34
35
  #frozen = false;
@@ -39,18 +40,19 @@ export class WorkflowRegistry {
39
40
  fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
40
41
  if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
41
42
  fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
42
- if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "modelAliases", "agentSetupHooks", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
43
+ 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())
43
44
  fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
44
45
  const functions = extension.functions ?? {};
45
46
  const variables = extension.variables ?? {};
46
47
  const modelAliases = extension.modelAliases ?? {};
47
48
  const agentSetupHooks = extension.agentSetupHooks ?? {};
49
+ const agentAttemptActions = extension.agentAttemptActions ?? {};
48
50
  const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
49
51
  if (!Array.isArray(roleDirectoryValues))
50
52
  fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
51
53
  const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
52
- if (!object(functions) || !object(variables) || !object(modelAliases) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && roleDirectories.length === 0))
53
- fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, or role directories");
54
+ 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))
55
+ fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, agent attempt actions, or role directories");
54
56
  const names = [...Object.keys(functions), ...Object.keys(variables)];
55
57
  if (new Set(names).size !== names.length)
56
58
  fail("GLOBAL_COLLISION", "Global name collision inside extension");
@@ -89,7 +91,13 @@ export class WorkflowRegistry {
89
91
  if (this.#hooks.has(name))
90
92
  fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
91
93
  }
92
- const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, ...(roleDirectories.length ? { roleDirectories } : {}) });
94
+ for (const [name, action] of Object.entries(agentAttemptActions)) {
95
+ 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")
96
+ fail("INVALID_METADATA", `Invalid agent attempt action: ${name}`);
97
+ if (this.#agentAttemptActions.has(name))
98
+ fail("DUPLICATE_NAME", `Agent attempt action already registered: ${name}`);
99
+ }
100
+ const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, agentAttemptActions, ...(roleDirectories.length ? { roleDirectories } : {}) });
93
101
  this.#extensions.add(stored);
94
102
  for (const directory of roleDirectories)
95
103
  if (!this.#roleDirectories.has(directory))
@@ -100,6 +108,8 @@ export class WorkflowRegistry {
100
108
  this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, resolve: alias.resolve });
101
109
  for (const [name, hook] of Object.entries(agentSetupHooks))
102
110
  this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
111
+ for (const [name, action] of Object.entries(agentAttemptActions))
112
+ this.#agentAttemptActions.set(name, action);
103
113
  }
104
114
  function(name) {
105
115
  if (!IDENTIFIER.test(name))
@@ -197,6 +207,7 @@ export class WorkflowRegistry {
197
207
  agentSetupHooks() {
198
208
  return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
199
209
  }
210
+ agentAttemptActions() { return Object.freeze(Object.fromEntries(this.#agentAttemptActions.entries())); }
200
211
  roleDirectories() {
201
212
  return [...this.#roleDirectories.keys()];
202
213
  }
@@ -250,6 +261,7 @@ function createWorkflowRegistryApi(registry) {
250
261
  roleDirectories: () => registry.roleDirectories(),
251
262
  roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
252
263
  agentSetupHooks: () => registry.agentSetupHooks(),
264
+ agentAttemptActions: () => registry.agentAttemptActions(),
253
265
  };
254
266
  }
255
267
  function workflowRegistryHost() {
@@ -1,6 +1,6 @@
1
1
  import { type SessionEntry, type SessionInfo } from "@earendil-works/pi-coding-agent";
2
2
  import { type AgentSetupSummary, type ModelSpec, type StaticWorkflowCall } from "./index.js";
3
- import { type PersistedRun } from "./persistence.js";
3
+ import { type PersistedRun, type RunSummary } from "./persistence.js";
4
4
  export interface ModelUsage {
5
5
  model: string;
6
6
  cost: number;
@@ -62,11 +62,21 @@ export interface InspectorViewState {
62
62
  selected: number;
63
63
  scroll: number;
64
64
  }
65
+ export type InspectMode = "tui" | "json" | "summary";
66
+ export interface PersistedSessionSummary {
67
+ schemaVersion: 1;
68
+ cwd: string;
69
+ sessionId: string;
70
+ runs: readonly RunSummary[];
71
+ }
65
72
  export declare function transcriptLines(entries: readonly SessionEntry[]): string[];
66
73
  export declare function transcriptFileLines(path: string): string[];
67
74
  export declare function matchSession(query: string, sessions: readonly SessionInfo[]): SessionInfo;
68
75
  export declare function loadSessionReport(path: string, home?: string): Promise<SessionReport>;
69
76
  export declare function renderInspector(report: SessionReport, state: InspectorViewState, width?: number, height?: number, highlighter?: (script: string) => string[]): string[];
77
+ export declare function loadPersistedSessionSummary(cwd: string, sessionId: string, home?: string, failedOnly?: boolean): Promise<PersistedSessionSummary>;
78
+ export declare function loadPersistedSummaries(cwd: string, sessionId: string | undefined, home?: string, failedOnly?: boolean): Promise<readonly PersistedSessionSummary[]>;
79
+ export declare function formatPersistedRunSummary(summary: RunSummary, sessionId?: string): string;
70
80
  export declare function showSessionInspector(report: SessionReport): Promise<void>;
71
81
  export declare function resolveSession(query: string, sessionDir?: string | undefined): Promise<SessionInfo>;
72
- export declare function runSessionInspector(sessionId?: string): Promise<void>;
82
+ export declare function runSessionInspector(sessionId?: string, mode?: InspectMode, cwd?: string, home?: string, write?: (text: string) => void, failedOnly?: boolean): Promise<void>;
@@ -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 } from "@earendil-works/pi-coding-agent";
7
7
  import { formatBudgetStatus, inspectWorkflowScript } from "./index.js";
8
- import { listRunIds, RunStore } from "./persistence.js";
8
+ import { listPersistedSessionIds, listRunIds, RunStore } from "./persistence.js";
9
9
  function text(content) {
10
10
  if (typeof content === "string")
11
11
  return content;
@@ -115,6 +115,13 @@ function readTranscript(path) {
115
115
  return undefined;
116
116
  }
117
117
  }
118
+ function attemptTranscriptPath(attempt) {
119
+ const session = attempt.session;
120
+ if (!session || session.transport !== "local" || typeof session.locator !== "object" || session.locator === null || Array.isArray(session.locator))
121
+ return undefined;
122
+ const path = session.locator.sessionFile;
123
+ return typeof path === "string" && path ? path : undefined;
124
+ }
118
125
  function resultRunId(result) {
119
126
  if (!result)
120
127
  return undefined;
@@ -161,33 +168,18 @@ async function agentReport(agent) {
161
168
  const fallbackThinking = agent.model.thinking;
162
169
  const attempts = [];
163
170
  for (const attempt of agent.attemptDetails ?? []) {
164
- const log = readTranscript(attempt.sessionFile);
171
+ const setup = attempt.setup;
172
+ const path = attemptTranscriptPath(attempt);
173
+ const log = path ? readTranscript(path) : undefined;
165
174
  if (log) {
166
- const model = log.model ?? fallbackModel;
175
+ const model = log.model ?? `${setup.model.provider}/${setup.model.model}`;
167
176
  const cost = log.cost;
168
- attempts.push({
169
- attempt: attempt.attempt,
170
- prompt: log.prompt ?? "(transcript unavailable)",
171
- model,
172
- ...(log.thinking !== undefined ? { thinking: log.thinking } : {}),
173
- cost,
174
- models: log.models.length ? log.models : [{ model, cost }],
175
- ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
176
- ...(attempt.setup ? { setup: attempt.setup } : {}),
177
- });
177
+ 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 });
178
178
  continue;
179
179
  }
180
+ const model = `${setup.model.provider}/${setup.model.model}`;
180
181
  const cost = attempt.accounting.cost;
181
- attempts.push({
182
- attempt: attempt.attempt,
183
- prompt: "(transcript unavailable)",
184
- model: fallbackModel,
185
- ...(fallbackThinking !== undefined ? { thinking: fallbackThinking } : {}),
186
- cost,
187
- models: [{ model: fallbackModel, cost }],
188
- ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
189
- ...(attempt.setup ? { setup: attempt.setup } : {}),
190
- });
182
+ 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 });
191
183
  }
192
184
  if (!attempts.length) {
193
185
  const cost = agent.accounting?.cost ?? 0;
@@ -346,6 +338,28 @@ export function renderInspector(report, state, width = 80, height = 24, highligh
346
338
  const scroll = Math.max(0, Math.min(state.scroll, Math.max(0, fitted.length - room)));
347
339
  return [...header, ...fitted.slice(scroll, scroll + room), ...footer].slice(0, height);
348
340
  }
341
+ export async function loadPersistedSessionSummary(cwd, sessionId, home = homedir(), failedOnly = false) {
342
+ const runs = [];
343
+ for (const runId of (await listRunIds(cwd, sessionId, home)).sort()) {
344
+ try {
345
+ const summary = await new RunStore(cwd, sessionId, runId, home).loadSummary();
346
+ if (!failedOnly || summary.state === "failed")
347
+ runs.push(summary);
348
+ }
349
+ catch { /* Ignore corrupt or concurrently removed runs. */ }
350
+ }
351
+ return { schemaVersion: 1, cwd, sessionId, runs };
352
+ }
353
+ export async function loadPersistedSummaries(cwd, sessionId, home = homedir(), failedOnly = false) {
354
+ const sessionIds = sessionId ? [sessionId] : (await listPersistedSessionIds(cwd, home)).sort();
355
+ const sessions = await Promise.all(sessionIds.map((id) => loadPersistedSessionSummary(cwd, id, home, failedOnly)));
356
+ return sessionId || !failedOnly ? sessions : sessions.filter((session) => session.runs.length > 0);
357
+ }
358
+ export function formatPersistedRunSummary(summary, sessionId = summary.sessionId) {
359
+ const counts = summary.agents.reduce((result, agent) => { result[agent.state] = (result[agent.state] ?? 0) + 1; return result; }, {});
360
+ const status = Object.entries(counts).map(([state, count]) => `${state}=${String(count)}`).join(", ") || "agents=0";
361
+ return `${sessionId} ${summary.runId} ${summary.workflowName} ${summary.state} ${status} updated=${summary.updatedAt}`;
362
+ }
349
363
  function nextState(current, key, workflowCount) {
350
364
  if (current.view === "list") {
351
365
  if (key === "up")
@@ -410,7 +424,19 @@ async function askSessionId() {
410
424
  export async function resolveSession(query, sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR) {
411
425
  return matchSession(query, await SessionManager.listAll(sessionDir));
412
426
  }
413
- export async function runSessionInspector(sessionId) {
427
+ export async function runSessionInspector(sessionId, mode = "tui", cwd = process.cwd(), home = homedir(), write = (text) => { stdout.write(text); }, failedOnly = false) {
428
+ if (mode !== "tui") {
429
+ const sessions = await loadPersistedSummaries(cwd, sessionId?.trim() || undefined, home, failedOnly);
430
+ if (mode === "json") {
431
+ const value = sessionId?.trim() ? sessions[0] : undefined;
432
+ write(`${JSON.stringify(value ?? { schemaVersion: 1, cwd, sessions })}\n`);
433
+ return;
434
+ }
435
+ const emptyLabel = failedOnly ? "no failed runs" : "no persisted runs";
436
+ const lines = sessions.flatMap((session) => session.runs.length ? session.runs.map((run) => formatPersistedRunSummary(run, session.sessionId)) : [`${session.sessionId} (${emptyLabel})`]);
437
+ write(`${lines.length ? lines.join("\n") : failedOnly ? "No failed workflow runs." : "No persisted workflow runs."}\n`);
438
+ return;
439
+ }
414
440
  const query = sessionId?.trim() || await askSessionId();
415
441
  if (!query)
416
442
  throw new Error("Session ID is required.");