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/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,
|
|
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/src/persistence.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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 {
|
package/src/session-inspector.ts
CHANGED
|
@@ -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
|
|
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 ??
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
83
|
-
export interface
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
export interface
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
readonly
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
126
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
683
|
-
|
|
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"))) : {};
|