@osolmaz/pi-workflows 0.13.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +146 -160
  2. package/dist/builtins/autoimplement.workflow.js +3 -1
  3. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  4. package/dist/builtins/change-verification.workflow.js +7 -2
  5. package/dist/builtins/change-verification.workflow.js.map +1 -1
  6. package/dist/builtins/metadata.d.ts +22 -0
  7. package/dist/builtins/metadata.js +10 -0
  8. package/dist/builtins/metadata.js.map +1 -0
  9. package/dist/builtins/monitor.workflow.js +4 -1
  10. package/dist/builtins/monitor.workflow.js.map +1 -1
  11. package/dist/builtins/sanity-check.workflow.js +4 -1
  12. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  13. package/dist/builtins/workspace-preparation.workflow.js +3 -1
  14. package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
  15. package/dist/controllers/index.d.ts +1 -2
  16. package/dist/controllers/index.js +0 -1
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.d.ts +85 -31
  19. package/dist/controllers/sqlite.js +541 -126
  20. package/dist/controllers/sqlite.js.map +1 -1
  21. package/dist/controllers/store.d.ts +1 -0
  22. package/dist/extension/controller-command.d.ts +22 -0
  23. package/dist/extension/controller-command.js +32 -0
  24. package/dist/extension/controller-command.js.map +1 -0
  25. package/dist/extension/index.d.ts +2 -9
  26. package/dist/extension/index.js +876 -2928
  27. package/dist/extension/index.js.map +1 -1
  28. package/dist/extension/message-card.d.ts +13 -0
  29. package/dist/extension/message-card.js +46 -0
  30. package/dist/extension/message-card.js.map +1 -0
  31. package/dist/extension/step-message.d.ts +5 -6
  32. package/dist/extension/step-message.js +41 -52
  33. package/dist/extension/step-message.js.map +1 -1
  34. package/dist/host/child-worker-supervisor.d.ts +50 -0
  35. package/dist/host/child-worker-supervisor.js +159 -0
  36. package/dist/host/child-worker-supervisor.js.map +1 -0
  37. package/dist/host/client.d.ts +48 -0
  38. package/dist/host/client.js +216 -0
  39. package/dist/host/client.js.map +1 -0
  40. package/dist/host/controller-worker-entry.d.ts +2 -0
  41. package/dist/host/controller-worker-entry.js +244 -0
  42. package/dist/host/controller-worker-entry.js.map +1 -0
  43. package/dist/host/controller-worker-protocol.d.ts +36 -0
  44. package/dist/host/controller-worker-protocol.js +49 -0
  45. package/dist/host/controller-worker-protocol.js.map +1 -0
  46. package/dist/host/controller-worker-supervisor.d.ts +21 -0
  47. package/dist/host/controller-worker-supervisor.js +54 -0
  48. package/dist/host/controller-worker-supervisor.js.map +1 -0
  49. package/dist/host/host-entry.d.ts +2 -0
  50. package/dist/host/host-entry.js +23 -0
  51. package/dist/host/host-entry.js.map +1 -0
  52. package/dist/host/processes.d.ts +17 -12
  53. package/dist/host/processes.js +154 -50
  54. package/dist/host/processes.js.map +1 -1
  55. package/dist/host/protocol.d.ts +38 -0
  56. package/dist/host/protocol.js +156 -0
  57. package/dist/host/protocol.js.map +1 -0
  58. package/dist/host/resolver-entry.d.ts +49 -0
  59. package/dist/host/resolver-entry.js +149 -0
  60. package/dist/host/resolver-entry.js.map +1 -0
  61. package/dist/host/rpc-executor.d.ts +14 -3
  62. package/dist/host/rpc-executor.js +63 -30
  63. package/dist/host/rpc-executor.js.map +1 -1
  64. package/dist/host/runner.d.ts +95 -29
  65. package/dist/host/runner.js +2311 -348
  66. package/dist/host/runner.js.map +1 -1
  67. package/dist/host/state.d.ts +174 -0
  68. package/dist/host/state.js +689 -0
  69. package/dist/host/state.js.map +1 -0
  70. package/dist/host/worker-entry.d.ts +10 -0
  71. package/dist/host/worker-entry.js +446 -0
  72. package/dist/host/worker-entry.js.map +1 -0
  73. package/dist/host/worker-protocol.d.ts +31 -0
  74. package/dist/host/worker-protocol.js +122 -0
  75. package/dist/host/worker-protocol.js.map +1 -0
  76. package/dist/host/worker-store.d.ts +80 -0
  77. package/dist/host/worker-store.js +91 -0
  78. package/dist/host/worker-store.js.map +1 -0
  79. package/dist/host/worker-supervisor.d.ts +22 -0
  80. package/dist/host/worker-supervisor.js +54 -0
  81. package/dist/host/worker-supervisor.js.map +1 -0
  82. package/dist/render/canvas.d.ts +2 -1
  83. package/dist/render/canvas.js +40 -15
  84. package/dist/render/canvas.js.map +1 -1
  85. package/dist/render/graph-render.d.ts +7 -3
  86. package/dist/render/graph-render.js +125 -74
  87. package/dist/render/graph-render.js.map +1 -1
  88. package/dist/state/database.js +3 -1
  89. package/dist/state/database.js.map +1 -1
  90. package/dist/state/index.d.ts +1 -0
  91. package/dist/state/index.js +1 -0
  92. package/dist/state/index.js.map +1 -1
  93. package/dist/state/prune.js +36 -10
  94. package/dist/state/prune.js.map +1 -1
  95. package/dist/state/schema.d.ts +1 -1
  96. package/dist/state/schema.js +171 -3
  97. package/dist/state/schema.js.map +1 -1
  98. package/dist/state/viewer.d.ts +46 -0
  99. package/dist/state/viewer.js +249 -0
  100. package/dist/state/viewer.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +1 -1
  102. package/dist/viewer/cli.js +47 -18
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/workflows/composition.js +25 -2
  105. package/dist/workflows/composition.js.map +1 -1
  106. package/dist/workflows/definition.d.ts +3 -1
  107. package/dist/workflows/definition.js +25 -0
  108. package/dist/workflows/definition.js.map +1 -1
  109. package/dist/workflows/engine.d.ts +1 -0
  110. package/dist/workflows/engine.js +157 -42
  111. package/dist/workflows/engine.js.map +1 -1
  112. package/dist/workflows/errors.d.ts +3 -1
  113. package/dist/workflows/errors.js +4 -7
  114. package/dist/workflows/errors.js.map +1 -1
  115. package/dist/workflows/human-decision.d.ts +3 -0
  116. package/dist/workflows/human-decision.js +31 -0
  117. package/dist/workflows/human-decision.js.map +1 -1
  118. package/dist/workflows/index.d.ts +1 -1
  119. package/dist/workflows/index.js +1 -1
  120. package/dist/workflows/index.js.map +1 -1
  121. package/dist/workflows/schema.js +17 -6
  122. package/dist/workflows/schema.js.map +1 -1
  123. package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
  124. package/dist/{viewer → workflows}/session-reducer.js +4 -0
  125. package/dist/workflows/session-reducer.js.map +1 -0
  126. package/dist/workflows/store.d.ts +87 -1
  127. package/dist/workflows/store.js +892 -93
  128. package/dist/workflows/store.js.map +1 -1
  129. package/dist/workflows/tool-input.d.ts +0 -22
  130. package/dist/workflows/tool-input.js +1 -44
  131. package/dist/workflows/tool-input.js.map +1 -1
  132. package/dist/workflows/types.d.ts +36 -4
  133. package/docs/2026-08-25-workflow-follow-ups.md +8 -6
  134. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
  135. package/docs/CONTROLLERS.md +11 -11
  136. package/docs/DEFERRED_TURNS.md +61 -27
  137. package/docs/HUMAN_DECISIONS.md +12 -4
  138. package/docs/SQLITE_STATE.md +42 -8
  139. package/docs/WORKFLOW_HOST.md +452 -0
  140. package/docs/development.md +46 -30
  141. package/docs/live-replay-protocol.md +129 -100
  142. package/docs/plans/2026-08-19-human-decision-gates-plan.md +34 -8
  143. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +399 -0
  144. package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
  145. package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
  146. package/docs/plans/tui-viewer-implementation-plan.md +5 -0
  147. package/docs/tui-viewer.md +18 -3
  148. package/docs/workflows.md +164 -156
  149. package/examples/workflows/command-batch.workflow.ts +2 -0
  150. package/examples/workflows/shell.workflow.ts +2 -1
  151. package/herdr-plugin.toml +1 -1
  152. package/package.json +1 -1
  153. package/skills/autodoc/SKILL.md +1 -1
  154. package/skills/autoimplement/SKILL.md +1 -1
  155. package/skills/autoplan/SKILL.md +1 -1
  156. package/skills/pi-workflows/SKILL.md +2 -0
  157. package/src/builtins/autoimplement.workflow.ts +3 -0
  158. package/src/builtins/change-verification.workflow.ts +7 -2
  159. package/src/builtins/metadata.ts +9 -0
  160. package/src/builtins/monitor.workflow.ts +4 -0
  161. package/src/builtins/sanity-check.workflow.ts +4 -0
  162. package/src/builtins/workspace-preparation.workflow.ts +3 -1
  163. package/src/controllers/index.ts +3 -5
  164. package/src/controllers/sqlite.ts +929 -225
  165. package/src/controllers/store.ts +1 -0
  166. package/src/extension/controller-command.ts +45 -0
  167. package/src/extension/index.ts +948 -3506
  168. package/src/extension/message-card.ts +61 -0
  169. package/src/extension/step-message.ts +58 -63
  170. package/src/host/child-worker-supervisor.ts +183 -0
  171. package/src/host/client.ts +293 -0
  172. package/src/host/controller-worker-entry.ts +311 -0
  173. package/src/host/controller-worker-protocol.ts +104 -0
  174. package/src/host/controller-worker-supervisor.ts +79 -0
  175. package/src/host/host-entry.ts +23 -0
  176. package/src/host/processes.ts +171 -54
  177. package/src/host/protocol.ts +196 -0
  178. package/src/host/resolver-entry.ts +241 -0
  179. package/src/host/rpc-executor.ts +76 -34
  180. package/src/host/runner.ts +2813 -422
  181. package/src/host/state.ts +1160 -0
  182. package/src/host/worker-entry.ts +533 -0
  183. package/src/host/worker-protocol.ts +165 -0
  184. package/src/host/worker-store.ts +229 -0
  185. package/src/host/worker-supervisor.ts +74 -0
  186. package/src/render/canvas.ts +44 -10
  187. package/src/render/graph-render.ts +145 -90
  188. package/src/state/database.ts +2 -1
  189. package/src/state/index.ts +14 -0
  190. package/src/state/prune.ts +35 -9
  191. package/src/state/schema.ts +171 -3
  192. package/src/state/viewer.ts +356 -0
  193. package/src/viewer/cli.ts +49 -17
  194. package/src/workflows/composition.ts +36 -2
  195. package/src/workflows/definition.ts +32 -0
  196. package/src/workflows/engine.ts +157 -54
  197. package/src/workflows/errors.ts +11 -2
  198. package/src/workflows/human-decision.ts +49 -0
  199. package/src/workflows/index.ts +2 -0
  200. package/src/workflows/schema.ts +19 -6
  201. package/src/{viewer → workflows}/session-reducer.ts +11 -1
  202. package/src/workflows/store.ts +1316 -108
  203. package/src/workflows/tool-input.ts +2 -60
  204. package/src/workflows/types.ts +32 -4
  205. package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
  206. package/dist/controllers/workflow-engine-scheduler.js +0 -175
  207. package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
  208. package/dist/extension/controller-host.d.ts +0 -48
  209. package/dist/extension/controller-host.js +0 -110
  210. package/dist/extension/controller-host.js.map +0 -1
  211. package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
  212. package/dist/extension/deferred-turn-coordinator.js +0 -143
  213. package/dist/extension/deferred-turn-coordinator.js.map +0 -1
  214. package/dist/extension/deferred-turn.d.ts +0 -44
  215. package/dist/extension/deferred-turn.js +0 -110
  216. package/dist/extension/deferred-turn.js.map +0 -1
  217. package/dist/extension/executor.d.ts +0 -86
  218. package/dist/extension/executor.js +0 -311
  219. package/dist/extension/executor.js.map +0 -1
  220. package/dist/extension/follow-up-coordinator.d.ts +0 -27
  221. package/dist/extension/follow-up-coordinator.js +0 -131
  222. package/dist/extension/follow-up-coordinator.js.map +0 -1
  223. package/dist/extension/recorder.d.ts +0 -84
  224. package/dist/extension/recorder.js +0 -528
  225. package/dist/extension/recorder.js.map +0 -1
  226. package/dist/extension/session-events.d.ts +0 -133
  227. package/dist/extension/session-events.js +0 -61
  228. package/dist/extension/session-events.js.map +0 -1
  229. package/dist/viewer/session-reducer.js.map +0 -1
  230. package/src/controllers/workflow-engine-scheduler.ts +0 -246
  231. package/src/extension/controller-host.ts +0 -167
  232. package/src/extension/deferred-turn-coordinator.ts +0 -171
  233. package/src/extension/deferred-turn.ts +0 -166
  234. package/src/extension/executor.ts +0 -411
  235. package/src/extension/follow-up-coordinator.ts +0 -151
  236. package/src/extension/recorder.ts +0 -655
  237. package/src/extension/session-events.ts +0 -121
@@ -1,411 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import type { AssistantMessage } from "@earendil-works/pi-ai";
3
- import type {
4
- AgentStepExecutor,
5
- AgentStepRequest,
6
- AgentStepSubmission,
7
- ConversationRange,
8
- } from "../workflows/types.js";
9
-
10
- export type SubmissionResult =
11
- | { accepted: true; message: string }
12
- | { accepted: false; message: string };
13
-
14
- export type PromptDeliveryKind = "step" | "reminder" | "resume";
15
-
16
- export type PromptDelivery = {
17
- prompt: string;
18
- contract: AgentStepRequest["contract"];
19
- presentation?: AgentStepRequest["presentation"];
20
- kind: PromptDeliveryKind;
21
- /** True when the agent is known to be mid-run, so delivery must be queued. */
22
- streaming: boolean;
23
- };
24
-
25
- /**
26
- * Bracketing hooks for conversation linkage: a mark is taken when the step
27
- * prompt is first delivered, and the recorded entry range since that mark is
28
- * attached to the accepted submission.
29
- */
30
- export type ConversationHooks = {
31
- beginAttempt?: (contract: AgentStepRequest["contract"]) => void;
32
- recoverAssistant?: (contract: AgentStepRequest["contract"]) => AgentStepSubmission | undefined;
33
- mark: () => number;
34
- rangeSince: (mark: number) => ConversationRange | undefined;
35
- };
36
-
37
- export type ConversationStepExecutorOptions = {
38
- /** Deliver a prompt into the pi conversation. */
39
- sendPrompt: (delivery: PromptDelivery) => void;
40
- /** Reminders sent when the agent settles without submitting. Default 2. */
41
- maxNudges?: number;
42
- /** Conversation linkage hooks, wired to the session recorder. */
43
- conversation?: ConversationHooks;
44
- /** Called when the engine aborts a pending agent step. */
45
- onAbort?: (contract: AgentStepRequest["contract"], reason: unknown) => void;
46
- };
47
-
48
- type PendingStep = {
49
- request: AgentStepRequest;
50
- resolve: (submission: AgentStepSubmission) => void;
51
- reject: (error: unknown) => void;
52
- nudgesSent: number;
53
- /** Conversation mark taken when the prompt was first delivered. */
54
- mark: number | null;
55
- /** Latest finalized assistant message observed for this attempt. */
56
- assistantMessage?: AssistantMessage;
57
- cleanup: () => void;
58
- /** Resolves when this step stops being the pending step. */
59
- cleared: Promise<void>;
60
- markCleared: () => void;
61
- };
62
-
63
- const DEFAULT_MAX_NUDGES = 2;
64
-
65
- /**
66
- * AgentStepExecutor that runs steps inside the current pi conversation. The
67
- * engine hands it a prompt; it delivers the prompt as a model-facing workflow
68
- * message and resolves once the model submits an accepted output through the `workflow`
69
- * tool. If the agent settles without submitting, it nudges the model a
70
- * bounded number of times before failing the step.
71
- */
72
- export class ConversationStepExecutor implements AgentStepExecutor {
73
- readonly assistantMessageMode = "visible" as const;
74
- private readonly sendPrompt: (delivery: PromptDelivery) => void;
75
- private readonly maxNudges: number;
76
- private readonly conversation: ConversationHooks | undefined;
77
- private readonly onAbort: ConversationStepExecutorOptions["onAbort"];
78
- private pending: PendingStep | null = null;
79
- private streaming = false;
80
- private heldByUser = false;
81
-
82
- constructor(options: ConversationStepExecutorOptions) {
83
- this.sendPrompt = options.sendPrompt;
84
- this.maxNudges = options.maxNudges ?? DEFAULT_MAX_NUDGES;
85
- this.conversation = options.conversation;
86
- this.onAbort = options.onAbort;
87
- }
88
-
89
- /** Track agent streaming state (wire to agent_start / agent_settled). */
90
- setStreaming(streaming: boolean): void {
91
- this.streaming = streaming;
92
- }
93
-
94
- get pendingStepId(): string | null {
95
- return this.pending?.request.contract.nodeId ?? null;
96
- }
97
-
98
- get pendingCompletion(): "submit" | "assistant" | null {
99
- return this.pending === null ? null : completionKind(this.pending.request);
100
- }
101
-
102
- /**
103
- * Hold the pending step for the user: no nudges are sent while held, so an
104
- * escape-interrupted conversation stays quiet until the user resumes.
105
- */
106
- hold(): void {
107
- this.heldByUser = true;
108
- }
109
-
110
- get held(): boolean {
111
- return this.heldByUser;
112
- }
113
-
114
- /**
115
- * Release a user hold. When a step is still pending, its prompt is
116
- * re-delivered so the model picks the step back up.
117
- */
118
- release(): void {
119
- if (!this.heldByUser) {
120
- return;
121
- }
122
- this.heldByUser = false;
123
- const pending = this.pending;
124
- if (!pending) {
125
- return;
126
- }
127
- try {
128
- this.conversation?.beginAttempt?.(pending.request.contract);
129
- this.sendPrompt(this.delivery(pending.request, pending.request.prompt, "resume"));
130
- } catch (error) {
131
- this.clearPending();
132
- pending.reject(error);
133
- }
134
- }
135
-
136
- async runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission> {
137
- if (this.pending) {
138
- throw new Error("Another workflow step is already awaiting output");
139
- }
140
- if (completionKind(request) === "assistant") {
141
- const recovered = this.conversation?.recoverAssistant?.(request.contract);
142
- if (recovered !== undefined) {
143
- return recovered;
144
- }
145
- }
146
- return await new Promise<AgentStepSubmission>((resolve, reject) => {
147
- const onAbort = () => {
148
- const reason: unknown = signal.reason ?? new Error("Workflow step aborted");
149
- if (this.pending?.request !== request) {
150
- return;
151
- }
152
- this.clearPending();
153
- try {
154
- this.onAbort?.(request.contract, reason);
155
- } finally {
156
- reject(reason);
157
- }
158
- };
159
- signal.addEventListener("abort", onAbort, { once: true });
160
- let markCleared!: () => void;
161
- const cleared = new Promise<void>((resolveCleared) => {
162
- markCleared = resolveCleared;
163
- });
164
- this.conversation?.beginAttempt?.(request.contract);
165
- this.pending = {
166
- request,
167
- resolve,
168
- reject,
169
- nudgesSent: 0,
170
- mark: this.conversation?.mark() ?? null,
171
- cleanup: () => signal.removeEventListener("abort", onAbort),
172
- cleared,
173
- markCleared,
174
- };
175
- if (signal.aborted) {
176
- onAbort();
177
- return;
178
- }
179
- try {
180
- this.sendPrompt(this.delivery(request, request.prompt, "step"));
181
- } catch (error) {
182
- // A failed delivery must not leave the step installed, or every
183
- // subsequent agent node would fail with "already awaiting output".
184
- this.clearPending();
185
- reject(error);
186
- }
187
- });
188
- }
189
-
190
- /** Called by the `workflow` tool when the model submits a step output. */
191
- async submit(stepId: string, attemptId: string, output: unknown): Promise<SubmissionResult> {
192
- const pending = this.pending;
193
- if (!pending) {
194
- return {
195
- accepted: false,
196
- message:
197
- "No workflow step is awaiting output. Do not call the workflow tool outside an active workflow step.",
198
- };
199
- }
200
- const expected = pending.request.contract.nodeId;
201
- if (stepId !== expected) {
202
- return {
203
- accepted: false,
204
- message: `Wrong step id ${JSON.stringify(stepId)}; the pending step is ${JSON.stringify(expected)}.`,
205
- };
206
- }
207
- // Loops revisit the same node id, so a delayed duplicate submission from
208
- // an earlier attempt would otherwise be accepted as this attempt's output.
209
- const expectedAttempt = pending.request.contract.attemptId;
210
- if (attemptId !== expectedAttempt) {
211
- return {
212
- accepted: false,
213
- message: `Stale attempt id ${JSON.stringify(attemptId)} for step ${JSON.stringify(
214
- stepId,
215
- )}; the pending attempt is ${JSON.stringify(expectedAttempt)}. Use the attempt id from the latest step contract.`,
216
- };
217
- }
218
- if (completionKind(pending.request) === "assistant") {
219
- return {
220
- accepted: false,
221
- message:
222
- "This step completes with a normal assistant response. Do not submit workflow output.",
223
- };
224
- }
225
- // Race validation against the step being cleared: a hung `validate`
226
- // callback must not leave this tool call (and therefore pi) blocked after
227
- // a timeout or cancel already resolved the run.
228
- const result = await Promise.race([
229
- pending.request.accept(output),
230
- pending.cleared.then(() => null),
231
- ]);
232
- // The step may have timed out or been cancelled (and a newer step
233
- // installed) while validation was awaited; a stale submission must not
234
- // clear or resolve the newer pending step.
235
- if (result === null || this.pending !== pending) {
236
- return {
237
- accepted: false,
238
- message: `Step ${JSON.stringify(stepId)} is no longer awaiting output.`,
239
- };
240
- }
241
- if (!result.ok) {
242
- return {
243
- accepted: false,
244
- message: `Output rejected for step ${JSON.stringify(stepId)}: ${result.error}`,
245
- };
246
- }
247
- this.clearPending();
248
- const conversation =
249
- pending.mark !== null ? this.conversation?.rangeSince(pending.mark) : undefined;
250
- pending.resolve({
251
- output: result.value,
252
- ...(conversation !== undefined ? { conversation } : {}),
253
- });
254
- return {
255
- accepted: true,
256
- message: `Output accepted for step ${JSON.stringify(stepId)}.`,
257
- };
258
- }
259
-
260
- /** Keep the latest finalized assistant message for an assistant-output step. */
261
- handleMessageEnd(message: unknown): void {
262
- const pending = this.pending;
263
- if (pending === null || completionKind(pending.request) !== "assistant") {
264
- return;
265
- }
266
- const assistant = assistantMessageLike(message);
267
- if (assistant !== undefined) {
268
- pending.assistantMessage = assistant as AssistantMessage;
269
- }
270
- }
271
-
272
- /**
273
- * Called when the agent settles. Returns true when a nudge was sent, false
274
- * when there was nothing to do. Submitted steps use bounded nudges;
275
- * assistant-output steps accept or fail the one visible response.
276
- */
277
- handleAgentSettled(): boolean {
278
- const pending = this.pending;
279
- if (!pending) {
280
- return false;
281
- }
282
- if (this.heldByUser) {
283
- // The user interrupted deliberately; reminding the model now would
284
- // steal the conversation back. The step waits for an explicit resume.
285
- return false;
286
- }
287
- if (completionKind(pending.request) === "assistant") {
288
- try {
289
- const output = visibleAssistantText(
290
- pending.assistantMessage,
291
- pending.request.contract.maxOutputChars,
292
- );
293
- const conversation =
294
- pending.mark !== null ? this.conversation?.rangeSince(pending.mark) : undefined;
295
- const assistantMessage = {
296
- sha256: createHash("sha256").update(output).digest("hex"),
297
- ...(conversation?.lastEntryId !== undefined ? { entryId: conversation.lastEntryId } : {}),
298
- ...(pending.request.contract.maxOutputChars !== undefined
299
- ? { maxChars: pending.request.contract.maxOutputChars }
300
- : {}),
301
- };
302
- this.clearPending();
303
- pending.resolve({
304
- output,
305
- assistantMessage,
306
- ...(conversation !== undefined ? { conversation } : {}),
307
- });
308
- } catch (error) {
309
- this.clearPending();
310
- pending.reject(error);
311
- }
312
- return false;
313
- }
314
- if (pending.nudgesSent >= this.maxNudges) {
315
- this.clearPending();
316
- pending.reject(
317
- new Error(
318
- `Agent settled ${pending.nudgesSent + 1} times without submitting step ${JSON.stringify(
319
- pending.request.contract.nodeId,
320
- )} via the workflow tool`,
321
- ),
322
- );
323
- return false;
324
- }
325
- pending.nudgesSent += 1;
326
- const { nodeId, attemptId } = pending.request.contract;
327
- try {
328
- this.conversation?.beginAttempt?.(pending.request.contract);
329
- this.sendPrompt(
330
- this.delivery(
331
- pending.request,
332
- [
333
- `Reminder: workflow step ${JSON.stringify(nodeId)} is still awaiting your output.`,
334
- "Complete it by calling the `workflow` tool with:",
335
- `{"action": "submit", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
336
- `Expected output: ${pending.request.contract.expectedOutput ?? "a JSON object with your result"}`,
337
- ].join("\n"),
338
- "reminder",
339
- ),
340
- );
341
- } catch (error) {
342
- // No reminder turn was started, so nothing would settle the step; fail
343
- // it promptly instead of waiting out the node timeout.
344
- this.clearPending();
345
- pending.reject(error);
346
- return false;
347
- }
348
- return true;
349
- }
350
-
351
- private delivery(
352
- request: AgentStepRequest,
353
- prompt: string,
354
- kind: PromptDeliveryKind,
355
- ): PromptDelivery {
356
- return {
357
- prompt,
358
- contract: request.contract,
359
- ...(request.presentation !== undefined ? { presentation: request.presentation } : {}),
360
- kind,
361
- streaming: this.streaming,
362
- };
363
- }
364
-
365
- private clearPending(): void {
366
- this.pending?.cleanup();
367
- this.pending?.markCleared();
368
- this.pending = null;
369
- }
370
- }
371
-
372
- function completionKind(request: AgentStepRequest): "submit" | "assistant" {
373
- return request.contract.completion;
374
- }
375
-
376
- function assistantMessageLike(
377
- message: unknown,
378
- ): Pick<AssistantMessage, "role" | "content" | "stopReason" | "errorMessage"> | undefined {
379
- if (message === null || typeof message !== "object") return undefined;
380
- const candidate = message as Partial<AssistantMessage>;
381
- if (candidate.role !== "assistant" || !Array.isArray(candidate.content)) return undefined;
382
- if (typeof candidate.stopReason !== "string") return undefined;
383
- return candidate as Pick<AssistantMessage, "role" | "content" | "stopReason" | "errorMessage">;
384
- }
385
-
386
- /** Extract the exact visible text blocks from one finalized assistant message. */
387
- export function visibleAssistantText(message: unknown, maxChars?: number): string {
388
- const assistant = assistantMessageLike(message);
389
- if (assistant === undefined) {
390
- throw new Error("Assistant step settled without a final assistant message");
391
- }
392
- if (assistant.stopReason !== "stop" && assistant.stopReason !== "length") {
393
- throw new Error(
394
- assistant.errorMessage?.trim() ||
395
- `Assistant step stopped with ${JSON.stringify(assistant.stopReason)} before a final response`,
396
- );
397
- }
398
- const text = assistant.content
399
- .filter((part) => part.type === "text")
400
- .map((part) => part.text)
401
- .join("\n");
402
- if (text.trim().length === 0) {
403
- throw new Error("Assistant step returned no visible text");
404
- }
405
- if (maxChars !== undefined && text.length > maxChars) {
406
- throw new Error(
407
- `Assistant response has ${text.length} characters, above the configured limit of ${maxChars}`,
408
- );
409
- }
410
- return text;
411
- }
@@ -1,151 +0,0 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { LeaseClaim } from "../state/mutation.js";
3
- import type { WorkflowFollowUpRecord } from "../workflows/settings.js";
4
- import { WorkflowRunStore } from "../workflows/store.js";
5
-
6
- const FOLLOW_UP_ID_PREFIX = "pi-workflows-follow-up:";
7
-
8
- export type FollowUpDelivery = {
9
- followUp: WorkflowFollowUpRecord;
10
- claim: LeaseClaim;
11
- };
12
-
13
- export class FollowUpCoordinator {
14
- private pending: FollowUpDelivery | undefined;
15
- private synchronizing = false;
16
-
17
- constructor(
18
- private readonly store: WorkflowRunStore,
19
- private readonly ownerId: string,
20
- private readonly sendUserMessage: (text: string, options?: { deliverAs?: "followUp" }) => void,
21
- ) {}
22
-
23
- async synchronize(ctx: ExtensionContext, workflowActive: boolean): Promise<void> {
24
- if (this.synchronizing) return;
25
- this.synchronizing = true;
26
- try {
27
- const sessionId = ctx.sessionManager.getSessionId();
28
- if (this.pending !== undefined) {
29
- const entryId = findFollowUpEntryId(
30
- ctx.sessionManager.getBranch(),
31
- this.pending.followUp.followUpId,
32
- );
33
- if (entryId === undefined) return;
34
- this.store.markFollowUpSent(this.pending.followUp.followUpId, this.pending.claim, entryId);
35
- this.releaseClaim(this.pending.claim);
36
- this.pending = undefined;
37
- }
38
- if (!ctx.isIdle() || workflowActive) return;
39
- const claimed = this.store.claimNextFollowUp(sessionId, this.ownerId);
40
- if (claimed === undefined) return;
41
- const entryId = findFollowUpEntryId(
42
- ctx.sessionManager.getBranch(),
43
- claimed.followUp.followUpId,
44
- );
45
- if (entryId !== undefined) {
46
- this.store.markFollowUpSent(claimed.followUp.followUpId, claimed.claim, entryId);
47
- this.releaseClaim(claimed.claim);
48
- return;
49
- }
50
- this.pending = claimed;
51
- try {
52
- this.sendUserMessage(followUpMessageText(claimed.followUp));
53
- } catch (error) {
54
- this.pending = undefined;
55
- this.releaseClaim(claimed.claim);
56
- throw error;
57
- }
58
- } finally {
59
- this.synchronizing = false;
60
- }
61
- }
62
-
63
- clear(): void {
64
- this.pending = undefined;
65
- }
66
-
67
- private releaseClaim(claim: LeaseClaim): void {
68
- try {
69
- this.store.releaseFollowUpClaim(claim);
70
- } catch {
71
- // The lease can expire or move after delivery evidence is saved.
72
- }
73
- }
74
- }
75
-
76
- export function followUpMessageText(
77
- followUp: Pick<WorkflowFollowUpRecord, "followUpId" | "prompt">,
78
- ): string {
79
- return `${followUp.prompt}\n\n<!-- ${FOLLOW_UP_ID_PREFIX}${followUp.followUpId} -->`;
80
- }
81
-
82
- export function findFollowUpEntryId(
83
- entries: readonly unknown[],
84
- followUpId: string,
85
- ): string | undefined {
86
- const idText = `${FOLLOW_UP_ID_PREFIX}${followUpId}`;
87
- for (let index = entries.length - 1; index >= 0; index -= 1) {
88
- const entry = entries[index];
89
- if (!isRecord(entry) || entry.type !== "message" || typeof entry.id !== "string") continue;
90
- const message = entry.message;
91
- if (!isRecord(message) || message.role !== "user") continue;
92
- if (messageText(message.content).includes(idText)) return entry.id;
93
- }
94
- return undefined;
95
- }
96
-
97
- export function findSettledPresentationEntries(
98
- entries: readonly unknown[],
99
- runId: string,
100
- ): { presentationEntryId: string; assistantEntryId: string } | undefined {
101
- for (let index = entries.length - 1; index >= 0; index -= 1) {
102
- const entry = entries[index];
103
- if (!isPresentationEntry(entry, runId)) continue;
104
- for (let childIndex = index + 1; childIndex < entries.length; childIndex += 1) {
105
- const child = entries[childIndex];
106
- if (!isRecord(child) || child.type !== "message" || typeof child.id !== "string") continue;
107
- const message = child.message;
108
- if (!isRecord(message)) continue;
109
- if (message.role === "user") break;
110
- if (
111
- message.role === "assistant" &&
112
- (message.stopReason === "stop" || message.stopReason === "length")
113
- ) {
114
- return { presentationEntryId: entry.id, assistantEntryId: child.id };
115
- }
116
- }
117
- }
118
- return undefined;
119
- }
120
-
121
- function isPresentationEntry(value: unknown, runId: string): value is { id: string } {
122
- if (!isRecord(value) || typeof value.id !== "string") return false;
123
- if (value.type === "custom_message") {
124
- return (
125
- value.customType === "pi-workflows-presentation" &&
126
- isRecord(value.details) &&
127
- value.details.runId === runId
128
- );
129
- }
130
- if (value.type !== "message" || !isRecord(value.message)) return false;
131
- return (
132
- value.message.role === "custom" &&
133
- value.message.customType === "pi-workflows-presentation" &&
134
- isRecord(value.message.details) &&
135
- value.message.details.runId === runId
136
- );
137
- }
138
-
139
- function messageText(value: unknown): string {
140
- if (typeof value === "string") return value;
141
- if (!Array.isArray(value)) return "";
142
- return value
143
- .flatMap((item) =>
144
- isRecord(item) && item.type === "text" && typeof item.text === "string" ? [item.text] : [],
145
- )
146
- .join("\n");
147
- }
148
-
149
- function isRecord(value: unknown): value is Record<string, unknown> {
150
- return typeof value === "object" && value !== null && !Array.isArray(value);
151
- }