pi-extensible-workflows 0.3.2 → 1.0.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 +9 -0
- package/dist/src/agent-execution.d.ts +81 -8
- package/dist/src/agent-execution.js +421 -71
- package/dist/src/doctor.d.ts +4 -1
- package/dist/src/doctor.js +56 -11
- package/dist/src/index.d.ts +217 -22
- package/dist/src/index.js +1416 -337
- package/dist/src/persistence.d.ts +26 -1
- package/dist/src/persistence.js +100 -6
- package/dist/src/session-inspector.d.ts +13 -1
- package/dist/src/session-inspector.js +22 -4
- package/dist/src/workflow-evals.js +1 -1
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +9 -5
- package/src/agent-execution.ts +350 -83
- package/src/doctor.ts +59 -12
- package/src/index.ts +1231 -360
- package/src/persistence.ts +76 -7
- package/src/session-inspector.ts +25 -7
- package/src/workflow-evals.ts +1 -1
|
@@ -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;
|
|
@@ -54,7 +69,9 @@ export declare class RunStore {
|
|
|
54
69
|
private stateWrite;
|
|
55
70
|
private worktreeWrite;
|
|
56
71
|
private snapshotWrite;
|
|
72
|
+
private launchSnapshotWrite;
|
|
57
73
|
private systemPromptWrite;
|
|
74
|
+
private conversationWrite;
|
|
58
75
|
constructor(cwd: string, sessionId: string, runId: string, home?: string);
|
|
59
76
|
create(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>): Promise<void>;
|
|
60
77
|
isComplete(): Promise<boolean>;
|
|
@@ -64,16 +81,24 @@ export declare class RunStore {
|
|
|
64
81
|
}>;
|
|
65
82
|
saveState(run: PersistedRun): Promise<void>;
|
|
66
83
|
updateState(update: (run: PersistedRun) => PersistedRun | Promise<PersistedRun>): Promise<PersistedRun>;
|
|
84
|
+
saveSnapshot(snapshot: Readonly<LaunchSnapshot>): Promise<void>;
|
|
85
|
+
appendEvent(event: WorkflowRunEvent): Promise<void>;
|
|
67
86
|
saveOwnership(nodes: readonly PersistedOwnershipNode[]): Promise<void>;
|
|
68
87
|
loadOwnership(): Promise<readonly PersistedOwnershipNode[]>;
|
|
69
88
|
systemPromptPath(): string;
|
|
70
89
|
recordSystemPrompt(entry: Omit<EffectiveSystemPrompt, "sha256">): Promise<void>;
|
|
71
90
|
systemPrompts(): Promise<readonly EffectiveSystemPrompt[]>;
|
|
91
|
+
conversationPath(): string;
|
|
92
|
+
conversation(id: string): Promise<PersistedConversation | undefined>;
|
|
93
|
+
saveConversation(conversation: PersistedConversation): Promise<void>;
|
|
72
94
|
private updateJournal;
|
|
73
95
|
complete(path: string, value: JsonValue): Promise<void>;
|
|
74
96
|
replay(path: string): Promise<CompletedOperation | undefined>;
|
|
75
97
|
awaitCheckpoint(checkpoint: AwaitingCheckpoint): Promise<boolean | undefined>;
|
|
76
98
|
awaitingCheckpoints(): Promise<readonly AwaitingCheckpoint[]>;
|
|
99
|
+
requestWorkflowDecision(request: PendingWorkflowDecision): Promise<void>;
|
|
100
|
+
pendingWorkflowDecisions(): Promise<readonly PendingWorkflowDecision[]>;
|
|
101
|
+
answerWorkflowDecision(proposalId: string, approved: boolean): Promise<PendingWorkflowDecision | undefined>;
|
|
77
102
|
answerCheckpoint(name: string, approved: boolean): Promise<AwaitingCheckpoint | undefined>;
|
|
78
103
|
private expectedWorktree;
|
|
79
104
|
private markerPath;
|
package/dist/src/persistence.js
CHANGED
|
@@ -5,6 +5,12 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { loadLaunchSnapshot, WorkflowError } from "./index.js";
|
|
8
|
+
function isConversationArtifact(value) {
|
|
9
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10
|
+
return false;
|
|
11
|
+
const artifact = value;
|
|
12
|
+
return artifact.version === 1 && Boolean(artifact.conversations) && typeof artifact.conversations === "object" && !Array.isArray(artifact.conversations);
|
|
13
|
+
}
|
|
8
14
|
const execute = promisify(execFile);
|
|
9
15
|
const gitIdentity = {
|
|
10
16
|
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",
|
|
@@ -192,8 +198,10 @@ export class RunStore {
|
|
|
192
198
|
stateWrite = Promise.resolve();
|
|
193
199
|
worktreeWrite = Promise.resolve();
|
|
194
200
|
snapshotWrite = Promise.resolve();
|
|
201
|
+
launchSnapshotWrite = Promise.resolve();
|
|
195
202
|
// ponytail: the session lease prevents concurrent RunStore writers for one run.
|
|
196
203
|
systemPromptWrite = Promise.resolve();
|
|
204
|
+
conversationWrite = Promise.resolve();
|
|
197
205
|
constructor(cwd, sessionId, runId, home = homedir()) {
|
|
198
206
|
this.cwd = cwd;
|
|
199
207
|
this.sessionId = sessionId;
|
|
@@ -208,12 +216,14 @@ export class RunStore {
|
|
|
208
216
|
await mkdir(dirname(this.directory), { recursive: true, mode: 0o700 });
|
|
209
217
|
await mkdir(temporary, { mode: 0o700 });
|
|
210
218
|
try {
|
|
219
|
+
await writeFile(join(temporary, "workflow.js"), snapshot.script, { encoding: "utf8", mode: 0o600 });
|
|
211
220
|
await atomicJson(join(temporary, "snapshot.json"), snapshot);
|
|
212
|
-
await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {} });
|
|
221
|
+
await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {}, decisions: {} });
|
|
213
222
|
await atomicJson(join(temporary, "ownership.json"), []);
|
|
214
223
|
await atomicJson(join(temporary, "worktrees.json"), []);
|
|
215
224
|
await atomicJson(join(temporary, "state.json"), run);
|
|
216
225
|
await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
|
|
226
|
+
await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
|
|
217
227
|
await rename(temporary, this.directory);
|
|
218
228
|
}
|
|
219
229
|
catch (error) {
|
|
@@ -261,6 +271,14 @@ export class RunStore {
|
|
|
261
271
|
await write;
|
|
262
272
|
return result;
|
|
263
273
|
}
|
|
274
|
+
async saveSnapshot(snapshot) {
|
|
275
|
+
const write = this.launchSnapshotWrite.then(() => atomicJson(join(this.directory, "snapshot.json"), snapshot));
|
|
276
|
+
this.launchSnapshotWrite = write.catch(() => undefined);
|
|
277
|
+
await write;
|
|
278
|
+
}
|
|
279
|
+
async appendEvent(event) {
|
|
280
|
+
await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.message === event.message) ? [] : [event])] }));
|
|
281
|
+
}
|
|
264
282
|
async saveOwnership(nodes) {
|
|
265
283
|
await atomicJson(join(this.directory, "ownership.json"), nodes);
|
|
266
284
|
}
|
|
@@ -284,6 +302,54 @@ export class RunStore {
|
|
|
284
302
|
return (await json(this.systemPromptPath()).catch((error) => { if (error.code === "ENOENT")
|
|
285
303
|
return { version: 1, entries: [] }; throw error; })).entries;
|
|
286
304
|
}
|
|
305
|
+
conversationPath() { return join(this.directory, "conversations.json"); }
|
|
306
|
+
async conversation(id) {
|
|
307
|
+
await this.conversationWrite;
|
|
308
|
+
let artifact;
|
|
309
|
+
try {
|
|
310
|
+
const raw = await json(this.conversationPath());
|
|
311
|
+
if (!isConversationArtifact(raw))
|
|
312
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
|
|
313
|
+
artifact = raw;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (error.code === "ENOENT")
|
|
317
|
+
return undefined;
|
|
318
|
+
if (error instanceof WorkflowError)
|
|
319
|
+
throw error;
|
|
320
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
321
|
+
}
|
|
322
|
+
return artifact.conversations[id];
|
|
323
|
+
}
|
|
324
|
+
async saveConversation(conversation) {
|
|
325
|
+
const write = this.conversationWrite.then(async () => {
|
|
326
|
+
const path = this.conversationPath();
|
|
327
|
+
let artifact;
|
|
328
|
+
try {
|
|
329
|
+
const raw = await json(path);
|
|
330
|
+
if (!isConversationArtifact(raw))
|
|
331
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
|
|
332
|
+
artifact = raw;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (error.code === "ENOENT")
|
|
336
|
+
artifact = { version: 1, conversations: {} };
|
|
337
|
+
else if (error instanceof WorkflowError)
|
|
338
|
+
throw error;
|
|
339
|
+
else
|
|
340
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
341
|
+
}
|
|
342
|
+
const previous = artifact.conversations[conversation.id];
|
|
343
|
+
if (previous && previous.head.turn + 1 !== conversation.head.turn)
|
|
344
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation head is not the previous turn: ${conversation.id}`);
|
|
345
|
+
if (!previous && conversation.head.turn !== 1)
|
|
346
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation must start at turn one: ${conversation.id}`);
|
|
347
|
+
artifact.conversations[conversation.id] = structuredClone(conversation);
|
|
348
|
+
await atomicJson(path, artifact);
|
|
349
|
+
});
|
|
350
|
+
this.conversationWrite = write.catch(() => undefined);
|
|
351
|
+
await write;
|
|
352
|
+
}
|
|
287
353
|
async updateJournal(update) {
|
|
288
354
|
let result;
|
|
289
355
|
const write = this.journalWrite.then(async () => {
|
|
@@ -322,13 +388,31 @@ export class RunStore {
|
|
|
322
388
|
const journal = await json(join(this.directory, "journal.json"));
|
|
323
389
|
return Object.values(journal.awaiting ?? {});
|
|
324
390
|
}
|
|
391
|
+
async requestWorkflowDecision(request) {
|
|
392
|
+
await this.updateJournal((journal) => { journal.decisions ??= {}; journal.decisions[request.proposalId] = request; });
|
|
393
|
+
}
|
|
394
|
+
async pendingWorkflowDecisions() {
|
|
395
|
+
await this.journalWrite;
|
|
396
|
+
const journal = await json(join(this.directory, "journal.json"));
|
|
397
|
+
return Object.values(journal.decisions ?? {});
|
|
398
|
+
}
|
|
399
|
+
async answerWorkflowDecision(proposalId, approved) {
|
|
400
|
+
return this.updateJournal((journal) => {
|
|
401
|
+
const request = journal.decisions?.[proposalId];
|
|
402
|
+
if (!request)
|
|
403
|
+
return undefined;
|
|
404
|
+
journal.completed[`decision/${proposalId}`] = { path: `decision/${proposalId}`, value: approved };
|
|
405
|
+
delete journal.decisions?.[proposalId];
|
|
406
|
+
return request;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
325
409
|
async answerCheckpoint(name, approved) {
|
|
326
410
|
return this.updateJournal((journal) => {
|
|
327
|
-
const checkpoint = Object.values(journal.awaiting).find((item) => item.name === name);
|
|
411
|
+
const checkpoint = Object.values(journal.awaiting ?? {}).find((item) => item.name === name);
|
|
328
412
|
if (!checkpoint || journal.completed[checkpoint.path])
|
|
329
413
|
return undefined;
|
|
330
414
|
journal.completed[checkpoint.path] = { path: checkpoint.path, value: approved };
|
|
331
|
-
journal.awaiting = Object.fromEntries(Object.entries(journal.awaiting).filter(([path]) => path !== checkpoint.path));
|
|
415
|
+
journal.awaiting = Object.fromEntries(Object.entries(journal.awaiting ?? {}).filter(([path]) => path !== checkpoint.path));
|
|
332
416
|
return checkpoint;
|
|
333
417
|
});
|
|
334
418
|
}
|
|
@@ -461,9 +545,19 @@ export class RunStore {
|
|
|
461
545
|
try {
|
|
462
546
|
const write = this.snapshotWrite.then(async () => {
|
|
463
547
|
const record = await this.worktree(owner);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
await git(record.path, ["
|
|
548
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
549
|
+
await git(record.path, ["add", "-A"]);
|
|
550
|
+
if (!(await git(record.path, ["status", "--porcelain"])).trim())
|
|
551
|
+
break;
|
|
552
|
+
try {
|
|
553
|
+
await git(record.path, ["commit", "-m", "pi-extensible-workflows runtime snapshot"], gitIdentity);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
if (attempt === 2)
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
467
561
|
return (await git(record.path, ["rev-parse", "HEAD"])).trim();
|
|
468
562
|
});
|
|
469
563
|
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);
|
|
@@ -14,7 +14,7 @@ export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "ba
|
|
|
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"];
|
|
17
|
+
const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
|
|
18
18
|
const AGENT_OPTION_NAMES = ["role", "model", "thinking", "tools", "retries"];
|
|
19
19
|
const expectationKeys = ["firstSignificantAction", "firstTool", "firstBatchToolSequence", "parentToolSequence", "workflowCallCount", "requiredOperations", "forbiddenOperations", "requiredRoles", "minimumAgentCalls", "requireOutputSchema", "expectedResults", "agentPolicies", "requiredAgentOrder", "requiredAgentStructures", "requiredDataFlow"];
|
|
20
20
|
const caseKeys = ["id", "prompt", "timeoutMs", "maxCost", "expectations", "expectedWorkflowCalls", "semanticCriteria"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Deterministic multi-agent workflow orchestration for Pi",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/vekexasia/pi-extensible-workflows.git" },
|
|
@@ -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
|
|
|
@@ -101,7 +104,8 @@ Registered extension functions receive `withWorktree` in their context, so they
|
|
|
101
104
|
- Preserve item metadata in workflow code between pipeline stages instead of requiring agents to echo it through `outputSchema`.
|
|
102
105
|
- Repeated work uses a JavaScript loop; each direct `agent(...)` call receives deterministic call-site and occurrence identity.
|
|
103
106
|
- Runs default to background; set tool-call `foreground: true` when asked to wait.
|
|
104
|
-
-
|
|
107
|
+
- Add `budget` only when the run needs aggregate token, cost, duration, or launch limits; each dimension accepts optional `soft` and `hard` values. Soft crossings request wrap-up, while hard exhaustion blocks further budgeted work.
|
|
108
|
+
- 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
109
|
- `parallel()` and `pipeline()` return keyed bare values. Await results before use.
|
|
106
110
|
- Interpolate results with `prompt("...{value}", { value })`; placeholders in plain strings stay literal.
|
|
107
111
|
- 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.
|