@osolmaz/pi-workflows 0.1.0 → 0.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 +107 -26
- package/dist/builtins/monitor.workflow.d.ts +71 -0
- package/dist/builtins/monitor.workflow.js +234 -0
- package/dist/builtins/monitor.workflow.js.map +1 -0
- package/dist/controllers/conditions.d.ts +6 -0
- package/dist/controllers/conditions.js +68 -0
- package/dist/controllers/conditions.js.map +1 -0
- package/dist/controllers/definition.d.ts +6 -0
- package/dist/controllers/definition.js +45 -0
- package/dist/controllers/definition.js.map +1 -0
- package/dist/controllers/effects.d.ts +14 -0
- package/dist/controllers/effects.js +104 -0
- package/dist/controllers/effects.js.map +1 -0
- package/dist/controllers/errors.d.ts +12 -0
- package/dist/controllers/errors.js +25 -0
- package/dist/controllers/errors.js.map +1 -0
- package/dist/controllers/index.d.ts +13 -0
- package/dist/controllers/index.js +13 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/json.d.ts +5 -0
- package/dist/controllers/json.js +57 -0
- package/dist/controllers/json.js.map +1 -0
- package/dist/controllers/loader.d.ts +23 -0
- package/dist/controllers/loader.js +74 -0
- package/dist/controllers/loader.js.map +1 -0
- package/dist/controllers/manager.d.ts +58 -0
- package/dist/controllers/manager.js +399 -0
- package/dist/controllers/manager.js.map +1 -0
- package/dist/controllers/results.d.ts +5 -0
- package/dist/controllers/results.js +32 -0
- package/dist/controllers/results.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +212 -0
- package/dist/controllers/sqlite.js +1009 -0
- package/dist/controllers/sqlite.js.map +1 -0
- package/dist/controllers/store.d.ts +112 -0
- package/dist/controllers/store.js +32 -0
- package/dist/controllers/store.js.map +1 -0
- package/dist/controllers/types.d.ts +159 -0
- package/dist/controllers/types.js +2 -0
- package/dist/controllers/types.js.map +1 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
- package/dist/controllers/workflow-engine-scheduler.js +93 -0
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
- package/dist/controllers/workflows.d.ts +27 -0
- package/dist/controllers/workflows.js +109 -0
- package/dist/controllers/workflows.js.map +1 -0
- package/dist/extension/controller-host.d.ts +47 -0
- package/dist/extension/controller-host.js +110 -0
- package/dist/extension/controller-host.js.map +1 -0
- package/dist/extension/executor.d.ts +14 -1
- package/dist/extension/executor.js +12 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +1118 -89
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +91 -0
- package/dist/extension/recorder.js +537 -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/extension/workflow-tool.d.ts +28 -0
- package/dist/extension/workflow-tool.js +33 -0
- package/dist/extension/workflow-tool.js.map +1 -0
- package/dist/host/processes.d.ts +24 -0
- package/dist/host/processes.js +114 -0
- package/dist/host/processes.js.map +1 -0
- package/dist/host/rpc-bridge.d.ts +9 -0
- package/dist/host/rpc-bridge.js +39 -0
- package/dist/host/rpc-bridge.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +38 -0
- package/dist/host/rpc-executor.js +254 -0
- package/dist/host/rpc-executor.js.map +1 -0
- package/dist/host/runner.d.ts +49 -0
- package/dist/host/runner.js +350 -0
- package/dist/host/runner.js.map +1 -0
- 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/cli.d.ts +7 -3
- package/dist/viewer/cli.js +150 -19
- package/dist/viewer/cli.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 +38 -0
- package/dist/workflows/engine.js +281 -20
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +23 -0
- package/dist/workflows/errors.js +38 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/graph.js +0 -5
- package/dist/workflows/graph.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/loader.d.ts +5 -3
- package/dist/workflows/loader.js +10 -1
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.js +1 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +98 -10
- package/dist/workflows/store.js +921 -46
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +136 -3
- package/docs/CONTROLLERS.md +215 -0
- package/docs/development.md +51 -24
- package/docs/live-replay-protocol.md +155 -0
- package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -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 +359 -55
- package/docs/session-event-journal.md +470 -0
- package/docs/tui-viewer.md +218 -0
- package/docs/workflows.md +131 -11
- package/examples/controllers/pull-request.controller.ts +215 -0
- package/package.json +11 -2
- package/src/builtins/monitor.workflow.ts +278 -0
- package/src/controllers/conditions.ts +110 -0
- package/src/controllers/definition.ts +65 -0
- package/src/controllers/effects.ts +123 -0
- package/src/controllers/errors.ts +27 -0
- package/src/controllers/index.ts +90 -0
- package/src/controllers/json.ts +62 -0
- package/src/controllers/loader.ts +104 -0
- package/src/controllers/manager.ts +533 -0
- package/src/controllers/results.ts +46 -0
- package/src/controllers/sqlite.ts +1427 -0
- package/src/controllers/store.ts +160 -0
- package/src/controllers/types.ts +183 -0
- package/src/controllers/workflow-engine-scheduler.ts +145 -0
- package/src/controllers/workflows.ts +152 -0
- package/src/extension/controller-host.ts +163 -0
- package/src/extension/executor.ts +29 -2
- package/src/extension/index.ts +1315 -109
- package/src/extension/recorder.ts +662 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/widget.ts +26 -24
- package/src/extension/workflow-tool.ts +59 -0
- package/src/host/processes.ts +119 -0
- package/src/host/rpc-bridge.ts +44 -0
- package/src/host/rpc-executor.ts +299 -0
- package/src/host/runner.ts +406 -0
- package/src/render/canvas.ts +19 -1
- package/src/render/graph-render.ts +277 -44
- package/src/viewer/cli.ts +167 -21
- 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 +365 -19
- package/src/workflows/errors.ts +45 -0
- package/src/workflows/graph.ts +0 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/loader.ts +13 -3
- package/src/workflows/schema.ts +1 -1
- package/src/workflows/store.ts +1157 -48
- package/src/workflows/types.ts +151 -3
package/src/viewer/cli.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import fs, { realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { SqliteControllerStore } from "../controllers/sqlite.js";
|
|
6
|
+
import { projectControllerStoreBaseDir } from "../controllers/store.js";
|
|
4
7
|
import { sanitizeText } from "../render/ansi.js";
|
|
5
8
|
import { listRunBundles, readRunBundle, workflowRunsBaseDir } from "../workflows/store.js";
|
|
6
9
|
import {
|
|
@@ -12,54 +15,99 @@ import {
|
|
|
12
15
|
} from "./render.js";
|
|
13
16
|
import { runViewer } from "./tui.js";
|
|
14
17
|
|
|
15
|
-
const USAGE = `pi-workflows —
|
|
18
|
+
const USAGE = `pi-workflows — workflow runs and controller resources
|
|
16
19
|
|
|
17
20
|
Usage:
|
|
18
21
|
pi-workflows view [runId] [--dir <runsDir>] [--once]
|
|
19
22
|
pi-workflows runs [--dir <runsDir>]
|
|
23
|
+
pi-workflows controllers [--controller-dir <dir>]
|
|
24
|
+
pi-workflows controller <controller> <key> [--controller-dir <dir>]
|
|
25
|
+
pi-workflows host [--project <dir>] [-- <extra pi args>]
|
|
20
26
|
|
|
21
27
|
Commands:
|
|
22
|
-
view
|
|
23
|
-
runs
|
|
28
|
+
view Open the live workflow TUI. With --once, print a snapshot.
|
|
29
|
+
runs List recent workflow runs.
|
|
30
|
+
controllers List durable controller resources.
|
|
31
|
+
controller Show one resource, its effects, child workflows, and events.
|
|
32
|
+
host Run the always-on workflow host in the foreground.
|
|
24
33
|
|
|
25
34
|
Options:
|
|
26
|
-
--dir <runsDir>
|
|
27
|
-
--
|
|
35
|
+
--dir <runsDir> Runs directory (default: ~/.pi/agent/workflows/runs)
|
|
36
|
+
--controller-dir <dir> Controller directory (default: project-scoped local store)
|
|
37
|
+
--once Render once without the interactive TUI
|
|
38
|
+
--project <dir> Project directory for the host (default: cwd)
|
|
28
39
|
`;
|
|
29
40
|
|
|
30
|
-
type CliArgs = {
|
|
41
|
+
export type CliArgs = {
|
|
31
42
|
command: string;
|
|
32
|
-
runId?: string
|
|
43
|
+
runId?: string;
|
|
44
|
+
controllerName?: string;
|
|
45
|
+
resourceKey?: string;
|
|
33
46
|
dir: string;
|
|
47
|
+
controllerDir: string;
|
|
34
48
|
once: boolean;
|
|
49
|
+
project?: string | undefined;
|
|
50
|
+
piArgs?: string[] | undefined;
|
|
35
51
|
};
|
|
36
52
|
|
|
37
53
|
export function parseCliArgs(argv: string[]): CliArgs {
|
|
38
54
|
const args = [...argv];
|
|
39
55
|
const command = args[0] && !args[0].startsWith("-") ? (args.shift() as string) : "view";
|
|
40
56
|
let dir = workflowRunsBaseDir();
|
|
57
|
+
let controllerDir = projectControllerStoreBaseDir(process.cwd());
|
|
41
58
|
let once = false;
|
|
42
|
-
|
|
59
|
+
const positionals: string[] = [];
|
|
60
|
+
let project: string | undefined;
|
|
61
|
+
const piArgs: string[] = [];
|
|
43
62
|
|
|
44
63
|
while (args.length > 0) {
|
|
45
64
|
const arg = args.shift() as string;
|
|
46
65
|
if (arg === "--dir") {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
dir = requiredValue(args, "--dir");
|
|
67
|
+
} else if (arg === "--controller-dir") {
|
|
68
|
+
controllerDir = requiredValue(args, "--controller-dir");
|
|
69
|
+
} else if (arg === "--project") {
|
|
70
|
+
project = requiredValue(args, "--project");
|
|
52
71
|
} else if (arg === "--once") {
|
|
53
72
|
once = true;
|
|
54
73
|
} else if (arg === "--help" || arg === "-h") {
|
|
55
|
-
return { command: "help", dir, once };
|
|
56
|
-
} else if (
|
|
57
|
-
|
|
58
|
-
} else {
|
|
74
|
+
return { command: "help", dir, controllerDir, once };
|
|
75
|
+
} else if (arg === "--") {
|
|
76
|
+
piArgs.push(...args.splice(0));
|
|
77
|
+
} else if (arg.startsWith("-")) {
|
|
59
78
|
throw new Error(`Unknown argument: ${arg}`);
|
|
79
|
+
} else {
|
|
80
|
+
positionals.push(arg);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (command === "host") {
|
|
85
|
+
return { command, dir, controllerDir, once, project, piArgs };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (command === "controller") {
|
|
89
|
+
if (positionals.length !== 2) {
|
|
90
|
+
throw new Error("controller requires <controller> and <key>");
|
|
60
91
|
}
|
|
92
|
+
return {
|
|
93
|
+
command,
|
|
94
|
+
controllerName: positionals[0] as string,
|
|
95
|
+
resourceKey: positionals[1] as string,
|
|
96
|
+
dir,
|
|
97
|
+
controllerDir,
|
|
98
|
+
once,
|
|
99
|
+
};
|
|
61
100
|
}
|
|
62
|
-
|
|
101
|
+
if (positionals.length > 1) {
|
|
102
|
+
throw new Error(`Unexpected argument: ${positionals[1]}`);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
command,
|
|
106
|
+
...(positionals[0] !== undefined ? { runId: positionals[0] } : {}),
|
|
107
|
+
dir,
|
|
108
|
+
controllerDir,
|
|
109
|
+
once,
|
|
110
|
+
};
|
|
63
111
|
}
|
|
64
112
|
|
|
65
113
|
async function printRuns(dir: string): Promise<void> {
|
|
@@ -70,7 +118,6 @@ async function printRuns(dir: string): Promise<void> {
|
|
|
70
118
|
}
|
|
71
119
|
for (const bundle of bundles) {
|
|
72
120
|
const state = bundle.state;
|
|
73
|
-
// Titles can interpolate untrusted run input; strip control sequences.
|
|
74
121
|
const title = state.runTitle ? ` — ${sanitizeText(state.runTitle)}` : "";
|
|
75
122
|
process.stdout.write(
|
|
76
123
|
`${statusLabel(state.status)} ${sanitizeText(state.workflowName)}${title} ${state.runId} ${formatDuration(
|
|
@@ -98,6 +145,57 @@ async function printOnce(dir: string, runId: string | undefined): Promise<void>
|
|
|
98
145
|
process.stdout.write(`${renderRunDetailLines(bundle, size).join("\n")}\n`);
|
|
99
146
|
}
|
|
100
147
|
|
|
148
|
+
function printControllers(controllerDir: string): void {
|
|
149
|
+
const store = openControllerStore(controllerDir);
|
|
150
|
+
if (store === undefined) {
|
|
151
|
+
process.stdout.write(`No controller resources found in ${controllerDir}\n`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const resources = store.listResources();
|
|
156
|
+
if (resources.length === 0) {
|
|
157
|
+
process.stdout.write(`No controller resources found in ${controllerDir}\n`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const resource of resources) {
|
|
161
|
+
const condition =
|
|
162
|
+
resource.status.conditions.find((item) => item.type === "Ready") ??
|
|
163
|
+
resource.status.conditions[0];
|
|
164
|
+
const conditionText =
|
|
165
|
+
condition === undefined
|
|
166
|
+
? "unknown"
|
|
167
|
+
: `${String(condition.status)}:${sanitizeText(condition.reason)}`;
|
|
168
|
+
process.stdout.write(
|
|
169
|
+
`${sanitizeText(resource.metadata.controller)} ${sanitizeText(resource.metadata.key)} generation=${resource.metadata.generation} ready=${conditionText}\n`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
store.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function printController(controllerDir: string, controller: string, key: string): void {
|
|
178
|
+
const store = openControllerStore(controllerDir);
|
|
179
|
+
if (store === undefined) {
|
|
180
|
+
throw new Error(`Controller store not found in ${controllerDir}`);
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const resource = store.getResource({ controller, key });
|
|
184
|
+
if (resource === undefined) {
|
|
185
|
+
throw new Error(`Controller resource not found: ${controller}/${key}`);
|
|
186
|
+
}
|
|
187
|
+
const value = {
|
|
188
|
+
resource,
|
|
189
|
+
effects: store.listEffects(resource.metadata.uid),
|
|
190
|
+
workflows: store.listWorkflows(resource.metadata.uid),
|
|
191
|
+
events: store.listEvents({ controller, key, limit: 50 }),
|
|
192
|
+
};
|
|
193
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
194
|
+
} finally {
|
|
195
|
+
store.close();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
101
199
|
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
|
|
102
200
|
let args: CliArgs;
|
|
103
201
|
try {
|
|
@@ -116,6 +214,21 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
116
214
|
await printRuns(args.dir);
|
|
117
215
|
return 0;
|
|
118
216
|
}
|
|
217
|
+
if (args.command === "controllers") {
|
|
218
|
+
printControllers(args.controllerDir);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
if (args.command === "controller") {
|
|
222
|
+
printController(
|
|
223
|
+
args.controllerDir,
|
|
224
|
+
args.controllerName as string,
|
|
225
|
+
args.resourceKey as string,
|
|
226
|
+
);
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
if (args.command === "host") {
|
|
230
|
+
return await runHost(args.project ?? process.cwd(), args.piArgs);
|
|
231
|
+
}
|
|
119
232
|
if (args.command === "view") {
|
|
120
233
|
if (args.once || !process.stdout.isTTY) {
|
|
121
234
|
await printOnce(args.dir, args.runId);
|
|
@@ -132,8 +245,41 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
132
245
|
}
|
|
133
246
|
}
|
|
134
247
|
|
|
248
|
+
async function runHost(project: string, piArgs: string[] | undefined): Promise<number> {
|
|
249
|
+
const { WorkflowHost } = await import("../host/runner.js");
|
|
250
|
+
const host = new WorkflowHost({
|
|
251
|
+
cwd: project,
|
|
252
|
+
piArgs: piArgs ?? [],
|
|
253
|
+
onLog: (message) => process.stdout.write(`[host] ${message}\n`),
|
|
254
|
+
});
|
|
255
|
+
await host.start();
|
|
256
|
+
await new Promise<void>((resolve) => {
|
|
257
|
+
const shutdown = () => {
|
|
258
|
+
void host.stop().then(() => resolve());
|
|
259
|
+
};
|
|
260
|
+
process.once("SIGTERM", shutdown);
|
|
261
|
+
process.once("SIGINT", shutdown);
|
|
262
|
+
});
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function openControllerStore(controllerDir: string): SqliteControllerStore | undefined {
|
|
267
|
+
const file = path.join(controllerDir, "controller.sqlite");
|
|
268
|
+
if (!fs.existsSync(file)) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
return new SqliteControllerStore(file, { readOnly: true });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function requiredValue(args: string[], option: string): string {
|
|
275
|
+
const value = args.shift();
|
|
276
|
+
if (!value) {
|
|
277
|
+
throw new Error(`${option} requires a path`);
|
|
278
|
+
}
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
|
|
135
282
|
const entryPath = process.argv[1];
|
|
136
|
-
// realpath so invocation through a bin symlink (npm link) still matches.
|
|
137
283
|
const resolvedEntry = entryPath !== undefined ? realpathSyncSafe(entryPath) : undefined;
|
|
138
284
|
if (resolvedEntry !== undefined && import.meta.url === pathToFileURL(resolvedEntry).href) {
|
|
139
285
|
main().then((code) => {
|
package/src/viewer/render.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import { ansi, fitWidth, sanitizeText } from "../render/ansi.js";
|
|
2
2
|
import { formatDuration, runElapsedMs } from "../render/format.js";
|
|
3
3
|
import { renderGraphLines } from "../render/graph-render.js";
|
|
4
|
+
import { decodeValueWith } from "../workflows/artifacts.js";
|
|
4
5
|
import type { LoadedRunBundle } from "../workflows/store.js";
|
|
5
6
|
import type { WorkflowRunStatus, WorkflowStepRecord } from "../workflows/types.js";
|
|
6
7
|
|
|
7
8
|
export { formatDuration, runElapsedMs };
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Replace `$artifact` references with a compact placeholder for display. The
|
|
12
|
+
* terminal viewer shows summaries; full artifact contents are for replay
|
|
13
|
+
* tooling.
|
|
14
|
+
*/
|
|
15
|
+
function withArtifactPlaceholders(value: unknown): unknown {
|
|
16
|
+
return decodeValueWith(value, (ref) => `«artifact ${formatBytes(ref.bytes)} ${ref.path}»`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatBytes(bytes: number): string {
|
|
20
|
+
if (bytes < 1024) {
|
|
21
|
+
return `${bytes}B`;
|
|
22
|
+
}
|
|
23
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
export type ViewportSize = {
|
|
10
27
|
width: number;
|
|
11
28
|
height: number;
|
|
@@ -24,10 +41,11 @@ export function statusLabel(status: WorkflowRunStatus): string {
|
|
|
24
41
|
return STATUS_COLORS[status](status);
|
|
25
42
|
}
|
|
26
43
|
|
|
27
|
-
function previewValue(
|
|
28
|
-
if (
|
|
44
|
+
function previewValue(rawValue: unknown, maxLength: number): string {
|
|
45
|
+
if (rawValue === undefined) {
|
|
29
46
|
return "";
|
|
30
47
|
}
|
|
48
|
+
const value = withArtifactPlaceholders(rawValue);
|
|
31
49
|
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
32
50
|
// Model-controlled values must not carry escape sequences into the terminal.
|
|
33
51
|
const singleLine = sanitizeText(text ?? "")
|
|
@@ -120,7 +138,7 @@ function nodeStatusLine(bundle: LoadedRunBundle, nodeId: string, width: number,
|
|
|
120
138
|
/** Pretty-printed JSON body of the selected step for the inspector pane. */
|
|
121
139
|
function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
|
|
122
140
|
const lines: string[] = [];
|
|
123
|
-
const body = step.error !== undefined ? step.error : step.output;
|
|
141
|
+
const body = step.error !== undefined ? step.error : withArtifactPlaceholders(step.output);
|
|
124
142
|
const rendered =
|
|
125
143
|
typeof body === "string" && step.error !== undefined ? body : JSON.stringify(body, null, 2);
|
|
126
144
|
for (const raw of (rendered ?? "null").split("\n")) {
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { WorkflowSessionEntryRecord, WorkflowSessionEventRecord } from "../workflows/types.js";
|
|
2
|
+
|
|
3
|
+
export type TemporalContentBlock = {
|
|
4
|
+
contentIndex: number;
|
|
5
|
+
kind: "text" | "thinking" | "toolCall";
|
|
6
|
+
text: string;
|
|
7
|
+
value?: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type TemporalMessage = {
|
|
11
|
+
messageId: string;
|
|
12
|
+
role: string;
|
|
13
|
+
status: "streaming" | "finished" | "error" | "settled" | "unsettled";
|
|
14
|
+
entryId?: string;
|
|
15
|
+
blocks: TemporalContentBlock[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TemporalTool = {
|
|
19
|
+
toolCallId: string;
|
|
20
|
+
messageId: string;
|
|
21
|
+
toolName: string;
|
|
22
|
+
status: "running" | "finished" | "failed";
|
|
23
|
+
updates: number;
|
|
24
|
+
args?: unknown;
|
|
25
|
+
result?: unknown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type TemporalSessionState = {
|
|
29
|
+
throughSeq: number;
|
|
30
|
+
messages: TemporalMessage[];
|
|
31
|
+
tools: TemporalTool[];
|
|
32
|
+
settledEntryIds: string[];
|
|
33
|
+
diagnostics: string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type MutableMessage = Omit<TemporalMessage, "blocks"> & {
|
|
37
|
+
blocks: Map<number, TemporalContentBlock>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function payloadString(payload: Record<string, unknown>, key: string): string | undefined {
|
|
41
|
+
const value = payload[key];
|
|
42
|
+
return typeof value === "string" ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function payloadIndex(payload: Record<string, unknown>): number | undefined {
|
|
46
|
+
const value = payload.contentIndex;
|
|
47
|
+
return Number.isSafeInteger(value) && (value as number) >= 0 ? (value as number) : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureBlock(
|
|
51
|
+
message: MutableMessage,
|
|
52
|
+
contentIndex: number,
|
|
53
|
+
kind: TemporalContentBlock["kind"],
|
|
54
|
+
): TemporalContentBlock {
|
|
55
|
+
const existing = message.blocks.get(contentIndex);
|
|
56
|
+
if (existing) {
|
|
57
|
+
return existing;
|
|
58
|
+
}
|
|
59
|
+
const block: TemporalContentBlock = { contentIndex, kind, text: "" };
|
|
60
|
+
message.blocks.set(contentIndex, block);
|
|
61
|
+
return block;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function entryIds(entries: WorkflowSessionEntryRecord[]): Set<string> {
|
|
65
|
+
return new Set(
|
|
66
|
+
entries.flatMap((record) => {
|
|
67
|
+
const id = record.entry.id;
|
|
68
|
+
return typeof id === "string" ? [id] : [];
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function foldSessionEvents(
|
|
74
|
+
entries: WorkflowSessionEntryRecord[],
|
|
75
|
+
events: WorkflowSessionEventRecord[],
|
|
76
|
+
throughSeq: number,
|
|
77
|
+
initial?: TemporalSessionState,
|
|
78
|
+
): TemporalSessionState {
|
|
79
|
+
const messages = new Map<string, MutableMessage>();
|
|
80
|
+
const messageOrder: string[] = [];
|
|
81
|
+
for (const message of initial?.messages ?? []) {
|
|
82
|
+
messages.set(message.messageId, {
|
|
83
|
+
...message,
|
|
84
|
+
blocks: new Map(message.blocks.map((block) => [block.contentIndex, { ...block }] as const)),
|
|
85
|
+
});
|
|
86
|
+
messageOrder.push(message.messageId);
|
|
87
|
+
}
|
|
88
|
+
const tools = new Map<string, TemporalTool>();
|
|
89
|
+
const toolOrder: string[] = [];
|
|
90
|
+
for (const tool of initial?.tools ?? []) {
|
|
91
|
+
tools.set(tool.toolCallId, { ...tool });
|
|
92
|
+
toolOrder.push(tool.toolCallId);
|
|
93
|
+
}
|
|
94
|
+
const settledEntryIds: string[] = [...(initial?.settledEntryIds ?? [])];
|
|
95
|
+
const knownEntries = entryIds(entries);
|
|
96
|
+
const diagnostics: string[] = [...(initial?.diagnostics ?? [])];
|
|
97
|
+
let expectedSeq = (initial?.throughSeq ?? 0) + 1;
|
|
98
|
+
let lastSeq = initial?.throughSeq ?? 0;
|
|
99
|
+
|
|
100
|
+
for (const event of events) {
|
|
101
|
+
if (event.seq <= lastSeq) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (event.seq > throughSeq) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (event.seq !== expectedSeq) {
|
|
108
|
+
diagnostics.push(`session event sequence gap at ${expectedSeq}`);
|
|
109
|
+
expectedSeq = event.seq;
|
|
110
|
+
}
|
|
111
|
+
expectedSeq += 1;
|
|
112
|
+
lastSeq = event.seq;
|
|
113
|
+
|
|
114
|
+
switch (event.type) {
|
|
115
|
+
case "message_started": {
|
|
116
|
+
if (!event.messageId) {
|
|
117
|
+
diagnostics.push(`message_started ${event.seq} has no messageId`);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (!messages.has(event.messageId)) {
|
|
121
|
+
messages.set(event.messageId, {
|
|
122
|
+
messageId: event.messageId,
|
|
123
|
+
role: payloadString(event.payload, "role") ?? "unknown",
|
|
124
|
+
status: "streaming",
|
|
125
|
+
blocks: new Map(),
|
|
126
|
+
});
|
|
127
|
+
messageOrder.push(event.messageId);
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "assistant_event": {
|
|
132
|
+
if (!event.messageId) {
|
|
133
|
+
diagnostics.push(`assistant_event ${event.seq} has no messageId`);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
const message = messages.get(event.messageId);
|
|
137
|
+
if (!message) {
|
|
138
|
+
diagnostics.push(`assistant_event ${event.seq} precedes message_started`);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const assistantType = payloadString(event.payload, "type");
|
|
142
|
+
const contentIndex = payloadIndex(event.payload);
|
|
143
|
+
if (
|
|
144
|
+
contentIndex !== undefined &&
|
|
145
|
+
(assistantType === "text_start" ||
|
|
146
|
+
assistantType === "thinking_start" ||
|
|
147
|
+
assistantType === "toolcall_start")
|
|
148
|
+
) {
|
|
149
|
+
ensureBlock(
|
|
150
|
+
message,
|
|
151
|
+
contentIndex,
|
|
152
|
+
assistantType === "text_start"
|
|
153
|
+
? "text"
|
|
154
|
+
: assistantType === "thinking_start"
|
|
155
|
+
? "thinking"
|
|
156
|
+
: "toolCall",
|
|
157
|
+
);
|
|
158
|
+
} else if (
|
|
159
|
+
contentIndex !== undefined &&
|
|
160
|
+
(assistantType === "text_delta" ||
|
|
161
|
+
assistantType === "thinking_delta" ||
|
|
162
|
+
assistantType === "toolcall_delta")
|
|
163
|
+
) {
|
|
164
|
+
const block = ensureBlock(
|
|
165
|
+
message,
|
|
166
|
+
contentIndex,
|
|
167
|
+
assistantType === "text_delta"
|
|
168
|
+
? "text"
|
|
169
|
+
: assistantType === "thinking_delta"
|
|
170
|
+
? "thinking"
|
|
171
|
+
: "toolCall",
|
|
172
|
+
);
|
|
173
|
+
block.text += payloadString(event.payload, "delta") ?? "";
|
|
174
|
+
} else if (
|
|
175
|
+
contentIndex !== undefined &&
|
|
176
|
+
(assistantType === "text_end" || assistantType === "thinking_end")
|
|
177
|
+
) {
|
|
178
|
+
const block = ensureBlock(
|
|
179
|
+
message,
|
|
180
|
+
contentIndex,
|
|
181
|
+
assistantType === "text_end" ? "text" : "thinking",
|
|
182
|
+
);
|
|
183
|
+
const content = payloadString(event.payload, "content") ?? "";
|
|
184
|
+
if (block.text !== content) {
|
|
185
|
+
diagnostics.push(`${assistantType} mismatch for ${event.messageId}:${contentIndex}`);
|
|
186
|
+
block.text = content;
|
|
187
|
+
}
|
|
188
|
+
} else if (contentIndex !== undefined && assistantType === "toolcall_end") {
|
|
189
|
+
const block = ensureBlock(message, contentIndex, "toolCall");
|
|
190
|
+
block.value = event.payload.toolCall;
|
|
191
|
+
} else if (assistantType === "done") {
|
|
192
|
+
message.status = "finished";
|
|
193
|
+
} else if (assistantType === "error") {
|
|
194
|
+
message.status = "error";
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case "message_finished": {
|
|
199
|
+
if (!event.messageId) {
|
|
200
|
+
diagnostics.push(`message_finished ${event.seq} has no messageId`);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
const message = messages.get(event.messageId);
|
|
204
|
+
if (!message) {
|
|
205
|
+
diagnostics.push(`message_finished ${event.seq} precedes message_started`);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
const settled = event.payload.settled === true;
|
|
209
|
+
const entryId = payloadString(event.payload, "entryId");
|
|
210
|
+
if (settled && entryId) {
|
|
211
|
+
message.status = "settled";
|
|
212
|
+
message.entryId = entryId;
|
|
213
|
+
settledEntryIds.push(entryId);
|
|
214
|
+
if (!knownEntries.has(entryId)) {
|
|
215
|
+
diagnostics.push(`settled entry ${entryId} is missing`);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
message.status = "unsettled";
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "tool_execution_started": {
|
|
223
|
+
if (!event.toolCallId || !event.messageId) {
|
|
224
|
+
diagnostics.push(`tool_execution_started ${event.seq} is uncorrelated`);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
const tool: TemporalTool = {
|
|
228
|
+
toolCallId: event.toolCallId,
|
|
229
|
+
messageId: event.messageId,
|
|
230
|
+
toolName: payloadString(event.payload, "toolName") ?? "tool",
|
|
231
|
+
status: "running",
|
|
232
|
+
updates: 0,
|
|
233
|
+
...(event.payload.args === undefined ? {} : { args: event.payload.args }),
|
|
234
|
+
};
|
|
235
|
+
tools.set(event.toolCallId, tool);
|
|
236
|
+
toolOrder.push(event.toolCallId);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "tool_execution_updated": {
|
|
240
|
+
const tool = event.toolCallId ? tools.get(event.toolCallId) : undefined;
|
|
241
|
+
if (tool) {
|
|
242
|
+
tool.updates += 1;
|
|
243
|
+
} else {
|
|
244
|
+
diagnostics.push(`tool_execution_updated ${event.seq} precedes start`);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case "tool_execution_finished": {
|
|
249
|
+
const tool = event.toolCallId ? tools.get(event.toolCallId) : undefined;
|
|
250
|
+
if (!tool) {
|
|
251
|
+
diagnostics.push(`tool_execution_finished ${event.seq} precedes start`);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
tool.status = event.payload.isError === true ? "failed" : "finished";
|
|
255
|
+
if (event.payload.result !== undefined) {
|
|
256
|
+
tool.result = event.payload.result;
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "turn_started":
|
|
261
|
+
case "turn_finished":
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
throughSeq: lastSeq,
|
|
270
|
+
messages: messageOrder.map((messageId) => {
|
|
271
|
+
const message = messages.get(messageId)!;
|
|
272
|
+
return {
|
|
273
|
+
messageId: message.messageId,
|
|
274
|
+
role: message.role,
|
|
275
|
+
status: message.status,
|
|
276
|
+
...(message.entryId === undefined ? {} : { entryId: message.entryId }),
|
|
277
|
+
blocks: [...message.blocks.values()].toSorted(
|
|
278
|
+
(left, right) => left.contentIndex - right.contentIndex,
|
|
279
|
+
),
|
|
280
|
+
};
|
|
281
|
+
}),
|
|
282
|
+
tools: toolOrder.map((toolCallId) => tools.get(toolCallId)!),
|
|
283
|
+
settledEntryIds,
|
|
284
|
+
diagnostics,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Fold the durable semantic journal through one sequence position. */
|
|
289
|
+
export function reduceSessionEvents(
|
|
290
|
+
entries: WorkflowSessionEntryRecord[],
|
|
291
|
+
events: WorkflowSessionEventRecord[],
|
|
292
|
+
throughSeq: number = Number.MAX_SAFE_INTEGER,
|
|
293
|
+
): TemporalSessionState {
|
|
294
|
+
return foldSessionEvents(entries, events, throughSeq);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* In-memory seek index. Checkpoints are viewer-only cache state and are never
|
|
299
|
+
* persisted into a run bundle.
|
|
300
|
+
*/
|
|
301
|
+
export class SessionReplayIndex {
|
|
302
|
+
private readonly entries: WorkflowSessionEntryRecord[];
|
|
303
|
+
private readonly events: WorkflowSessionEventRecord[];
|
|
304
|
+
private readonly checkpoints: Array<{ seq: number; state: TemporalSessionState }> = [];
|
|
305
|
+
private readonly timestamps: Array<{ at: number; seq: number }>;
|
|
306
|
+
|
|
307
|
+
constructor(
|
|
308
|
+
entries: WorkflowSessionEntryRecord[],
|
|
309
|
+
events: WorkflowSessionEventRecord[],
|
|
310
|
+
checkpointInterval = 256,
|
|
311
|
+
) {
|
|
312
|
+
this.entries = entries;
|
|
313
|
+
this.events = events;
|
|
314
|
+
let lastTimestamp = Number.NEGATIVE_INFINITY;
|
|
315
|
+
this.timestamps = events.map((event) => {
|
|
316
|
+
const parsed = Date.parse(event.at);
|
|
317
|
+
lastTimestamp = Math.max(lastTimestamp, Number.isFinite(parsed) ? parsed : lastTimestamp);
|
|
318
|
+
return { at: lastTimestamp, seq: event.seq };
|
|
319
|
+
});
|
|
320
|
+
let state = foldSessionEvents(entries, events, 0);
|
|
321
|
+
this.checkpoints.push({ seq: 0, state });
|
|
322
|
+
for (let end = checkpointInterval - 1; end < events.length; end += checkpointInterval) {
|
|
323
|
+
const seq = events[end]?.seq ?? state.throughSeq;
|
|
324
|
+
state = foldSessionEvents(entries, events, seq, state);
|
|
325
|
+
this.checkpoints.push({ seq, state });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
stateAtSeq(throughSeq: number): TemporalSessionState {
|
|
330
|
+
const checkpoint = this.checkpoints.findLast(({ seq }) => seq <= throughSeq);
|
|
331
|
+
return foldSessionEvents(this.entries, this.events, throughSeq, checkpoint?.state);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
seqAtOrBefore(timestampMs: number): number {
|
|
335
|
+
let low = 0;
|
|
336
|
+
let high = this.timestamps.length;
|
|
337
|
+
while (low < high) {
|
|
338
|
+
const middle = (low + high) >>> 1;
|
|
339
|
+
if ((this.timestamps[middle]?.at ?? Number.POSITIVE_INFINITY) <= timestampMs) {
|
|
340
|
+
low = middle + 1;
|
|
341
|
+
} else {
|
|
342
|
+
high = middle;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return low === 0 ? 0 : (this.timestamps[low - 1]?.seq ?? 0);
|
|
346
|
+
}
|
|
347
|
+
}
|