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.
- package/README.md +2 -31
- package/dist/src/agent-execution.d.ts +67 -9
- package/dist/src/agent-execution.js +385 -141
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +156 -22
- package/dist/src/doctor-cleanup.js +68 -31
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +12 -9
- package/dist/src/host.js +525 -195
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +3 -2
- package/dist/src/persistence.d.ts +43 -8
- package/dist/src/persistence.js +54 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +50 -24
- package/dist/src/types.d.ts +141 -60
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +28 -7
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +302 -107
- package/src/bundles.ts +471 -0
- package/src/cli.ts +130 -22
- package/src/doctor-cleanup.ts +38 -4
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +27 -15
- package/src/host.ts +454 -175
- package/src/index.ts +5 -4
- package/src/persistence.ts +58 -4
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +49 -26
- package/src/types.ts +55 -30
- package/src/validation.ts +19 -6
- package/src/workflow-artifacts.ts +1 -0
- package/src/workflow-evals.ts +24 -3
package/dist/src/index.d.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,
|
|
12
|
-
export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
13
|
-
export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook,
|
|
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
|
|
15
|
-
|
|
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:
|
|
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 {};
|
package/dist/src/persistence.js
CHANGED
|
@@ -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;
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/registry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 ??
|
|
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.");
|