@pikku/core 0.12.19 → 0.12.21

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 (146) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/function-runner.js +53 -3
  6. package/dist/function/functions.types.d.ts +3 -2
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/content-service.d.ts +66 -37
  21. package/dist/services/in-memory-queue-service.js +1 -1
  22. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  23. package/dist/services/in-memory-workflow-service.js +31 -13
  24. package/dist/services/index.d.ts +1 -1
  25. package/dist/services/local-content.d.ts +10 -12
  26. package/dist/services/local-content.js +54 -38
  27. package/dist/services/user-session-service.d.ts +1 -1
  28. package/dist/testing/service-tests.js +24 -0
  29. package/dist/types/core.types.d.ts +4 -0
  30. package/dist/types/state.types.d.ts +2 -18
  31. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  32. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  33. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  34. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  35. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  36. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  37. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  38. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  39. package/dist/wirings/channel/channel-handler.js +1 -1
  40. package/dist/wirings/channel/channel-store.d.ts +13 -0
  41. package/dist/wirings/channel/channel.types.d.ts +3 -0
  42. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  43. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  44. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  45. package/dist/wirings/cli/cli-runner.js +24 -5
  46. package/dist/wirings/cli/command-parser.js +19 -4
  47. package/dist/wirings/http/http-runner.js +72 -36
  48. package/dist/wirings/http/http.types.d.ts +1 -0
  49. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  50. package/dist/wirings/http/web-request.js +32 -0
  51. package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
  52. package/dist/wirings/rpc/rpc-runner.js +45 -11
  53. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  54. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  55. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  56. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  57. package/dist/wirings/workflow/index.d.ts +1 -1
  58. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  59. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  60. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  61. package/package.json +1 -1
  62. package/run-tests.sh +4 -1
  63. package/src/dev/hot-reload.test.ts +62 -11
  64. package/src/dev/hot-reload.ts +28 -7
  65. package/src/errors/error-handler.ts +8 -2
  66. package/src/errors/error.test.ts +2 -0
  67. package/src/function/function-runner.test.ts +500 -10
  68. package/src/function/function-runner.ts +68 -3
  69. package/src/function/functions.types.ts +3 -2
  70. package/src/index.ts +22 -3
  71. package/src/middleware/index.ts +11 -2
  72. package/src/middleware/telemetry.ts +2 -2
  73. package/src/middleware-runner.test.ts +16 -16
  74. package/src/middleware-runner.ts +42 -30
  75. package/src/permissions.test.ts +27 -24
  76. package/src/permissions.ts +41 -25
  77. package/src/pikku-request.test.ts +35 -0
  78. package/src/pikku-request.ts +1 -1
  79. package/src/pikku-state.ts +2 -3
  80. package/src/run-tests-script.test.ts +18 -0
  81. package/src/services/ai-agent-runner-service.ts +1 -0
  82. package/src/services/content-service.ts +79 -51
  83. package/src/services/in-memory-queue-service.ts +1 -1
  84. package/src/services/in-memory-session-store.ts +1 -2
  85. package/src/services/in-memory-workflow-service.test.ts +33 -11
  86. package/src/services/in-memory-workflow-service.ts +49 -13
  87. package/src/services/index.ts +10 -1
  88. package/src/services/local-content.test.ts +54 -0
  89. package/src/services/local-content.ts +80 -53
  90. package/src/services/typed-credential-service.ts +3 -3
  91. package/src/services/typed-secret-service.ts +3 -3
  92. package/src/services/typed-variables-service.ts +3 -3
  93. package/src/services/user-session-service.ts +4 -4
  94. package/src/testing/service-tests.ts +30 -0
  95. package/src/types/core.types.ts +6 -2
  96. package/src/types/state.types.ts +2 -13
  97. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  98. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  99. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  100. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  101. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  102. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  103. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  104. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  105. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  106. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  107. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  108. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  109. package/src/wirings/channel/channel-handler.test.ts +272 -0
  110. package/src/wirings/channel/channel-handler.ts +1 -1
  111. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  112. package/src/wirings/channel/channel-store.ts +19 -0
  113. package/src/wirings/channel/channel.types.ts +4 -0
  114. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  115. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  116. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  117. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  118. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  119. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  120. package/src/wirings/cli/cli-runner.test.ts +255 -2
  121. package/src/wirings/cli/cli-runner.ts +31 -10
  122. package/src/wirings/cli/cli.types.ts +4 -8
  123. package/src/wirings/cli/command-parser.test.ts +83 -0
  124. package/src/wirings/cli/command-parser.ts +23 -4
  125. package/src/wirings/http/http-runner.test.ts +296 -1
  126. package/src/wirings/http/http-runner.ts +87 -57
  127. package/src/wirings/http/http.types.ts +11 -26
  128. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  129. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  130. package/src/wirings/http/web-request.test.ts +115 -0
  131. package/src/wirings/http/web-request.ts +43 -5
  132. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  133. package/src/wirings/queue/queue.types.ts +2 -5
  134. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  135. package/src/wirings/rpc/rpc-runner.ts +60 -21
  136. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  137. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  138. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  139. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  140. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  141. package/src/wirings/workflow/index.ts +1 -0
  142. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  143. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  144. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  145. package/src/wirings/workflow/workflow.types.ts +68 -5
  146. package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,5 @@
1
1
  import type { SerializedError } from '../../types/core.types.js';
2
- import type { PikkuWorkflowWire, StepState, WorkflowPlannedStep, WorkflowRun, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
2
+ import type { PikkuWorkflowWire, StepState, WorkflowPlannedStep, WorkflowRun, WorkflowRunMirror, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
3
3
  import type { WorkflowService } from '../../services/workflow-service.js';
4
4
  import { PikkuError } from '../../errors/error-handler.js';
5
5
  /**
@@ -35,6 +35,12 @@ export declare class WorkflowNotFoundError extends PikkuError {
35
35
  export declare class WorkflowRunNotFoundError extends PikkuError {
36
36
  constructor(runId: string);
37
37
  }
38
+ export declare class WorkflowRunFailedError extends PikkuError {
39
+ payload: {
40
+ message?: string;
41
+ };
42
+ constructor(message?: string);
43
+ }
38
44
  export declare class WorkflowServiceNotInitialized extends Error {
39
45
  }
40
46
  export declare class WorkflowStepNameNotString extends Error {
@@ -48,7 +54,19 @@ export declare class WorkflowStepNameNotString extends Error {
48
54
  export declare abstract class PikkuWorkflowService implements WorkflowService {
49
55
  private inlineRuns;
50
56
  protected get logger(): import("../../services/logger.js").Logger;
51
- constructor();
57
+ protected mirror?: WorkflowRunMirror;
58
+ constructor(options?: {
59
+ wireQueues?: boolean;
60
+ mirror?: WorkflowRunMirror;
61
+ });
62
+ private safeMirror;
63
+ rewireQueueWorkers(): void;
64
+ /**
65
+ * Wire the queue-based orchestrator/step/sleeper workers.
66
+ * Subclasses that orchestrate without queues (e.g. Durable Objects) should
67
+ * pass `wireQueues: false` to the base constructor and skip this entirely.
68
+ */
69
+ protected wireQueueWorkers(): void;
52
70
  /**
53
71
  * Check if a run is executing inline (without queues)
54
72
  */
@@ -62,7 +80,11 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
62
80
  */
63
81
  unregisterInlineRun(runId: string): void;
64
82
  registerWorkflowVersions(): Promise<void>;
65
- abstract createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
83
+ createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
84
+ deterministic?: boolean;
85
+ plannedSteps?: WorkflowPlannedStep[];
86
+ }): Promise<string>;
87
+ protected abstract createRunImpl(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
66
88
  deterministic?: boolean;
67
89
  plannedSteps?: WorkflowPlannedStep[];
68
90
  }): Promise<string>;
@@ -90,7 +112,8 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
90
112
  * @param id - Run ID
91
113
  * @param status - New status
92
114
  */
93
- abstract updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
115
+ updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
116
+ protected abstract updateRunStatusImpl(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
94
117
  /**
95
118
  * Insert initial step state (called by orchestrator)
96
119
  * Creates pending step in both workflow_step and workflow_step_history
@@ -101,7 +124,8 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
101
124
  * @param stepOptions - Step options (retries, retryDelay)
102
125
  * @returns Step state with generated stepId
103
126
  */
104
- abstract insertStepState(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: WorkflowStepOptions): Promise<StepState>;
127
+ insertStepState(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: WorkflowStepOptions): Promise<StepState>;
128
+ protected abstract insertStepStateImpl(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: WorkflowStepOptions): Promise<StepState>;
105
129
  /**
106
130
  * Get step state by cache key (read-only)
107
131
  * @param runId - Run ID
@@ -114,33 +138,38 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
114
138
  * Updates both workflow_step and workflow_step_history
115
139
  * @param stepId - Step ID
116
140
  */
117
- abstract setStepRunning(stepId: string): Promise<void>;
141
+ setStepRunning(stepId: string): Promise<void>;
142
+ protected abstract setStepRunningImpl(stepId: string): Promise<void>;
118
143
  /**
119
144
  * Mark step as scheduled (queued for execution)
120
145
  * Updates both workflow_step and workflow_step_history
121
146
  * @param stepId - Step ID
122
147
  */
123
- abstract setStepScheduled(stepId: string): Promise<void>;
148
+ setStepScheduled(stepId: string): Promise<void>;
149
+ protected abstract setStepScheduledImpl(stepId: string): Promise<void>;
124
150
  /**
125
151
  * Store step result and mark as succeeded
126
152
  * Updates both workflow_step and workflow_step_history
127
153
  * @param stepId - Step ID
128
154
  * @param result - Step result
129
155
  */
130
- abstract setStepResult(stepId: string, result: any): Promise<void>;
156
+ setStepResult(stepId: string, result: any): Promise<void>;
157
+ protected abstract setStepResultImpl(stepId: string, result: any): Promise<void>;
131
158
  /**
132
159
  * Set the child workflow run ID on a step
133
160
  * @param stepId - Step ID
134
161
  * @param childRunId - Child workflow run ID
135
162
  */
136
- abstract setStepChildRunId(stepId: string, childRunId: string): Promise<void>;
163
+ setStepChildRunId(stepId: string, childRunId: string): Promise<void>;
164
+ protected abstract setStepChildRunIdImpl(stepId: string, childRunId: string): Promise<void>;
137
165
  /**
138
166
  * Store step error and mark as failed
139
167
  * Updates both workflow_step and workflow_step_history
140
168
  * @param stepId - Step ID
141
169
  * @param error - Error object
142
170
  */
143
- abstract setStepError(stepId: string, error: Error): Promise<void>;
171
+ setStepError(stepId: string, error: Error): Promise<void>;
172
+ protected abstract setStepErrorImpl(stepId: string, error: Error): Promise<void>;
144
173
  /**
145
174
  * Create a new retry attempt for a failed step
146
175
  * Inserts new pending step in both workflow_step and workflow_step_history
@@ -149,7 +178,8 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
149
178
  * @param failedStepId - Failed step ID to copy from
150
179
  * @returns New step state for the retry attempt
151
180
  */
152
- abstract createRetryAttempt(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
181
+ createRetryAttempt(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
182
+ protected abstract createRetryAttemptImpl(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
153
183
  /**
154
184
  * Execute function within a run lock to prevent concurrent modifications
155
185
  * @param id - Run ID
@@ -198,22 +228,26 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
198
228
  * @param stepId - Step ID
199
229
  * @param branchKey - Branch key selected by graph.branch()
200
230
  */
201
- abstract setBranchTaken(stepId: string, branchKey: string): Promise<void>;
231
+ setBranchTaken(stepId: string, branchKey: string): Promise<void>;
232
+ protected abstract setBranchTakenImpl(stepId: string, branchKey: string): Promise<void>;
202
233
  /**
203
234
  * Update a state variable in the workflow run's state
204
235
  * @param runId - Run ID
205
236
  * @param name - Variable name
206
237
  * @param value - Value to store
207
238
  */
208
- abstract updateRunState(runId: string, name: string, value: unknown): Promise<void>;
239
+ updateRunState(runId: string, name: string, value: unknown): Promise<void>;
240
+ protected abstract updateRunStateImpl(runId: string, name: string, value: unknown): Promise<void>;
209
241
  /**
210
242
  * Get the entire state object for a workflow run
211
243
  * @param runId - Run ID
212
244
  * @returns The state object with all variables
213
245
  */
214
246
  abstract getRunState(runId: string): Promise<Record<string, unknown>>;
215
- abstract upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
216
- abstract updateWorkflowVersionStatus(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
247
+ upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
248
+ protected abstract upsertWorkflowVersionImpl(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
249
+ updateWorkflowVersionStatus(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
250
+ protected abstract updateWorkflowVersionStatusImpl(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
217
251
  abstract getWorkflowVersion(name: string, graphHash: string): Promise<{
218
252
  graph: any;
219
253
  source: string;
@@ -228,7 +262,7 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
228
262
  * @param runId - Run ID
229
263
  */
230
264
  resumeWorkflow(runId: string, workflowName?: string): Promise<void>;
231
- queueStepWorker(runId: string, stepName: string, rpcName: string, data: any): Promise<void>;
265
+ queueStepWorker(runId: string, stepName: string, rpcName: string, data: any, stepOptions?: WorkflowStepOptions): Promise<void>;
232
266
  /**
233
267
  * Execute a workflow sleep step completion
234
268
  * Sets the step result to null and resumes the workflow
@@ -241,6 +275,32 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
241
275
  * @param retryDelay - Delay in milliseconds or duration string (optional)
242
276
  */
243
277
  protected scheduleOrchestratorRetry(runId: string, retryDelay?: number | string, workflowName?: string): Promise<void>;
278
+ /**
279
+ * Dispatch a workflow step to be executed asynchronously.
280
+ *
281
+ * Default implementation enqueues a step worker job via the queue service.
282
+ * Subclasses with non-queue transports (e.g. Durable Objects) override this
283
+ * to dispatch via their own mechanism (RPC to a step worker, etc.).
284
+ *
285
+ * On return, the workflow is paused via `WorkflowAsyncException` thrown by
286
+ * the caller; the step transport is responsible for calling back into the
287
+ * orchestrator (via `resumeWorkflow` or equivalent) when the step completes.
288
+ *
289
+ * @returns true if dispatch was async (caller should pause), false to fall
290
+ * through to the inline execution path.
291
+ */
292
+ protected dispatchStep(runId: string, stepName: string, rpcName: string, data: unknown, stepOptions?: WorkflowStepOptions): Promise<boolean>;
293
+ /**
294
+ * Schedule a workflow sleep wakeup at the given duration.
295
+ *
296
+ * Default implementation uses the scheduler service to enqueue a delayed
297
+ * sleeper RPC. Subclasses with native timer primitives (e.g. Durable Object
298
+ * alarms) override this to schedule directly without going through queues.
299
+ *
300
+ * @returns true if the wakeup was scheduled remotely (caller should pause),
301
+ * false to fall through to inline `setTimeout` behavior.
302
+ */
303
+ protected scheduleSleep(runId: string, stepId: string, duration: number | string): Promise<boolean>;
244
304
  /**
245
305
  * Start a new workflow run
246
306
  * Automatically detects workflow type (DSL or graph) from meta and executes accordingly
@@ -284,6 +344,11 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
284
344
  * Get the orchestrator queue name for a specific workflow.
285
345
  * Checks queue meta for a per-workflow queue first (e.g. wf-orchestrator-{name}),
286
346
  * falls back to the shared orchestrator queue.
347
+ *
348
+ * Reads from `queue.meta` (always populated globally) rather than
349
+ * `queue.registrations` (only populated for queues this unit consumes).
350
+ * In a per-unit deploy the orchestrator unit doesn't consume per-step
351
+ * queues — but it produces to them — so registrations would miss them.
287
352
  */
288
353
  protected getOrchestratorQueueName(workflowName?: string): string;
289
354
  protected getStepWorkerQueueName(rpcName?: string): string;
@@ -92,6 +92,17 @@ addError(WorkflowRunNotFoundError, {
92
92
  status: 404,
93
93
  message: 'Workflow run not found.',
94
94
  });
95
+ export class WorkflowRunFailedError extends PikkuError {
96
+ payload;
97
+ constructor(message) {
98
+ super(`Workflow run failed: ${message ?? 'unknown'}`);
99
+ this.payload = { message };
100
+ }
101
+ }
102
+ addError(WorkflowRunFailedError, {
103
+ status: 422,
104
+ message: 'Workflow run failed.',
105
+ });
95
106
  export class WorkflowServiceNotInitialized extends Error {
96
107
  }
97
108
  export class WorkflowStepNameNotString extends Error {
@@ -115,7 +126,38 @@ export class PikkuWorkflowService {
115
126
  get logger() {
116
127
  return getSingletonServices()?.logger;
117
128
  }
118
- constructor() {
129
+ mirror;
130
+ constructor(options = {}) {
131
+ const wireQueues = options.wireQueues ?? true;
132
+ this.mirror = options.mirror;
133
+ if (wireQueues) {
134
+ this.wireQueueWorkers();
135
+ }
136
+ }
137
+ async safeMirror(fn) {
138
+ if (!this.mirror)
139
+ return;
140
+ try {
141
+ await fn();
142
+ }
143
+ catch (err) {
144
+ try {
145
+ this.logger?.warn?.(`[pikku] WorkflowRunMirror write failed: ${err?.message ?? err}`);
146
+ }
147
+ catch {
148
+ // logger unavailable (e.g. singleton services not initialized) — swallow
149
+ }
150
+ }
151
+ }
152
+ rewireQueueWorkers() {
153
+ this.wireQueueWorkers();
154
+ }
155
+ /**
156
+ * Wire the queue-based orchestrator/step/sleeper workers.
157
+ * Subclasses that orchestrate without queues (e.g. Durable Objects) should
158
+ * pass `wireQueues: false` to the base constructor and skip this entirely.
159
+ */
160
+ wireQueueWorkers() {
119
161
  const functions = pikkuState(null, 'function', 'functions');
120
162
  const functionsMeta = pikkuState(null, 'function', 'meta');
121
163
  // Minimal meta for internal workflow functions (satisfies FunctionMeta)
@@ -170,6 +212,9 @@ export class PikkuWorkflowService {
170
212
  func: pikkuWorkflowSleeperFunc,
171
213
  });
172
214
  }
215
+ if (!functionsMeta.pikkuWorkflowSleeper) {
216
+ functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper');
217
+ }
173
218
  }
174
219
  /**
175
220
  * Check if a run is executing inline (without queues)
@@ -197,6 +242,11 @@ export class PikkuWorkflowService {
197
242
  await this.upsertWorkflowVersion(name, meta.graphHash, meta, meta.source);
198
243
  }
199
244
  }
245
+ async createRun(workflowName, input, inline, graphHash, wire, options) {
246
+ const runId = await this.createRunImpl(workflowName, input, inline, graphHash, wire, options);
247
+ await this.safeMirror(() => this.mirror.createRun(runId, workflowName, input, inline, graphHash, wire, options));
248
+ return runId;
249
+ }
200
250
  /**
201
251
  * Get minimal workflow run status with step summaries.
202
252
  * Used by the public API — the console addon provides the full verbose view.
@@ -240,6 +290,126 @@ export class PikkuWorkflowService {
240
290
  : undefined,
241
291
  };
242
292
  }
293
+ /**
294
+ * Update workflow run status
295
+ * @param id - Run ID
296
+ * @param status - New status
297
+ */
298
+ async updateRunStatus(id, status, output, error) {
299
+ await this.updateRunStatusImpl(id, status, output, error);
300
+ await this.safeMirror(() => this.mirror.updateRunStatus(id, status, output, error));
301
+ }
302
+ /**
303
+ * Insert initial step state (called by orchestrator)
304
+ * Creates pending step in both workflow_step and workflow_step_history
305
+ * @param runId - Run ID
306
+ * @param stepName - Step cache key
307
+ * @param rpcName - RPC function name
308
+ * @param data - Step input data
309
+ * @param stepOptions - Step options (retries, retryDelay)
310
+ * @returns Step state with generated stepId
311
+ */
312
+ async insertStepState(runId, stepName, rpcName, data, stepOptions) {
313
+ const step = await this.insertStepStateImpl(runId, stepName, rpcName, data, stepOptions);
314
+ await this.safeMirror(() => this.mirror.insertStepState(runId, { ...step, stepName, rpcName, data }));
315
+ return step;
316
+ }
317
+ /**
318
+ * Mark step as running
319
+ * Updates both workflow_step and workflow_step_history
320
+ * @param stepId - Step ID
321
+ */
322
+ async setStepRunning(stepId) {
323
+ await this.setStepRunningImpl(stepId);
324
+ await this.safeMirror(() => this.mirror.setStepRunning(stepId));
325
+ }
326
+ /**
327
+ * Mark step as scheduled (queued for execution)
328
+ * Updates both workflow_step and workflow_step_history
329
+ * @param stepId - Step ID
330
+ */
331
+ async setStepScheduled(stepId) {
332
+ await this.setStepScheduledImpl(stepId);
333
+ await this.safeMirror(() => this.mirror.setStepScheduled(stepId));
334
+ }
335
+ /**
336
+ * Store step result and mark as succeeded
337
+ * Updates both workflow_step and workflow_step_history
338
+ * @param stepId - Step ID
339
+ * @param result - Step result
340
+ */
341
+ async setStepResult(stepId, result) {
342
+ await this.setStepResultImpl(stepId, result);
343
+ await this.safeMirror(() => this.mirror.setStepResult(stepId, result));
344
+ }
345
+ /**
346
+ * Set the child workflow run ID on a step
347
+ * @param stepId - Step ID
348
+ * @param childRunId - Child workflow run ID
349
+ */
350
+ async setStepChildRunId(stepId, childRunId) {
351
+ await this.setStepChildRunIdImpl(stepId, childRunId);
352
+ await this.safeMirror(() => this.mirror.setStepChildRunId(stepId, childRunId));
353
+ }
354
+ /**
355
+ * Store step error and mark as failed
356
+ * Updates both workflow_step and workflow_step_history
357
+ * @param stepId - Step ID
358
+ * @param error - Error object
359
+ */
360
+ async setStepError(stepId, error) {
361
+ await this.setStepErrorImpl(stepId, error);
362
+ const serialized = {
363
+ message: error.message,
364
+ stack: error.stack,
365
+ code: error.code,
366
+ };
367
+ await this.safeMirror(() => this.mirror.setStepError(stepId, serialized));
368
+ }
369
+ /**
370
+ * Create a new retry attempt for a failed step
371
+ * Inserts new pending step in both workflow_step and workflow_step_history
372
+ * Resets status to 'pending' with new stepId
373
+ * Copies metadata (rpcName, data, retries, retryDelay) from failed attempt
374
+ * @param failedStepId - Failed step ID to copy from
375
+ * @returns New step state for the retry attempt
376
+ */
377
+ async createRetryAttempt(failedStepId, status) {
378
+ const newStep = await this.createRetryAttemptImpl(failedStepId, status);
379
+ const stepName = newStep.stepName ?? '';
380
+ await this.safeMirror(() => this.mirror.createRetryAttempt(failedStepId, {
381
+ ...newStep,
382
+ stepName,
383
+ }));
384
+ return newStep;
385
+ }
386
+ /**
387
+ * Set the branch key for a graph node step
388
+ * @param stepId - Step ID
389
+ * @param branchKey - Branch key selected by graph.branch()
390
+ */
391
+ async setBranchTaken(stepId, branchKey) {
392
+ await this.setBranchTakenImpl(stepId, branchKey);
393
+ await this.safeMirror(() => this.mirror.setBranchTaken(stepId, branchKey));
394
+ }
395
+ /**
396
+ * Update a state variable in the workflow run's state
397
+ * @param runId - Run ID
398
+ * @param name - Variable name
399
+ * @param value - Value to store
400
+ */
401
+ async updateRunState(runId, name, value) {
402
+ await this.updateRunStateImpl(runId, name, value);
403
+ await this.safeMirror(() => this.mirror.updateRunState(runId, name, value));
404
+ }
405
+ async upsertWorkflowVersion(name, graphHash, graph, source, status) {
406
+ await this.upsertWorkflowVersionImpl(name, graphHash, graph, source, status);
407
+ await this.safeMirror(() => this.mirror.upsertWorkflowVersion(name, graphHash, graph, source, status));
408
+ }
409
+ async updateWorkflowVersionStatus(name, graphHash, status) {
410
+ await this.updateWorkflowVersionStatusImpl(name, graphHash, status);
411
+ await this.safeMirror(() => this.mirror.updateWorkflowVersionStatus(name, graphHash, status));
412
+ }
243
413
  // ============================================================================
244
414
  // Workflow Lifecycle Methods
245
415
  // ============================================================================
@@ -257,14 +427,25 @@ export class PikkuWorkflowService {
257
427
  runId,
258
428
  });
259
429
  }
260
- async queueStepWorker(runId, stepName, rpcName, data) {
430
+ async queueStepWorker(runId, stepName, rpcName, data, stepOptions) {
261
431
  const queueService = this.verifyQueueService();
432
+ const retries = stepOptions?.retries ?? 0;
433
+ const retryDelay = stepOptions?.retryDelay;
262
434
  await queueService.add(this.getStepWorkerQueueName(rpcName), JSON.parse(JSON.stringify({
263
435
  runId,
264
436
  stepName,
265
437
  rpcName,
266
438
  data,
267
- })));
439
+ })), retries > 0 || retryDelay
440
+ ? {
441
+ attempts: retries + 1,
442
+ backoff: typeof retryDelay === 'number'
443
+ ? { type: 'fixed', delay: retryDelay }
444
+ : retryDelay === 'exponential'
445
+ ? 'exponential'
446
+ : undefined,
447
+ }
448
+ : undefined);
268
449
  }
269
450
  /**
270
451
  * Execute a workflow sleep step completion
@@ -288,6 +469,58 @@ export class PikkuWorkflowService {
288
469
  }
289
470
  await queueService.add(this.getOrchestratorQueueName(workflowName), { runId }, retryDelay ? { delay: getDurationInMilliseconds(retryDelay) } : undefined);
290
471
  }
472
+ /**
473
+ * Dispatch a workflow step to be executed asynchronously.
474
+ *
475
+ * Default implementation enqueues a step worker job via the queue service.
476
+ * Subclasses with non-queue transports (e.g. Durable Objects) override this
477
+ * to dispatch via their own mechanism (RPC to a step worker, etc.).
478
+ *
479
+ * On return, the workflow is paused via `WorkflowAsyncException` thrown by
480
+ * the caller; the step transport is responsible for calling back into the
481
+ * orchestrator (via `resumeWorkflow` or equivalent) when the step completes.
482
+ *
483
+ * @returns true if dispatch was async (caller should pause), false to fall
484
+ * through to the inline execution path.
485
+ */
486
+ async dispatchStep(runId, stepName, rpcName, data, stepOptions) {
487
+ if (this.isInline(runId) || !getSingletonServices()?.queueService) {
488
+ return false;
489
+ }
490
+ const retries = stepOptions?.retries ?? 0;
491
+ const retryDelay = stepOptions?.retryDelay;
492
+ await getSingletonServices().queueService.add(this.getStepWorkerQueueName(rpcName), JSON.parse(JSON.stringify({
493
+ runId,
494
+ stepName,
495
+ rpcName,
496
+ data,
497
+ })), {
498
+ attempts: retries + 1,
499
+ backoff: typeof retryDelay === 'number'
500
+ ? { type: 'fixed', delay: retryDelay }
501
+ : retryDelay === 'exponential'
502
+ ? 'exponential'
503
+ : undefined,
504
+ });
505
+ return true;
506
+ }
507
+ /**
508
+ * Schedule a workflow sleep wakeup at the given duration.
509
+ *
510
+ * Default implementation uses the scheduler service to enqueue a delayed
511
+ * sleeper RPC. Subclasses with native timer primitives (e.g. Durable Object
512
+ * alarms) override this to schedule directly without going through queues.
513
+ *
514
+ * @returns true if the wakeup was scheduled remotely (caller should pause),
515
+ * false to fall through to inline `setTimeout` behavior.
516
+ */
517
+ async scheduleSleep(runId, stepId, duration) {
518
+ if (this.isInline(runId) || !getSingletonServices()?.schedulerService) {
519
+ return false;
520
+ }
521
+ await getSingletonServices().schedulerService.scheduleRPC(duration, this.getConfig().sleeperRPCName, { runId, stepId });
522
+ return true;
523
+ }
291
524
  /**
292
525
  * Start a new workflow run
293
526
  * Automatically detects workflow type (DSL or graph) from meta and executes accordingly
@@ -341,7 +574,13 @@ export class PikkuWorkflowService {
341
574
  if (error.name !== 'WorkflowAsyncException' &&
342
575
  error.name !== 'WorkflowCancelledException' &&
343
576
  error.name !== 'WorkflowSuspendedException') {
577
+ await this.updateRunStatus(runId, 'failed', undefined, {
578
+ name: error.name,
579
+ message: error.message,
580
+ stack: error.stack,
581
+ });
344
582
  getSingletonServices().logger.error(`Workflow ${name} (run ${runId}) failed:`, error);
583
+ throw error;
345
584
  }
346
585
  }
347
586
  finally {
@@ -363,7 +602,7 @@ export class PikkuWorkflowService {
363
602
  }
364
603
  if (WORKFLOW_END_STATES.has(run.status)) {
365
604
  if (run.status === 'failed') {
366
- throw new Error(run.error?.message || 'Workflow failed');
605
+ throw new WorkflowRunFailedError(run.error?.message);
367
606
  }
368
607
  if (run.status === 'cancelled') {
369
608
  throw new Error('Workflow was cancelled');
@@ -677,31 +916,14 @@ export class PikkuWorkflowService {
677
916
  }
678
917
  // Step is pending - schedule it
679
918
  await this.setStepScheduled(stepState.stepId);
680
- // Enqueue step worker (unless inline mode)
681
- if (!this.isInline(runId) && getSingletonServices().queueService) {
682
- // Map step retry options to queue job options
683
- const retries = stepOptions?.retries ?? 0;
684
- const retryDelay = stepOptions?.retryDelay;
685
- await getSingletonServices().queueService.add(this.getStepWorkerQueueName(rpcName), JSON.parse(JSON.stringify({
686
- runId,
687
- stepName,
688
- rpcName,
689
- data,
690
- })), {
691
- // attempts includes initial attempt, retries doesn't
692
- attempts: retries + 1,
693
- // Map retry delay to backoff
694
- backoff: typeof retryDelay === 'number'
695
- ? { type: 'fixed', delay: retryDelay }
696
- : retryDelay === 'exponential'
697
- ? 'exponential'
698
- : undefined,
699
- });
700
- // Pause workflow - step will callback when done
919
+ // Hand off to subclass-overridable transport. Default behavior enqueues
920
+ // via the queue service; DO-style subclasses RPC to a step worker.
921
+ const dispatched = await this.dispatchStep(runId, stepName, rpcName, data, stepOptions);
922
+ if (dispatched) {
701
923
  throw new WorkflowAsyncException(runId, stepName);
702
924
  }
703
- else {
704
- // Inline or no queue service - execute locally with retry loop
925
+ {
926
+ // Inline (no transport available) - execute locally with retry loop
705
927
  const retries = stepOptions?.retries ?? 0;
706
928
  const retryDelay = stepOptions?.retryDelay;
707
929
  let currentStepState = stepState;
@@ -873,22 +1095,16 @@ export class PikkuWorkflowService {
873
1095
  }
874
1096
  // Step is pending - schedule it
875
1097
  await this.setStepScheduled(stepState.stepId);
876
- // Check if inline mode or no scheduler service
877
- if (!this.isInline(runId) && getSingletonServices().schedulerService) {
878
- // Remote mode - schedule sleep via scheduler service
879
- await getSingletonServices().schedulerService.scheduleRPC(duration, this.getConfig().sleeperRPCName, {
880
- runId,
881
- stepId: stepState.stepId,
882
- });
883
- // Pause workflow - sleep will callback when done
1098
+ // Hand off to subclass-overridable transport. Default behavior schedules
1099
+ // a delayed sleeper RPC via the scheduler service; DO-style subclasses
1100
+ // override to use native timer primitives (e.g. setAlarm).
1101
+ const scheduled = await this.scheduleSleep(runId, stepState.stepId, duration);
1102
+ if (scheduled) {
884
1103
  throw new WorkflowAsyncException(runId, stepName);
885
1104
  }
886
- else {
887
- // Inline mode - use setTimeout with actual duration
888
- await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(duration)));
889
- await this.setStepResult(stepState.stepId, null);
890
- return;
891
- }
1105
+ // Inline mode - use setTimeout with actual duration
1106
+ await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(duration)));
1107
+ await this.setStepResult(stepState.stepId, null);
892
1108
  }
893
1109
  getSuspendStepName(reason) {
894
1110
  if (!reason) {
@@ -975,12 +1191,17 @@ export class PikkuWorkflowService {
975
1191
  * Get the orchestrator queue name for a specific workflow.
976
1192
  * Checks queue meta for a per-workflow queue first (e.g. wf-orchestrator-{name}),
977
1193
  * falls back to the shared orchestrator queue.
1194
+ *
1195
+ * Reads from `queue.meta` (always populated globally) rather than
1196
+ * `queue.registrations` (only populated for queues this unit consumes).
1197
+ * In a per-unit deploy the orchestrator unit doesn't consume per-step
1198
+ * queues — but it produces to them — so registrations would miss them.
978
1199
  */
979
1200
  getOrchestratorQueueName(workflowName) {
980
1201
  if (workflowName) {
981
1202
  const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`;
982
- const registrations = pikkuState(null, 'queue', 'registrations');
983
- if (registrations.has(perWorkflow)) {
1203
+ const meta = pikkuState(null, 'queue', 'meta');
1204
+ if (meta[perWorkflow]) {
984
1205
  return perWorkflow;
985
1206
  }
986
1207
  }
@@ -989,8 +1210,8 @@ export class PikkuWorkflowService {
989
1210
  getStepWorkerQueueName(rpcName) {
990
1211
  if (rpcName) {
991
1212
  const perStep = `wf-step-${toKebab(rpcName)}`;
992
- const registrations = pikkuState(null, 'queue', 'registrations');
993
- if (registrations.has(perStep)) {
1213
+ const meta = pikkuState(null, 'queue', 'meta');
1214
+ if (meta[perStep]) {
994
1215
  return perStep;
995
1216
  }
996
1217
  }
@@ -143,6 +143,40 @@ export interface WorkflowRunService {
143
143
  }>>;
144
144
  deleteRun(id: string): Promise<boolean>;
145
145
  }
146
+ /**
147
+ * Write-only companion to `WorkflowRunService`. An executor (a
148
+ * `PikkuWorkflowService` subclass) can be given a mirror; every write
149
+ * the executor makes to its own canonical store is then forwarded here,
150
+ * so an external read store (e.g. a DB queried by the console UI) stays
151
+ * in sync with DO/Redis/etc-driven runs.
152
+ *
153
+ * Mirror failures are logged but never fail the workflow — the mirror is
154
+ * an index, not the source of truth.
155
+ */
156
+ export interface WorkflowRunMirror {
157
+ createRun(runId: string, workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
158
+ deterministic?: boolean;
159
+ plannedSteps?: WorkflowPlannedStep[];
160
+ }): Promise<void>;
161
+ updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
162
+ insertStepState(runId: string, step: StepState & {
163
+ stepName: string;
164
+ rpcName: string | null;
165
+ data: any;
166
+ }): Promise<void>;
167
+ setStepRunning(stepId: string): Promise<void>;
168
+ setStepScheduled(stepId: string): Promise<void>;
169
+ setStepResult(stepId: string, result: any): Promise<void>;
170
+ setStepChildRunId(stepId: string, childRunId: string): Promise<void>;
171
+ setStepError(stepId: string, error: SerializedError): Promise<void>;
172
+ createRetryAttempt(failedStepId: string, newStep: StepState & {
173
+ stepName: string;
174
+ }): Promise<void>;
175
+ setBranchTaken(stepId: string, branchKey: string): Promise<void>;
176
+ updateRunState(runId: string, name: string, value: unknown): Promise<void>;
177
+ upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
178
+ updateWorkflowVersionStatus(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
179
+ }
146
180
  /**
147
181
  * Core workflow definition
148
182
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.19",
3
+ "version": "0.12.21",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",