@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.
Files changed (154) hide show
  1. package/README.md +18 -2
  2. package/dist/channels/adapter-entry.d.ts +3 -0
  3. package/dist/channels/adapter-entry.js +203 -0
  4. package/dist/channels/adapter-entry.js.map +1 -0
  5. package/dist/channels/config.d.ts +43 -0
  6. package/dist/channels/config.js +234 -0
  7. package/dist/channels/config.js.map +1 -0
  8. package/dist/channels/protocol.d.ts +94 -0
  9. package/dist/channels/protocol.js +219 -0
  10. package/dist/channels/protocol.js.map +1 -0
  11. package/dist/channels/telegram.d.ts +46 -0
  12. package/dist/channels/telegram.js +285 -0
  13. package/dist/channels/telegram.js.map +1 -0
  14. package/dist/client/client.d.ts +1 -0
  15. package/dist/client/client.js +5 -1
  16. package/dist/client/client.js.map +1 -1
  17. package/dist/client/index.d.ts +1 -1
  18. package/dist/client/protocol.d.ts +1 -1
  19. package/dist/client/protocol.js +11 -5
  20. package/dist/client/protocol.js.map +1 -1
  21. package/dist/client/view.d.ts +42 -16
  22. package/dist/client/view.js.map +1 -1
  23. package/dist/controllers/index.d.ts +1 -1
  24. package/dist/controllers/index.js.map +1 -1
  25. package/dist/controllers/sqlite.d.ts +12 -165
  26. package/dist/controllers/sqlite.js +124 -435
  27. package/dist/controllers/sqlite.js.map +1 -1
  28. package/dist/extension/index.d.ts +23 -2
  29. package/dist/extension/index.js +619 -535
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/recorder.d.ts +95 -0
  32. package/dist/extension/recorder.js +530 -0
  33. package/dist/extension/recorder.js.map +1 -0
  34. package/dist/extension/remote-recorder-store.d.ts +25 -0
  35. package/dist/extension/remote-recorder-store.js +81 -0
  36. package/dist/extension/remote-recorder-store.js.map +1 -0
  37. package/dist/extension/session-events.d.ts +134 -0
  38. package/dist/extension/session-events.js +60 -0
  39. package/dist/extension/session-events.js.map +1 -0
  40. package/dist/extension/session-view.d.ts +2 -0
  41. package/dist/extension/session-view.js +60 -2
  42. package/dist/extension/session-view.js.map +1 -1
  43. package/dist/extension/step-message.d.ts +6 -4
  44. package/dist/extension/step-message.js +12 -6
  45. package/dist/extension/step-message.js.map +1 -1
  46. package/dist/extension/widget.d.ts +2 -2
  47. package/dist/extension/widget.js +28 -18
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/extension/workflow-message-coordinator.d.ts +27 -0
  50. package/dist/extension/workflow-message-coordinator.js +294 -0
  51. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  52. package/dist/host/channel-effects.d.ts +72 -0
  53. package/dist/host/channel-effects.js +271 -0
  54. package/dist/host/channel-effects.js.map +1 -0
  55. package/dist/host/channel-supervisor.d.ts +21 -0
  56. package/dist/host/channel-supervisor.js +54 -0
  57. package/dist/host/channel-supervisor.js.map +1 -0
  58. package/dist/host/runner.d.ts +43 -8
  59. package/dist/host/runner.js +1567 -331
  60. package/dist/host/runner.js.map +1 -1
  61. package/dist/host/state.d.ts +19 -16
  62. package/dist/host/state.js +251 -63
  63. package/dist/host/state.js.map +1 -1
  64. package/dist/host/view.d.ts +19 -9
  65. package/dist/host/view.js +222 -118
  66. package/dist/host/view.js.map +1 -1
  67. package/dist/state/prune.js +3 -25
  68. package/dist/state/prune.js.map +1 -1
  69. package/dist/state/schema.js +76 -97
  70. package/dist/state/schema.js.map +1 -1
  71. package/dist/state/workflow-messages.d.ts +96 -0
  72. package/dist/state/workflow-messages.js +335 -0
  73. package/dist/state/workflow-messages.js.map +1 -0
  74. package/dist/viewer/render.js +1 -1
  75. package/dist/viewer/render.js.map +1 -1
  76. package/dist/viewer/tui.d.ts +1 -1
  77. package/dist/viewer/tui.js +65 -8
  78. package/dist/viewer/tui.js.map +1 -1
  79. package/dist/workflows/human-decision.d.ts +2 -0
  80. package/dist/workflows/human-decision.js +8 -2
  81. package/dist/workflows/human-decision.js.map +1 -1
  82. package/dist/workflows/index.d.ts +1 -1
  83. package/dist/workflows/index.js.map +1 -1
  84. package/dist/workflows/settings.d.ts +1 -8
  85. package/dist/workflows/settings.js.map +1 -1
  86. package/dist/workflows/store.d.ts +5 -19
  87. package/dist/workflows/store.js +121 -354
  88. package/dist/workflows/store.js.map +1 -1
  89. package/dist/workflows/tool-input.d.ts +21 -0
  90. package/dist/workflows/tool-input.js +23 -1
  91. package/dist/workflows/tool-input.js.map +1 -1
  92. package/dist/workflows/types.d.ts +5 -5
  93. package/dist/workflows/workflow-message-content.d.ts +38 -0
  94. package/dist/workflows/workflow-message-content.js +157 -0
  95. package/dist/workflows/workflow-message-content.js.map +1 -0
  96. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  97. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  98. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  99. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  100. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  101. package/docs/2026-09-01-unified-workflow-client-plan.md +5 -1
  102. package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
  103. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  104. package/docs/DEFERRED_TURNS.md +66 -294
  105. package/docs/HUMAN_DECISIONS.md +29 -30
  106. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  107. package/docs/SQLITE_STATE.md +19 -15
  108. package/docs/WORKFLOW_HOST.md +93 -28
  109. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  110. package/docs/live-replay-protocol.md +8 -4
  111. package/docs/tui-viewer.md +3 -1
  112. package/docs/workflows.md +34 -27
  113. package/herdr-plugin.toml +1 -1
  114. package/package.json +1 -1
  115. package/plugins/herdr/viewer.mjs +14 -5
  116. package/src/channels/adapter-entry.ts +220 -0
  117. package/src/channels/config.ts +296 -0
  118. package/src/channels/protocol.ts +333 -0
  119. package/src/channels/telegram.ts +335 -0
  120. package/src/client/client.ts +6 -2
  121. package/src/client/index.ts +2 -1
  122. package/src/client/protocol.ts +11 -5
  123. package/src/client/view.ts +43 -18
  124. package/src/controllers/index.ts +0 -1
  125. package/src/controllers/sqlite.ts +164 -844
  126. package/src/extension/index.ts +693 -646
  127. package/src/extension/recorder.ts +687 -0
  128. package/src/extension/remote-recorder-store.ts +126 -0
  129. package/src/extension/session-events.ts +119 -0
  130. package/src/extension/session-view.ts +86 -3
  131. package/src/extension/step-message.ts +19 -9
  132. package/src/extension/widget.ts +37 -17
  133. package/src/extension/workflow-message-coordinator.ts +334 -0
  134. package/src/host/channel-effects.ts +465 -0
  135. package/src/host/channel-supervisor.ts +73 -0
  136. package/src/host/runner.ts +2120 -531
  137. package/src/host/state.ts +333 -94
  138. package/src/host/view.ts +249 -121
  139. package/src/state/prune.ts +3 -31
  140. package/src/state/schema.ts +76 -97
  141. package/src/state/workflow-messages.ts +548 -0
  142. package/src/viewer/render.ts +1 -5
  143. package/src/viewer/tui.ts +71 -6
  144. package/src/workflows/human-decision.ts +18 -2
  145. package/src/workflows/index.ts +0 -1
  146. package/src/workflows/settings.ts +1 -20
  147. package/src/workflows/store.ts +173 -489
  148. package/src/workflows/tool-input.ts +36 -1
  149. package/src/workflows/types.ts +5 -5
  150. package/src/workflows/workflow-message-content.ts +197 -0
  151. package/dist/extension/decision-channels.d.ts +0 -134
  152. package/dist/extension/decision-channels.js +0 -1307
  153. package/dist/extension/decision-channels.js.map +0 -1
  154. 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
+ }