@osolmaz/pi-workflows 0.14.0 → 0.15.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 (235) hide show
  1. package/README.md +78 -110
  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 +0 -1
  16. package/dist/controllers/index.js +0 -1
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.d.ts +51 -0
  19. package/dist/controllers/sqlite.js +433 -57
  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 -3447
  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/schema.d.ts +1 -1
  94. package/dist/state/schema.js +171 -3
  95. package/dist/state/schema.js.map +1 -1
  96. package/dist/state/viewer.d.ts +46 -0
  97. package/dist/state/viewer.js +249 -0
  98. package/dist/state/viewer.js.map +1 -0
  99. package/dist/viewer/cli.d.ts +1 -1
  100. package/dist/viewer/cli.js +47 -18
  101. package/dist/viewer/cli.js.map +1 -1
  102. package/dist/workflows/composition.js +25 -2
  103. package/dist/workflows/composition.js.map +1 -1
  104. package/dist/workflows/definition.d.ts +3 -1
  105. package/dist/workflows/definition.js +25 -0
  106. package/dist/workflows/definition.js.map +1 -1
  107. package/dist/workflows/engine.d.ts +1 -0
  108. package/dist/workflows/engine.js +157 -42
  109. package/dist/workflows/engine.js.map +1 -1
  110. package/dist/workflows/errors.d.ts +3 -1
  111. package/dist/workflows/errors.js +4 -7
  112. package/dist/workflows/errors.js.map +1 -1
  113. package/dist/workflows/human-decision.d.ts +3 -0
  114. package/dist/workflows/human-decision.js +31 -0
  115. package/dist/workflows/human-decision.js.map +1 -1
  116. package/dist/workflows/index.d.ts +1 -1
  117. package/dist/workflows/index.js +1 -1
  118. package/dist/workflows/index.js.map +1 -1
  119. package/dist/workflows/schema.js +17 -6
  120. package/dist/workflows/schema.js.map +1 -1
  121. package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
  122. package/dist/{viewer → workflows}/session-reducer.js +4 -0
  123. package/dist/workflows/session-reducer.js.map +1 -0
  124. package/dist/workflows/store.d.ts +87 -1
  125. package/dist/workflows/store.js +892 -93
  126. package/dist/workflows/store.js.map +1 -1
  127. package/dist/workflows/tool-input.d.ts +0 -26
  128. package/dist/workflows/tool-input.js +2 -50
  129. package/dist/workflows/tool-input.js.map +1 -1
  130. package/dist/workflows/types.d.ts +36 -4
  131. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
  132. package/docs/CONTROLLERS.md +11 -11
  133. package/docs/DEFERRED_TURNS.md +23 -2
  134. package/docs/SQLITE_STATE.md +41 -31
  135. package/docs/WORKFLOW_HOST.md +452 -0
  136. package/docs/development.md +46 -30
  137. package/docs/live-replay-protocol.md +129 -100
  138. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +42 -0
  139. package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
  140. package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
  141. package/docs/plans/tui-viewer-implementation-plan.md +5 -0
  142. package/docs/tui-viewer.md +18 -3
  143. package/docs/workflows.md +162 -210
  144. package/examples/workflows/command-batch.workflow.ts +2 -0
  145. package/examples/workflows/shell.workflow.ts +2 -1
  146. package/herdr-plugin.toml +1 -1
  147. package/package.json +1 -1
  148. package/src/builtins/autoimplement.workflow.ts +3 -0
  149. package/src/builtins/change-verification.workflow.ts +7 -2
  150. package/src/builtins/metadata.ts +9 -0
  151. package/src/builtins/monitor.workflow.ts +4 -0
  152. package/src/builtins/sanity-check.workflow.ts +4 -0
  153. package/src/builtins/workspace-preparation.workflow.ts +3 -1
  154. package/src/controllers/index.ts +0 -5
  155. package/src/controllers/sqlite.ts +709 -76
  156. package/src/controllers/store.ts +1 -0
  157. package/src/extension/controller-command.ts +45 -0
  158. package/src/extension/index.ts +944 -4163
  159. package/src/extension/message-card.ts +61 -0
  160. package/src/extension/step-message.ts +58 -63
  161. package/src/host/child-worker-supervisor.ts +183 -0
  162. package/src/host/client.ts +293 -0
  163. package/src/host/controller-worker-entry.ts +311 -0
  164. package/src/host/controller-worker-protocol.ts +104 -0
  165. package/src/host/controller-worker-supervisor.ts +79 -0
  166. package/src/host/host-entry.ts +23 -0
  167. package/src/host/processes.ts +171 -54
  168. package/src/host/protocol.ts +196 -0
  169. package/src/host/resolver-entry.ts +241 -0
  170. package/src/host/rpc-executor.ts +76 -34
  171. package/src/host/runner.ts +2813 -422
  172. package/src/host/state.ts +1160 -0
  173. package/src/host/worker-entry.ts +533 -0
  174. package/src/host/worker-protocol.ts +165 -0
  175. package/src/host/worker-store.ts +229 -0
  176. package/src/host/worker-supervisor.ts +74 -0
  177. package/src/render/canvas.ts +44 -10
  178. package/src/render/graph-render.ts +145 -90
  179. package/src/state/database.ts +2 -1
  180. package/src/state/index.ts +14 -0
  181. package/src/state/schema.ts +171 -3
  182. package/src/state/viewer.ts +356 -0
  183. package/src/viewer/cli.ts +49 -17
  184. package/src/workflows/composition.ts +36 -2
  185. package/src/workflows/definition.ts +32 -0
  186. package/src/workflows/engine.ts +157 -54
  187. package/src/workflows/errors.ts +11 -2
  188. package/src/workflows/human-decision.ts +49 -0
  189. package/src/workflows/index.ts +2 -0
  190. package/src/workflows/schema.ts +19 -6
  191. package/src/{viewer → workflows}/session-reducer.ts +11 -1
  192. package/src/workflows/store.ts +1316 -108
  193. package/src/workflows/tool-input.ts +3 -69
  194. package/src/workflows/types.ts +32 -4
  195. package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
  196. package/dist/controllers/workflow-engine-scheduler.js +0 -175
  197. package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
  198. package/dist/extension/controller-host.d.ts +0 -48
  199. package/dist/extension/controller-host.js +0 -110
  200. package/dist/extension/controller-host.js.map +0 -1
  201. package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
  202. package/dist/extension/deferred-turn-coordinator.js +0 -143
  203. package/dist/extension/deferred-turn-coordinator.js.map +0 -1
  204. package/dist/extension/deferred-turn.d.ts +0 -44
  205. package/dist/extension/deferred-turn.js +0 -110
  206. package/dist/extension/deferred-turn.js.map +0 -1
  207. package/dist/extension/executor.d.ts +0 -86
  208. package/dist/extension/executor.js +0 -311
  209. package/dist/extension/executor.js.map +0 -1
  210. package/dist/extension/follow-up-coordinator.d.ts +0 -27
  211. package/dist/extension/follow-up-coordinator.js +0 -131
  212. package/dist/extension/follow-up-coordinator.js.map +0 -1
  213. package/dist/extension/recorder.d.ts +0 -84
  214. package/dist/extension/recorder.js +0 -528
  215. package/dist/extension/recorder.js.map +0 -1
  216. package/dist/extension/restart-policy.d.ts +0 -38
  217. package/dist/extension/restart-policy.js +0 -116
  218. package/dist/extension/restart-policy.js.map +0 -1
  219. package/dist/extension/session-events.d.ts +0 -133
  220. package/dist/extension/session-events.js +0 -61
  221. package/dist/extension/session-events.js.map +0 -1
  222. package/dist/extension/terminal-decision.d.ts +0 -51
  223. package/dist/extension/terminal-decision.js +0 -110
  224. package/dist/extension/terminal-decision.js.map +0 -1
  225. package/dist/viewer/session-reducer.js.map +0 -1
  226. package/src/controllers/workflow-engine-scheduler.ts +0 -246
  227. package/src/extension/controller-host.ts +0 -167
  228. package/src/extension/deferred-turn-coordinator.ts +0 -171
  229. package/src/extension/deferred-turn.ts +0 -166
  230. package/src/extension/executor.ts +0 -411
  231. package/src/extension/follow-up-coordinator.ts +0 -151
  232. package/src/extension/recorder.ts +0 -655
  233. package/src/extension/restart-policy.ts +0 -163
  234. package/src/extension/session-events.ts +0 -121
  235. package/src/extension/terminal-decision.ts +0 -172
@@ -1,246 +0,0 @@
1
- import { compositionMetadata } from "../workflows/composition.js";
2
- import { WorkflowEngine, workflowIdentityMismatch } from "../workflows/engine.js";
3
- import type { WorkflowRunStore } from "../workflows/store.js";
4
- import type {
5
- WorkflowDefinition,
6
- WorkflowRunResult,
7
- WorkflowRunStatus,
8
- WorkflowSource,
9
- } from "../workflows/types.js";
10
- import type {
11
- ControllerFollowUpResult,
12
- ControllerQueueFollowUpRequest,
13
- ControllerRemoveFollowUpRequest,
14
- ControllerSettingsChangeRequest,
15
- ControllerSettingsChangeResult,
16
- } from "./types.js";
17
- import type {
18
- ControllerWorkflowControlRequest,
19
- ControllerWorkflowScheduler,
20
- WorkflowSchedulerRequest,
21
- WorkflowSchedulerResult,
22
- } from "./workflows.js";
23
-
24
- export type ResolvedChildWorkflow = {
25
- workflow: WorkflowDefinition;
26
- workflowSource?: WorkflowSource;
27
- };
28
-
29
- export type WorkflowEngineSchedulerOptions = {
30
- store: WorkflowRunStore;
31
- resolveWorkflow: (name: string) => Promise<ResolvedChildWorkflow>;
32
- createEngine: (request: WorkflowSchedulerRequest) => WorkflowEngine;
33
- /**
34
- * Release resources the engine held after its run settles, for example a
35
- * headless child process. Called once per finished run.
36
- */
37
- disposeEngine?: (engine: WorkflowEngine) => Promise<void>;
38
- };
39
-
40
- export class WorkflowEngineScheduler implements ControllerWorkflowScheduler {
41
- private readonly active = new Map<string, { runId: string; promise: Promise<void> }>();
42
-
43
- constructor(private readonly options: WorkflowEngineSchedulerOptions) {}
44
-
45
- async ensure(
46
- request: WorkflowSchedulerRequest,
47
- signal: AbortSignal,
48
- onComplete: (result: WorkflowSchedulerResult) => void,
49
- ): Promise<WorkflowSchedulerResult> {
50
- if (signal.aborted) {
51
- throw signal.reason ?? new Error("Workflow scheduling aborted");
52
- }
53
- const activeKey = requestKey(request);
54
- const active = this.active.get(activeKey);
55
- if (active !== undefined) {
56
- return { state: "running", runId: active.runId };
57
- }
58
-
59
- const bundle = this.options.store.readRun(request.runId, { includeTrace: true });
60
- if (bundle !== null) {
61
- const recovered =
62
- bundle.state.status === "running"
63
- ? await this.options.store.markRunInterrupted(request.runId)
64
- : bundle;
65
- if (recovered !== null) {
66
- const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
67
- return resultFromStatus(
68
- recovered.state.status,
69
- request.runId,
70
- recovered.state.error,
71
- lastTraceEvent?.type === "run_interrupted",
72
- );
73
- }
74
- return { state: "pending" };
75
- }
76
-
77
- const resolved = await this.options.resolveWorkflow(request.workflow);
78
- if (signal.aborted) {
79
- throw signal.reason ?? new Error("Workflow scheduling aborted");
80
- }
81
- const runId = request.runId;
82
- const engine = this.options.createEngine(request);
83
- const promise = engine
84
- .run(resolved.workflow, request.input, {
85
- runId,
86
- ...(resolved.workflowSource !== undefined
87
- ? { workflowSource: resolved.workflowSource }
88
- : {}),
89
- })
90
- .then((result) => {
91
- callCompletion(onComplete, resultFromRun(result));
92
- })
93
- .catch((error: unknown) => {
94
- callCompletion(onComplete, {
95
- state: "failed",
96
- runId,
97
- error: error instanceof Error ? error.message : String(error),
98
- });
99
- })
100
- .finally(() => {
101
- this.active.delete(activeKey);
102
- void this.options.disposeEngine?.(engine).catch(() => undefined);
103
- });
104
- this.active.set(activeKey, { runId, promise });
105
- return { state: "running", runId };
106
- }
107
-
108
- async changeSettings(
109
- request: ControllerWorkflowControlRequest<ControllerSettingsChangeRequest>,
110
- signal: AbortSignal,
111
- ): Promise<ControllerSettingsChangeResult> {
112
- signal.throwIfAborted();
113
- const bundle = this.options.store.readRun(request.runId);
114
- if (bundle === null) throw new Error(`Workflow run not found: ${request.runId}`);
115
- const resolved = await this.options.resolveWorkflow(bundle.state.workflowName);
116
- signal.throwIfAborted();
117
- if (workflowIdentityMismatch(bundle.state, resolved.workflow, resolved.workflowSource)) {
118
- throw new Error(`Workflow source changed since run ${request.runId} started`);
119
- }
120
- const scopes = this.options.store.listSettingsScopes(request.runId);
121
- const scopeId = request.scopeId ?? (scopes.length === 1 ? scopes[0]?.scopeId : undefined);
122
- if (scopeId === undefined) {
123
- throw new Error("Controller settings changes require scopeId when several scopes exist");
124
- }
125
- const scope = this.options.store.getSettingsScope(scopeId);
126
- if (scope === undefined || scope.activeRunId !== request.runId) {
127
- throw new Error(`Workflow settings scope not found: ${scopeId}`);
128
- }
129
- const definition =
130
- scope.mountPath === ""
131
- ? resolved.workflow.settings
132
- : compositionMetadata(resolved.workflow)?.scopes[scope.mountPath]?.settings;
133
- if (definition === undefined) {
134
- throw new Error(`Workflow scope ${scopeId} does not declare editable settings`);
135
- }
136
- const result = await this.options.store.changeSettings(definition, {
137
- runId: request.runId,
138
- scopeId,
139
- requestId: request.actorRequestKey,
140
- ...(request.expectedChangeNumber !== undefined
141
- ? { expectedChangeNumber: request.expectedChangeNumber }
142
- : {}),
143
- actor: { type: "controller", id: request.controllerResourceUid },
144
- source: "controller-request",
145
- patch: request.patch,
146
- });
147
- return {
148
- runId: request.runId,
149
- scopeId,
150
- changeNumber: result.scope.changeNumber,
151
- adopted: result.adopted,
152
- };
153
- }
154
-
155
- async queueFollowUp(
156
- request: ControllerWorkflowControlRequest<ControllerQueueFollowUpRequest>,
157
- signal: AbortSignal,
158
- ): Promise<ControllerFollowUpResult> {
159
- signal.throwIfAborted();
160
- const targetSessionId = this.options.store.originSessionId(request.runId);
161
- if (targetSessionId === undefined) {
162
- throw new Error("Controller follow-up requires a workflow run with an origin Pi session");
163
- }
164
- const result = this.options.store.queueFollowUp({
165
- runId: request.runId,
166
- requestId: request.actorRequestKey,
167
- targetSessionId,
168
- actor: { type: "controller", id: request.controllerResourceUid },
169
- source: "controller-request",
170
- prompt: request.prompt,
171
- });
172
- return {
173
- runId: request.runId,
174
- followUpId: result.followUp.followUpId,
175
- order: result.followUp.order,
176
- state: result.followUp.state,
177
- adopted: result.adopted,
178
- };
179
- }
180
-
181
- async removeFollowUp(
182
- request: ControllerWorkflowControlRequest<ControllerRemoveFollowUpRequest>,
183
- signal: AbortSignal,
184
- ): Promise<ControllerFollowUpResult> {
185
- signal.throwIfAborted();
186
- const result = this.options.store.removeFollowUp({
187
- runId: request.runId,
188
- followUpId: request.followUpId,
189
- actor: { type: "controller", id: request.controllerResourceUid },
190
- source: "controller-request",
191
- });
192
- return {
193
- runId: request.runId,
194
- followUpId: result.followUpId,
195
- order: result.order,
196
- state: result.state,
197
- adopted: false,
198
- };
199
- }
200
-
201
- async waitForIdle(): Promise<void> {
202
- await Promise.all([...this.active.values()].map((entry) => entry.promise));
203
- }
204
- }
205
-
206
- function callCompletion(
207
- callback: (result: WorkflowSchedulerResult) => void,
208
- result: WorkflowSchedulerResult,
209
- ): void {
210
- try {
211
- callback(result);
212
- } catch {
213
- // A later reconciliation recovers completion from the immutable run event.
214
- }
215
- }
216
-
217
- function requestKey(request: WorkflowSchedulerRequest): string {
218
- return `${request.requestId}:${request.attempt}`;
219
- }
220
-
221
- function resultFromRun(result: WorkflowRunResult): WorkflowSchedulerResult {
222
- return resultFromStatus(result.state.status, result.state.runId, result.state.error);
223
- }
224
-
225
- function resultFromStatus(
226
- status: WorkflowRunStatus,
227
- runId: string,
228
- error?: string,
229
- interrupted = false,
230
- ): WorkflowSchedulerResult {
231
- if (interrupted) {
232
- return { state: "interrupted", runId, ...(error !== undefined ? { error } : {}) };
233
- }
234
- switch (status) {
235
- case "running":
236
- return { state: "running", runId };
237
- case "waiting":
238
- return { state: "waiting", runId };
239
- case "completed":
240
- return { state: "succeeded", runId };
241
- case "failed":
242
- case "timed_out":
243
- case "cancelled":
244
- return { state: "failed", runId, ...(error !== undefined ? { error } : {}) };
245
- }
246
- }
@@ -1,167 +0,0 @@
1
- import {
2
- ControllerManager,
3
- SqliteControllerStore,
4
- loadDiscoveredControllers,
5
- type AnyControllerDefinition,
6
- type ControllerResource,
7
- type ControllerWorkflowScheduler,
8
- type WorkflowSchedulerRequest,
9
- type WorkflowSchedulerResult,
10
- } from "../controllers/index.js";
11
-
12
- export type ParsedControllerArgs =
13
- | { kind: "list" }
14
- | { kind: "get"; controller: string; key: string }
15
- | { kind: "apply"; controller: string; key: string; spec: unknown }
16
- | { kind: "reconcile"; controller: string; key: string }
17
- | { kind: "delete"; controller: string; key: string }
18
- | { kind: "start" }
19
- | { kind: "stop" };
20
-
21
- export type PiChildWorkflowStarter = (
22
- request: WorkflowSchedulerRequest,
23
- signal: AbortSignal,
24
- onComplete: (result: WorkflowSchedulerResult) => void,
25
- ) => Promise<WorkflowSchedulerResult>;
26
-
27
- export function parseControllerArgs(args: string): ParsedControllerArgs {
28
- const trimmed = args.trim();
29
- if (trimmed.length === 0 || trimmed === "list") {
30
- return { kind: "list" };
31
- }
32
- if (trimmed === "start" || trimmed === "stop") {
33
- return { kind: trimmed };
34
- }
35
- const apply = trimmed.match(/^apply\s+(\S+)\s+(\S+)\s+([\s\S]+)$/);
36
- if (apply !== null) {
37
- let spec: unknown;
38
- try {
39
- spec = JSON.parse(apply[3] as string) as unknown;
40
- } catch (error) {
41
- throw new Error(
42
- `Invalid controller spec JSON: ${error instanceof Error ? error.message : String(error)}`,
43
- );
44
- }
45
- return {
46
- kind: "apply",
47
- controller: apply[1] as string,
48
- key: apply[2] as string,
49
- spec,
50
- };
51
- }
52
- const operation = trimmed.match(/^(get|reconcile|delete)\s+(\S+)\s+(\S+)$/);
53
- if (operation !== null) {
54
- return {
55
- kind: operation[1] as "get" | "reconcile" | "delete",
56
- controller: operation[2] as string,
57
- key: operation[3] as string,
58
- };
59
- }
60
- throw new Error(
61
- "Usage: /controller [list|get <controller> <key>|apply <controller> <key> <json>|reconcile <controller> <key>|delete <controller> <key>|start|stop]",
62
- );
63
- }
64
-
65
- export class PiControllerHost {
66
- readonly definitions: AnyControllerDefinition[];
67
- readonly store: SqliteControllerStore;
68
- readonly manager: ControllerManager;
69
- private running = false;
70
-
71
- private constructor(
72
- definitions: AnyControllerDefinition[],
73
- store: SqliteControllerStore,
74
- manager: ControllerManager,
75
- ) {
76
- this.definitions = definitions;
77
- this.store = store;
78
- this.manager = manager;
79
- }
80
-
81
- static async create(options: {
82
- cwd: string;
83
- startChild: PiChildWorkflowStarter;
84
- workflowControls?: Pick<
85
- ControllerWorkflowScheduler,
86
- "changeSettings" | "queueFollowUp" | "removeFollowUp"
87
- >;
88
- }): Promise<PiControllerHost | undefined> {
89
- const definitions = await loadDiscoveredControllers({ cwd: options.cwd });
90
- if (definitions.length === 0) {
91
- return undefined;
92
- }
93
- const store = new SqliteControllerStore(undefined, { projectPath: options.cwd });
94
- const manager = new ControllerManager({
95
- store,
96
- controllers: definitions,
97
- workflowScheduler: { ensure: options.startChild, ...options.workflowControls },
98
- });
99
- return new PiControllerHost(definitions, store, manager);
100
- }
101
-
102
- start(): void {
103
- this.manager.start();
104
- this.running = true;
105
- }
106
-
107
- async stop(): Promise<void> {
108
- await this.manager.stop();
109
- this.running = false;
110
- }
111
-
112
- async close(): Promise<void> {
113
- await this.stop();
114
- this.store.close();
115
- }
116
-
117
- get isRunning(): boolean {
118
- return this.running;
119
- }
120
-
121
- list(): string {
122
- const names = this.definitions.map((item) => item.name).join(", ");
123
- const resources = this.store.listResources();
124
- if (resources.length === 0) {
125
- return `Controllers: ${names}. No resources.`;
126
- }
127
- const rows = resources.map((resource) => resourceSummary(resource)).join("; ");
128
- return `Controllers: ${names}. Resources: ${rows}.`;
129
- }
130
-
131
- get(controller: string, key: string): string {
132
- const resource = this.store.getResource({ controller, key });
133
- if (resource === undefined) {
134
- throw new Error(`Controller resource not found: ${controller}/${key}`);
135
- }
136
- return JSON.stringify(
137
- {
138
- resource,
139
- effects: this.store.listEffects(resource.metadata.uid),
140
- workflows: this.store.listWorkflows(resource.metadata.uid),
141
- },
142
- null,
143
- 2,
144
- );
145
- }
146
-
147
- apply(controller: string, key: string, spec: unknown): ControllerResource {
148
- return this.manager.putResourceByName(controller, key, spec);
149
- }
150
-
151
- reconcile(controller: string, key: string): void {
152
- this.manager.enqueue({ controller, key });
153
- }
154
-
155
- delete(controller: string, key: string): void {
156
- this.manager.requestDeletion({ controller, key });
157
- }
158
- }
159
-
160
- function resourceSummary(resource: ControllerResource): string {
161
- const condition =
162
- resource.status.conditions.find((item) => item.type === "Ready") ??
163
- resource.status.conditions[0];
164
- const state =
165
- condition === undefined ? "unknown" : `${String(condition.status)}:${condition.reason}`;
166
- return `${resource.metadata.controller}/${resource.metadata.key} generation=${resource.metadata.generation} ready=${state}`;
167
- }
@@ -1,171 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import type {
3
- SqliteControllerStore,
4
- WorkflowTurnIntentRecord,
5
- WorkflowTurnIntentResolution,
6
- } from "../controllers/sqlite.js";
7
- import { deferredTurnMessageId } from "./deferred-turn.js";
8
-
9
- export type BranchIntentResolution = {
10
- resolution: WorkflowTurnIntentResolution;
11
- messageId: string;
12
- };
13
-
14
- type NaturalDelivery = {
15
- runId: string;
16
- targetSessionId: string;
17
- resolution: Exclude<WorkflowTurnIntentResolution, "fallback">;
18
- send: (turnIntentId?: string) => void;
19
- };
20
-
21
- type FallbackDelivery = {
22
- targetSessionId: string;
23
- send: (intent: WorkflowTurnIntentRecord) => void;
24
- };
25
-
26
- export class DeferredTurnCoordinator {
27
- private readonly store: () => SqliteControllerStore | null;
28
- private readonly branchResolution: (intentId: string) => BranchIntentResolution | null;
29
- private readonly leaseMs: number;
30
- private readonly deferredNatural = new Map<string, NaturalDelivery>();
31
-
32
- constructor(options: {
33
- store: () => SqliteControllerStore | null;
34
- branchResolution: (intentId: string) => BranchIntentResolution | null;
35
- leaseMs: number;
36
- }) {
37
- this.store = options.store;
38
- this.branchResolution = options.branchResolution;
39
- this.leaseMs = options.leaseMs;
40
- }
41
-
42
- sendNatural(delivery: NaturalDelivery, idle: boolean): "sent" | "deferred" | "suppressed" {
43
- const store = this.store();
44
- const intent = store?.findPendingWorkflowTurnIntent({
45
- runId: delivery.runId,
46
- targetSessionId: delivery.targetSessionId,
47
- });
48
- if (store === null || store === undefined || intent === undefined) {
49
- delivery.send();
50
- return "sent";
51
- }
52
- if (!idle) {
53
- this.deferredNatural.set(intent.intentId, delivery);
54
- return "deferred";
55
- }
56
- const result = this.deliverNatural(intent.intentId, delivery);
57
- if (result === "deferred") this.deferredNatural.set(intent.intentId, delivery);
58
- return result;
59
- }
60
-
61
- flushNatural(idle: boolean): number {
62
- if (!idle) return 0;
63
- let sent = 0;
64
- for (const [intentId, delivery] of this.deferredNatural) {
65
- const result = this.deliverNatural(intentId, delivery);
66
- if (result !== "deferred") {
67
- this.deferredNatural.delete(intentId);
68
- }
69
- if (result === "sent") sent += 1;
70
- }
71
- return sent;
72
- }
73
-
74
- deliverFallbacks(delivery: FallbackDelivery, idle: boolean): number {
75
- const store = this.store();
76
- if (!idle || store === null) return 0;
77
- const claimToken = randomUUID();
78
- const intents = store.claimEligibleWorkflowTurnIntents({
79
- targetSessionId: delivery.targetSessionId,
80
- claimToken,
81
- leaseMs: this.leaseMs,
82
- });
83
- let sent = 0;
84
- for (const [index, intent] of intents.entries()) {
85
- const branch = this.branchResolution(intent.intentId);
86
- if (branch !== null) {
87
- store.resolveWorkflowTurnIntent({
88
- intentId: intent.intentId,
89
- targetSessionId: delivery.targetSessionId,
90
- claimToken,
91
- resolution: branch.resolution,
92
- messageId: branch.messageId,
93
- });
94
- continue;
95
- }
96
- try {
97
- delivery.send(intent);
98
- store.resolveWorkflowTurnIntent({
99
- intentId: intent.intentId,
100
- targetSessionId: delivery.targetSessionId,
101
- claimToken,
102
- resolution: "fallback",
103
- messageId: deferredTurnMessageId(intent.intentId, "fallback"),
104
- });
105
- sent += 1;
106
- } catch (error) {
107
- for (const pending of intents.slice(index)) {
108
- store.releaseWorkflowTurnIntentClaim({
109
- intentId: pending.intentId,
110
- targetSessionId: delivery.targetSessionId,
111
- claimToken,
112
- });
113
- }
114
- throw error;
115
- }
116
- }
117
- return sent;
118
- }
119
-
120
- clearDeferred(): void {
121
- this.deferredNatural.clear();
122
- }
123
-
124
- private deliverNatural(
125
- intentId: string,
126
- delivery: NaturalDelivery,
127
- ): "sent" | "deferred" | "suppressed" {
128
- const store = this.store();
129
- if (store === null) return "deferred";
130
- const claimToken = randomUUID();
131
- const intent = store.claimWorkflowTurnIntent({
132
- intentId,
133
- targetSessionId: delivery.targetSessionId,
134
- claimToken,
135
- leaseMs: this.leaseMs,
136
- });
137
- if (intent === undefined) {
138
- const current = store.getWorkflowTurnIntent(intentId);
139
- return current?.resolvedAt === null ? "deferred" : "suppressed";
140
- }
141
- const branch = this.branchResolution(intent.intentId);
142
- if (branch !== null) {
143
- store.resolveWorkflowTurnIntent({
144
- intentId: intent.intentId,
145
- targetSessionId: delivery.targetSessionId,
146
- claimToken,
147
- resolution: branch.resolution,
148
- messageId: branch.messageId,
149
- });
150
- return "suppressed";
151
- }
152
- try {
153
- delivery.send(intent.intentId);
154
- store.resolveWorkflowTurnIntent({
155
- intentId: intent.intentId,
156
- targetSessionId: delivery.targetSessionId,
157
- claimToken,
158
- resolution: delivery.resolution,
159
- messageId: deferredTurnMessageId(intent.intentId, delivery.resolution),
160
- });
161
- return "sent";
162
- } catch (error) {
163
- store.releaseWorkflowTurnIntentClaim({
164
- intentId: intent.intentId,
165
- targetSessionId: delivery.targetSessionId,
166
- claimToken,
167
- });
168
- throw error;
169
- }
170
- }
171
- }