pi-extensible-workflows 0.3.2 → 1.0.1
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 +9 -0
- package/dist/src/agent-execution.d.ts +81 -8
- package/dist/src/agent-execution.js +465 -75
- package/dist/src/ambient-workflow-evals.js +14 -33
- package/dist/src/doctor.d.ts +4 -1
- package/dist/src/doctor.js +57 -14
- package/dist/src/index.d.ts +222 -24
- package/dist/src/index.js +1548 -410
- package/dist/src/persistence.d.ts +28 -1
- package/dist/src/persistence.js +126 -9
- package/dist/src/session-inspector.d.ts +13 -1
- package/dist/src/session-inspector.js +22 -4
- package/dist/src/workflow-evals.d.ts +1 -0
- package/dist/src/workflow-evals.js +23 -25
- package/package.json +5 -2
- package/skills/pi-extensible-workflows/SKILL.md +14 -6
- package/src/agent-execution.ts +391 -86
- package/src/ambient-workflow-evals.ts +18 -28
- package/src/doctor.ts +60 -16
- package/src/index.ts +1332 -395
- package/src/persistence.ts +98 -10
- package/src/session-inspector.ts +25 -7
- package/src/workflow-evals.ts +13 -16
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JsonValue, LaunchSnapshot, RunRecord } from "./index.js";
|
|
1
|
+
import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./index.js";
|
|
2
2
|
import type { OwnershipRecord } from "./agent-execution.js";
|
|
3
3
|
export interface NativeSessionReference {
|
|
4
4
|
sessionId: string;
|
|
@@ -11,6 +11,20 @@ export interface EffectiveSystemPrompt {
|
|
|
11
11
|
sha256: string;
|
|
12
12
|
prompt: string;
|
|
13
13
|
}
|
|
14
|
+
export interface ConversationHead {
|
|
15
|
+
turn: number;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
sessionFile: string;
|
|
18
|
+
leafId: string;
|
|
19
|
+
systemPrompt: string;
|
|
20
|
+
systemPromptSha256: string;
|
|
21
|
+
toolDefinitionsSha256: string;
|
|
22
|
+
}
|
|
23
|
+
export interface PersistedConversation {
|
|
24
|
+
id: string;
|
|
25
|
+
policy: JsonValue;
|
|
26
|
+
head: ConversationHead;
|
|
27
|
+
}
|
|
14
28
|
export interface PersistedRun extends RunRecord {
|
|
15
29
|
nativeSessions: readonly NativeSessionReference[];
|
|
16
30
|
}
|
|
@@ -24,6 +38,7 @@ export interface AwaitingCheckpoint {
|
|
|
24
38
|
prompt: string;
|
|
25
39
|
context: JsonValue;
|
|
26
40
|
}
|
|
41
|
+
export type PendingWorkflowDecision = BudgetApprovalRequest;
|
|
27
42
|
export type PersistedOwnershipNode = OwnershipRecord;
|
|
28
43
|
export interface WorktreeReference {
|
|
29
44
|
owner: string;
|
|
@@ -45,6 +60,8 @@ export declare class SessionLease {
|
|
|
45
60
|
export declare function acquireSessionLease(cwd: string, sessionId: string, home?: string): Promise<SessionLease>;
|
|
46
61
|
export declare function listRunIds(cwd: string, sessionId: string, home?: string): Promise<string[]>;
|
|
47
62
|
export declare function structuralPath(...names: string[]): string;
|
|
63
|
+
export declare function atomicWriteFile(path: string, content: string): Promise<void>;
|
|
64
|
+
export declare function atomicWriteFile(path: string, content: string, sync: true): void;
|
|
48
65
|
export declare class RunStore {
|
|
49
66
|
readonly cwd: string;
|
|
50
67
|
readonly sessionId: string;
|
|
@@ -54,7 +71,9 @@ export declare class RunStore {
|
|
|
54
71
|
private stateWrite;
|
|
55
72
|
private worktreeWrite;
|
|
56
73
|
private snapshotWrite;
|
|
74
|
+
private launchSnapshotWrite;
|
|
57
75
|
private systemPromptWrite;
|
|
76
|
+
private conversationWrite;
|
|
58
77
|
constructor(cwd: string, sessionId: string, runId: string, home?: string);
|
|
59
78
|
create(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>): Promise<void>;
|
|
60
79
|
isComplete(): Promise<boolean>;
|
|
@@ -64,16 +83,24 @@ export declare class RunStore {
|
|
|
64
83
|
}>;
|
|
65
84
|
saveState(run: PersistedRun): Promise<void>;
|
|
66
85
|
updateState(update: (run: PersistedRun) => PersistedRun | Promise<PersistedRun>): Promise<PersistedRun>;
|
|
86
|
+
saveSnapshot(snapshot: Readonly<LaunchSnapshot>): Promise<void>;
|
|
87
|
+
appendEvent(event: WorkflowRunEvent): Promise<void>;
|
|
67
88
|
saveOwnership(nodes: readonly PersistedOwnershipNode[]): Promise<void>;
|
|
68
89
|
loadOwnership(): Promise<readonly PersistedOwnershipNode[]>;
|
|
69
90
|
systemPromptPath(): string;
|
|
70
91
|
recordSystemPrompt(entry: Omit<EffectiveSystemPrompt, "sha256">): Promise<void>;
|
|
71
92
|
systemPrompts(): Promise<readonly EffectiveSystemPrompt[]>;
|
|
93
|
+
conversationPath(): string;
|
|
94
|
+
conversation(id: string): Promise<PersistedConversation | undefined>;
|
|
95
|
+
saveConversation(conversation: PersistedConversation): Promise<void>;
|
|
72
96
|
private updateJournal;
|
|
73
97
|
complete(path: string, value: JsonValue): Promise<void>;
|
|
74
98
|
replay(path: string): Promise<CompletedOperation | undefined>;
|
|
75
99
|
awaitCheckpoint(checkpoint: AwaitingCheckpoint): Promise<boolean | undefined>;
|
|
76
100
|
awaitingCheckpoints(): Promise<readonly AwaitingCheckpoint[]>;
|
|
101
|
+
requestWorkflowDecision(request: PendingWorkflowDecision): Promise<void>;
|
|
102
|
+
pendingWorkflowDecisions(): Promise<readonly PendingWorkflowDecision[]>;
|
|
103
|
+
answerWorkflowDecision(proposalId: string, approved: boolean): Promise<PendingWorkflowDecision | undefined>;
|
|
77
104
|
answerCheckpoint(name: string, approved: boolean): Promise<AwaitingCheckpoint | undefined>;
|
|
78
105
|
private expectedWorktree;
|
|
79
106
|
private markerPath;
|
package/dist/src/persistence.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
import { renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { access, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
6
|
import { homedir } from "node:os";
|
|
6
7
|
import { promisify } from "node:util";
|
|
7
8
|
import { loadLaunchSnapshot, WorkflowError } from "./index.js";
|
|
9
|
+
function isConversationArtifact(value) {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
11
|
+
return false;
|
|
12
|
+
const artifact = value;
|
|
13
|
+
return artifact.version === 1 && Boolean(artifact.conversations) && typeof artifact.conversations === "object" && !Array.isArray(artifact.conversations);
|
|
14
|
+
}
|
|
8
15
|
const execute = promisify(execFile);
|
|
9
16
|
const gitIdentity = {
|
|
10
17
|
GIT_AUTHOR_NAME: "pi-extensible-workflows", GIT_AUTHOR_EMAIL: "pi-extensible-workflows@localhost", GIT_COMMITTER_NAME: "pi-extensible-workflows", GIT_COMMITTER_EMAIL: "pi-extensible-workflows@localhost",
|
|
@@ -176,10 +183,32 @@ export function structuralPath(...names) {
|
|
|
176
183
|
throw new WorkflowError("INVALID_METADATA", "Structural paths require non-empty explicit names");
|
|
177
184
|
return names.map((name) => encodeURIComponent(name)).join("/");
|
|
178
185
|
}
|
|
179
|
-
|
|
186
|
+
export function atomicWriteFile(path, content, sync = false) {
|
|
180
187
|
const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
if (sync) {
|
|
189
|
+
try {
|
|
190
|
+
writeFileSync(temporary, content, { encoding: "utf8", mode: 0o600 });
|
|
191
|
+
renameSync(temporary, path);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
try {
|
|
195
|
+
rmSync(temporary, { force: true });
|
|
196
|
+
}
|
|
197
|
+
catch { /* Preserve the original write error. */ }
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
return writeFile(temporary, content, { encoding: "utf8", mode: 0o600 }).then(() => rename(temporary, path)).catch(async (error) => {
|
|
203
|
+
try {
|
|
204
|
+
await rm(temporary, { force: true });
|
|
205
|
+
}
|
|
206
|
+
catch { /* Preserve the original write error. */ }
|
|
207
|
+
throw error;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function atomicJson(path, value) {
|
|
211
|
+
await atomicWriteFile(path, `${JSON.stringify(value)}\n`);
|
|
183
212
|
}
|
|
184
213
|
async function json(path) { return JSON.parse(await readFile(path, "utf8")); }
|
|
185
214
|
export class RunStore {
|
|
@@ -192,8 +221,10 @@ export class RunStore {
|
|
|
192
221
|
stateWrite = Promise.resolve();
|
|
193
222
|
worktreeWrite = Promise.resolve();
|
|
194
223
|
snapshotWrite = Promise.resolve();
|
|
224
|
+
launchSnapshotWrite = Promise.resolve();
|
|
195
225
|
// ponytail: the session lease prevents concurrent RunStore writers for one run.
|
|
196
226
|
systemPromptWrite = Promise.resolve();
|
|
227
|
+
conversationWrite = Promise.resolve();
|
|
197
228
|
constructor(cwd, sessionId, runId, home = homedir()) {
|
|
198
229
|
this.cwd = cwd;
|
|
199
230
|
this.sessionId = sessionId;
|
|
@@ -208,12 +239,14 @@ export class RunStore {
|
|
|
208
239
|
await mkdir(dirname(this.directory), { recursive: true, mode: 0o700 });
|
|
209
240
|
await mkdir(temporary, { mode: 0o700 });
|
|
210
241
|
try {
|
|
242
|
+
await writeFile(join(temporary, "workflow.js"), snapshot.script, { encoding: "utf8", mode: 0o600 });
|
|
211
243
|
await atomicJson(join(temporary, "snapshot.json"), snapshot);
|
|
212
|
-
await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {} });
|
|
244
|
+
await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {}, decisions: {} });
|
|
213
245
|
await atomicJson(join(temporary, "ownership.json"), []);
|
|
214
246
|
await atomicJson(join(temporary, "worktrees.json"), []);
|
|
215
247
|
await atomicJson(join(temporary, "state.json"), run);
|
|
216
248
|
await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
|
|
249
|
+
await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
|
|
217
250
|
await rename(temporary, this.directory);
|
|
218
251
|
}
|
|
219
252
|
catch (error) {
|
|
@@ -261,6 +294,14 @@ export class RunStore {
|
|
|
261
294
|
await write;
|
|
262
295
|
return result;
|
|
263
296
|
}
|
|
297
|
+
async saveSnapshot(snapshot) {
|
|
298
|
+
const write = this.launchSnapshotWrite.then(() => atomicJson(join(this.directory, "snapshot.json"), snapshot));
|
|
299
|
+
this.launchSnapshotWrite = write.catch(() => undefined);
|
|
300
|
+
await write;
|
|
301
|
+
}
|
|
302
|
+
async appendEvent(event) {
|
|
303
|
+
await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.type === event.type && current.message === event.message) ? [] : [event])] }));
|
|
304
|
+
}
|
|
264
305
|
async saveOwnership(nodes) {
|
|
265
306
|
await atomicJson(join(this.directory, "ownership.json"), nodes);
|
|
266
307
|
}
|
|
@@ -284,6 +325,54 @@ export class RunStore {
|
|
|
284
325
|
return (await json(this.systemPromptPath()).catch((error) => { if (error.code === "ENOENT")
|
|
285
326
|
return { version: 1, entries: [] }; throw error; })).entries;
|
|
286
327
|
}
|
|
328
|
+
conversationPath() { return join(this.directory, "conversations.json"); }
|
|
329
|
+
async conversation(id) {
|
|
330
|
+
await this.conversationWrite;
|
|
331
|
+
let artifact;
|
|
332
|
+
try {
|
|
333
|
+
const raw = await json(this.conversationPath());
|
|
334
|
+
if (!isConversationArtifact(raw))
|
|
335
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
|
|
336
|
+
artifact = raw;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (error.code === "ENOENT")
|
|
340
|
+
return undefined;
|
|
341
|
+
if (error instanceof WorkflowError)
|
|
342
|
+
throw error;
|
|
343
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
344
|
+
}
|
|
345
|
+
return artifact.conversations[id];
|
|
346
|
+
}
|
|
347
|
+
async saveConversation(conversation) {
|
|
348
|
+
const write = this.conversationWrite.then(async () => {
|
|
349
|
+
const path = this.conversationPath();
|
|
350
|
+
let artifact;
|
|
351
|
+
try {
|
|
352
|
+
const raw = await json(path);
|
|
353
|
+
if (!isConversationArtifact(raw))
|
|
354
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
|
|
355
|
+
artifact = raw;
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (error.code === "ENOENT")
|
|
359
|
+
artifact = { version: 1, conversations: {} };
|
|
360
|
+
else if (error instanceof WorkflowError)
|
|
361
|
+
throw error;
|
|
362
|
+
else
|
|
363
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
364
|
+
}
|
|
365
|
+
const previous = artifact.conversations[conversation.id];
|
|
366
|
+
if (previous && previous.head.turn + 1 !== conversation.head.turn)
|
|
367
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation head is not the previous turn: ${conversation.id}`);
|
|
368
|
+
if (!previous && conversation.head.turn !== 1)
|
|
369
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation must start at turn one: ${conversation.id}`);
|
|
370
|
+
artifact.conversations[conversation.id] = structuredClone(conversation);
|
|
371
|
+
await atomicJson(path, artifact);
|
|
372
|
+
});
|
|
373
|
+
this.conversationWrite = write.catch(() => undefined);
|
|
374
|
+
await write;
|
|
375
|
+
}
|
|
287
376
|
async updateJournal(update) {
|
|
288
377
|
let result;
|
|
289
378
|
const write = this.journalWrite.then(async () => {
|
|
@@ -322,13 +411,31 @@ export class RunStore {
|
|
|
322
411
|
const journal = await json(join(this.directory, "journal.json"));
|
|
323
412
|
return Object.values(journal.awaiting ?? {});
|
|
324
413
|
}
|
|
414
|
+
async requestWorkflowDecision(request) {
|
|
415
|
+
await this.updateJournal((journal) => { journal.decisions ??= {}; journal.decisions[request.proposalId] = request; });
|
|
416
|
+
}
|
|
417
|
+
async pendingWorkflowDecisions() {
|
|
418
|
+
await this.journalWrite;
|
|
419
|
+
const journal = await json(join(this.directory, "journal.json"));
|
|
420
|
+
return Object.values(journal.decisions ?? {});
|
|
421
|
+
}
|
|
422
|
+
async answerWorkflowDecision(proposalId, approved) {
|
|
423
|
+
return this.updateJournal((journal) => {
|
|
424
|
+
const request = journal.decisions?.[proposalId];
|
|
425
|
+
if (!request)
|
|
426
|
+
return undefined;
|
|
427
|
+
journal.completed[`decision/${proposalId}`] = { path: `decision/${proposalId}`, value: approved };
|
|
428
|
+
delete journal.decisions?.[proposalId];
|
|
429
|
+
return request;
|
|
430
|
+
});
|
|
431
|
+
}
|
|
325
432
|
async answerCheckpoint(name, approved) {
|
|
326
433
|
return this.updateJournal((journal) => {
|
|
327
|
-
const checkpoint = Object.values(journal.awaiting).find((item) => item.name === name);
|
|
434
|
+
const checkpoint = Object.values(journal.awaiting ?? {}).find((item) => item.name === name);
|
|
328
435
|
if (!checkpoint || journal.completed[checkpoint.path])
|
|
329
436
|
return undefined;
|
|
330
437
|
journal.completed[checkpoint.path] = { path: checkpoint.path, value: approved };
|
|
331
|
-
journal.awaiting = Object.fromEntries(Object.entries(journal.awaiting).filter(([path]) => path !== checkpoint.path));
|
|
438
|
+
journal.awaiting = Object.fromEntries(Object.entries(journal.awaiting ?? {}).filter(([path]) => path !== checkpoint.path));
|
|
332
439
|
return checkpoint;
|
|
333
440
|
});
|
|
334
441
|
}
|
|
@@ -461,9 +568,19 @@ export class RunStore {
|
|
|
461
568
|
try {
|
|
462
569
|
const write = this.snapshotWrite.then(async () => {
|
|
463
570
|
const record = await this.worktree(owner);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
await git(record.path, ["
|
|
571
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
572
|
+
await git(record.path, ["add", "-A"]);
|
|
573
|
+
if (!(await git(record.path, ["status", "--porcelain"])).trim())
|
|
574
|
+
break;
|
|
575
|
+
try {
|
|
576
|
+
await git(record.path, ["commit", "-m", "pi-extensible-workflows runtime snapshot"], gitIdentity);
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
if (attempt === 2)
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
467
584
|
return (await git(record.path, ["rev-parse", "HEAD"])).trim();
|
|
468
585
|
});
|
|
469
586
|
this.snapshotWrite = write.then(() => undefined, () => undefined);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SessionEntry, type SessionInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type ModelSpec, type StaticWorkflowCall } from "./index.js";
|
|
2
|
+
import { type AgentSetupSummary, type ModelSpec, type StaticWorkflowCall } from "./index.js";
|
|
3
|
+
import { type PersistedRun } from "./persistence.js";
|
|
3
4
|
export interface ModelUsage {
|
|
4
5
|
model: string;
|
|
5
6
|
cost: number;
|
|
@@ -12,16 +13,19 @@ export interface AttemptReport {
|
|
|
12
13
|
cost: number;
|
|
13
14
|
models: readonly ModelUsage[];
|
|
14
15
|
error?: string;
|
|
16
|
+
setup?: AgentSetupSummary;
|
|
15
17
|
}
|
|
16
18
|
export interface AgentReport {
|
|
17
19
|
name: string;
|
|
18
20
|
label?: string;
|
|
19
21
|
state: string;
|
|
20
22
|
role?: string;
|
|
23
|
+
requestedModel?: string;
|
|
21
24
|
model: string;
|
|
22
25
|
thinking?: ModelSpec["thinking"];
|
|
23
26
|
cost: number;
|
|
24
27
|
attempts: readonly AttemptReport[];
|
|
28
|
+
setup?: AgentSetupSummary;
|
|
25
29
|
}
|
|
26
30
|
export interface WorkflowReport {
|
|
27
31
|
name: string;
|
|
@@ -34,6 +38,14 @@ export interface WorkflowReport {
|
|
|
34
38
|
cost: number;
|
|
35
39
|
models: readonly ModelUsage[];
|
|
36
40
|
agents: readonly AgentReport[];
|
|
41
|
+
budget?: PersistedRun["budget"];
|
|
42
|
+
budgetVersion?: number;
|
|
43
|
+
usage?: PersistedRun["usage"];
|
|
44
|
+
budgetEvents?: PersistedRun["budgetEvents"];
|
|
45
|
+
events?: readonly {
|
|
46
|
+
type: string;
|
|
47
|
+
message: string;
|
|
48
|
+
}[];
|
|
37
49
|
}
|
|
38
50
|
export interface SessionReport {
|
|
39
51
|
id: string;
|
|
@@ -4,7 +4,7 @@ import { emitKeypressEvents } from "node:readline";
|
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { stdin, stdout } from "node:process";
|
|
6
6
|
import { highlightCode, initTheme, SessionManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { inspectWorkflowScript } from "./index.js";
|
|
7
|
+
import { formatBudgetStatus, inspectWorkflowScript } from "./index.js";
|
|
8
8
|
import { listRunIds, RunStore } from "./persistence.js";
|
|
9
9
|
function text(content) {
|
|
10
10
|
if (typeof content === "string")
|
|
@@ -170,6 +170,7 @@ async function agentReport(agent) {
|
|
|
170
170
|
cost,
|
|
171
171
|
models: log.models.length ? log.models : [{ model, cost }],
|
|
172
172
|
...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
|
|
173
|
+
...(attempt.setup ? { setup: attempt.setup } : {}),
|
|
173
174
|
});
|
|
174
175
|
continue;
|
|
175
176
|
}
|
|
@@ -182,6 +183,7 @@ async function agentReport(agent) {
|
|
|
182
183
|
cost,
|
|
183
184
|
models: [{ model: fallbackModel, cost }],
|
|
184
185
|
...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
|
|
186
|
+
...(attempt.setup ? { setup: attempt.setup } : {}),
|
|
185
187
|
});
|
|
186
188
|
}
|
|
187
189
|
if (!attempts.length) {
|
|
@@ -189,7 +191,7 @@ async function agentReport(agent) {
|
|
|
189
191
|
attempts.push({ attempt: 1, prompt: "(transcript unavailable)", model: fallbackModel, ...(fallbackThinking !== undefined ? { thinking: fallbackThinking } : {}), cost, models: [{ model: fallbackModel, cost }] });
|
|
190
192
|
}
|
|
191
193
|
const latest = attempts[attempts.length - 1];
|
|
192
|
-
return { name: agent.name, ...(agent.label ? { label: agent.label } : {}), state: agent.state, ...(agent.role ? { role: agent.role } : {}), model: latest?.model ?? fallbackModel, ...(latest?.thinking !== undefined ? { thinking: latest.thinking } : {}), cost: attempts.reduce((sum, attempt) => sum + attempt.cost, 0), attempts };
|
|
194
|
+
return { name: agent.name, ...(agent.label ? { label: agent.label } : {}), state: agent.state, ...(agent.role ? { role: agent.role } : {}), ...(agent.requestedModel ? { requestedModel: agent.requestedModel } : {}), model: latest?.model ?? fallbackModel, ...(latest?.thinking !== undefined ? { thinking: latest.thinking } : {}), cost: attempts.reduce((sum, attempt) => sum + attempt.cost, 0), attempts, ...(latest?.setup ? { setup: latest.setup } : {}) };
|
|
193
195
|
}
|
|
194
196
|
export function matchSession(query, sessions) {
|
|
195
197
|
const exact = sessions.filter(({ id }) => id === query);
|
|
@@ -242,6 +244,11 @@ export async function loadSessionReport(path, home = homedir()) {
|
|
|
242
244
|
cost: agents.reduce((sum, agent) => sum + agent.cost, 0),
|
|
243
245
|
models,
|
|
244
246
|
agents,
|
|
247
|
+
...(loaded?.run.budget ? { budget: loaded.run.budget } : {}),
|
|
248
|
+
...(loaded?.run.budgetVersion !== undefined ? { budgetVersion: loaded.run.budgetVersion } : {}),
|
|
249
|
+
...(loaded?.run.usage ? { usage: loaded.run.usage } : {}),
|
|
250
|
+
...(loaded?.run.budgetEvents ? { budgetEvents: loaded.run.budgetEvents } : {}),
|
|
251
|
+
...(loaded?.run.events?.length ? { events: loaded.run.events } : {})
|
|
245
252
|
});
|
|
246
253
|
}
|
|
247
254
|
const workflowCost = workflows.reduce((sum, workflow) => sum + workflow.cost, 0);
|
|
@@ -263,7 +270,9 @@ function detailLines(workflow) {
|
|
|
263
270
|
style(ansi.bold + ansi.cyan, workflow.name),
|
|
264
271
|
`${workflow.status} · ${money(workflow.cost)}${workflow.runId ? ` · ${workflow.runId}` : ""}`,
|
|
265
272
|
workflow.description ?? "",
|
|
273
|
+
...(workflow.events?.length ? ["", style(ansi.bold, "Run events"), ...workflow.events.map((event) => `${event.type}: ${event.message}`)] : []),
|
|
266
274
|
"",
|
|
275
|
+
...(workflow.budget ? formatBudgetStatus({ budget: workflow.budget, ...(workflow.budgetVersion !== undefined ? { budgetVersion: workflow.budgetVersion } : {}), ...(workflow.usage ? { usage: workflow.usage } : {}), ...(workflow.budgetEvents ? { budgetEvents: workflow.budgetEvents } : {}) }).map((line) => `Budget ${line}`) : []),
|
|
267
276
|
style(ansi.bold, "Models"),
|
|
268
277
|
modelSummary(workflow.models),
|
|
269
278
|
"",
|
|
@@ -278,9 +287,18 @@ function detailLines(workflow) {
|
|
|
278
287
|
if (!workflow.agents.length)
|
|
279
288
|
lines.push("(no agent run was persisted)");
|
|
280
289
|
for (const agent of workflow.agents) {
|
|
281
|
-
lines.push("", style(agent.state === "completed" ? ansi.green : agent.state === "failed" ? ansi.red : ansi.yellow, `${agent.label ?? agent.name} [${agent.state}]`), `${agent.role ? `role=${agent.role} · ` : ""}${agent.model}${agent.thinking !== undefined ? `:${agent.thinking}` : ""} · ${money(agent.cost)}`);
|
|
290
|
+
lines.push("", style(agent.state === "completed" ? ansi.green : agent.state === "failed" ? ansi.red : ansi.yellow, `${agent.label ?? agent.name} [${agent.state}]`), `${agent.role ? `role=${agent.role} · ` : ""}${agent.requestedModel ? `requested=${agent.requestedModel} · ` : ""}${agent.model}${agent.thinking !== undefined ? `:${agent.thinking}` : ""} · ${money(agent.cost)}`);
|
|
282
291
|
for (const attempt of agent.attempts) {
|
|
283
|
-
lines.push(`Attempt ${String(attempt.attempt)} · ${attempt.model}${attempt.thinking !== undefined ? `:${attempt.thinking}` : ""} · ${money(attempt.cost)}${attempt.error ? ` · ${attempt.error}` : ""}`, `Prompt: ${attempt.prompt}
|
|
292
|
+
lines.push(`Attempt ${String(attempt.attempt)} · ${attempt.model}${attempt.thinking !== undefined ? `:${attempt.thinking}` : ""} · ${money(attempt.cost)}${attempt.error ? ` · ${attempt.error}` : ""}`, `Prompt: ${attempt.prompt}`, ...(attempt.setup ? [
|
|
293
|
+
`Hooks: ${attempt.setup.hookNames.join(", ") || "(none)"}`,
|
|
294
|
+
`Effective: model=${attempt.setup.model.provider}/${attempt.setup.model.model}${attempt.setup.model.thinking ? `:${attempt.setup.model.thinking}` : ""} tools=${attempt.setup.tools.join(",") || "(none)"} cwd=${attempt.setup.cwd}`,
|
|
295
|
+
...(attempt.setup.disabledAgentResources ? [
|
|
296
|
+
`Disabled skills: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
|
|
297
|
+
`Disabled extensions: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
|
|
298
|
+
`Unmatched skills: ${attempt.setup.disabledAgentResources.unmatchedSkills.join(", ") || "(none)"}`,
|
|
299
|
+
`Unmatched extensions: ${attempt.setup.disabledAgentResources.unmatchedExtensions.join(", ") || "(none)"}`,
|
|
300
|
+
] : []),
|
|
301
|
+
] : []));
|
|
284
302
|
}
|
|
285
303
|
}
|
|
286
304
|
return lines.filter((line, index) => line || index !== 2);
|
|
@@ -252,6 +252,7 @@ export interface CaptureCaseInput {
|
|
|
252
252
|
maxCost: number;
|
|
253
253
|
}
|
|
254
254
|
export declare function parseSemanticJudge(raw: string, criteria: readonly SemanticCriterion[]): CriterionResult[];
|
|
255
|
+
export declare function findSessionFile(directory: string, sessionId: string): string | undefined;
|
|
255
256
|
export declare function captureEvalCase(input: CaptureCaseInput): Promise<EvalCaseResult>;
|
|
256
257
|
export interface IsolatedProcessOptions {
|
|
257
258
|
childPath: string;
|
|
@@ -8,13 +8,12 @@ import { Value } from "typebox/value";
|
|
|
8
8
|
import { getAgentDir, parseFrontmatter, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { CAPTURE_ERROR_PREFIX, CAPTURE_IDENTITY, resolveWorkflowSkillPath } from "./eval-capture-extension.js";
|
|
10
10
|
export { resolveWorkflowSkillPath } from "./eval-capture-extension.js";
|
|
11
|
-
import { ERROR_CODES, inspectWorkflowScript, loadAgentDefinitions, runWorkflow, WorkflowError } from "./index.js";
|
|
11
|
+
import { ERROR_CODES, inspectWorkflowScript, isObject, loadAgentDefinitions, runWorkflow, WORKFLOW_CALL_KINDS, WorkflowError } from "./index.js";
|
|
12
12
|
const CASE_PROCESS_GRACE_MS = 1_000;
|
|
13
13
|
export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow"]);
|
|
14
14
|
const EVAL_MODEL_TOKEN = "$EVAL_MODEL";
|
|
15
15
|
const semantic = (description) => [{ id: "intent", description }];
|
|
16
16
|
const JSON_RESULT_TYPES = ["null", "boolean", "number", "integer", "string", "array", "object"];
|
|
17
|
-
const WORKFLOW_CALL_KINDS = ["agent", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
|
|
18
17
|
const AGENT_OPTION_NAMES = ["role", "model", "thinking", "tools", "retries"];
|
|
19
18
|
const expectationKeys = ["firstSignificantAction", "firstTool", "firstBatchToolSequence", "parentToolSequence", "workflowCallCount", "requiredOperations", "forbiddenOperations", "requiredRoles", "minimumAgentCalls", "requireOutputSchema", "expectedResults", "agentPolicies", "requiredAgentOrder", "requiredAgentStructures", "requiredDataFlow"];
|
|
20
19
|
const caseKeys = ["id", "prompt", "timeoutMs", "maxCost", "expectations", "expectedWorkflowCalls", "semanticCriteria"];
|
|
@@ -234,7 +233,6 @@ catch (error) {
|
|
|
234
233
|
cases.push(candidate);
|
|
235
234
|
} return Object.freeze(cases); }
|
|
236
235
|
export const INITIAL_WORKFLOW_EVAL_CASES = loadWorkflowEvalCases();
|
|
237
|
-
function isObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
238
236
|
function isJson(value) { if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
239
237
|
return true; if (typeof value === "number")
|
|
240
238
|
return Number.isFinite(value); if (Array.isArray(value))
|
|
@@ -945,6 +943,27 @@ function seedEvalProject(cwd, home, model) {
|
|
|
945
943
|
writeFileSync(path, `${content.slice(0, frontmatterEnd).replace(/^model:.*$/m, `model: ${model}`)}${content.slice(frontmatterEnd)}`);
|
|
946
944
|
}
|
|
947
945
|
}
|
|
946
|
+
export function findSessionFile(directory, sessionId) {
|
|
947
|
+
if (!existsSync(directory))
|
|
948
|
+
return undefined;
|
|
949
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
950
|
+
const path = join(directory, entry.name);
|
|
951
|
+
if (entry.isDirectory()) {
|
|
952
|
+
const found = findSessionFile(path, sessionId);
|
|
953
|
+
if (found)
|
|
954
|
+
return found;
|
|
955
|
+
}
|
|
956
|
+
else if (entry.name.endsWith(".jsonl")) {
|
|
957
|
+
try {
|
|
958
|
+
const header = JSON.parse(readFileSync(path, "utf8").split("\n")[0] ?? "{}");
|
|
959
|
+
if (isObject(header) && header.id === sessionId)
|
|
960
|
+
return path;
|
|
961
|
+
}
|
|
962
|
+
catch { /* Ignore incomplete sessions. */ }
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return undefined;
|
|
966
|
+
}
|
|
948
967
|
async function findParentSession(cwd, sessionDir, sessionId) {
|
|
949
968
|
try {
|
|
950
969
|
const sessions = await SessionManager.list(cwd, sessionDir);
|
|
@@ -953,28 +972,7 @@ async function findParentSession(cwd, sessionDir, sessionId) {
|
|
|
953
972
|
return found.path;
|
|
954
973
|
}
|
|
955
974
|
catch { /* Fall through to the JSONL scan. */ }
|
|
956
|
-
|
|
957
|
-
if (!existsSync(directory))
|
|
958
|
-
return undefined;
|
|
959
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
960
|
-
const path = join(directory, entry.name);
|
|
961
|
-
if (entry.isDirectory()) {
|
|
962
|
-
const found = visit(path);
|
|
963
|
-
if (found)
|
|
964
|
-
return found;
|
|
965
|
-
}
|
|
966
|
-
else if (entry.name.endsWith(".jsonl")) {
|
|
967
|
-
try {
|
|
968
|
-
const header = JSON.parse(readFileSync(path, "utf8").split("\n")[0] ?? "{}");
|
|
969
|
-
if (isObject(header) && header.id === sessionId)
|
|
970
|
-
return path;
|
|
971
|
-
}
|
|
972
|
-
catch { /* Ignore incomplete files. */ }
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
return undefined;
|
|
976
|
-
};
|
|
977
|
-
return visit(sessionDir);
|
|
975
|
+
return findSessionFile(sessionDir, sessionId);
|
|
978
976
|
}
|
|
979
977
|
function emptyAccounting(cost = 0) { return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost, models: [] }; }
|
|
980
978
|
function emptyMetrics(requiredWorkflowCallCount = 1) { return { parentUsageThroughCandidate: null, parentOutputTokensThroughCandidate: null, nonWorkflowToolSequenceBeforeCandidate: [], nonWorkflowToolCallCountBeforeCandidate: 0, workflowCallCountBeforeCandidate: 0, invalidWorkflowCallCount: 0, productionValidationErrorCodes: [], candidateCallIndices: [], staticCandidates: [], semanticCriteria: [], anyValidCandidate: false, requiredWorkflowCallCount, surplusWorkflowCallCount: 0 }; }
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Deterministic multi-agent workflow orchestration for Pi",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
|
-
"repository": {
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/vekexasia/pi-extensible-workflows.git"
|
|
9
|
+
},
|
|
7
10
|
"type": "module",
|
|
8
11
|
"keywords": [
|
|
9
12
|
"pi-package",
|
|
@@ -34,10 +34,11 @@ return agent(
|
|
|
34
34
|
|
|
35
35
|
To pass structured input from the main agent, include `args`:
|
|
36
36
|
```json
|
|
37
|
-
{ "workflow": "
|
|
37
|
+
{ "workflow": "workflowName", "args": { "issue": 42 } }
|
|
38
38
|
```
|
|
39
39
|
Inside the workflow, read `args.issue`; omitted `args` is `null`.
|
|
40
|
-
|
|
40
|
+
Use `workflow_stop` with the exact run ID to stop an active background run from the current Pi session.
|
|
41
|
+
If `workflow_catalog` is available, call it once before creating the first workflow for a task. Use the returned global functions, variables, registered workflows, and configured model aliases as needed for the rest of that task. Alias targets are catalog metadata, not an availability probe. Do not try to reinvent already exposed functions.
|
|
41
42
|
|
|
42
43
|
Pass downstream only needed results. Workflow JavaScript has no imports, filesystem, network, process, or timers; delegate such work to agents with the required tools.
|
|
43
44
|
|
|
@@ -46,7 +47,7 @@ Pass downstream only needed results. Workflow JavaScript has no imports, filesys
|
|
|
46
47
|
```typescript
|
|
47
48
|
interface AgentOptions {
|
|
48
49
|
label?: string; // optional non-empty display name
|
|
49
|
-
model?:
|
|
50
|
+
model?: string; // configured alias or provider/model[:thinking]
|
|
50
51
|
thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
51
52
|
role?: string; // one of the available workflow roles
|
|
52
53
|
tools?: string[]; // [] = no tools; omitted uses role or launch tools
|
|
@@ -56,7 +57,9 @@ interface AgentOptions {
|
|
|
56
57
|
}
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
Extensions may define additional JSON-compatible agent option keys such as `advisor: true`. Core-owned keys still use the validation and role constraints above; extension options are passed to setup hooks and native setup but are not inherited by child agents.
|
|
61
|
+
|
|
62
|
+
Agent calls are unnamed. Direct `agent(...)` calls receive hidden source call-site identity; JavaScript aliases for workflow calls are unsupported. Calls from one source call site must not race outside `parallel` or `pipeline`, whose structural keys keep replay deterministic.
|
|
60
63
|
|
|
61
64
|
## Shared worktree scope
|
|
62
65
|
|
|
@@ -89,7 +92,11 @@ const results = await parallel("implementation", {
|
|
|
89
92
|
});
|
|
90
93
|
```
|
|
91
94
|
|
|
92
|
-
Registered extension functions receive `withWorktree` in their context, so they may create a shared scope internally.
|
|
95
|
+
Registered extension functions receive `withWorktree` in their context, so they may create a shared scope internally. They can compose other registered functions without importing their source:
|
|
96
|
+
```ts
|
|
97
|
+
const report = await context.invoke("reviewRepository", { focus: "security" });
|
|
98
|
+
```
|
|
99
|
+
Their public inputs and outputs must remain JSON; callbacks cannot cross the extension-function boundary.
|
|
93
100
|
|
|
94
101
|
## Rules
|
|
95
102
|
|
|
@@ -101,7 +108,8 @@ Registered extension functions receive `withWorktree` in their context, so they
|
|
|
101
108
|
- Preserve item metadata in workflow code between pipeline stages instead of requiring agents to echo it through `outputSchema`.
|
|
102
109
|
- Repeated work uses a JavaScript loop; each direct `agent(...)` call receives deterministic call-site and occurrence identity.
|
|
103
110
|
- Runs default to background; set tool-call `foreground: true` when asked to wait.
|
|
104
|
-
-
|
|
111
|
+
- Add `budget` only when the run needs aggregate limits. The only valid dimension names are exactly `tokens`, `costUsd`, `durationMs`, and `agentLaunches` (never `cost`, `duration`, `launches`, or other shorthand). Each dimension is `{ soft?: number, hard?: number }`; `soft` must be less than `hard`.
|
|
112
|
+
- A `budget_exhausted` run is resumable through `workflow_resume`. Omitted patch values stay unchanged, explicit `null` removes a limit, and any relaxation requires an exact human-approved proposal through `workflow_respond`.
|
|
105
113
|
- `parallel()` and `pipeline()` return keyed bare values. Await results before use.
|
|
106
114
|
- Interpolate results with `prompt("...{value}", { value })`; placeholders in plain strings stay literal.
|
|
107
115
|
- Use `outputSchema` only when another phase must compare, aggregate, or validate the result. Never add it to a final agent whose prose is returned directly. Keep only fields the consumer needs, and avoid repeating the same evidence in multiple schemas.
|