@osolmaz/pi-workflows 0.1.0 → 0.2.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 +36 -21
- package/dist/extension/executor.d.ts +14 -1
- package/dist/extension/executor.js +11 -1
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.js +79 -7
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +85 -0
- package/dist/extension/recorder.js +525 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/widget.js +25 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/render/canvas.d.ts +1 -1
- package/dist/render/canvas.js +5 -0
- package/dist/render/canvas.js.map +1 -1
- package/dist/render/graph-render.d.ts +5 -0
- package/dist/render/graph-render.js +211 -48
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +19 -3
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +45 -0
- package/dist/viewer/session-reducer.js +266 -0
- package/dist/viewer/session-reducer.js.map +1 -0
- package/dist/workflows/artifacts.d.ts +40 -0
- package/dist/workflows/artifacts.js +155 -0
- package/dist/workflows/artifacts.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -0
- package/dist/workflows/engine.js +38 -7
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/store.d.ts +53 -9
- package/dist/workflows/store.js +523 -43
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +126 -3
- package/docs/development.md +43 -19
- package/docs/live-replay-protocol.md +155 -0
- package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
- package/docs/plans/session-event-replay-implementation-plan.md +494 -0
- package/docs/plans/tui-viewer-implementation-plan.md +64 -0
- package/docs/run-bundles.md +320 -55
- package/docs/session-event-journal.md +470 -0
- package/docs/tui-viewer.md +218 -0
- package/package.json +2 -1
- package/src/extension/executor.ts +28 -1
- package/src/extension/index.ts +87 -7
- package/src/extension/recorder.ts +633 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/widget.ts +26 -24
- package/src/render/canvas.ts +19 -1
- package/src/render/graph-render.ts +277 -44
- package/src/viewer/render.ts +21 -3
- package/src/viewer/session-reducer.ts +347 -0
- package/src/workflows/artifacts.ts +188 -0
- package/src/workflows/engine.ts +39 -7
- package/src/workflows/index.ts +15 -0
- package/src/workflows/store.ts +649 -49
- package/src/workflows/types.ts +141 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osolmaz/pi-workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Workflow engine, JSON control-flow tool, and live terminal viewer for the pi coding agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsc -p tsconfig.build.json",
|
|
44
44
|
"check": "npm run format:check && npm run lint && npm run typecheck && npm run build && npm run test:coverage",
|
|
45
|
+
"fixtures": "node scripts/export-layout-fixtures.mjs",
|
|
45
46
|
"format": "oxfmt --write",
|
|
46
47
|
"format:check": "oxfmt --check",
|
|
47
48
|
"lint": "oxlint --deny-warnings src test examples",
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AgentStepExecutor,
|
|
3
3
|
AgentStepRequest,
|
|
4
4
|
AgentStepSubmission,
|
|
5
|
+
ConversationRange,
|
|
5
6
|
} from "../workflows/types.js";
|
|
6
7
|
|
|
7
8
|
export type SubmissionResult =
|
|
@@ -14,11 +15,24 @@ export type PromptDelivery = {
|
|
|
14
15
|
streaming: boolean;
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Bracketing hooks for conversation linkage: a mark is taken when the step
|
|
20
|
+
* prompt is first delivered, and the recorded entry range since that mark is
|
|
21
|
+
* attached to the accepted submission.
|
|
22
|
+
*/
|
|
23
|
+
export type ConversationHooks = {
|
|
24
|
+
beginAttempt?: (contract: AgentStepRequest["contract"]) => void;
|
|
25
|
+
mark: () => number;
|
|
26
|
+
rangeSince: (mark: number) => ConversationRange | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
17
29
|
export type ConversationStepExecutorOptions = {
|
|
18
30
|
/** Deliver a prompt into the pi conversation. */
|
|
19
31
|
sendPrompt: (delivery: PromptDelivery) => void;
|
|
20
32
|
/** Reminders sent when the agent settles without submitting. Default 2. */
|
|
21
33
|
maxNudges?: number;
|
|
34
|
+
/** Conversation linkage hooks, wired to the session recorder. */
|
|
35
|
+
conversation?: ConversationHooks;
|
|
22
36
|
};
|
|
23
37
|
|
|
24
38
|
type PendingStep = {
|
|
@@ -26,6 +40,8 @@ type PendingStep = {
|
|
|
26
40
|
resolve: (submission: AgentStepSubmission) => void;
|
|
27
41
|
reject: (error: unknown) => void;
|
|
28
42
|
nudgesSent: number;
|
|
43
|
+
/** Conversation mark taken when the prompt was first delivered. */
|
|
44
|
+
mark: number | null;
|
|
29
45
|
cleanup: () => void;
|
|
30
46
|
/** Resolves when this step stops being the pending step. */
|
|
31
47
|
cleared: Promise<void>;
|
|
@@ -44,6 +60,7 @@ const DEFAULT_MAX_NUDGES = 2;
|
|
|
44
60
|
export class ConversationStepExecutor implements AgentStepExecutor {
|
|
45
61
|
private readonly sendPrompt: (delivery: PromptDelivery) => void;
|
|
46
62
|
private readonly maxNudges: number;
|
|
63
|
+
private readonly conversation: ConversationHooks | undefined;
|
|
47
64
|
private pending: PendingStep | null = null;
|
|
48
65
|
private streaming = false;
|
|
49
66
|
private heldByUser = false;
|
|
@@ -51,6 +68,7 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
51
68
|
constructor(options: ConversationStepExecutorOptions) {
|
|
52
69
|
this.sendPrompt = options.sendPrompt;
|
|
53
70
|
this.maxNudges = options.maxNudges ?? DEFAULT_MAX_NUDGES;
|
|
71
|
+
this.conversation = options.conversation;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
/** Track agent streaming state (wire to agent_start / agent_settled). */
|
|
@@ -88,6 +106,7 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
88
106
|
return;
|
|
89
107
|
}
|
|
90
108
|
try {
|
|
109
|
+
this.conversation?.beginAttempt?.(pending.request.contract);
|
|
91
110
|
this.sendPrompt({ prompt: pending.request.prompt, streaming: this.streaming });
|
|
92
111
|
} catch (error) {
|
|
93
112
|
this.clearPending();
|
|
@@ -110,11 +129,13 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
110
129
|
const cleared = new Promise<void>((resolveCleared) => {
|
|
111
130
|
markCleared = resolveCleared;
|
|
112
131
|
});
|
|
132
|
+
this.conversation?.beginAttempt?.(request.contract);
|
|
113
133
|
this.pending = {
|
|
114
134
|
request,
|
|
115
135
|
resolve,
|
|
116
136
|
reject,
|
|
117
137
|
nudgesSent: 0,
|
|
138
|
+
mark: this.conversation?.mark() ?? null,
|
|
118
139
|
cleanup: () => signal.removeEventListener("abort", onAbort),
|
|
119
140
|
cleared,
|
|
120
141
|
markCleared,
|
|
@@ -185,7 +206,12 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
185
206
|
};
|
|
186
207
|
}
|
|
187
208
|
this.clearPending();
|
|
188
|
-
|
|
209
|
+
const conversation =
|
|
210
|
+
pending.mark !== null ? this.conversation?.rangeSince(pending.mark) : undefined;
|
|
211
|
+
pending.resolve({
|
|
212
|
+
output: result.value,
|
|
213
|
+
...(conversation !== undefined ? { conversation } : {}),
|
|
214
|
+
});
|
|
189
215
|
return {
|
|
190
216
|
accepted: true,
|
|
191
217
|
message: [
|
|
@@ -224,6 +250,7 @@ export class ConversationStepExecutor implements AgentStepExecutor {
|
|
|
224
250
|
pending.nudgesSent += 1;
|
|
225
251
|
const { nodeId, attemptId } = pending.request.contract;
|
|
226
252
|
try {
|
|
253
|
+
this.conversation?.beginAttempt?.(pending.request.contract);
|
|
227
254
|
this.sendPrompt({
|
|
228
255
|
prompt: [
|
|
229
256
|
`Reminder: workflow step ${JSON.stringify(nodeId)} is still awaiting your output.`,
|
package/src/extension/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { Type } from "typebox";
|
|
|
7
7
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
8
8
|
import { errorMessage } from "../workflows/errors.js";
|
|
9
9
|
import { discoverWorkflows, loadWorkflowFile, resolveWorkflowRef } from "../workflows/loader.js";
|
|
10
|
-
import { createDefinitionSnapshot } from "../workflows/store.js";
|
|
10
|
+
import { WorkflowRunStore, createDefinitionSnapshot } from "../workflows/store.js";
|
|
11
11
|
import type {
|
|
12
12
|
WorkflowDefinition,
|
|
13
13
|
WorkflowDefinitionSnapshot,
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
WorkflowRunState,
|
|
16
16
|
} from "../workflows/types.js";
|
|
17
17
|
import { ConversationStepExecutor } from "./executor.js";
|
|
18
|
+
import { SessionRecorder } from "./recorder.js";
|
|
18
19
|
import { buildWidgetView } from "./widget.js";
|
|
19
20
|
|
|
20
21
|
const WIDGET_KEY = "pi-workflows";
|
|
@@ -37,6 +38,7 @@ type ActiveRun = {
|
|
|
37
38
|
workflowName: string;
|
|
38
39
|
engine: WorkflowEngine;
|
|
39
40
|
executor: ConversationStepExecutor;
|
|
41
|
+
recorder: SessionRecorder | null;
|
|
40
42
|
snapshot: WorkflowDefinitionSnapshot;
|
|
41
43
|
presentationPrompt: WorkflowDefinition["presentationPrompt"];
|
|
42
44
|
generation: number;
|
|
@@ -294,6 +296,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
294
296
|
if (activeRun === run) {
|
|
295
297
|
activeRun = null;
|
|
296
298
|
}
|
|
299
|
+
// Normally already stopped via onRunFinishing; this covers observers of
|
|
300
|
+
// runs that ended without reaching that hook.
|
|
301
|
+
void run.recorder?.stop();
|
|
297
302
|
stopWidgetTicker();
|
|
298
303
|
const { state } = result;
|
|
299
304
|
updateWidget(ctx, state, run.snapshot);
|
|
@@ -335,9 +340,36 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
335
340
|
sendPrompt: ({ prompt, streaming }) => {
|
|
336
341
|
pi.sendUserMessage(prompt, streaming ? { deliverAs: "steer" } : undefined);
|
|
337
342
|
},
|
|
343
|
+
conversation: {
|
|
344
|
+
beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
|
|
345
|
+
mark: () => run.recorder?.mark() ?? 0,
|
|
346
|
+
rangeSince: (mark) => run.recorder?.rangeSince(mark),
|
|
347
|
+
},
|
|
338
348
|
});
|
|
349
|
+
// The store is shared between the engine and the session recorder so the
|
|
350
|
+
// trace sequence stays single-writer (see docs/run-bundles.md).
|
|
351
|
+
const store = new WorkflowRunStore();
|
|
339
352
|
const engine = new WorkflowEngine({
|
|
340
353
|
executor,
|
|
354
|
+
store,
|
|
355
|
+
// Awaited by the engine after run_started is persisted, so the session
|
|
356
|
+
// binding and its trace event always precede node and terminal events.
|
|
357
|
+
onRunStarted: async (runDir, state) => {
|
|
358
|
+
const recorder = new SessionRecorder(store, runDir, state.runId);
|
|
359
|
+
try {
|
|
360
|
+
await recorder.bind(ctx);
|
|
361
|
+
run.recorder = recorder;
|
|
362
|
+
} catch {
|
|
363
|
+
// Binding is best-effort: a session without UI access or an
|
|
364
|
+
// ephemeral context must not fail the run.
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
// Awaited by the engine before the terminal snapshot. If completion was
|
|
368
|
+
// submitted from the workflow tool, capture stays open through Pi's
|
|
369
|
+
// final tool, message, and turn hooks before it drains.
|
|
370
|
+
onRunFinishing: async () => {
|
|
371
|
+
await run.recorder?.finish();
|
|
372
|
+
},
|
|
341
373
|
onEvent: (_event, state: WorkflowRunState) => {
|
|
342
374
|
if (run.runId === null) {
|
|
343
375
|
run.runId = state.runId;
|
|
@@ -351,6 +383,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
351
383
|
workflowName: workflow.name,
|
|
352
384
|
engine,
|
|
353
385
|
executor,
|
|
386
|
+
recorder: null,
|
|
354
387
|
snapshot,
|
|
355
388
|
presentationPrompt: workflow.presentationPrompt,
|
|
356
389
|
generation,
|
|
@@ -368,6 +401,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
368
401
|
if (activeRun === run) {
|
|
369
402
|
activeRun = null;
|
|
370
403
|
}
|
|
404
|
+
void run.recorder?.stop();
|
|
371
405
|
stopWidgetTicker();
|
|
372
406
|
clearWidget(ctx);
|
|
373
407
|
notify(ctx, `Workflow ${workflow.name} crashed: ${errorMessage(error)}`, "error");
|
|
@@ -488,12 +522,21 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
488
522
|
attempt: Type.String({ description: "The attempt id from the workflow step contract" }),
|
|
489
523
|
output: Type.Unknown({ description: "The step output, matching the expected output shape" }),
|
|
490
524
|
}),
|
|
491
|
-
async execute(_toolCallId, params) {
|
|
525
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
492
526
|
if (!activeRun) {
|
|
493
527
|
throw new Error(
|
|
494
528
|
"No workflow is running. Do not call the workflow tool outside a workflow.",
|
|
495
529
|
);
|
|
496
530
|
}
|
|
531
|
+
// Flush the conversation into the bundle before accepting, so the
|
|
532
|
+
// attempt's recorded range includes the assistant message that carries
|
|
533
|
+
// this submission. Pi guarantees ctx.sessionManager is synchronized
|
|
534
|
+
// through the current assistant tool-calling message before tool_call
|
|
535
|
+
// dispatch, which precedes tool execution (docs/extensions.md, "Tool
|
|
536
|
+
// Events"). The tool result of this call itself lands after the range
|
|
537
|
+
// by design: it is the submission receipt, not the submission.
|
|
538
|
+
await activeRun.recorder?.record(ctx).catch(() => undefined);
|
|
539
|
+
await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
|
|
497
540
|
const result = await activeRun.executor.submit(params.step, params.attempt, params.output);
|
|
498
541
|
if (!result.accepted) {
|
|
499
542
|
throw new Error(result.message);
|
|
@@ -552,19 +595,56 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
552
595
|
);
|
|
553
596
|
});
|
|
554
597
|
|
|
555
|
-
pi.on("
|
|
556
|
-
|
|
598
|
+
pi.on("turn_start", (event) => {
|
|
599
|
+
activeRun?.recorder?.handleTurnStart(event);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
603
|
+
await activeRun?.recorder?.handleTurnEnd(event, ctx).catch(() => undefined);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
pi.on("message_start", async (event, ctx) => {
|
|
607
|
+
await activeRun?.recorder?.handleMessageStart(event, ctx).catch(() => undefined);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
pi.on("message_update", (event) => {
|
|
611
|
+
activeRun?.recorder?.handleMessageUpdate(event);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
pi.on("message_end", (event) => {
|
|
615
|
+
activeRun?.recorder?.handleMessageEnd(event);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
pi.on("tool_execution_start", (event) => {
|
|
619
|
+
activeRun?.recorder?.handleToolStart(event);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
pi.on("tool_execution_update", (event) => {
|
|
623
|
+
activeRun?.recorder?.handleToolUpdate(event);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
pi.on("tool_execution_end", (event) => {
|
|
627
|
+
activeRun?.recorder?.handleToolEnd(event);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
631
|
+
const run = activeRun;
|
|
632
|
+
if (!run) {
|
|
557
633
|
presentationPending = null;
|
|
558
634
|
return;
|
|
559
635
|
}
|
|
560
|
-
|
|
561
|
-
|
|
636
|
+
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
637
|
+
run.recorder?.settleAttempt();
|
|
638
|
+
run.executor.setStreaming(false);
|
|
639
|
+
run.executor.handleAgentSettled();
|
|
562
640
|
});
|
|
563
641
|
|
|
564
642
|
pi.on("session_shutdown", () => {
|
|
565
643
|
sessionClosed = true;
|
|
566
644
|
supersedePresentation();
|
|
567
|
-
activeRun
|
|
645
|
+
const run = activeRun;
|
|
646
|
+
run?.engine.cancel();
|
|
647
|
+
void run?.recorder?.stop();
|
|
568
648
|
activeRun = null;
|
|
569
649
|
presentationPending = null;
|
|
570
650
|
clearWidgetTimer();
|