@osolmaz/pi-workflows 0.16.0 → 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 +18 -2
- 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/client.d.ts +1 -0
- package/dist/client/client.js +5 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +11 -5
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +42 -16
- package/dist/client/view.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +12 -165
- package/dist/controllers/sqlite.js +124 -435
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -2
- package/dist/extension/index.js +619 -535
- 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 +2 -0
- package/dist/extension/session-view.js +60 -2
- 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 +2 -2
- package/dist/extension/widget.js +28 -18
- 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/runner.d.ts +43 -8
- package/dist/host/runner.js +1567 -331
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +19 -16
- package/dist/host/state.js +251 -63
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +19 -9
- package/dist/host/view.js +222 -118
- package/dist/host/view.js.map +1 -1
- package/dist/state/prune.js +3 -25
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +76 -97
- 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/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +1 -1
- package/dist/viewer/tui.js +65 -8
- 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 +5 -19
- package/dist/workflows/store.js +121 -354
- 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 +5 -1
- package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
- 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 +19 -15
- package/docs/WORKFLOW_HOST.md +93 -28
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/live-replay-protocol.md +8 -4
- package/docs/tui-viewer.md +3 -1
- package/docs/workflows.md +34 -27
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +14 -5
- 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/client.ts +6 -2
- package/src/client/index.ts +2 -1
- package/src/client/protocol.ts +11 -5
- package/src/client/view.ts +43 -18
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +164 -844
- package/src/extension/index.ts +693 -646
- 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 +86 -3
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +37 -17
- 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/runner.ts +2120 -531
- package/src/host/state.ts +333 -94
- package/src/host/view.ts +249 -121
- package/src/state/prune.ts +3 -31
- package/src/state/schema.ts +76 -97
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +71 -6
- 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 +173 -489
- 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/src/extension/decision-channels.ts +0 -1826
|
@@ -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
|
+
}
|