@osolmaz/pi-workflows 0.15.3 → 0.16.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 +24 -8
- package/dist/channels/adapter-entry.d.ts +3 -0
- package/dist/channels/adapter-entry.js +203 -0
- package/dist/channels/adapter-entry.js.map +1 -0
- package/dist/channels/config.d.ts +43 -0
- package/dist/channels/config.js +234 -0
- package/dist/channels/config.js.map +1 -0
- package/dist/channels/protocol.d.ts +94 -0
- package/dist/channels/protocol.js +219 -0
- package/dist/channels/protocol.js.map +1 -0
- package/dist/channels/telegram.d.ts +46 -0
- package/dist/channels/telegram.js +285 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +102 -0
- package/dist/client/client.js +737 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +275 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +144 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +47 -157
- package/dist/controllers/sqlite.js +230 -406
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -1
- package/dist/extension/index.js +734 -555
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +95 -0
- package/dist/extension/recorder.js +530 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/remote-recorder-store.d.ts +25 -0
- package/dist/extension/remote-recorder-store.js +81 -0
- package/dist/extension/remote-recorder-store.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/session-view.d.ts +9 -2
- package/dist/extension/session-view.js +117 -51
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/step-message.d.ts +6 -4
- package/dist/extension/step-message.js +12 -6
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.d.ts +3 -2
- package/dist/extension/widget.js +41 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +27 -0
- package/dist/extension/workflow-message-coordinator.js +294 -0
- package/dist/extension/workflow-message-coordinator.js.map +1 -0
- package/dist/host/channel-effects.d.ts +72 -0
- package/dist/host/channel-effects.js +271 -0
- package/dist/host/channel-effects.js.map +1 -0
- package/dist/host/channel-supervisor.d.ts +21 -0
- package/dist/host/channel-supervisor.js +54 -0
- package/dist/host/channel-supervisor.js.map +1 -0
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +54 -8
- package/dist/host/runner.js +1997 -305
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +25 -19
- package/dist/host/state.js +303 -89
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +83 -0
- package/dist/host/view.js +975 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +9 -33
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +88 -98
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +96 -0
- package/dist/state/workflow-messages.js +335 -0
- package/dist/state/workflow-messages.js.map +1 -0
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +245 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +2 -0
- package/dist/workflows/human-decision.js +8 -2
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/settings.d.ts +1 -8
- package/dist/workflows/settings.js.map +1 -1
- package/dist/workflows/store.d.ts +67 -20
- package/dist/workflows/store.js +451 -378
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +21 -0
- package/dist/workflows/tool-input.js +23 -1
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +5 -5
- package/dist/workflows/workflow-message-content.d.ts +38 -0
- package/dist/workflows/workflow-message-content.js +157 -0
- package/dist/workflows/workflow-message-content.js.map +1 -0
- package/docs/2026-08-18-herdr-piw-plan.md +2 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +26 -50
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/DEFERRED_TURNS.md +66 -294
- package/docs/HUMAN_DECISIONS.md +29 -30
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
- package/docs/SQLITE_STATE.md +31 -25
- package/docs/WORKFLOW_HOST.md +155 -77
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +74 -132
- package/docs/tui-viewer.md +12 -14
- package/docs/workflows.md +78 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/plugins/herdr/viewer.mjs +14 -5
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/channels/adapter-entry.ts +220 -0
- package/src/channels/config.ts +296 -0
- package/src/channels/protocol.ts +333 -0
- package/src/channels/telegram.ts +335 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +939 -0
- package/src/client/index.ts +25 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +333 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +163 -0
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +339 -808
- package/src/extension/index.ts +853 -665
- package/src/extension/recorder.ts +687 -0
- package/src/extension/remote-recorder-store.ts +126 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/session-view.ts +157 -51
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +53 -25
- package/src/extension/workflow-message-coordinator.ts +334 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +2639 -450
- package/src/host/state.ts +404 -137
- package/src/host/view.ts +1212 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +14 -42
- package/src/state/schema.ts +88 -98
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +261 -124
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +650 -511
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +5 -5
- package/src/workflows/workflow-message-content.ts +197 -0
- package/dist/extension/decision-channels.d.ts +0 -134
- package/dist/extension/decision-channels.js +0 -1307
- package/dist/extension/decision-channels.js.map +0 -1
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/extension/decision-channels.ts +0 -1826
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { RUN_VIEW_SCHEMA, SESSION_VIEW_SCHEMA, } from "../client/view.js";
|
|
3
|
+
import { canonicalJson, parseJson } from "../state/json.js";
|
|
4
|
+
import { WorkflowMessageStore } from "../state/workflow-messages.js";
|
|
5
|
+
export const WORKFLOW_PAGE_KINDS = [
|
|
6
|
+
"steps",
|
|
7
|
+
"trace",
|
|
8
|
+
"trace_at_step",
|
|
9
|
+
"session_entries",
|
|
10
|
+
"session_events",
|
|
11
|
+
"settings",
|
|
12
|
+
"follow_ups",
|
|
13
|
+
"updates",
|
|
14
|
+
];
|
|
15
|
+
const INLINE_CONTENT_BYTES = 16 * 1024;
|
|
16
|
+
const VIEW_PAGE_BYTES = 64 * 1024;
|
|
17
|
+
const VIEW_PAGE_ITEMS = 256;
|
|
18
|
+
const CONTENT_CHUNK_BYTES = 192 * 1024;
|
|
19
|
+
const CONTENT_CACHE_BYTES = 64 * 1024 * 1024;
|
|
20
|
+
const VIEW_CACHE_ITEMS = 64;
|
|
21
|
+
const TERMINAL_VIEW_RETENTION_MS = 60_000;
|
|
22
|
+
export class HostViewStore {
|
|
23
|
+
state;
|
|
24
|
+
queue;
|
|
25
|
+
hostState;
|
|
26
|
+
runs;
|
|
27
|
+
hasLiveWorker;
|
|
28
|
+
hasActiveSessionTurn;
|
|
29
|
+
contentRecords = new Map();
|
|
30
|
+
listCache = new Map();
|
|
31
|
+
runCache = new Map();
|
|
32
|
+
sessionCache = new Map();
|
|
33
|
+
contentBytes = 0;
|
|
34
|
+
originActivityRevision = 0;
|
|
35
|
+
workflowMessages;
|
|
36
|
+
constructor(state, queue, hostState, runs, hasLiveWorker, hasActiveSessionTurn) {
|
|
37
|
+
this.state = state;
|
|
38
|
+
this.queue = queue;
|
|
39
|
+
this.hostState = hostState;
|
|
40
|
+
this.runs = runs;
|
|
41
|
+
this.hasLiveWorker = hasLiveWorker;
|
|
42
|
+
this.hasActiveSessionTurn = hasActiveSessionTurn;
|
|
43
|
+
this.workflowMessages = hostState.workflowMessages;
|
|
44
|
+
}
|
|
45
|
+
noteOriginActivityChange() {
|
|
46
|
+
this.originActivityRevision += 1;
|
|
47
|
+
}
|
|
48
|
+
list(cursor = 0, limit) {
|
|
49
|
+
return this.state.readTransaction(() => {
|
|
50
|
+
const pageSize = Math.min(limit ?? VIEW_PAGE_ITEMS, VIEW_PAGE_ITEMS);
|
|
51
|
+
const current = this.queue.workflowRunListRevision();
|
|
52
|
+
const revision = `${current.revision}:${this.workflowActivityRevision()}`;
|
|
53
|
+
const cacheKey = `${cursor}:${pageSize}`;
|
|
54
|
+
const cached = this.listCache.get(cacheKey);
|
|
55
|
+
if (cached?.revision === revision) {
|
|
56
|
+
refreshCacheEntry(this.listCache, cacheKey, cached);
|
|
57
|
+
return cached.page;
|
|
58
|
+
}
|
|
59
|
+
const loaded = this.queue.listWorkflowRunViews({ offset: cursor, limit: pageSize });
|
|
60
|
+
const summaries = loaded.runs.map((run) => {
|
|
61
|
+
const display = this.projectDisplay(run.runId, this.display(run, {
|
|
62
|
+
status: run.runStateStatus,
|
|
63
|
+
paused: run.paused,
|
|
64
|
+
error: run.errorMessage,
|
|
65
|
+
}));
|
|
66
|
+
return {
|
|
67
|
+
runId: run.runId,
|
|
68
|
+
workflowName: run.workflowName,
|
|
69
|
+
originSessionId: run.originSessionId,
|
|
70
|
+
createdAt: run.createdAt,
|
|
71
|
+
updatedAt: run.updatedAt,
|
|
72
|
+
display,
|
|
73
|
+
manifest: manifest(run, display.status),
|
|
74
|
+
live: display.status === "running" || display.status === "waiting",
|
|
75
|
+
possiblyInterrupted: run.status === "parked" && display.status !== "paused",
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
const items = byteBoundedForwardPage(summaries, (summary) => toJson(summary)).map((item) => item);
|
|
79
|
+
const page = {
|
|
80
|
+
schema: "pi-workflows.run-list-page.v1",
|
|
81
|
+
revision,
|
|
82
|
+
start: cursor,
|
|
83
|
+
total: loaded.total,
|
|
84
|
+
items,
|
|
85
|
+
};
|
|
86
|
+
rememberCacheEntry(this.listCache, cacheKey, { revision, page });
|
|
87
|
+
return page;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
run(runId) {
|
|
91
|
+
return this.state.readTransaction(() => {
|
|
92
|
+
const version = this.runVersion(runId);
|
|
93
|
+
const cached = this.runCache.get(runId);
|
|
94
|
+
if (cached?.version === version) {
|
|
95
|
+
refreshCacheEntry(this.runCache, runId, cached);
|
|
96
|
+
return cached.view;
|
|
97
|
+
}
|
|
98
|
+
const view = this.readRun(runId);
|
|
99
|
+
rememberCacheEntry(this.runCache, runId, { version, view });
|
|
100
|
+
return view;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
page(runId, request) {
|
|
104
|
+
return this.state.readTransaction(() => this.readRun(runId, request));
|
|
105
|
+
}
|
|
106
|
+
readRun(runId, page) {
|
|
107
|
+
const queue = this.queue.getWorkflowRunView(runId);
|
|
108
|
+
const counts = this.runs.readRunViewCounts(runId);
|
|
109
|
+
if (queue === undefined || counts === null)
|
|
110
|
+
return null;
|
|
111
|
+
const graphCursor = page?.kind === "steps"
|
|
112
|
+
? clampCursor(page.cursor, counts.steps)
|
|
113
|
+
: Math.max(0, counts.steps - 1);
|
|
114
|
+
const traceCursor = page?.kind === "trace_at_step"
|
|
115
|
+
? this.runs.traceCursorForStep(runId, page.cursor, counts.trace)
|
|
116
|
+
: page?.kind === "trace"
|
|
117
|
+
? page.cursor
|
|
118
|
+
: undefined;
|
|
119
|
+
const stepRange = viewRange(counts.steps, page?.kind === "steps" ? page.cursor : undefined);
|
|
120
|
+
const traceRange = viewRange(counts.trace, traceCursor);
|
|
121
|
+
const entryRange = viewRange(counts.sessionEntries, page?.kind === "session_entries" ? page.cursor : undefined);
|
|
122
|
+
const eventRange = viewRange(counts.sessionEvents, page?.kind === "session_events" ? page.cursor : undefined);
|
|
123
|
+
const settingsRange = viewRange(counts.settings, page?.kind === "settings" ? page.cursor : undefined);
|
|
124
|
+
const followUpRange = viewRange(counts.followUps, page?.kind === "follow_ups" ? page.cursor : undefined);
|
|
125
|
+
const updateRange = viewRange(counts.updates, page?.kind === "updates" ? page.cursor : undefined);
|
|
126
|
+
const loaded = this.runs.readRunView(runId, {
|
|
127
|
+
steps: stepRange,
|
|
128
|
+
trace: traceRange,
|
|
129
|
+
sessionEntries: entryRange,
|
|
130
|
+
sessionEvents: eventRange,
|
|
131
|
+
settings: settingsRange,
|
|
132
|
+
followUps: followUpRange,
|
|
133
|
+
updates: updateRange,
|
|
134
|
+
graphCursor,
|
|
135
|
+
});
|
|
136
|
+
if (loaded === null)
|
|
137
|
+
return null;
|
|
138
|
+
const stepPage = byteBoundedCandidatePage(loaded.state.steps, stepRange.start, counts.steps, page?.kind === "steps" ? page.cursor : undefined, (step) => this.projectStep(runId, step));
|
|
139
|
+
const tracePage = byteBoundedCandidatePage(loaded.traceEvents, traceRange.start, counts.trace, traceCursor, (event) => this.projectTraceEvent(runId, event));
|
|
140
|
+
const entryPage = byteBoundedCandidatePage(loaded.sessionEntries, entryRange.start, counts.sessionEntries, page?.kind === "session_entries" ? page.cursor : undefined, (entry) => this.projectSessionEntry(runId, entry));
|
|
141
|
+
const eventPage = byteBoundedCandidatePage(loaded.sessionEvents, eventRange.start, counts.sessionEvents, page?.kind === "session_events" ? page.cursor : undefined, (event) => this.projectSessionEvent(runId, event));
|
|
142
|
+
const settingsPage = byteBoundedCandidatePage(loaded.settingsScopes, settingsRange.start, counts.settings, page?.kind === "settings" ? page.cursor : undefined, (scope) => this.projectRecordField(runId, scope, "settings"));
|
|
143
|
+
const followUps = loaded.followUpQueue?.followUps ?? [];
|
|
144
|
+
const followUpPage = byteBoundedCandidatePage(followUps, followUpRange.start, counts.followUps, page?.kind === "follow_ups" ? page.cursor : undefined, (followUp) => this.projectRecordField(runId, followUp, "prompt"));
|
|
145
|
+
const updates = loaded.state.updates ?? [];
|
|
146
|
+
const updatePage = byteBoundedCandidatePage(updates, updateRange.start, counts.updates, page?.kind === "updates" ? page.cursor : undefined, (update) => this.projectUpdate(runId, update));
|
|
147
|
+
const followUpQueue = loaded.followUpQueue === null
|
|
148
|
+
? null
|
|
149
|
+
: projectFollowUpQueue(loaded.followUpQueue, followUpPage.items);
|
|
150
|
+
const completeGraphSteps = loaded.graphSteps.map((step) => toCompactStepJson(step));
|
|
151
|
+
const completeTakenTransitions = loaded.takenTransitions.map((transition) => toJson(transition));
|
|
152
|
+
const graphSteps = byteBoundedForwardPage(completeGraphSteps, (step) => step);
|
|
153
|
+
const takenTransitions = byteBoundedForwardPage(completeTakenTransitions, (transition) => transition).filter((transition) => typeof transition === "string");
|
|
154
|
+
const graphHistory = this.projectValue(runId, {
|
|
155
|
+
steps: completeGraphSteps,
|
|
156
|
+
transitions: completeTakenTransitions,
|
|
157
|
+
});
|
|
158
|
+
const revision = this.presentationRevision(runId);
|
|
159
|
+
const display = this.projectDisplay(runId, this.display(queue, loaded.state));
|
|
160
|
+
return {
|
|
161
|
+
schema: RUN_VIEW_SCHEMA,
|
|
162
|
+
runId,
|
|
163
|
+
revision,
|
|
164
|
+
display,
|
|
165
|
+
manifest: manifest(queue, display.status),
|
|
166
|
+
state: this.projectState(runId, loaded.state, stepPage.items, updatePage.items),
|
|
167
|
+
workflow: this.projectWorkflow(runId, loaded.snapshot),
|
|
168
|
+
queue: projectQueue(queue),
|
|
169
|
+
updates: updatePage.items,
|
|
170
|
+
graphSteps,
|
|
171
|
+
graphStepStart: 0,
|
|
172
|
+
graphStepTotal: loaded.graphSteps.length,
|
|
173
|
+
takenTransitions,
|
|
174
|
+
graphHistory,
|
|
175
|
+
takenTransitionStart: 0,
|
|
176
|
+
takenTransitionTotal: loaded.takenTransitions.length,
|
|
177
|
+
graphCursor,
|
|
178
|
+
stepStart: stepPage.start,
|
|
179
|
+
stepTotal: stepPage.total,
|
|
180
|
+
tracePage,
|
|
181
|
+
session: {
|
|
182
|
+
binding: toJson(loaded.sessionBinding),
|
|
183
|
+
entryPage,
|
|
184
|
+
eventPage,
|
|
185
|
+
capture: toJson(loaded.sessionCapture),
|
|
186
|
+
integrity: toJson(loaded.sessionIntegrity),
|
|
187
|
+
replayCheckpoint: this.projectReplayCheckpoint(runId, this.runs.readSessionReplayCheckpoint(runId, eventPage.start)),
|
|
188
|
+
},
|
|
189
|
+
settingsScopes: settingsPage.items,
|
|
190
|
+
settingsStart: settingsPage.start,
|
|
191
|
+
settingsTotal: settingsPage.total,
|
|
192
|
+
followUpQueue: toJson(followUpQueue),
|
|
193
|
+
followUpStart: followUpPage.start,
|
|
194
|
+
followUpTotal: followUpPage.total,
|
|
195
|
+
updateStart: updatePage.start,
|
|
196
|
+
updateTotal: updatePage.total,
|
|
197
|
+
live: display.status === "running" || display.status === "waiting",
|
|
198
|
+
possiblyInterrupted: queue.status === "parked" && display.status !== "paused",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
session(sessionId, coordinator = null) {
|
|
202
|
+
return this.state.readTransaction(() => {
|
|
203
|
+
const activeQueue = this.queue.findSessionReservationView(sessionId);
|
|
204
|
+
const retainedRunId = activeQueue === undefined ? this.retainedTerminalRunId(sessionId) : undefined;
|
|
205
|
+
const runId = activeQueue?.runId ?? retainedRunId;
|
|
206
|
+
const pending = this.hostState.listPendingInteractions(sessionId);
|
|
207
|
+
const pendingInteractions = byteBoundedForwardPage(pending, (request) => this.projectRecordField(request.runId, request, "contract"));
|
|
208
|
+
const workflowMessages = this.workflowMessages.listSession(sessionId);
|
|
209
|
+
const eligible = workflowMessages.find((message) => this.isMessageEligible(message));
|
|
210
|
+
const next = coordinator === null || (coordinator.active && !coordinator.branchReportRequired)
|
|
211
|
+
? eligible
|
|
212
|
+
: undefined;
|
|
213
|
+
const open = this.openWorkflowMessage(workflowMessages);
|
|
214
|
+
const openTurn = open === undefined
|
|
215
|
+
? undefined
|
|
216
|
+
: this.workflowMessages.openTurnForMessage(open.workflowMessageId);
|
|
217
|
+
return {
|
|
218
|
+
schema: SESSION_VIEW_SCHEMA,
|
|
219
|
+
sessionId,
|
|
220
|
+
run: runId === undefined ? null : this.run(runId),
|
|
221
|
+
pendingInteractions,
|
|
222
|
+
pendingInteractionStart: 0,
|
|
223
|
+
pendingInteractionTotal: pending.length,
|
|
224
|
+
workflowMessages,
|
|
225
|
+
workflowMessageStart: 0,
|
|
226
|
+
workflowMessageTotal: workflowMessages.length,
|
|
227
|
+
workflowMessageWindowComplete: true,
|
|
228
|
+
nextWorkflowMessageId: next?.workflowMessageId ?? null,
|
|
229
|
+
openWorkflowMessageId: open?.workflowMessageId ?? null,
|
|
230
|
+
openWorkflowTurn: openTurn ?? null,
|
|
231
|
+
coordinatorEpoch: coordinator?.epoch ?? null,
|
|
232
|
+
coordinatorActive: coordinator?.active ?? false,
|
|
233
|
+
branchReportRequired: coordinator?.branchReportRequired ?? false,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
clearTerminal(sessionId, runId, now = Date.now()) {
|
|
238
|
+
return this.state.transaction(() => {
|
|
239
|
+
const retained = this.retainedTerminalRunId(sessionId, now);
|
|
240
|
+
if (retained === undefined)
|
|
241
|
+
return null;
|
|
242
|
+
if (runId !== undefined && retained !== runId) {
|
|
243
|
+
throw new Error(`Retained terminal workflow does not match run ${runId}`);
|
|
244
|
+
}
|
|
245
|
+
this.state.connection
|
|
246
|
+
.prepare(`INSERT INTO session_terminal_views(target_session_id, cleared_run_id, cleared_at)
|
|
247
|
+
VALUES (?, ?, ?)
|
|
248
|
+
ON CONFLICT(target_session_id) DO UPDATE SET
|
|
249
|
+
cleared_run_id = excluded.cleared_run_id, cleared_at = excluded.cleared_at`)
|
|
250
|
+
.run(sessionId, retained, now);
|
|
251
|
+
this.sessionCache.delete(sessionId);
|
|
252
|
+
return retained;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
content(runId, contentPath, offset) {
|
|
256
|
+
const key = contentKey(runId, contentPath);
|
|
257
|
+
const record = this.contentRecords.get(key) ?? this.recoverContent(runId, contentPath);
|
|
258
|
+
if (record === undefined)
|
|
259
|
+
return null;
|
|
260
|
+
this.contentRecords.delete(key);
|
|
261
|
+
this.contentRecords.set(key, record);
|
|
262
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > record.bytes.byteLength) {
|
|
263
|
+
throw new Error("Workflow content offset is outside the content range");
|
|
264
|
+
}
|
|
265
|
+
const nextOffset = Math.min(record.bytes.byteLength, offset + CONTENT_CHUNK_BYTES);
|
|
266
|
+
return {
|
|
267
|
+
schema: "pi-workflows.content-chunk.v1",
|
|
268
|
+
runId,
|
|
269
|
+
path: record.path,
|
|
270
|
+
mediaType: record.mediaType,
|
|
271
|
+
bytes: record.bytes.byteLength,
|
|
272
|
+
sha256: record.sha256,
|
|
273
|
+
offset,
|
|
274
|
+
nextOffset,
|
|
275
|
+
complete: nextOffset === record.bytes.byteLength,
|
|
276
|
+
data: record.bytes.subarray(offset, nextOffset).toString("base64"),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
projectStep(runId, step) {
|
|
280
|
+
const projected = toJson(step);
|
|
281
|
+
if (!isJsonObject(projected))
|
|
282
|
+
return projected;
|
|
283
|
+
for (const field of ["prompt", "output", "assistantMessage"]) {
|
|
284
|
+
const value = projected[field];
|
|
285
|
+
if (value !== undefined)
|
|
286
|
+
projected[field] = this.projectValue(runId, value);
|
|
287
|
+
}
|
|
288
|
+
return projected;
|
|
289
|
+
}
|
|
290
|
+
projectTraceEvent(runId, event) {
|
|
291
|
+
return this.projectRecordField(runId, event, "payload");
|
|
292
|
+
}
|
|
293
|
+
projectSessionEntry(runId, entry) {
|
|
294
|
+
return this.projectRecordField(runId, entry, "entry");
|
|
295
|
+
}
|
|
296
|
+
projectSessionEvent(runId, event) {
|
|
297
|
+
return this.projectRecordField(runId, event, "payload");
|
|
298
|
+
}
|
|
299
|
+
projectUpdate(runId, update) {
|
|
300
|
+
return this.projectRecordField(runId, update, "data");
|
|
301
|
+
}
|
|
302
|
+
projectReplayCheckpoint(runId, value) {
|
|
303
|
+
return value === null ? null : this.projectValue(runId, value);
|
|
304
|
+
}
|
|
305
|
+
projectRecordField(runId, value, field) {
|
|
306
|
+
const projected = toJson(value);
|
|
307
|
+
if (isJsonObject(projected)) {
|
|
308
|
+
const fieldValue = projected[field];
|
|
309
|
+
if (fieldValue !== undefined)
|
|
310
|
+
projected[field] = this.projectValue(runId, fieldValue);
|
|
311
|
+
}
|
|
312
|
+
return projected;
|
|
313
|
+
}
|
|
314
|
+
projectState(runId, stateValue, steps, updates) {
|
|
315
|
+
const state = toJson(stateValue);
|
|
316
|
+
if (!isJsonObject(state))
|
|
317
|
+
return state;
|
|
318
|
+
for (const field of ["input", "outputs", "results", "humanDecision", "finalOutput"]) {
|
|
319
|
+
const value = state[field];
|
|
320
|
+
if (value !== undefined)
|
|
321
|
+
state[field] = this.projectValue(runId, value);
|
|
322
|
+
}
|
|
323
|
+
state.steps = steps;
|
|
324
|
+
state.updates = updates;
|
|
325
|
+
return state;
|
|
326
|
+
}
|
|
327
|
+
projectWorkflow(runId, value) {
|
|
328
|
+
const original = toJson(value);
|
|
329
|
+
const workflow = escapeArtifactSentinels(original);
|
|
330
|
+
if (Buffer.byteLength(canonicalJson(workflow)) <= VIEW_PAGE_BYTES * 2)
|
|
331
|
+
return workflow;
|
|
332
|
+
if (!isJsonObject(workflow) ||
|
|
333
|
+
!isJsonObject(workflow.nodes) ||
|
|
334
|
+
typeof workflow.schema !== "string" ||
|
|
335
|
+
typeof workflow.name !== "string" ||
|
|
336
|
+
typeof workflow.startAt !== "string" ||
|
|
337
|
+
!Array.isArray(workflow.edges)) {
|
|
338
|
+
return this.registerContent(runId, original, "application/json");
|
|
339
|
+
}
|
|
340
|
+
const nodeEntries = Object.entries(workflow.nodes);
|
|
341
|
+
const boundedNodeEntries = byteBoundedForwardPage(nodeEntries, ([nodeId, node]) => [
|
|
342
|
+
nodeId,
|
|
343
|
+
this.projectWorkflowNode(runId, node),
|
|
344
|
+
]);
|
|
345
|
+
const nodes = Object.fromEntries(boundedNodeEntries.flatMap((entry) => Array.isArray(entry) && typeof entry[0] === "string" && entry[1] !== undefined
|
|
346
|
+
? [[entry[0], entry[1]]]
|
|
347
|
+
: []));
|
|
348
|
+
const edges = byteBoundedForwardPage(workflow.edges, (edge) => this.projectValue(runId, edge));
|
|
349
|
+
return {
|
|
350
|
+
schema: workflow.schema,
|
|
351
|
+
name: workflow.name,
|
|
352
|
+
startAt: workflow.startAt,
|
|
353
|
+
nodes,
|
|
354
|
+
nodeStart: 0,
|
|
355
|
+
nodeTotal: nodeEntries.length,
|
|
356
|
+
edges,
|
|
357
|
+
edgeStart: 0,
|
|
358
|
+
edgeTotal: workflow.edges.length,
|
|
359
|
+
content: this.registerContent(runId, original, "application/json"),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
projectWorkflowNode(runId, value) {
|
|
363
|
+
if (!isJsonObject(value))
|
|
364
|
+
return this.projectValue(runId, value);
|
|
365
|
+
const projected = {};
|
|
366
|
+
for (const field of [
|
|
367
|
+
"nodeType",
|
|
368
|
+
"timeoutMs",
|
|
369
|
+
"statusDetail",
|
|
370
|
+
"actionExecution",
|
|
371
|
+
"settingsRoute",
|
|
372
|
+
"effect",
|
|
373
|
+
"mountPath",
|
|
374
|
+
"localNodeId",
|
|
375
|
+
"includeTransition",
|
|
376
|
+
]) {
|
|
377
|
+
const fieldValue = value[field];
|
|
378
|
+
if (fieldValue !== undefined)
|
|
379
|
+
projected[field] = this.projectValue(runId, fieldValue);
|
|
380
|
+
}
|
|
381
|
+
return projected;
|
|
382
|
+
}
|
|
383
|
+
projectDisplay(runId, display) {
|
|
384
|
+
if (display.reason === null ||
|
|
385
|
+
Buffer.byteLength(display.reason, "utf8") <= INLINE_CONTENT_BYTES) {
|
|
386
|
+
return display;
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
...display,
|
|
390
|
+
reason: "Complete workflow failure details are available.",
|
|
391
|
+
reasonContent: this.registerContent(runId, display.reason, "text/plain"),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
projectValue(runId, value) {
|
|
395
|
+
const safeValue = escapeArtifactSentinels(value);
|
|
396
|
+
const mediaType = typeof value === "string" ? "text/plain" : "application/json";
|
|
397
|
+
const bytes = mediaType === "text/plain"
|
|
398
|
+
? Buffer.from(value, "utf8")
|
|
399
|
+
: Buffer.from(canonicalJson(value), "utf8");
|
|
400
|
+
return bytes.byteLength <= INLINE_CONTENT_BYTES
|
|
401
|
+
? safeValue
|
|
402
|
+
: this.registerContent(runId, value, mediaType);
|
|
403
|
+
}
|
|
404
|
+
registerContent(runId, value, mediaType) {
|
|
405
|
+
const bytes = Buffer.from(mediaType === "text/plain" ? value : canonicalJson(value), "utf8");
|
|
406
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
407
|
+
const persistedDigest = this.runs.persistViewContent(runId, bytes, mediaType);
|
|
408
|
+
if (persistedDigest !== sha256)
|
|
409
|
+
throw new Error("Workflow view content digest changed");
|
|
410
|
+
const extension = mediaType === "text/plain" ? "txt" : "json";
|
|
411
|
+
const contentPath = `artifacts/sha256/${sha256}.${extension}`;
|
|
412
|
+
this.rememberContent({
|
|
413
|
+
runId,
|
|
414
|
+
path: contentPath,
|
|
415
|
+
mediaType,
|
|
416
|
+
bytes,
|
|
417
|
+
sha256,
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
$artifact: {
|
|
421
|
+
path: contentPath,
|
|
422
|
+
mediaType,
|
|
423
|
+
bytes: bytes.byteLength,
|
|
424
|
+
sha256,
|
|
425
|
+
opaque: true,
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
rememberContent(record) {
|
|
430
|
+
const key = contentKey(record.runId, record.path);
|
|
431
|
+
const previous = this.contentRecords.get(key);
|
|
432
|
+
if (previous !== undefined)
|
|
433
|
+
this.contentBytes -= previous.bytes.byteLength;
|
|
434
|
+
this.contentRecords.delete(key);
|
|
435
|
+
this.contentRecords.set(key, record);
|
|
436
|
+
this.contentBytes += record.bytes.byteLength;
|
|
437
|
+
while (this.contentBytes > CONTENT_CACHE_BYTES && this.contentRecords.size > 1) {
|
|
438
|
+
const oldest = this.contentRecords.entries().next().value;
|
|
439
|
+
if (oldest === undefined)
|
|
440
|
+
break;
|
|
441
|
+
this.contentRecords.delete(oldest[0]);
|
|
442
|
+
this.contentBytes -= oldest[1].bytes.byteLength;
|
|
443
|
+
}
|
|
444
|
+
return record;
|
|
445
|
+
}
|
|
446
|
+
recoverContent(runId, contentPath) {
|
|
447
|
+
const match = /^artifacts\/sha256\/([0-9a-f]{64})\.(json|txt)$/u.exec(contentPath);
|
|
448
|
+
if (match === null)
|
|
449
|
+
return undefined;
|
|
450
|
+
const mediaType = match[2] === "txt" ? "text/plain" : "application/json";
|
|
451
|
+
const blob = this.runs.readContentBlob(runId, match[1], mediaType);
|
|
452
|
+
if (blob === undefined)
|
|
453
|
+
return undefined;
|
|
454
|
+
return this.rememberContent({
|
|
455
|
+
runId,
|
|
456
|
+
path: contentPath,
|
|
457
|
+
mediaType,
|
|
458
|
+
bytes: blob.content,
|
|
459
|
+
sha256: match[1],
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
clearConnection(_connectionId) {
|
|
463
|
+
// Coordinator fencing is process-local in the host. Durable turn state is
|
|
464
|
+
// reconciled from the Pi branch after the next coordinator connects.
|
|
465
|
+
}
|
|
466
|
+
retainedTerminalRunId(sessionId, now = Date.now()) {
|
|
467
|
+
const messages = this.workflowMessages
|
|
468
|
+
.listSession(sessionId)
|
|
469
|
+
.filter((message) => message.kind === "terminal")
|
|
470
|
+
.reverse();
|
|
471
|
+
for (const message of messages) {
|
|
472
|
+
const run = this.state.connection
|
|
473
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
474
|
+
.get(message.runId);
|
|
475
|
+
if (!isObjectRecord(run) || !isTerminalStatus(run.status))
|
|
476
|
+
continue;
|
|
477
|
+
const clear = this.state.connection
|
|
478
|
+
.prepare(`SELECT cleared_run_id AS clearedRunId, cleared_at AS clearedAt
|
|
479
|
+
FROM session_terminal_views WHERE target_session_id = ?`)
|
|
480
|
+
.get(sessionId);
|
|
481
|
+
if (isObjectRecord(clear) &&
|
|
482
|
+
clear.clearedRunId === message.runId &&
|
|
483
|
+
typeof clear.clearedAt === "number" &&
|
|
484
|
+
clear.clearedAt >= Date.parse(message.createdAt)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (message.status === "pending")
|
|
488
|
+
return message.runId;
|
|
489
|
+
if (message.status !== "sent")
|
|
490
|
+
continue;
|
|
491
|
+
const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
|
|
492
|
+
if (turn === undefined || turn.state === "started")
|
|
493
|
+
return message.runId;
|
|
494
|
+
if (turn.endedAt !== null && Date.parse(turn.endedAt) + TERMINAL_VIEW_RETENTION_MS > now) {
|
|
495
|
+
return message.runId;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
isMessageEligible(message) {
|
|
501
|
+
if (message.status !== "pending")
|
|
502
|
+
return false;
|
|
503
|
+
if (message.kind === "step" || message.kind === "decision") {
|
|
504
|
+
const request = this.state.connection
|
|
505
|
+
.prepare(`SELECT i.status, r.paused FROM interactive_requests i
|
|
506
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
507
|
+
WHERE i.request_id = ? AND i.run_id = ?`)
|
|
508
|
+
.get(message.sourceId, message.runId);
|
|
509
|
+
if (!isObjectRecord(request) || request.status !== "pending")
|
|
510
|
+
return false;
|
|
511
|
+
return message.kind === "decision" || request.paused === 0;
|
|
512
|
+
}
|
|
513
|
+
if (message.kind === "notification")
|
|
514
|
+
return true;
|
|
515
|
+
if (message.kind === "terminal") {
|
|
516
|
+
const run = this.state.connection
|
|
517
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
518
|
+
.get(message.runId);
|
|
519
|
+
return isObjectRecord(run) && isTerminalStatus(run.status);
|
|
520
|
+
}
|
|
521
|
+
const source = this.state.connection
|
|
522
|
+
.prepare(`SELECT f.run_id AS runId, f.order_number AS orderNumber, f.status
|
|
523
|
+
FROM workflow_follow_ups f WHERE f.follow_up_id = ?`)
|
|
524
|
+
.get(message.sourceId);
|
|
525
|
+
if (!isObjectRecord(source) ||
|
|
526
|
+
source.status !== "queued" ||
|
|
527
|
+
typeof source.orderNumber !== "number" ||
|
|
528
|
+
typeof source.runId !== "string") {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
const leaf = this.terminalChainLeaf(source.runId);
|
|
532
|
+
if (leaf === undefined || leaf.status !== "completed")
|
|
533
|
+
return false;
|
|
534
|
+
const terminal = this.workflowMessages
|
|
535
|
+
.listRun(leaf.runId)
|
|
536
|
+
.filter((candidate) => candidate.kind === "terminal" && candidate.status === "sent")
|
|
537
|
+
.at(-1);
|
|
538
|
+
if (terminal === undefined)
|
|
539
|
+
return false;
|
|
540
|
+
const terminalTurn = this.workflowMessages.latestTurnForMessage(terminal.workflowMessageId);
|
|
541
|
+
if (terminalTurn?.state !== "ended")
|
|
542
|
+
return false;
|
|
543
|
+
const prior = this.state.connection
|
|
544
|
+
.prepare(`SELECT follow_up_id AS followUpId, status FROM workflow_follow_ups
|
|
545
|
+
WHERE run_id = ? AND order_number < ? ORDER BY order_number`)
|
|
546
|
+
.all(source.runId, source.orderNumber);
|
|
547
|
+
for (const item of prior) {
|
|
548
|
+
if (!isObjectRecord(item) ||
|
|
549
|
+
typeof item.followUpId !== "string" ||
|
|
550
|
+
typeof item.status !== "string") {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
if (item.status === "removed" || item.status === "cancelled")
|
|
554
|
+
continue;
|
|
555
|
+
const priorMessage = this.workflowMessages.latestForSource("followUp", item.followUpId);
|
|
556
|
+
if (priorMessage === undefined ||
|
|
557
|
+
this.workflowMessages.latestTurnForMessage(priorMessage.workflowMessageId)?.state !==
|
|
558
|
+
"ended") {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const reservation = this.state.connection
|
|
563
|
+
.prepare(`SELECT 1 AS present FROM run_bindings b JOIN runs r ON r.run_id = b.run_id
|
|
564
|
+
WHERE b.origin_session_id = ? AND r.run_id <> ?
|
|
565
|
+
AND r.status IN ('queued', 'running', 'waiting') LIMIT 1`)
|
|
566
|
+
.get(message.targetSessionId, message.runId);
|
|
567
|
+
return reservation === undefined;
|
|
568
|
+
}
|
|
569
|
+
terminalChainLeaf(runId) {
|
|
570
|
+
const row = this.state.connection
|
|
571
|
+
.prepare(`WITH RECURSIVE chain(run_id, status, depth, created_at) AS (
|
|
572
|
+
SELECT run_id, status, 0, created_at FROM runs WHERE run_id = ?
|
|
573
|
+
UNION ALL
|
|
574
|
+
SELECT child.run_id, child.status, chain.depth + 1, child.created_at
|
|
575
|
+
FROM runs child JOIN chain ON child.parent_run_id = chain.run_id
|
|
576
|
+
)
|
|
577
|
+
SELECT run_id AS runId, status FROM chain
|
|
578
|
+
ORDER BY depth DESC, created_at DESC LIMIT 1`)
|
|
579
|
+
.get(runId);
|
|
580
|
+
return isObjectRecord(row) && typeof row.runId === "string" && typeof row.status === "string"
|
|
581
|
+
? { runId: row.runId, status: row.status }
|
|
582
|
+
: undefined;
|
|
583
|
+
}
|
|
584
|
+
openWorkflowMessage(messages) {
|
|
585
|
+
for (const message of [...messages].reverse()) {
|
|
586
|
+
if (message.status !== "sent")
|
|
587
|
+
continue;
|
|
588
|
+
if (message.kind === "step") {
|
|
589
|
+
const request = this.state.connection
|
|
590
|
+
.prepare(`SELECT i.status, r.paused FROM interactive_requests i
|
|
591
|
+
JOIN runs r ON r.run_id = i.run_id WHERE i.request_id = ?`)
|
|
592
|
+
.get(message.sourceId);
|
|
593
|
+
if (isObjectRecord(request) && request.status === "pending" && request.paused === 0) {
|
|
594
|
+
return message;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
else if (message.kind === "terminal" || message.kind === "followUp") {
|
|
598
|
+
const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
|
|
599
|
+
if (turn === undefined || turn.state === "started")
|
|
600
|
+
return message;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
workflowActivityRevision() {
|
|
606
|
+
const row = this.state.connection
|
|
607
|
+
.prepare(`SELECT
|
|
608
|
+
COALESCE((SELECT max(updated_at) FROM workflow_messages), 0) AS messageUpdatedAt,
|
|
609
|
+
COALESCE((SELECT max(COALESCE(ended_at, started_at)) FROM workflow_turns), 0) AS turnUpdatedAt`)
|
|
610
|
+
.get();
|
|
611
|
+
return isObjectRecord(row) &&
|
|
612
|
+
typeof row.messageUpdatedAt === "number" &&
|
|
613
|
+
typeof row.turnUpdatedAt === "number"
|
|
614
|
+
? `${row.messageUpdatedAt}:${row.turnUpdatedAt}${this.originActivityRevision === 0 ? "" : `-${this.originActivityRevision}`}`
|
|
615
|
+
: `0:0${this.originActivityRevision === 0 ? "" : `-${this.originActivityRevision}`}`;
|
|
616
|
+
}
|
|
617
|
+
display(queue, state) {
|
|
618
|
+
return reduceWorkflowDisplay({
|
|
619
|
+
queueStatus: queue.status,
|
|
620
|
+
durableStatus: state?.status,
|
|
621
|
+
paused: state?.paused === true,
|
|
622
|
+
ambiguous: this.hasAmbiguousEffect(queue.runId),
|
|
623
|
+
workerActive: this.hasLiveWorker(queue.runId),
|
|
624
|
+
originTurnActive: this.hasActivity(queue.runId),
|
|
625
|
+
pendingInteraction: this.hasPendingInteraction(queue.runId),
|
|
626
|
+
errorMessage: state?.error ?? queue.errorMessage,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
hasActivity(runId) {
|
|
630
|
+
const row = this.state.connection
|
|
631
|
+
.prepare(`SELECT t.target_session_id AS targetSessionId FROM workflow_turns t
|
|
632
|
+
JOIN workflow_messages m ON m.workflow_message_id = t.workflow_message_id
|
|
633
|
+
WHERE t.run_id = ? AND t.state = 'started' AND m.kind IN ('step', 'terminal') LIMIT 1`)
|
|
634
|
+
.get(runId);
|
|
635
|
+
return (isObjectRecord(row) &&
|
|
636
|
+
typeof row.targetSessionId === "string" &&
|
|
637
|
+
this.hasActiveSessionTurn(row.targetSessionId));
|
|
638
|
+
}
|
|
639
|
+
hasPendingInteraction(runId) {
|
|
640
|
+
const row = this.state.connection
|
|
641
|
+
.prepare(`SELECT 1 AS present FROM interactive_requests
|
|
642
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`)
|
|
643
|
+
.get(runId);
|
|
644
|
+
return row !== undefined;
|
|
645
|
+
}
|
|
646
|
+
hasAmbiguousEffect(runId) {
|
|
647
|
+
const row = this.state.connection
|
|
648
|
+
.prepare(`SELECT 1 AS present FROM effects e JOIN runs r ON r.resource_id = e.source_resource_id
|
|
649
|
+
WHERE r.run_id = ? AND e.status = 'ambiguous' LIMIT 1`)
|
|
650
|
+
.get(runId);
|
|
651
|
+
return row !== undefined;
|
|
652
|
+
}
|
|
653
|
+
runVersion(runId) {
|
|
654
|
+
const row = this.state.connection
|
|
655
|
+
.prepare(`SELECT res.revision, r.status AS runStatus, r.paused,
|
|
656
|
+
q.updated_at AS updatedAt,
|
|
657
|
+
COALESCE(v.presentation_revision, 0) AS presentationRevision
|
|
658
|
+
FROM runs r JOIN resources res ON res.resource_id = r.resource_id
|
|
659
|
+
JOIN run_queue q ON q.run_id = r.run_id
|
|
660
|
+
LEFT JOIN viewer_runs v ON v.run_id = r.run_id
|
|
661
|
+
WHERE r.run_id = ?`)
|
|
662
|
+
.get(runId);
|
|
663
|
+
if (!isRunVersionRow(row))
|
|
664
|
+
return "missing";
|
|
665
|
+
return [
|
|
666
|
+
row.revision,
|
|
667
|
+
row.updatedAt,
|
|
668
|
+
row.presentationRevision,
|
|
669
|
+
row.runStatus,
|
|
670
|
+
row.paused,
|
|
671
|
+
this.workflowActivityRevision(),
|
|
672
|
+
this.hasLiveWorker(runId),
|
|
673
|
+
this.hasActivity(runId),
|
|
674
|
+
this.hasPendingInteraction(runId),
|
|
675
|
+
this.hasAmbiguousEffect(runId),
|
|
676
|
+
].join(":");
|
|
677
|
+
}
|
|
678
|
+
pendingSessionRevision(sessionId) {
|
|
679
|
+
const row = this.state.connection
|
|
680
|
+
.prepare(`SELECT count(*) AS count, COALESCE(sum(revision), 0) AS revisionSum,
|
|
681
|
+
COALESCE(max(updated_at), 0) AS updatedAt
|
|
682
|
+
FROM interactive_requests
|
|
683
|
+
WHERE target_session_id = ? AND status = 'pending'`)
|
|
684
|
+
.get(sessionId);
|
|
685
|
+
if (!isSessionRevisionRow(row))
|
|
686
|
+
throw new Error("Session view revision is invalid");
|
|
687
|
+
return `${row.count}:${row.revisionSum}:${row.updatedAt}`;
|
|
688
|
+
}
|
|
689
|
+
presentationRevision(runId) {
|
|
690
|
+
const row = this.state.connection
|
|
691
|
+
.prepare(`SELECT presentation_revision AS revision FROM viewer_runs WHERE run_id = ?`)
|
|
692
|
+
.get(runId);
|
|
693
|
+
return isRevisionRow(row) ? row.revision : 0;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
export function reduceWorkflowDisplay(facts) {
|
|
697
|
+
let status;
|
|
698
|
+
let activity = null;
|
|
699
|
+
let reason = null;
|
|
700
|
+
if (facts.ambiguous) {
|
|
701
|
+
status = "ambiguous";
|
|
702
|
+
reason = "An external effect needs explicit recovery.";
|
|
703
|
+
}
|
|
704
|
+
else if (facts.workerActive || facts.originTurnActive) {
|
|
705
|
+
status = "running";
|
|
706
|
+
activity = facts.workerActive ? "supervised_worker" : "origin_turn";
|
|
707
|
+
}
|
|
708
|
+
else if (facts.durableStatus === "completed" ||
|
|
709
|
+
facts.durableStatus === "failed" ||
|
|
710
|
+
facts.durableStatus === "timed_out" ||
|
|
711
|
+
facts.durableStatus === "cancelled") {
|
|
712
|
+
status = facts.durableStatus;
|
|
713
|
+
reason = facts.errorMessage;
|
|
714
|
+
}
|
|
715
|
+
else if (facts.paused) {
|
|
716
|
+
status = "paused";
|
|
717
|
+
reason = "The workflow is durably paused.";
|
|
718
|
+
}
|
|
719
|
+
else if (facts.pendingInteraction || facts.durableStatus === "waiting") {
|
|
720
|
+
status = "waiting";
|
|
721
|
+
reason = "The workflow is waiting for origin-session input.";
|
|
722
|
+
}
|
|
723
|
+
else if (facts.queueStatus === "parked" || facts.queueStatus === "queued") {
|
|
724
|
+
status = "queued";
|
|
725
|
+
reason = facts.queueStatus === "parked" ? "The workflow is ready to resume." : null;
|
|
726
|
+
}
|
|
727
|
+
else if (facts.queueStatus === "done") {
|
|
728
|
+
status = "completed";
|
|
729
|
+
}
|
|
730
|
+
else if (facts.queueStatus === "failed" || facts.queueStatus === "cancelled") {
|
|
731
|
+
status = facts.queueStatus;
|
|
732
|
+
reason = facts.errorMessage;
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
status = "running";
|
|
736
|
+
}
|
|
737
|
+
const controls = [];
|
|
738
|
+
if (status === "running" || status === "waiting")
|
|
739
|
+
controls.push("pause", "cancel");
|
|
740
|
+
else if (status === "paused")
|
|
741
|
+
controls.push("resume", "cancel");
|
|
742
|
+
else if (status === "queued") {
|
|
743
|
+
if (facts.queueStatus === "parked")
|
|
744
|
+
controls.push("resume");
|
|
745
|
+
controls.push("cancel");
|
|
746
|
+
}
|
|
747
|
+
if (status === "waiting")
|
|
748
|
+
controls.push("answer");
|
|
749
|
+
if (status === "ambiguous")
|
|
750
|
+
controls.push("review");
|
|
751
|
+
return { status, activity, controls, reason };
|
|
752
|
+
}
|
|
753
|
+
function manifest(run, status) {
|
|
754
|
+
return {
|
|
755
|
+
schema: "pi-workflows.run-manifest.v1",
|
|
756
|
+
runId: run.runId,
|
|
757
|
+
workflowName: run.workflowName,
|
|
758
|
+
workflowSource: workflowRootSource(run.workflowSource),
|
|
759
|
+
startedAt: run.startedAt ?? run.createdAt,
|
|
760
|
+
...(run.finishedAt === null ? {} : { finishedAt: run.finishedAt }),
|
|
761
|
+
status,
|
|
762
|
+
traceSchema: "pi-workflows.trace-event.v1",
|
|
763
|
+
paths: {
|
|
764
|
+
workflow: "host",
|
|
765
|
+
state: "host",
|
|
766
|
+
trace: "host",
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function projectQueue(run) {
|
|
771
|
+
return {
|
|
772
|
+
runId: run.runId,
|
|
773
|
+
workflowName: run.workflowName,
|
|
774
|
+
workflowSourceRef: run.workflowSourceRef,
|
|
775
|
+
initialized: run.initialized,
|
|
776
|
+
definitionDigest: run.definitionDigest,
|
|
777
|
+
status: run.status,
|
|
778
|
+
originSessionId: run.originSessionId,
|
|
779
|
+
executionMode: run.executionMode,
|
|
780
|
+
parentRunId: run.parentRunId,
|
|
781
|
+
rootRunId: run.rootRunId,
|
|
782
|
+
lineageKind: run.lineageKind,
|
|
783
|
+
restartNumber: run.restartNumber,
|
|
784
|
+
parentTerminalFingerprint: run.parentTerminalFingerprint,
|
|
785
|
+
errorCode: run.errorCode,
|
|
786
|
+
createdAt: run.createdAt,
|
|
787
|
+
updatedAt: run.updatedAt,
|
|
788
|
+
startedAt: run.startedAt,
|
|
789
|
+
finishedAt: run.finishedAt,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function workflowRootSource(value) {
|
|
793
|
+
const sourceSet = toJson(value);
|
|
794
|
+
if (!isJsonObject(sourceSet))
|
|
795
|
+
throw new Error("Workflow queue source set is invalid");
|
|
796
|
+
const root = sourceSet.root;
|
|
797
|
+
if (root === undefined || !isJsonObject(root)) {
|
|
798
|
+
throw new Error("Workflow queue source set is invalid");
|
|
799
|
+
}
|
|
800
|
+
return root;
|
|
801
|
+
}
|
|
802
|
+
function isObjectRecord(value) {
|
|
803
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
804
|
+
}
|
|
805
|
+
function isTerminalStatus(value) {
|
|
806
|
+
return (value === "completed" || value === "failed" || value === "timed_out" || value === "cancelled");
|
|
807
|
+
}
|
|
808
|
+
function contentKey(runId, contentPath) {
|
|
809
|
+
return `${runId}\u0000${contentPath}`;
|
|
810
|
+
}
|
|
811
|
+
function escapeArtifactSentinels(value) {
|
|
812
|
+
if (Array.isArray(value))
|
|
813
|
+
return value.map(escapeArtifactSentinels);
|
|
814
|
+
if (!isJsonObject(value))
|
|
815
|
+
return value;
|
|
816
|
+
const escaped = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, escapeArtifactSentinels(item)]));
|
|
817
|
+
return Object.keys(value).length === 1 &&
|
|
818
|
+
(Object.hasOwn(value, "$artifact") || Object.hasOwn(value, "$escaped"))
|
|
819
|
+
? { $escaped: escaped }
|
|
820
|
+
: escaped;
|
|
821
|
+
}
|
|
822
|
+
function projectFollowUpQueue(value, items) {
|
|
823
|
+
const queue = toJson(value);
|
|
824
|
+
if (!isJsonObject(queue))
|
|
825
|
+
return queue;
|
|
826
|
+
delete queue.followUps;
|
|
827
|
+
queue.items = items;
|
|
828
|
+
return queue;
|
|
829
|
+
}
|
|
830
|
+
function refreshCacheEntry(cache, key, value) {
|
|
831
|
+
cache.delete(key);
|
|
832
|
+
cache.set(key, value);
|
|
833
|
+
}
|
|
834
|
+
function rememberCacheEntry(cache, key, value) {
|
|
835
|
+
refreshCacheEntry(cache, key, value);
|
|
836
|
+
while (cache.size > VIEW_CACHE_ITEMS) {
|
|
837
|
+
const oldest = cache.keys().next().value;
|
|
838
|
+
if (oldest === undefined)
|
|
839
|
+
break;
|
|
840
|
+
cache.delete(oldest);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function viewRange(total, cursor) {
|
|
844
|
+
const start = workflowPageStart(total, cursor);
|
|
845
|
+
return { start, limit: Math.min(VIEW_PAGE_ITEMS, Math.max(0, total - start)) };
|
|
846
|
+
}
|
|
847
|
+
function byteBoundedForwardPage(values, project) {
|
|
848
|
+
const items = [];
|
|
849
|
+
let bytes = 0;
|
|
850
|
+
for (const value of values) {
|
|
851
|
+
if (items.length >= VIEW_PAGE_ITEMS)
|
|
852
|
+
break;
|
|
853
|
+
const item = project(value);
|
|
854
|
+
const itemBytes = Buffer.byteLength(canonicalJson(item)) + (items.length === 0 ? 0 : 1);
|
|
855
|
+
if (items.length > 0 && bytes + itemBytes > VIEW_PAGE_BYTES)
|
|
856
|
+
break;
|
|
857
|
+
items.push(item);
|
|
858
|
+
bytes += itemBytes;
|
|
859
|
+
}
|
|
860
|
+
return items;
|
|
861
|
+
}
|
|
862
|
+
function byteBoundedCandidatePage(values, candidateStart, total, requestedCursor, project) {
|
|
863
|
+
if (values.length === 0)
|
|
864
|
+
return { start: candidateStart, total, items: [] };
|
|
865
|
+
const globalCursor = clampCursor(requestedCursor ?? Math.max(0, total - 1), total);
|
|
866
|
+
const localCursor = Math.min(Math.max(0, globalCursor - candidateStart), values.length - 1);
|
|
867
|
+
const page = byteBoundedPage(values, localCursor, project);
|
|
868
|
+
return { start: candidateStart + page.start, total, items: page.items };
|
|
869
|
+
}
|
|
870
|
+
function byteBoundedPage(values, requestedCursor, project) {
|
|
871
|
+
const total = values.length;
|
|
872
|
+
if (total === 0)
|
|
873
|
+
return { start: 0, total: 0, items: [] };
|
|
874
|
+
const cursor = clampCursor(requestedCursor ?? total - 1, total);
|
|
875
|
+
const selected = project(values[cursor]);
|
|
876
|
+
const selectedBytes = Buffer.byteLength(canonicalJson(selected));
|
|
877
|
+
const indexed = new Map([[cursor, selected]]);
|
|
878
|
+
let pageBytes = selectedBytes;
|
|
879
|
+
let left = cursor - 1;
|
|
880
|
+
let right = cursor + 1;
|
|
881
|
+
let leftBlocked = false;
|
|
882
|
+
let rightBlocked = false;
|
|
883
|
+
let preferLeft = true;
|
|
884
|
+
while (indexed.size < VIEW_PAGE_ITEMS && (!leftBlocked || !rightBlocked)) {
|
|
885
|
+
const index = preferLeft ? left : right;
|
|
886
|
+
const inRange = index >= 0 && index < total;
|
|
887
|
+
if (!inRange) {
|
|
888
|
+
if (preferLeft)
|
|
889
|
+
leftBlocked = true;
|
|
890
|
+
else
|
|
891
|
+
rightBlocked = true;
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
const item = project(values[index]);
|
|
895
|
+
const itemBytes = Buffer.byteLength(canonicalJson(item)) + 1;
|
|
896
|
+
if (pageBytes + itemBytes > VIEW_PAGE_BYTES) {
|
|
897
|
+
if (preferLeft)
|
|
898
|
+
leftBlocked = true;
|
|
899
|
+
else
|
|
900
|
+
rightBlocked = true;
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
indexed.set(index, item);
|
|
904
|
+
pageBytes += itemBytes;
|
|
905
|
+
if (preferLeft)
|
|
906
|
+
left -= 1;
|
|
907
|
+
else
|
|
908
|
+
right += 1;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
preferLeft = !preferLeft;
|
|
912
|
+
}
|
|
913
|
+
const ordered = [...indexed.entries()].sort(([leftIndex], [rightIndex]) => leftIndex - rightIndex);
|
|
914
|
+
return {
|
|
915
|
+
start: ordered[0]?.[0] ?? cursor,
|
|
916
|
+
total,
|
|
917
|
+
items: ordered.map(([, item]) => item),
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
export function toCompactStepJson(step) {
|
|
921
|
+
return toJson({
|
|
922
|
+
attemptId: step.attemptId,
|
|
923
|
+
nodeId: step.nodeId,
|
|
924
|
+
nodeType: step.nodeType,
|
|
925
|
+
outcome: step.outcome,
|
|
926
|
+
startedAt: step.startedAt,
|
|
927
|
+
finishedAt: step.finishedAt,
|
|
928
|
+
prompt: null,
|
|
929
|
+
output: null,
|
|
930
|
+
...(step.settingsScopeId === undefined ? {} : { settingsScopeId: step.settingsScopeId }),
|
|
931
|
+
...(step.settingsChangeNumber === undefined
|
|
932
|
+
? {}
|
|
933
|
+
: { settingsChangeNumber: step.settingsChangeNumber }),
|
|
934
|
+
...(step.settingsHash === undefined ? {} : { settingsHash: step.settingsHash }),
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
export function workflowPageStart(total, cursor) {
|
|
938
|
+
if (total <= 256)
|
|
939
|
+
return 0;
|
|
940
|
+
if (cursor === undefined)
|
|
941
|
+
return total - 256;
|
|
942
|
+
const center = clampCursor(cursor, total);
|
|
943
|
+
return Math.min(Math.max(0, center - 128), total - 256);
|
|
944
|
+
}
|
|
945
|
+
function clampCursor(cursor, total) {
|
|
946
|
+
return total === 0 ? 0 : Math.min(cursor, total - 1);
|
|
947
|
+
}
|
|
948
|
+
function toJson(value) {
|
|
949
|
+
return parseJson(canonicalJson(value));
|
|
950
|
+
}
|
|
951
|
+
function isJsonObject(value) {
|
|
952
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
953
|
+
}
|
|
954
|
+
function isRunVersionRow(value) {
|
|
955
|
+
return (typeof value === "object" &&
|
|
956
|
+
value !== null &&
|
|
957
|
+
typeof value.revision === "number" &&
|
|
958
|
+
typeof value.updatedAt === "number" &&
|
|
959
|
+
typeof value.presentationRevision === "number" &&
|
|
960
|
+
typeof value.runStatus === "string" &&
|
|
961
|
+
typeof value.paused === "number");
|
|
962
|
+
}
|
|
963
|
+
function isSessionRevisionRow(value) {
|
|
964
|
+
return (typeof value === "object" &&
|
|
965
|
+
value !== null &&
|
|
966
|
+
typeof value.count === "number" &&
|
|
967
|
+
typeof value.revisionSum === "number" &&
|
|
968
|
+
typeof value.updatedAt === "number");
|
|
969
|
+
}
|
|
970
|
+
function isRevisionRow(value) {
|
|
971
|
+
return (typeof value === "object" &&
|
|
972
|
+
value !== null &&
|
|
973
|
+
typeof value.revision === "number");
|
|
974
|
+
}
|
|
975
|
+
//# sourceMappingURL=view.js.map
|