@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
package/src/extension/widget.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { WorkflowDisplayStatus } from "../client/view.js";
|
|
3
4
|
import { formatDuration } from "../render/format.js";
|
|
4
5
|
import { nodeTypeGlyph } from "../render/node-type.js";
|
|
5
6
|
import {
|
|
@@ -14,17 +15,19 @@ import { sanitizeText } from "../workflows/text.js";
|
|
|
14
15
|
import type {
|
|
15
16
|
WorkflowDefinitionSnapshot,
|
|
16
17
|
WorkflowRunState,
|
|
17
|
-
WorkflowRunStatus,
|
|
18
18
|
WorkflowUpdateRecord,
|
|
19
19
|
} from "../workflows/types.js";
|
|
20
20
|
|
|
21
|
-
const STATUS_GLYPHS: Record<
|
|
21
|
+
const STATUS_GLYPHS: Record<WorkflowDisplayStatus, string> = {
|
|
22
|
+
queued: "…",
|
|
22
23
|
running: "◐",
|
|
23
|
-
waiting: "
|
|
24
|
+
waiting: "○",
|
|
25
|
+
paused: "⏸",
|
|
24
26
|
completed: "✓",
|
|
25
27
|
failed: "✗",
|
|
26
28
|
timed_out: "✗",
|
|
27
29
|
cancelled: "✗",
|
|
30
|
+
ambiguous: "!",
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -35,17 +38,28 @@ const STATUS_GLYPHS: Record<WorkflowRunStatus, string> = {
|
|
|
35
38
|
const PI_MAX_WIDGET_LINES = 10;
|
|
36
39
|
const MAX_NODE_ERROR_CHARS = 120;
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
function displayedRunningNode(
|
|
42
|
+
state: WorkflowRunState,
|
|
43
|
+
displayStatus: WorkflowDisplayStatus | undefined,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
return state.currentNode ?? (displayStatus === "running" ? state.waitingOn : undefined);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function nodeGlyph(
|
|
49
|
+
state: WorkflowRunState,
|
|
50
|
+
nodeId: string,
|
|
51
|
+
displayStatus?: WorkflowDisplayStatus,
|
|
52
|
+
): string {
|
|
53
|
+
if (displayedRunningNode(state, displayStatus) === nodeId) {
|
|
40
54
|
return "◐";
|
|
41
55
|
}
|
|
56
|
+
if (state.waitingOn === nodeId) {
|
|
57
|
+
return "⏸";
|
|
58
|
+
}
|
|
42
59
|
const result = state.results[nodeId];
|
|
43
60
|
if (!result) {
|
|
44
61
|
return "·";
|
|
45
62
|
}
|
|
46
|
-
if (state.waitingOn === nodeId) {
|
|
47
|
-
return "⏸";
|
|
48
|
-
}
|
|
49
63
|
return result.outcome === "ok" ? "✓" : "✗";
|
|
50
64
|
}
|
|
51
65
|
|
|
@@ -79,35 +93,43 @@ export function buildWidgetView(
|
|
|
79
93
|
theme?: WidgetTheme,
|
|
80
94
|
updateHistory?: WorkflowUpdateRecord[],
|
|
81
95
|
actionHint?: string,
|
|
96
|
+
displayStatus?: WorkflowDisplayStatus,
|
|
97
|
+
displayReason?: string | null,
|
|
98
|
+
controls?: readonly string[],
|
|
82
99
|
): WidgetView {
|
|
83
100
|
const availableWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : width;
|
|
84
101
|
if (availableWidth === 0) return { lines: [], scroll: 0, maxScroll: 0 };
|
|
85
102
|
|
|
86
103
|
// `held` covers pauses the state cannot see yet: an escape-interrupted
|
|
87
104
|
// step or a pause requested while the current node is still finishing.
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
105
|
+
const status = displayStatus ?? (held || state.paused === true ? "paused" : state.status);
|
|
106
|
+
const paused = status === "paused";
|
|
107
|
+
const glyph = STATUS_GLYPHS[status];
|
|
108
|
+
const statusText = status;
|
|
91
109
|
// Titles, status details, and errors can carry model- or shell-controlled
|
|
92
110
|
// text; never let escape sequences or newlines reach the terminal.
|
|
93
111
|
const title = state.runTitle ? ` — ${sanitizeText(state.runTitle)}` : "";
|
|
94
|
-
const headerTone = statusTone(
|
|
112
|
+
const headerTone = statusTone(status);
|
|
95
113
|
const header = `${paint(theme, headerTone, glyph)} workflow ${sanitizeText(state.workflowName)}${title} ${paint(theme, headerTone, `[${statusText}]`)}`;
|
|
96
114
|
|
|
97
115
|
const footer: string[] = [];
|
|
116
|
+
if (displayReason) {
|
|
117
|
+
footer.push(paint(theme, "dim", ` ${sanitizeText(displayReason)}`));
|
|
118
|
+
}
|
|
119
|
+
if (controls !== undefined && controls.length > 0) {
|
|
120
|
+
footer.push(paint(theme, "dim", ` controls: ${controls.join(", ")}`));
|
|
121
|
+
}
|
|
98
122
|
if (state.error) {
|
|
99
123
|
footer.push(paint(theme, "error", ` error: ${truncate(sanitizeText(state.error), 120)}`));
|
|
100
124
|
}
|
|
101
|
-
if (
|
|
102
|
-
footer.push(
|
|
103
|
-
paint(theme, "warning", ` waiting on checkpoint: ${sanitizeText(state.waitingOn)}`),
|
|
104
|
-
);
|
|
125
|
+
if (status === "waiting" && state.waitingOn) {
|
|
126
|
+
footer.push(paint(theme, "warning", ` waiting on step: ${sanitizeText(state.waitingOn)}`));
|
|
105
127
|
}
|
|
106
128
|
const hint = actionHint?.trim() ? sanitizeText(actionHint) : undefined;
|
|
107
129
|
const progress = progressLines(state, now, updateHistory).slice(0, 4);
|
|
108
130
|
const baseBudget = PI_MAX_WIDGET_LINES - 1 - footer.length - progress.length;
|
|
109
131
|
const nodes = displayNodeIds(snapshot).map((nodeId) =>
|
|
110
|
-
compactNodeLine(state, snapshot, nodeId, now, paused, theme),
|
|
132
|
+
compactNodeLine(state, snapshot, nodeId, now, paused, status, theme),
|
|
111
133
|
);
|
|
112
134
|
const combineHintWithWindow =
|
|
113
135
|
hint !== undefined && nodes.length > 0 && nodes.length + 1 > baseBudget;
|
|
@@ -255,15 +277,17 @@ function compactNodeLine(
|
|
|
255
277
|
nodeId: string,
|
|
256
278
|
now: Date,
|
|
257
279
|
paused: boolean,
|
|
280
|
+
displayStatus: WorkflowDisplayStatus,
|
|
258
281
|
theme?: WidgetTheme,
|
|
259
282
|
): string {
|
|
260
283
|
const node = snapshot.nodes[nodeId];
|
|
261
|
-
const
|
|
284
|
+
const runningNode = displayedRunningNode(state, displayStatus);
|
|
285
|
+
const isCurrent = runningNode === nodeId;
|
|
286
|
+
const isWaiting = state.waitingOn === nodeId && !isCurrent;
|
|
287
|
+
const glyph = nodeGlyph(state, nodeId, displayStatus);
|
|
262
288
|
const typeGlyph = node ? nodeTypeGlyph(node.nodeType, node.actionExecution) : "?";
|
|
263
289
|
const name = sanitizeText(nodeId);
|
|
264
|
-
const segments = nodeRuntimeSegments(state, snapshot, nodeId, now);
|
|
265
|
-
const isCurrent = state.currentNode === nodeId;
|
|
266
|
-
const isWaiting = state.waitingOn === nodeId;
|
|
290
|
+
const segments = nodeRuntimeSegments(state, snapshot, nodeId, now, isCurrent);
|
|
267
291
|
|
|
268
292
|
if (isCurrent || isWaiting) {
|
|
269
293
|
const tone = paused || isWaiting ? "warning" : "accent";
|
|
@@ -296,6 +320,7 @@ function nodeRuntimeSegments(
|
|
|
296
320
|
snapshot: WorkflowDefinitionSnapshot,
|
|
297
321
|
nodeId: string,
|
|
298
322
|
now: Date,
|
|
323
|
+
isCurrent: boolean,
|
|
299
324
|
): string[] {
|
|
300
325
|
const segments: string[] = [];
|
|
301
326
|
const node = snapshot.nodes[nodeId];
|
|
@@ -307,11 +332,11 @@ function nodeRuntimeSegments(
|
|
|
307
332
|
segments.push("assistant response");
|
|
308
333
|
}
|
|
309
334
|
const completedAttempts = state.steps.filter((step) => step.nodeId === nodeId).length;
|
|
310
|
-
const attempts = completedAttempts + (
|
|
335
|
+
const attempts = completedAttempts + (isCurrent ? 1 : 0);
|
|
311
336
|
if (attempts > 1) {
|
|
312
337
|
segments.push(`↻${attempts}`);
|
|
313
338
|
}
|
|
314
|
-
if (
|
|
339
|
+
if (isCurrent && state.currentSettingsChangeNumber !== undefined) {
|
|
315
340
|
segments.push(`settings ${state.currentSettingsChangeNumber}`);
|
|
316
341
|
} else {
|
|
317
342
|
const latest = state.steps.findLast((step) => step.nodeId === nodeId);
|
|
@@ -321,8 +346,8 @@ function nodeRuntimeSegments(
|
|
|
321
346
|
}
|
|
322
347
|
|
|
323
348
|
const result = state.results[nodeId];
|
|
324
|
-
if (
|
|
325
|
-
if (state.statusDetail) {
|
|
349
|
+
if (isCurrent) {
|
|
350
|
+
if (state.currentNode === nodeId && state.statusDetail) {
|
|
326
351
|
segments.push(sanitizeText(state.statusDetail));
|
|
327
352
|
}
|
|
328
353
|
const elapsed = elapsedSince(state.currentNodeStartedAt, now);
|
|
@@ -407,7 +432,10 @@ function statusTone(status: string): ThemeColor {
|
|
|
407
432
|
case "cancelled":
|
|
408
433
|
return "error";
|
|
409
434
|
case "waiting":
|
|
435
|
+
case "paused":
|
|
410
436
|
return "warning";
|
|
437
|
+
case "ambiguous":
|
|
438
|
+
return "error";
|
|
411
439
|
case "running":
|
|
412
440
|
default:
|
|
413
441
|
return "accent";
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { WorkflowClient } from "../client/client.js";
|
|
4
|
+
import type { WorkflowSessionView, WorkflowTurnReport } from "../client/view.js";
|
|
5
|
+
import type { JsonValue } from "../state/json.js";
|
|
6
|
+
import type { WorkflowMessage, WorkflowTurnStopReason } from "../state/workflow-messages.js";
|
|
7
|
+
|
|
8
|
+
const WORKFLOW_MESSAGE_ID_FIELD = "workflowMessageId";
|
|
9
|
+
|
|
10
|
+
type PendingTurn = {
|
|
11
|
+
workflowTurnId: string;
|
|
12
|
+
workflowMessageId: string | null;
|
|
13
|
+
runId: string | null;
|
|
14
|
+
startedReported: boolean;
|
|
15
|
+
end: { stopReason: WorkflowTurnStopReason; responseSessionEntryId: string | null } | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Adds every host-owned workflow message to one Pi session through one public API path. */
|
|
19
|
+
export class WorkflowMessageCoordinator {
|
|
20
|
+
private readonly queued = new Set<string>();
|
|
21
|
+
private readonly closedTurnMessages = new Set<string>();
|
|
22
|
+
private synchronizing = false;
|
|
23
|
+
private lastBranchEpoch: string | null = null;
|
|
24
|
+
private view: WorkflowSessionView | null = null;
|
|
25
|
+
private turn: PendingTurn | null = null;
|
|
26
|
+
|
|
27
|
+
updateView(view: WorkflowSessionView): void {
|
|
28
|
+
this.view = view;
|
|
29
|
+
const visibleIds = new Set(view.workflowMessages.map((message) => message.workflowMessageId));
|
|
30
|
+
for (const messageId of this.closedTurnMessages) {
|
|
31
|
+
if (!visibleIds.has(messageId)) this.closedTurnMessages.delete(messageId);
|
|
32
|
+
}
|
|
33
|
+
for (const message of view.workflowMessages) {
|
|
34
|
+
if (message.status === "sent" || message.status === "cancelled") {
|
|
35
|
+
this.queued.delete(message.workflowMessageId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
this.turn !== null &&
|
|
40
|
+
this.turn.workflowMessageId === null &&
|
|
41
|
+
view.openWorkflowMessageId !== null
|
|
42
|
+
) {
|
|
43
|
+
const message = messageById(view, view.openWorkflowMessageId);
|
|
44
|
+
if (message !== undefined && messageStartsTurn(message)) this.bindTurn(message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
branchChanged(): void {
|
|
49
|
+
this.lastBranchEpoch = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
startTurn(): void {
|
|
53
|
+
if (this.turn !== null && this.turn.end === null) return;
|
|
54
|
+
this.turn = {
|
|
55
|
+
workflowTurnId: `workflow-turn-${randomUUID()}`,
|
|
56
|
+
workflowMessageId: null,
|
|
57
|
+
runId: null,
|
|
58
|
+
startedReported: false,
|
|
59
|
+
end: null,
|
|
60
|
+
};
|
|
61
|
+
const candidate = this.turnCandidate();
|
|
62
|
+
if (candidate !== undefined) this.bindTurn(candidate);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
endTurn(stopReason: WorkflowTurnStopReason, responseSessionEntryId: string | null): void {
|
|
66
|
+
if (this.turn === null) this.startTurn();
|
|
67
|
+
if (this.turn !== null) this.turn.end = { stopReason, responseSessionEntryId };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
activeTurnMessage(): WorkflowMessage | undefined {
|
|
71
|
+
if (this.view === null || this.turn?.workflowMessageId === null || this.turn === null) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return messageById(this.view, this.turn.workflowMessageId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async synchronize(
|
|
78
|
+
pi: ExtensionAPI,
|
|
79
|
+
client: WorkflowClient,
|
|
80
|
+
ctx: Pick<ExtensionContext, "hasPendingMessages" | "isIdle" | "sessionManager">,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (this.synchronizing || this.view === null) return;
|
|
83
|
+
this.synchronizing = true;
|
|
84
|
+
try {
|
|
85
|
+
const view = this.view;
|
|
86
|
+
if (!view.coordinatorActive || view.coordinatorEpoch === null) return;
|
|
87
|
+
if (this.turn === null && !ctx.isIdle() && view.openWorkflowTurn !== null) {
|
|
88
|
+
this.turn = {
|
|
89
|
+
workflowTurnId: view.openWorkflowTurn.workflowTurnId,
|
|
90
|
+
workflowMessageId: view.openWorkflowTurn.workflowMessageId,
|
|
91
|
+
runId: view.openWorkflowTurn.runId,
|
|
92
|
+
startedReported: false,
|
|
93
|
+
end: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
view.branchReportRequired ||
|
|
98
|
+
this.lastBranchEpoch !== view.coordinatorEpoch ||
|
|
99
|
+
this.hasUnconfirmedBranchEntry(ctx, view)
|
|
100
|
+
) {
|
|
101
|
+
await this.reportBranch(client, ctx, view);
|
|
102
|
+
}
|
|
103
|
+
await this.flushTurn(client, view);
|
|
104
|
+
const messageId = view.nextWorkflowMessageId;
|
|
105
|
+
if (messageId === null || this.queued.has(messageId)) return;
|
|
106
|
+
const message = messageById(view, messageId);
|
|
107
|
+
if (message === undefined || message.status !== "pending") return;
|
|
108
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
109
|
+
|
|
110
|
+
this.queued.add(messageId);
|
|
111
|
+
const existingEntry = branchWorkflowEntries(ctx.sessionManager.getBranch()).get(messageId);
|
|
112
|
+
if (existingEntry !== undefined) {
|
|
113
|
+
await this.reportBranch(client, ctx, view);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// There must be no asynchronous boundary between these final checks and
|
|
118
|
+
// the documented Pi sendMessage call.
|
|
119
|
+
if (
|
|
120
|
+
this.view !== view ||
|
|
121
|
+
!view.coordinatorActive ||
|
|
122
|
+
view.coordinatorEpoch === null ||
|
|
123
|
+
view.nextWorkflowMessageId !== messageId ||
|
|
124
|
+
!ctx.isIdle() ||
|
|
125
|
+
ctx.hasPendingMessages() ||
|
|
126
|
+
branchWorkflowEntries(ctx.sessionManager.getBranch()).has(messageId)
|
|
127
|
+
) {
|
|
128
|
+
this.queued.delete(messageId);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
pi.sendMessage(
|
|
133
|
+
{
|
|
134
|
+
customType: message.content.customType,
|
|
135
|
+
content: message.content.content,
|
|
136
|
+
display: message.content.display,
|
|
137
|
+
details: message.content.details,
|
|
138
|
+
},
|
|
139
|
+
{ triggerTurn: message.content.triggerTurn },
|
|
140
|
+
);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.queued.delete(messageId);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
await this.reportBranch(client, ctx, view);
|
|
146
|
+
await this.flushTurn(client, view);
|
|
147
|
+
} finally {
|
|
148
|
+
this.synchronizing = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
clear(): void {
|
|
153
|
+
this.queued.clear();
|
|
154
|
+
this.closedTurnMessages.clear();
|
|
155
|
+
this.view = null;
|
|
156
|
+
this.turn = null;
|
|
157
|
+
this.lastBranchEpoch = null;
|
|
158
|
+
this.synchronizing = false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private turnCandidate(): WorkflowMessage | undefined {
|
|
162
|
+
const view = this.view;
|
|
163
|
+
if (view === null) return undefined;
|
|
164
|
+
if (view.openWorkflowMessageId !== null) {
|
|
165
|
+
const open = messageById(view, view.openWorkflowMessageId);
|
|
166
|
+
if (
|
|
167
|
+
open !== undefined &&
|
|
168
|
+
!this.closedTurnMessages.has(open.workflowMessageId) &&
|
|
169
|
+
messageStartsTurn(open)
|
|
170
|
+
) {
|
|
171
|
+
return open;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const messageId of this.queued) {
|
|
175
|
+
const queued = messageById(view, messageId);
|
|
176
|
+
if (queued?.status === "pending" && messageStartsTurn(queued)) return queued;
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private bindTurn(message: WorkflowMessage): void {
|
|
182
|
+
if (this.turn === null) return;
|
|
183
|
+
this.turn.workflowMessageId = message.workflowMessageId;
|
|
184
|
+
this.turn.runId = message.runId;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async flushTurn(client: WorkflowClient, view: WorkflowSessionView): Promise<void> {
|
|
188
|
+
const pending = this.turn;
|
|
189
|
+
if (
|
|
190
|
+
pending === null ||
|
|
191
|
+
pending.workflowMessageId === null ||
|
|
192
|
+
pending.runId === null ||
|
|
193
|
+
view.coordinatorEpoch === null ||
|
|
194
|
+
this.lastBranchEpoch !== view.coordinatorEpoch
|
|
195
|
+
) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const message = messageById(view, pending.workflowMessageId);
|
|
199
|
+
if (message?.status !== "sent") return;
|
|
200
|
+
if (!pending.startedReported) {
|
|
201
|
+
await reportTurn(client, {
|
|
202
|
+
state: "started",
|
|
203
|
+
workflowMessageId: pending.workflowMessageId,
|
|
204
|
+
workflowTurnId: pending.workflowTurnId,
|
|
205
|
+
runId: pending.runId,
|
|
206
|
+
targetSessionId: view.sessionId,
|
|
207
|
+
coordinatorEpoch: view.coordinatorEpoch,
|
|
208
|
+
});
|
|
209
|
+
pending.startedReported = true;
|
|
210
|
+
}
|
|
211
|
+
if (pending.end === null) return;
|
|
212
|
+
await reportTurn(client, {
|
|
213
|
+
state: "ended",
|
|
214
|
+
workflowMessageId: pending.workflowMessageId,
|
|
215
|
+
workflowTurnId: pending.workflowTurnId,
|
|
216
|
+
runId: pending.runId,
|
|
217
|
+
targetSessionId: view.sessionId,
|
|
218
|
+
coordinatorEpoch: view.coordinatorEpoch,
|
|
219
|
+
stopReason: pending.end.stopReason,
|
|
220
|
+
responseSessionEntryId: pending.end.responseSessionEntryId,
|
|
221
|
+
});
|
|
222
|
+
if (message.kind === "terminal" || message.kind === "followUp") {
|
|
223
|
+
this.closedTurnMessages.add(pending.workflowMessageId);
|
|
224
|
+
}
|
|
225
|
+
for (const current of new Set([view, this.view])) {
|
|
226
|
+
if (current?.openWorkflowTurn?.workflowTurnId === pending.workflowTurnId) {
|
|
227
|
+
current.openWorkflowTurn = null;
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
current?.openWorkflowMessageId === pending.workflowMessageId &&
|
|
231
|
+
(message.kind === "terminal" || message.kind === "followUp")
|
|
232
|
+
) {
|
|
233
|
+
current.openWorkflowMessageId = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.turn = null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async reportBranch(
|
|
240
|
+
client: WorkflowClient,
|
|
241
|
+
ctx: Pick<ExtensionContext, "hasPendingMessages" | "isIdle" | "sessionManager">,
|
|
242
|
+
view: WorkflowSessionView,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
if (view.coordinatorEpoch === null) return;
|
|
245
|
+
const allowed = new Set(view.workflowMessages.map((message) => message.workflowMessageId));
|
|
246
|
+
const branch = branchWorkflowEntries(ctx.sessionManager.getBranch());
|
|
247
|
+
const entries = [...branch]
|
|
248
|
+
.filter(([workflowMessageId]) => allowed.has(workflowMessageId))
|
|
249
|
+
.map(([workflowMessageId, piSessionEntryId]) => ({ workflowMessageId, piSessionEntryId }));
|
|
250
|
+
const isIdle = ctx.isIdle();
|
|
251
|
+
const hasPendingMessages = ctx.hasPendingMessages();
|
|
252
|
+
const response = await client.request({
|
|
253
|
+
operation: "workflowMessage.reportBranch",
|
|
254
|
+
payload: {
|
|
255
|
+
targetSessionId: view.sessionId,
|
|
256
|
+
coordinatorEpoch: view.coordinatorEpoch,
|
|
257
|
+
entries,
|
|
258
|
+
isIdle,
|
|
259
|
+
hasPendingMessages,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
if (response.outcome !== "accepted" && response.outcome !== "adopted") {
|
|
263
|
+
throw new Error(response.error ?? "Workflow host rejected the Pi branch report");
|
|
264
|
+
}
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
const message = messageById(view, entry.workflowMessageId);
|
|
267
|
+
if (message !== undefined) {
|
|
268
|
+
message.status = "sent";
|
|
269
|
+
message.piSessionEntryId = entry.piSessionEntryId;
|
|
270
|
+
}
|
|
271
|
+
this.queued.delete(entry.workflowMessageId);
|
|
272
|
+
}
|
|
273
|
+
this.lastBranchEpoch = view.coordinatorEpoch;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private hasUnconfirmedBranchEntry(
|
|
277
|
+
ctx: Pick<ExtensionContext, "sessionManager">,
|
|
278
|
+
view: WorkflowSessionView,
|
|
279
|
+
): boolean {
|
|
280
|
+
const branch = branchWorkflowEntries(ctx.sessionManager.getBranch());
|
|
281
|
+
return view.workflowMessages.some(
|
|
282
|
+
(message) => message.status !== "sent" && branch.has(message.workflowMessageId),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function branchWorkflowEntries(entries: readonly unknown[]): Map<string, string> {
|
|
288
|
+
const found = new Map<string, string>();
|
|
289
|
+
for (const value of entries) {
|
|
290
|
+
if (!isRecord(value) || typeof value.id !== "string") continue;
|
|
291
|
+
if (value.type !== "custom_message" && value.role !== "custom") continue;
|
|
292
|
+
if (!isRecord(value.details)) continue;
|
|
293
|
+
const workflowMessageId = value.details[WORKFLOW_MESSAGE_ID_FIELD];
|
|
294
|
+
if (typeof workflowMessageId === "string") found.set(workflowMessageId, value.id);
|
|
295
|
+
}
|
|
296
|
+
return found;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function responseEntryId(entries: readonly unknown[]): string | null {
|
|
300
|
+
for (const value of [...entries].reverse()) {
|
|
301
|
+
if (!isRecord(value) || typeof value.id !== "string") continue;
|
|
302
|
+
if (value.type === "message" && isRecord(value.message) && value.message.role === "assistant") {
|
|
303
|
+
return value.id;
|
|
304
|
+
}
|
|
305
|
+
if (value.role === "assistant") return value.id;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function messageById(
|
|
311
|
+
view: WorkflowSessionView,
|
|
312
|
+
workflowMessageId: string,
|
|
313
|
+
): WorkflowMessage | undefined {
|
|
314
|
+
return view.workflowMessages.find((message) => message.workflowMessageId === workflowMessageId);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function messageStartsTurn(message: WorkflowMessage): boolean {
|
|
318
|
+
return message.kind === "step" || message.kind === "terminal" || message.kind === "followUp";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function reportTurn(client: WorkflowClient, report: WorkflowTurnReport): Promise<void> {
|
|
322
|
+
const response = await client.request({
|
|
323
|
+
operation: "workflowTurn.report",
|
|
324
|
+
runId: report.runId,
|
|
325
|
+
payload: report as unknown as JsonValue,
|
|
326
|
+
});
|
|
327
|
+
if (response.outcome !== "accepted" && response.outcome !== "adopted") {
|
|
328
|
+
throw new Error(response.error ?? "Workflow host rejected the model-turn report");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
333
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
334
|
+
}
|