pi-extensible-workflows 3.1.0 → 3.3.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 +11 -47
- package/dist/src/agent-execution.d.ts +4 -0
- package/dist/src/agent-execution.js +146 -60
- 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 +146 -21
- package/dist/src/doctor-cleanup.js +4 -2
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.js +13 -4
- package/dist/src/host.d.ts +51 -8
- package/dist/src/host.js +1181 -487
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +3 -1
- package/dist/src/persistence.d.ts +40 -1
- package/dist/src/persistence.js +51 -0
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +36 -2
- package/dist/src/types.d.ts +19 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +42 -2
- package/dist/src/workflow-artifacts.d.ts +13 -0
- package/dist/src/workflow-artifacts.js +39 -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/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +21 -28
- package/src/agent-execution.ts +102 -30
- package/src/bundles.ts +471 -0
- package/src/cli.ts +118 -21
- package/src/doctor-cleanup.ts +3 -2
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +13 -4
- package/src/host.ts +992 -447
- package/src/index.ts +4 -2
- package/src/persistence.ts +53 -1
- package/src/session-inspector.ts +36 -4
- package/src/types.ts +12 -5
- package/src/validation.ts +33 -2
- package/src/workflow-artifacts.ts +34 -0
- package/src/workflow-evals.ts +24 -3
package/src/index.ts
CHANGED
|
@@ -5,9 +5,11 @@ export * from "./validation.js";
|
|
|
5
5
|
export * from "./registry.js";
|
|
6
6
|
export * from "./execution.js";
|
|
7
7
|
export * from "./host.js";
|
|
8
|
+
export * from "./workflow-artifacts.js";
|
|
9
|
+
export * from "./bundles.js";
|
|
8
10
|
export { default } from "./host.js";
|
|
9
|
-
export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|
|
10
|
-
export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
11
|
+
export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|
|
12
|
+
export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, RunSummary, RunSummaryAgent, RunSummaryArtifacts, WorktreeReference } from "./persistence.js";
|
|
11
13
|
export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
12
14
|
export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./agent-execution.js";
|
|
13
15
|
export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
package/src/persistence.ts
CHANGED
|
@@ -5,7 +5,7 @@ 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";
|
|
@@ -13,11 +13,24 @@ import { loadLaunchSnapshot } from "./utils.js";
|
|
|
13
13
|
export interface NativeSessionReference { sessionId: string; sessionFile: string }
|
|
14
14
|
export interface EffectiveSystemPrompt { sessionId: string; attempt: number; turn: number; sha256: string; prompt: string }
|
|
15
15
|
export interface PersistedRun extends RunRecord { nativeSessions: readonly NativeSessionReference[] }
|
|
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,12 +245,25 @@ 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; }
|
|
@@ -240,11 +276,25 @@ export class RunStore {
|
|
|
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");
|
|
241
277
|
return { run, snapshot: loadLaunchSnapshot(await json<LaunchSnapshot>(join(this.directory, "snapshot.json"))) };
|
|
242
278
|
}
|
|
279
|
+
async loadSummary(): Promise<RunSummary> {
|
|
280
|
+
await this.stateWrite;
|
|
281
|
+
await this.journalWrite;
|
|
282
|
+
await this.summaryWrite;
|
|
283
|
+
const run = await json<PersistedRun>(join(this.directory, "state.json"));
|
|
284
|
+
const journal = await json<Journal>(join(this.directory, "journal.json"));
|
|
285
|
+
const previous = await json<RunSummary>(join(this.directory, "summary.json")).catch(() => undefined);
|
|
286
|
+
const [stateStat, journalStat] = await Promise.all([stat(join(this.directory, "state.json")), stat(join(this.directory, "journal.json"))]);
|
|
287
|
+
const fallbackCreatedAt = new Date(stateStat.mtimeMs).toISOString();
|
|
288
|
+
const previousUpdatedAt = previous?.updatedAt === undefined ? Number.NaN : Date.parse(previous.updatedAt);
|
|
289
|
+
const updatedAt = new Date(Math.max(stateStat.mtimeMs, journalStat.mtimeMs, Number.isNaN(previousUpdatedAt) ? 0 : previousUpdatedAt)).toISOString();
|
|
290
|
+
return summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt, updatedAt);
|
|
291
|
+
}
|
|
243
292
|
|
|
244
293
|
async saveState(run: PersistedRun): Promise<void> {
|
|
245
294
|
const write = this.stateWrite.then(async () => {
|
|
246
295
|
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
296
|
await atomicJson(join(this.directory, "state.json"), run);
|
|
297
|
+
this.refreshSummaryBestEffort();
|
|
248
298
|
});
|
|
249
299
|
this.stateWrite = write.catch(() => undefined);
|
|
250
300
|
await write;
|
|
@@ -258,6 +308,7 @@ export class RunStore {
|
|
|
258
308
|
result = await update(current);
|
|
259
309
|
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
310
|
await atomicJson(join(this.directory, "state.json"), result);
|
|
311
|
+
this.refreshSummaryBestEffort();
|
|
261
312
|
});
|
|
262
313
|
this.stateWrite = write.catch(() => undefined);
|
|
263
314
|
await write;
|
|
@@ -308,6 +359,7 @@ export class RunStore {
|
|
|
308
359
|
journal.awaiting ??= {};
|
|
309
360
|
result = await update(journal);
|
|
310
361
|
await atomicJson(journalPath, journal);
|
|
362
|
+
this.refreshSummaryBestEffort();
|
|
311
363
|
});
|
|
312
364
|
this.journalWrite = write.catch(() => undefined);
|
|
313
365
|
await write;
|
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 };
|
|
@@ -334,7 +336,26 @@ export function renderInspector(report: SessionReport, state: InspectorViewState
|
|
|
334
336
|
const scroll = Math.max(0, Math.min(state.scroll, Math.max(0, fitted.length - room)));
|
|
335
337
|
return [...header, ...fitted.slice(scroll, scroll + room), ...footer].slice(0, height);
|
|
336
338
|
}
|
|
337
|
-
|
|
339
|
+
export async function loadPersistedSessionSummary(cwd: string, sessionId: string, home = homedir(), failedOnly = false): Promise<PersistedSessionSummary> {
|
|
340
|
+
const runs: RunSummary[] = [];
|
|
341
|
+
for (const runId of (await listRunIds(cwd, sessionId, home)).sort()) {
|
|
342
|
+
try {
|
|
343
|
+
const summary = await new RunStore(cwd, sessionId, runId, home).loadSummary();
|
|
344
|
+
if (!failedOnly || summary.state === "failed") runs.push(summary);
|
|
345
|
+
} catch { /* Ignore corrupt or concurrently removed runs. */ }
|
|
346
|
+
}
|
|
347
|
+
return { schemaVersion: 1, cwd, sessionId, runs };
|
|
348
|
+
}
|
|
349
|
+
export async function loadPersistedSummaries(cwd: string, sessionId: string | undefined, home = homedir(), failedOnly = false): Promise<readonly PersistedSessionSummary[]> {
|
|
350
|
+
const sessionIds = sessionId ? [sessionId] : (await listPersistedSessionIds(cwd, home)).sort();
|
|
351
|
+
const sessions = await Promise.all(sessionIds.map((id) => loadPersistedSessionSummary(cwd, id, home, failedOnly)));
|
|
352
|
+
return sessionId || !failedOnly ? sessions : sessions.filter((session) => session.runs.length > 0);
|
|
353
|
+
}
|
|
354
|
+
export function formatPersistedRunSummary(summary: RunSummary, sessionId = summary.sessionId): string {
|
|
355
|
+
const counts = summary.agents.reduce<Record<string, number>>((result, agent) => { result[agent.state] = (result[agent.state] ?? 0) + 1; return result; }, {});
|
|
356
|
+
const status = Object.entries(counts).map(([state, count]) => `${state}=${String(count)}`).join(", ") || "agents=0";
|
|
357
|
+
return `${sessionId} ${summary.runId} ${summary.workflowName} ${summary.state} ${status} updated=${summary.updatedAt}`;
|
|
358
|
+
}
|
|
338
359
|
function nextState(current: InspectorViewState, key: string, workflowCount: number): InspectorViewState {
|
|
339
360
|
if (current.view === "list") {
|
|
340
361
|
if (key === "up") return { ...current, selected: Math.max(0, current.selected - 1) };
|
|
@@ -385,8 +406,19 @@ async function askSessionId(): Promise<string> {
|
|
|
385
406
|
export async function resolveSession(query: string, sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR): Promise<SessionInfo> {
|
|
386
407
|
return matchSession(query, await SessionManager.listAll(sessionDir));
|
|
387
408
|
}
|
|
388
|
-
|
|
389
|
-
|
|
409
|
+
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> {
|
|
410
|
+
if (mode !== "tui") {
|
|
411
|
+
const sessions = await loadPersistedSummaries(cwd, sessionId?.trim() || undefined, home, failedOnly);
|
|
412
|
+
if (mode === "json") {
|
|
413
|
+
const value = sessionId?.trim() ? sessions[0] : undefined;
|
|
414
|
+
write(`${JSON.stringify(value ?? { schemaVersion: 1, cwd, sessions })}\n`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const emptyLabel = failedOnly ? "no failed runs" : "no persisted runs";
|
|
418
|
+
const lines = sessions.flatMap((session) => session.runs.length ? session.runs.map((run) => formatPersistedRunSummary(run, session.sessionId)) : [`${session.sessionId} (${emptyLabel})`]);
|
|
419
|
+
write(`${lines.length ? lines.join("\n") : failedOnly ? "No failed workflow runs." : "No persisted workflow runs."}\n`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
390
422
|
const query = sessionId?.trim() || await askSessionId();
|
|
391
423
|
if (!query) throw new Error("Session ID is required.");
|
|
392
424
|
const session = await resolveSession(query);
|
package/src/types.ts
CHANGED
|
@@ -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;
|
|
@@ -68,19 +68,24 @@ export interface WorkflowSettingsResolution { globalSettingsPath: string; projec
|
|
|
68
68
|
export interface AgentResourceExclusions { skills: readonly string[]; extensions: readonly string[] }
|
|
69
69
|
export interface AgentResourcePolicy { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: AgentResourceExclusions; project: AgentResourceExclusions; effective: AgentResourceExclusions; unmatchedSkills: string[]; unmatchedExtensions: string[]; excludedSkills?: string[]; excludedExtensions?: 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
74
|
export interface AgentAttemptSummary { attempt: number; sessionId: string; sessionFile: string; error?: { code: string; message: string }; accounting: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; setup?: AgentSetupSummary }
|
|
74
75
|
export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
|
|
75
76
|
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[]; 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 }
|
|
77
|
+
export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; resultPath?: string; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined; lastEventAt?: number }
|
|
78
|
+
export type WorkflowDeliveryMode = "foreground" | "background";
|
|
79
|
+
export type WorkflowDeliveryStatus = "attached" | "pending" | "delivered";
|
|
80
|
+
export interface WorkflowRunDelivery { mode: WorkflowDeliveryMode; state: WorkflowDeliveryStatus; toolCallId?: string }
|
|
77
81
|
export interface WorkflowRunEvent { type: string; message: string }
|
|
78
82
|
export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
|
|
79
83
|
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[] }
|
|
84
|
+
export interface RunRecord { id: string; workflowName: string; cwd: string; sessionId: string; state: RunState; parentRunId?: string; retry?: WorkflowRetryProvenance; phase?: string; phaseHistory?: readonly WorkflowPhaseRecord[]; agents: readonly AgentRecord[]; activeShells?: number; error?: WorkflowErrorShape; failedAt?: string; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[]; delivery?: WorkflowRunDelivery }
|
|
81
85
|
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
|
|
82
|
-
export
|
|
83
|
-
export interface
|
|
86
|
+
export type WorkflowLaunchMode = "foreground" | "background";
|
|
87
|
+
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; overrideSystemPrompt?: boolean; disabledAgentResources?: AgentResourceExclusions }
|
|
88
|
+
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
89
|
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
90
|
export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[] }; schemas: readonly JsonSchema[]; dynamicAgentRoles: boolean }
|
|
86
91
|
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 }
|
|
@@ -117,8 +122,10 @@ export interface SessionInput {
|
|
|
117
122
|
agentDir?: string;
|
|
118
123
|
customTools?: SessionCustomTools;
|
|
119
124
|
resultTool?: ToolDefinition;
|
|
125
|
+
systemPrompt?: string;
|
|
120
126
|
systemPromptAppend?: string;
|
|
121
127
|
extensionFactories?: InlineExtension[];
|
|
128
|
+
additionalSkillPaths?: readonly string[];
|
|
122
129
|
resourcePolicy?: AgentResourcePolicy;
|
|
123
130
|
options?: AgentOptions;
|
|
124
131
|
}
|
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";
|
|
@@ -324,6 +328,32 @@ function workflowCallsWithStructure(program: acorn.Program): Array<{ call: Workf
|
|
|
324
328
|
visit(program, { execution: "sequential", structure: [] });
|
|
325
329
|
return calls.sort((left, right) => left.call.start - right.call.start);
|
|
326
330
|
}
|
|
331
|
+
function memberCall(node: acorn.AnyNode | undefined, objectName: string, propertyName: string): boolean {
|
|
332
|
+
if (node?.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.object.type !== "Identifier" || node.callee.object.name !== objectName || node.callee.property.type !== "Identifier") return false;
|
|
333
|
+
return node.callee.property.name === propertyName;
|
|
334
|
+
}
|
|
335
|
+
function mapCallback(node: acorn.AnyNode): acorn.AnyNode | undefined {
|
|
336
|
+
if (!memberCall(node, "Promise", "all") && !memberCall(node, "Promise", "allSettled")) return undefined;
|
|
337
|
+
if (node.type !== "CallExpression") return undefined;
|
|
338
|
+
const source = node.arguments[0];
|
|
339
|
+
if (source?.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || !["map", "flatMap"].includes(source.callee.property.name)) return undefined;
|
|
340
|
+
const callback = source.arguments[0];
|
|
341
|
+
return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? callback : undefined;
|
|
342
|
+
}
|
|
343
|
+
function hasUnscopedAgent(node: acorn.AnyNode, scoped = false): boolean {
|
|
344
|
+
const operation = workflowCallKind(node);
|
|
345
|
+
if (operation === "agent") return !scoped;
|
|
346
|
+
const nestedScope = scoped || operation === "parallel" || operation === "pipeline";
|
|
347
|
+
return astChildren(node).some((child) => hasUnscopedAgent(child, nestedScope));
|
|
348
|
+
}
|
|
349
|
+
function validateObviousConcurrentAgentCalls(program: acorn.Program): void {
|
|
350
|
+
const visit = (node: acorn.AnyNode): void => {
|
|
351
|
+
const callback = mapCallback(node);
|
|
352
|
+
if (callback && hasUnscopedAgent(callback)) fail("INVALID_METADATA", "Promise.all/map agent fan-out cannot prove stable call-site identity; use parallel(...) or pipeline(...)");
|
|
353
|
+
for (const child of astChildren(node)) visit(child);
|
|
354
|
+
};
|
|
355
|
+
visit(program);
|
|
356
|
+
}
|
|
327
357
|
function validateDirectPrimitiveReferences(program: acorn.AnyNode, name: string): void {
|
|
328
358
|
const visit = (node: acorn.AnyNode, parent?: acorn.AnyNode): void => {
|
|
329
359
|
if (node.type === "Identifier" && node.name === name) {
|
|
@@ -606,6 +636,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
606
636
|
validateDirectPrimitiveReferences(program, "shell");
|
|
607
637
|
for (const [index, schema] of schemas.entries()) validateSchema(schema, `schema[${String(index)}]`);
|
|
608
638
|
const calls = workflowCalls(program);
|
|
639
|
+
validateObviousConcurrentAgentCalls(program);
|
|
609
640
|
const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
|
|
610
641
|
for (const call of calls) {
|
|
611
642
|
const operation = call.callee.name;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { JsonValue } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface WorkflowArtifact { extension: ".js" | ".json" | ".md"; content: string }
|
|
8
|
+
export type WorkflowTui = { stop(): void; start(): void; requestRender(force?: boolean): void };
|
|
9
|
+
|
|
10
|
+
export function workflowScriptArtifact(script: string): WorkflowArtifact { return { extension: ".js", content: script }; }
|
|
11
|
+
export function workflowResultArtifact(value: JsonValue): WorkflowArtifact { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
|
|
12
|
+
|
|
13
|
+
async function spawnWorkflowEditor(command: string, path: string): Promise<number | null> {
|
|
14
|
+
const [editor, ...editorArgs] = command.split(" ");
|
|
15
|
+
if (!editor) return null;
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
try {
|
|
18
|
+
const child = spawn(editor, [...editorArgs, path], { stdio: "inherit", shell: process.platform === "win32" });
|
|
19
|
+
child.once("error", () => { resolve(null); });
|
|
20
|
+
child.once("close", (code) => { resolve(code); });
|
|
21
|
+
} catch { resolve(null); }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null> {
|
|
26
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-workflow-editor-"));
|
|
27
|
+
const path = join(directory, `artifact${artifact.extension}`);
|
|
28
|
+
try {
|
|
29
|
+
await writeFile(path, artifact.content, { encoding: "utf8", mode: 0o600 });
|
|
30
|
+
tui.stop();
|
|
31
|
+
try { return await spawnWorkflowEditor(command, path); }
|
|
32
|
+
finally { tui.start(); tui.requestRender(true); }
|
|
33
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
34
|
+
}
|
package/src/workflow-evals.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface AgentOrderExpectation { role?: string; model?: string; promptIn
|
|
|
28
28
|
export interface DataFlowExpectation { binding: string; toAgentIndex: number }
|
|
29
29
|
export interface ParentAssistantBatch { index: number; parts: readonly JsonValue[]; tools: readonly string[]; usage?: ParentUsage }
|
|
30
30
|
export interface ParentToolResult { toolCallId?: string; details?: JsonValue; isError?: boolean; text?: string }
|
|
31
|
+
export interface ParentToolCall { name: string; arguments: JsonValue }
|
|
31
32
|
export interface ParentUsage { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number; cost: number; models: readonly { model: string; cost: number }[] }
|
|
32
33
|
export interface ParentOracle { assistantBatches: readonly ParentAssistantBatch[]; workflowToolResults: readonly ParentToolResult[]; skillReads: readonly string[]; firstSignificantAction?: SignificantAction; firstTool?: string; firstBatchToolSequence: readonly string[]; toolsBeforeFirstWorkflow: readonly string[]; firstWorkflowBatchToolSequence: readonly string[]; parentToolSequence: readonly string[]; workflowCallCount: number; usage: ParentUsage }
|
|
33
34
|
export interface CapturedWorkflowCall { batch: number; toolCallId?: string; arguments: JsonValue; script?: string }
|
|
@@ -91,7 +92,7 @@ export interface EvalCaseResult {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
const CASE_PROCESS_GRACE_MS = 1_000;
|
|
94
|
-
export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow"] as const);
|
|
95
|
+
export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow", "workflow_retry"] as const);
|
|
95
96
|
const EVAL_MODEL_TOKEN = "$EVAL_MODEL";
|
|
96
97
|
const semantic = (description: string): readonly SemanticCriterion[] => [{ id: "intent", description }];
|
|
97
98
|
const JSON_RESULT_TYPES = ["null", "boolean", "number", "integer", "string", "array", "object"] as const;
|
|
@@ -216,6 +217,13 @@ export function extractParentOracle(entries: readonly unknown[]): ParentOracle {
|
|
|
216
217
|
return { assistantBatches: batches, workflowToolResults, skillReads, ...(firstSignificantAction ? { firstSignificantAction } : {}), ...(parentToolSequence[0] ? { firstTool: parentToolSequence[0] } : {}), firstBatchToolSequence: firstBatch?.tools ?? [], toolsBeforeFirstWorkflow: firstWorkflowIndex < 0 ? parentToolSequence : parentToolSequence.slice(0, firstWorkflowIndex), firstWorkflowBatchToolSequence: firstWorkflowBatch?.tools ?? [], parentToolSequence, workflowCallCount: parentToolSequence.filter((name) => name === "workflow").length, usage: { ...totals, models: [...modelCosts].map(([model, cost]) => ({ model, cost })) } };
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
export function extractParentToolCalls(oracle: ParentOracle): ParentToolCall[] {
|
|
221
|
+
return oracle.assistantBatches.flatMap(({ parts }) => parts.flatMap((part) => {
|
|
222
|
+
if (!isObject(part) || part.type !== "toolCall" || typeof part.name !== "string") return [];
|
|
223
|
+
return [{ name: part.name, arguments: isJson(part.arguments) ? part.arguments : null }];
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
export function extractParentOracleFile(path: string): ParentOracle {
|
|
220
228
|
const entries = readFileSync(path, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line) as unknown);
|
|
221
229
|
return extractParentOracle(entries);
|
|
@@ -280,6 +288,19 @@ export function evalExpectationErrors(oracle: ParentOracle, expectations: EvalEx
|
|
|
280
288
|
return errors;
|
|
281
289
|
}
|
|
282
290
|
|
|
291
|
+
const RECOVERY_SELECTIONS: Readonly<Record<string, { tool: string; arguments: JsonValue }>> = {
|
|
292
|
+
"recovery-failed-run": { tool: "workflow_retry", arguments: { runId: "failed-run-42" } },
|
|
293
|
+
"recovery-completed-worktree": { tool: "workflow", arguments: { name: "borrow-worktree", script: "return true;", parentRunId: "completed-run-42" } },
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export function recoverySelectionErrors(evalCase: Pick<WorkflowEvalCase, "id">, oracle: ParentOracle): string[] {
|
|
297
|
+
const expected = RECOVERY_SELECTIONS[evalCase.id];
|
|
298
|
+
if (!expected) return [];
|
|
299
|
+
const actual = extractParentToolCalls(oracle);
|
|
300
|
+
if (actual.length === 1 && actual[0]?.name === expected.tool && equalJson(actual[0].arguments, expected.arguments)) return [];
|
|
301
|
+
return [`${evalCase.id} parent tool calls were ${JSON.stringify(actual)}; expected ${JSON.stringify([expected])}`];
|
|
302
|
+
}
|
|
303
|
+
|
|
283
304
|
export function replayExpectationErrors(calls: readonly CapturedWorkflowCall[], reports: readonly ReplayReport[], expectations: EvalExpectations): string[] {
|
|
284
305
|
const errors: string[] = [];
|
|
285
306
|
const staticCalls = calls.flatMap((call) => { try { return call.script ? inspectWorkflowScript(call.script) : []; } catch { return []; } });
|
|
@@ -595,7 +616,7 @@ function semanticJudgePrompt(evalCase: WorkflowEvalCase, calls: readonly Capture
|
|
|
595
616
|
const usedRoles = new Set(calls.flatMap(({ script }) => { try { return script ? inspectWorkflowScript(script).flatMap((call) => call.kind === "agent" && call.role ? [call.role] : []) : []; } catch { return []; } }));
|
|
596
617
|
const roleText = [...usedRoles].map((role) => `${role}: ${roles[role]?.description ?? "no description"}`).join("\n") || "none";
|
|
597
618
|
const docs = "agent(prompt, options) delegates; shell(command, options) runs a deterministic host command and returns exitCode/stdout/stderr; parallel(name, tasks) runs independent tasks concurrently; pipeline(name, items, stages) applies ordered stages; prompt(template, data) carries values into prompts. A role owns model/thinking/tools policy.";
|
|
598
|
-
return `Judge whether the captured workflow design satisfies each criterion. Do not execute it. Return only JSON: {"criteria":[{"id":"criterion id","pass":true,"evidence":"specific script evidence"}]}.\n\nOriginal request:\n${evalCase.prompt}\n\nCriteria:\n${JSON.stringify(evalCase.semanticCriteria ?? [])}\n\nDSL:\n${docs}\n\nRelevant roles:\n${roleText}\n\nCaptured workflow
|
|
619
|
+
return `Judge whether the captured workflow design satisfies each criterion. Do not execute it. Return only JSON: {"criteria":[{"id":"criterion id","pass":true,"evidence":"specific script evidence"}]}.\n\nOriginal request:\n${evalCase.prompt}\n\nCriteria:\n${JSON.stringify(evalCase.semanticCriteria ?? [])}\n\nDSL:\n${docs}\n\nRelevant roles:\n${roleText}\n\nCaptured workflow call(s):\n${calls.map((call, index) => `--- ${String(index)} ---\nArguments:\n${JSON.stringify(call.arguments)}\nScript:\n${call.script ?? "<missing>"}`).join("\n")}`;
|
|
599
620
|
}
|
|
600
621
|
|
|
601
622
|
async function runSemanticJudge(input: CaptureCaseInput, calls: readonly CapturedWorkflowCall[], cwd: string, home: string, sessionDir: string, maxCost: number): Promise<JudgeProcessResult> {
|
|
@@ -720,7 +741,7 @@ export async function captureEvalCase(input: CaptureCaseInput): Promise<EvalCase
|
|
|
720
741
|
const parentUsageThroughCandidate = usageThroughCandidate(oracle, workflows, selection.callIndices);
|
|
721
742
|
const parentAccounting = parentUsageThroughCandidate ?? oracle.usage;
|
|
722
743
|
const unsafeTool = oracle.parentToolSequence.find((tool) => !SAFE_PARENT_EVAL_TOOLS.includes(tool as (typeof SAFE_PARENT_EVAL_TOOLS)[number]));
|
|
723
|
-
const errors = [...evalExpectationErrors(oracle, input.case.expectations), ...validation.errors, ...(unsafeTool ? [`parent tool is outside the safe eval allowlist: ${unsafeTool}`] : [])];
|
|
744
|
+
const errors = [...evalExpectationErrors(oracle, input.case.expectations), ...recoverySelectionErrors(input.case, oracle), ...validation.errors, ...(unsafeTool ? [`parent tool is outside the safe eval allowlist: ${unsafeTool}`] : [])];
|
|
724
745
|
if (requiredCount > 0 && selection.callIndices.length === 0) errors.push("Catastrophic validity failure: no production-valid workflow candidate satisfied static expectations.");
|
|
725
746
|
let judge: SemanticJudgeReport | undefined;
|
|
726
747
|
let judgeProcess: JudgeProcessResult | undefined;
|