@pikku/core 0.12.69 → 0.12.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +34 -2
- package/dist/function/functions.types.d.ts +27 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/pikku-state.js +1 -0
- package/dist/services/http-scenario-actors.d.ts +12 -4
- package/dist/services/http-scenario-actors.js +47 -45
- package/dist/services/in-memory-queue-service.d.ts +6 -0
- package/dist/services/in-memory-queue-service.js +8 -1
- package/dist/services/in-memory-workflow-service.d.ts +3 -5
- package/dist/services/in-memory-workflow-service.js +10 -19
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.js +1 -0
- package/dist/services/meta-service.d.ts +5 -1
- package/dist/services/meta-service.js +44 -18
- package/dist/services/scenario-actors-service.d.ts +108 -2
- package/dist/services/scenario-actors-service.js +40 -1
- package/dist/services/workflow-service.d.ts +7 -5
- package/dist/types/core.types.d.ts +28 -3
- package/dist/types/state.types.d.ts +3 -1
- package/dist/wirings/actor-flow/actor-flow.types.d.ts +1 -1
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/actor-flow/run-conversation.d.ts +10 -10
- package/dist/wirings/actor-flow/run-conversation.js +27 -27
- package/dist/wirings/ai-agent/ai-agent-agui.js +0 -8
- package/dist/wirings/ai-agent/ai-agent-prepare.js +1 -2
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -6
- package/dist/wirings/cli/command-parser.js +11 -1
- package/dist/wirings/rpc/rpc-runner.js +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +52 -3
- package/dist/wirings/workflow/feature.d.ts +28 -0
- package/dist/wirings/workflow/feature.js +57 -0
- package/dist/wirings/workflow/graph/graph-runner.js +3 -2
- package/dist/wirings/workflow/graph/graph-validation.d.ts +0 -2
- package/dist/wirings/workflow/graph/graph-validation.js +0 -142
- package/dist/wirings/workflow/graph/index.d.ts +1 -1
- package/dist/wirings/workflow/graph/index.js +1 -1
- package/dist/wirings/workflow/index.d.ts +13 -3
- package/dist/wirings/workflow/index.js +15 -2
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +121 -0
- package/dist/wirings/workflow/pikku-scenario-service.js +419 -0
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +170 -23
- package/dist/wirings/workflow/pikku-workflow-service.js +338 -297
- package/dist/wirings/workflow/scenario-cookie-jar.d.ts +29 -0
- package/dist/wirings/workflow/scenario-cookie-jar.js +51 -0
- package/dist/wirings/workflow/scenario-poll.d.ts +20 -0
- package/dist/wirings/workflow/scenario-poll.js +25 -0
- package/dist/wirings/workflow/scenario-prose.d.ts +38 -0
- package/dist/wirings/workflow/scenario-prose.js +45 -0
- package/dist/wirings/workflow/scenario-step-guards.d.ts +16 -0
- package/dist/wirings/workflow/scenario-step-guards.js +29 -0
- package/dist/wirings/workflow/scenario-step.types.d.ts +148 -0
- package/dist/wirings/workflow/scenario-step.types.js +1 -0
- package/dist/wirings/workflow/workflow.types.d.ts +82 -8
- package/package.json +3 -1
- package/src/function/functions.types.ts +32 -0
- package/src/index.ts +1 -0
- package/src/internal.ts +5 -1
- package/src/pikku-state.ts +1 -0
- package/src/services/http-scenario-actors.test.ts +85 -1
- package/src/services/http-scenario-actors.ts +65 -51
- package/src/services/in-memory-queue-service.test.ts +66 -1
- package/src/services/in-memory-queue-service.ts +13 -2
- package/src/services/in-memory-workflow-service.ts +12 -25
- package/src/services/index.ts +5 -0
- package/src/services/meta-service.test.ts +79 -0
- package/src/services/meta-service.ts +61 -26
- package/src/services/scenario-actors-service.ts +157 -2
- package/src/services/workflow-service.ts +7 -4
- package/src/types/core.types.ts +34 -2
- package/src/types/state.types.ts +3 -0
- package/src/wirings/actor-flow/actor-flow.types.ts +1 -1
- package/src/wirings/actor-flow/index.ts +1 -1
- package/src/wirings/actor-flow/run-conversation.test.ts +12 -6
- package/src/wirings/actor-flow/run-conversation.ts +36 -41
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +0 -16
- package/src/wirings/ai-agent/ai-agent-agui.ts +0 -9
- package/src/wirings/ai-agent/ai-agent-prepare.ts +1 -2
- package/src/wirings/ai-agent/ai-agent.types.ts +0 -7
- package/src/wirings/cli/command-parser.test.ts +60 -0
- package/src/wirings/cli/command-parser.ts +12 -1
- package/src/wirings/rpc/rpc-runner.test.ts +28 -5
- package/src/wirings/rpc/rpc-runner.ts +1 -1
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +86 -2
- package/src/wirings/workflow/feature.test.ts +131 -0
- package/src/wirings/workflow/feature.ts +78 -0
- package/src/wirings/workflow/graph/graph-runner.ts +3 -2
- package/src/wirings/workflow/graph/graph-validation.test.ts +1 -144
- package/src/wirings/workflow/graph/graph-validation.ts +0 -196
- package/src/wirings/workflow/graph/index.ts +1 -5
- package/src/wirings/workflow/index.ts +73 -6
- package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +55 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +572 -419
- package/src/wirings/workflow/scenario-cookie-jar.test.ts +108 -0
- package/src/wirings/workflow/scenario-cookie-jar.ts +65 -0
- package/src/wirings/workflow/scenario-expectations.test.ts +153 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +212 -0
- package/src/wirings/workflow/scenario-poll.test.ts +66 -0
- package/src/wirings/workflow/scenario-poll.ts +36 -0
- package/src/wirings/workflow/scenario-prose.test.ts +152 -0
- package/src/wirings/workflow/scenario-prose.ts +79 -0
- package/src/wirings/workflow/scenario-service.test.ts +155 -0
- package/src/wirings/workflow/scenario-step-guards.ts +43 -0
- package/src/wirings/workflow/scenario-step.test.ts +442 -9
- package/src/wirings/workflow/scenario-step.types.ts +157 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-payload.test.ts +59 -0
- package/src/wirings/workflow/workflow-mirror.test.ts +178 -0
- package/src/wirings/workflow/workflow-replay-snapshot.test.ts +139 -0
- package/src/wirings/workflow/workflow-run-context.test.ts +177 -0
- package/src/wirings/workflow/workflow-run-polling.test.ts +132 -0
- package/src/wirings/workflow/workflow-step-ordinal.test.ts +4 -4
- package/src/wirings/workflow/workflow.types.ts +99 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
pikkuState,
|
|
12
12
|
} from '../../pikku-state.js'
|
|
13
13
|
import { getDurationInMilliseconds } from '../../time-utils.js'
|
|
14
|
-
import { createHttpScenarioActors } from '../../services/http-scenario-actors.js'
|
|
15
14
|
|
|
16
15
|
const resolveWorkflowMeta = (
|
|
17
16
|
name: string
|
|
@@ -46,10 +45,10 @@ const toKebab = (s: string) =>
|
|
|
46
45
|
s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
47
46
|
import type { PikkuWire, SerializedError } from '../../types/core.types.js'
|
|
48
47
|
import type { QueueService } from '../queue/queue.types.js'
|
|
49
|
-
import { runScheduledTask } from '../scheduler/scheduler-runner.js'
|
|
50
48
|
import type {
|
|
51
49
|
ApprovalOutcome,
|
|
52
|
-
|
|
50
|
+
CoreWorkflow,
|
|
51
|
+
PikkuWorkflowWire,
|
|
53
52
|
StepState,
|
|
54
53
|
StepStatus,
|
|
55
54
|
WorkflowApprovalOptions,
|
|
@@ -63,9 +62,6 @@ import type {
|
|
|
63
62
|
WorkflowQueueOptions,
|
|
64
63
|
WorkflowServiceConfig,
|
|
65
64
|
WorkflowStepOptions,
|
|
66
|
-
WorkflowExpectEventuallyOptions,
|
|
67
|
-
WorkflowExpectErrorOptions,
|
|
68
|
-
WorkflowExpectServiceOptions,
|
|
69
65
|
} from './workflow.types.js'
|
|
70
66
|
import {
|
|
71
67
|
continueGraph,
|
|
@@ -246,16 +242,174 @@ const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
|
|
|
246
242
|
'suspended',
|
|
247
243
|
])
|
|
248
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Everything an extension needs to run something of its own around a run's
|
|
247
|
+
* function: the registration it was resolved from, the run itself (so `input`
|
|
248
|
+
* is the same object the function is called with), and the wire it is given.
|
|
249
|
+
*/
|
|
250
|
+
export interface RunLifecycleContext {
|
|
251
|
+
runId: string
|
|
252
|
+
run: WorkflowRun
|
|
253
|
+
workflowMeta: any
|
|
254
|
+
workflow: CoreWorkflow
|
|
255
|
+
wire: PikkuWire
|
|
256
|
+
packageName: string | null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The slice of the run engine an extension is allowed to drive, handed to it at
|
|
261
|
+
* construction. It exists so that recording a durable step stays available to
|
|
262
|
+
* an extension without `inlineStep` and friends becoming public API on every
|
|
263
|
+
* workflow service a production app instantiates.
|
|
264
|
+
*/
|
|
265
|
+
export interface WorkflowRunEngine {
|
|
266
|
+
inlineStep(
|
|
267
|
+
runId: string,
|
|
268
|
+
logicalStepName: string,
|
|
269
|
+
fn: Function,
|
|
270
|
+
stepOptions?: WorkflowStepOptions,
|
|
271
|
+
data?: any,
|
|
272
|
+
funcName?: string
|
|
273
|
+
): Promise<any>
|
|
274
|
+
updateRunStatus(
|
|
275
|
+
runId: string,
|
|
276
|
+
status: WorkflowStatus,
|
|
277
|
+
output?: any,
|
|
278
|
+
error?: SerializedError
|
|
279
|
+
): Promise<void>
|
|
280
|
+
onChildWorkflowFailed(run: WorkflowRun, error: unknown): Promise<void>
|
|
281
|
+
verifyStepName(stepName: unknown): void
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A capability layered onto a run without being a workflow service itself.
|
|
286
|
+
*
|
|
287
|
+
* The engine names nothing about what an extension is for: a bundler drops an
|
|
288
|
+
* unused *module* but never an unused class member, so the alternative — a
|
|
289
|
+
* subclass carrying the capability — puts it in every server built on Pikku
|
|
290
|
+
* whether or not the app ever uses it. Scenarios are the one implementation
|
|
291
|
+
* today (`PikkuScenarioService` in `@pikku/core/scenario`).
|
|
292
|
+
*/
|
|
293
|
+
export interface WorkflowRunExtension {
|
|
294
|
+
/** Per-run state resolved once the run has an id. */
|
|
295
|
+
attachRunContext(
|
|
296
|
+
runId: string,
|
|
297
|
+
workflowMeta: any,
|
|
298
|
+
options?: Record<string, any>
|
|
299
|
+
): Promise<void>
|
|
300
|
+
/** Release whatever `attachRunContext` stored. */
|
|
301
|
+
detachRunContext(runId: string): void
|
|
302
|
+
/** Contribute extra members to the wire a run's function is invoked with. */
|
|
303
|
+
decorateRunWire(
|
|
304
|
+
wire: PikkuWire,
|
|
305
|
+
context: {
|
|
306
|
+
runId: string
|
|
307
|
+
workflowMeta: any
|
|
308
|
+
workflowWire: PikkuWorkflowWire
|
|
309
|
+
}
|
|
310
|
+
): void
|
|
311
|
+
/** Contribute extra members to the workflow wire itself, in place. */
|
|
312
|
+
decorateWorkflowWire(
|
|
313
|
+
workflowWire: PikkuWorkflowWire,
|
|
314
|
+
context: {
|
|
315
|
+
name: string
|
|
316
|
+
runId: string
|
|
317
|
+
rpcService: any
|
|
318
|
+
addonNamespace?: string | null
|
|
319
|
+
}
|
|
320
|
+
): void
|
|
321
|
+
/**
|
|
322
|
+
* Called immediately before the run's function, inside the run lock. Throwing
|
|
323
|
+
* skips the function and fails the run.
|
|
324
|
+
*/
|
|
325
|
+
onBeforeRunFunc(context: RunLifecycleContext): Promise<void>
|
|
326
|
+
/**
|
|
327
|
+
* Called once the run reaches a terminal state, in a `finally`, so it runs
|
|
328
|
+
* whether the function passed or threw. `interrupted` means the run suspended
|
|
329
|
+
* or went async and is still mid-flight. Throwing here propagates, replacing
|
|
330
|
+
* a pending failure, so an extension that must not mask one handles that
|
|
331
|
+
* itself.
|
|
332
|
+
*/
|
|
333
|
+
onAfterRunFunc(
|
|
334
|
+
context: RunLifecycleContext,
|
|
335
|
+
outcome: 'completed' | 'failed' | 'interrupted',
|
|
336
|
+
failure: unknown
|
|
337
|
+
): Promise<void>
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* States a run never leaves. `suspended` is deliberately absent: a suspended
|
|
342
|
+
* run stops a poll loop but can still be resumed, so anything the process holds
|
|
343
|
+
* for it has to survive.
|
|
344
|
+
*/
|
|
345
|
+
const WORKFLOW_TERMINAL_STATES: ReadonlySet<string> = new Set([
|
|
346
|
+
'completed',
|
|
347
|
+
'failed',
|
|
348
|
+
'cancelled',
|
|
349
|
+
])
|
|
350
|
+
|
|
351
|
+
/** First wait when polling a run, before the backoff starts widening it. */
|
|
352
|
+
const WORKFLOW_POLL_MIN_MS = 10
|
|
353
|
+
|
|
354
|
+
/** How much each successive wait grows, up to the caller's ceiling. */
|
|
355
|
+
const WORKFLOW_POLL_FACTOR = 1.6
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Ceiling for the wait on an inline sub-workflow. Lower than a top-level run's
|
|
359
|
+
* default, because the parent step is blocked on it and every wait here is
|
|
360
|
+
* added latency in the middle of a workflow rather than at its edge.
|
|
361
|
+
*/
|
|
362
|
+
const WORKFLOW_CHILD_POLL_MAX_MS = 500
|
|
363
|
+
|
|
249
364
|
/**
|
|
250
365
|
* Abstract workflow state service
|
|
251
366
|
* Implementations provide pluggable storage backends (SQLite, PostgreSQL, etc.)
|
|
252
367
|
* Combines orchestration and step execution
|
|
253
368
|
*/
|
|
369
|
+
/**
|
|
370
|
+
* Everything the engine holds in memory for a run that is executing in this
|
|
371
|
+
* process. One entry, one lifetime: created when the run starts executing here
|
|
372
|
+
* and dropped when nothing is holding it open any more.
|
|
373
|
+
*
|
|
374
|
+
* The `replay` half is rebuilt from scratch on every orchestrator tick; the
|
|
375
|
+
* rest outlives individual ticks and belongs to whoever started the run.
|
|
376
|
+
*/
|
|
377
|
+
type RunContext = {
|
|
378
|
+
/** Executing straight through in-process, without a queue. */
|
|
379
|
+
inline: boolean
|
|
380
|
+
replay?: {
|
|
381
|
+
/** How many times this walk has reached each logical step name. */
|
|
382
|
+
ordinals: Map<string, number>
|
|
383
|
+
/** The step key the walk last reached — the next step's predecessor. */
|
|
384
|
+
lastStep?: string
|
|
385
|
+
/** Every step of the run as this replay found it, keyed by step name. */
|
|
386
|
+
steps?: Map<string, StepState>
|
|
387
|
+
/** The run as this replay found it. Only its immutable half is reused. */
|
|
388
|
+
run?: WorkflowRun
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
254
392
|
export abstract class PikkuWorkflowService implements WorkflowService {
|
|
255
|
-
private
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
393
|
+
private runExtension?: WorkflowRunExtension
|
|
394
|
+
|
|
395
|
+
private runContexts = new Map<string, RunContext>()
|
|
396
|
+
|
|
397
|
+
private contextFor(runId: string): RunContext {
|
|
398
|
+
let context = this.runContexts.get(runId)
|
|
399
|
+
if (!context) {
|
|
400
|
+
context = { inline: false }
|
|
401
|
+
this.runContexts.set(runId, context)
|
|
402
|
+
}
|
|
403
|
+
return context
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Drop a run's context once nothing is holding it open. */
|
|
407
|
+
private releaseContext(runId: string): void {
|
|
408
|
+
const context = this.runContexts.get(runId)
|
|
409
|
+
if (!context) return
|
|
410
|
+
if (context.inline || context.replay) return
|
|
411
|
+
this.runContexts.delete(runId)
|
|
412
|
+
}
|
|
259
413
|
|
|
260
414
|
protected get logger() {
|
|
261
415
|
return getSingletonServices()?.logger
|
|
@@ -283,19 +437,36 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
283
437
|
}
|
|
284
438
|
}
|
|
285
439
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Perform a state write, then shadow it to the mirror.
|
|
442
|
+
*
|
|
443
|
+
* The mirror is an observability sink, never a second source of truth, and
|
|
444
|
+
* both halves of that follow from this one shape: it is only ever told about
|
|
445
|
+
* a write that already landed, and a mirror that is down or throwing cannot
|
|
446
|
+
* fail — or even be seen by — the workflow it is watching.
|
|
447
|
+
*
|
|
448
|
+
* @param write - the authoritative write; its result is what the caller gets
|
|
449
|
+
* @param mirror - shadows the write, given the live mirror and what was written
|
|
450
|
+
*/
|
|
451
|
+
private async mirrored<T>(
|
|
452
|
+
write: () => Promise<T>,
|
|
453
|
+
mirror: (mirror: WorkflowRunMirror, written: T) => Promise<void>
|
|
454
|
+
): Promise<T> {
|
|
455
|
+
const written = await write()
|
|
456
|
+
if (this.mirror) {
|
|
291
457
|
try {
|
|
292
|
-
this.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
458
|
+
await mirror(this.mirror, written)
|
|
459
|
+
} catch (err: any) {
|
|
460
|
+
try {
|
|
461
|
+
this.logger?.warn?.(
|
|
462
|
+
`[pikku] WorkflowRunMirror write failed: ${err?.message ?? err}`
|
|
463
|
+
)
|
|
464
|
+
} catch {
|
|
465
|
+
// logger unavailable (e.g. singleton services not initialized) — swallow
|
|
466
|
+
}
|
|
297
467
|
}
|
|
298
468
|
}
|
|
469
|
+
return written
|
|
299
470
|
}
|
|
300
471
|
|
|
301
472
|
/**
|
|
@@ -431,21 +602,24 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
431
602
|
* Check if a run is executing inline (without queues)
|
|
432
603
|
*/
|
|
433
604
|
protected isInline(runId: string): boolean {
|
|
434
|
-
return this.
|
|
605
|
+
return this.runContexts.get(runId)?.inline === true
|
|
435
606
|
}
|
|
436
607
|
|
|
437
608
|
/**
|
|
438
609
|
* Register a run as inline (for graph-runner to use)
|
|
439
610
|
*/
|
|
440
611
|
public registerInlineRun(runId: string): void {
|
|
441
|
-
this.
|
|
612
|
+
this.contextFor(runId).inline = true
|
|
442
613
|
}
|
|
443
614
|
|
|
444
615
|
/**
|
|
445
616
|
* Unregister a run from inline tracking
|
|
446
617
|
*/
|
|
447
618
|
public unregisterInlineRun(runId: string): void {
|
|
448
|
-
this.
|
|
619
|
+
const context = this.runContexts.get(runId)
|
|
620
|
+
if (!context) return
|
|
621
|
+
context.inline = false
|
|
622
|
+
this.releaseContext(runId)
|
|
449
623
|
}
|
|
450
624
|
|
|
451
625
|
public async registerWorkflowVersions(): Promise<void> {
|
|
@@ -467,26 +641,27 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
467
641
|
plannedSteps?: WorkflowPlannedStep[]
|
|
468
642
|
}
|
|
469
643
|
): Promise<string> {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
644
|
+
return this.mirrored(
|
|
645
|
+
() =>
|
|
646
|
+
this.createRunImpl(
|
|
647
|
+
workflowName,
|
|
648
|
+
input,
|
|
649
|
+
inline,
|
|
650
|
+
graphHash,
|
|
651
|
+
wire,
|
|
652
|
+
options
|
|
653
|
+
),
|
|
654
|
+
(mirror, runId) =>
|
|
655
|
+
mirror.createRun(
|
|
656
|
+
runId,
|
|
657
|
+
workflowName,
|
|
658
|
+
input,
|
|
659
|
+
inline,
|
|
660
|
+
graphHash,
|
|
661
|
+
wire,
|
|
662
|
+
options
|
|
663
|
+
)
|
|
488
664
|
)
|
|
489
|
-
return runId
|
|
490
665
|
}
|
|
491
666
|
|
|
492
667
|
protected abstract createRunImpl(
|
|
@@ -613,10 +788,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
613
788
|
output?: any,
|
|
614
789
|
error?: SerializedError
|
|
615
790
|
): Promise<void> {
|
|
616
|
-
await this.
|
|
617
|
-
|
|
618
|
-
|
|
791
|
+
await this.mirrored(
|
|
792
|
+
() => this.updateRunStatusImpl(id, status, output, error),
|
|
793
|
+
(mirror) => mirror.updateRunStatus(id, status, output, error)
|
|
619
794
|
)
|
|
795
|
+
if (WORKFLOW_TERMINAL_STATES.has(status)) {
|
|
796
|
+
// The run is over: release whatever this process opened for it. Queued
|
|
797
|
+
// runs never pass through the inline path that does this, so their
|
|
798
|
+
// context was held for the life of the process.
|
|
799
|
+
this.runExtension?.detachRunContext(id)
|
|
800
|
+
this.releaseContext(id)
|
|
801
|
+
}
|
|
620
802
|
}
|
|
621
803
|
|
|
622
804
|
protected abstract updateRunStatusImpl(
|
|
@@ -631,7 +813,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
631
813
|
* Creates pending step in both workflow_step and workflow_step_history
|
|
632
814
|
* @param runId - Run ID
|
|
633
815
|
* @param stepName - Step cache key
|
|
634
|
-
* @param rpcName - RPC
|
|
816
|
+
* @param rpcName - The name this step was dispatched by: an RPC for a
|
|
817
|
+
* `workflow.do` step, a step function for a scenario step, null for a
|
|
818
|
+
* closure. Nothing dispatches off this value — it is recorded so a reader
|
|
819
|
+
* can join a step back to the function that ran it.
|
|
635
820
|
* @param data - Step input data
|
|
636
821
|
* @param stepOptions - Step options (retries, retryDelay)
|
|
637
822
|
* @returns Step state with generated stepId
|
|
@@ -644,18 +829,19 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
644
829
|
stepOptions?: WorkflowStepOptions,
|
|
645
830
|
fromStepName?: string
|
|
646
831
|
): Promise<StepState> {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
832
|
+
return this.mirrored(
|
|
833
|
+
() =>
|
|
834
|
+
this.insertStepStateImpl(
|
|
835
|
+
runId,
|
|
836
|
+
stepName,
|
|
837
|
+
rpcName,
|
|
838
|
+
data,
|
|
839
|
+
stepOptions,
|
|
840
|
+
fromStepName
|
|
841
|
+
),
|
|
842
|
+
(mirror, step) =>
|
|
843
|
+
mirror.insertStepState(runId, { ...step, stepName, rpcName, data })
|
|
657
844
|
)
|
|
658
|
-
return step
|
|
659
845
|
}
|
|
660
846
|
|
|
661
847
|
protected abstract insertStepStateImpl(
|
|
@@ -681,8 +867,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
681
867
|
* @param stepId - Step ID
|
|
682
868
|
*/
|
|
683
869
|
public async setStepRunning(stepId: string): Promise<void> {
|
|
684
|
-
await this.
|
|
685
|
-
|
|
870
|
+
await this.mirrored(
|
|
871
|
+
() => this.setStepRunningImpl(stepId),
|
|
872
|
+
(mirror) => mirror.setStepRunning(stepId)
|
|
873
|
+
)
|
|
686
874
|
}
|
|
687
875
|
|
|
688
876
|
protected abstract setStepRunningImpl(stepId: string): Promise<void>
|
|
@@ -693,8 +881,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
693
881
|
* @param stepId - Step ID
|
|
694
882
|
*/
|
|
695
883
|
public async setStepScheduled(stepId: string): Promise<void> {
|
|
696
|
-
await this.
|
|
697
|
-
|
|
884
|
+
await this.mirrored(
|
|
885
|
+
() => this.setStepScheduledImpl(stepId),
|
|
886
|
+
(mirror) => mirror.setStepScheduled(stepId)
|
|
887
|
+
)
|
|
698
888
|
}
|
|
699
889
|
|
|
700
890
|
protected abstract setStepScheduledImpl(stepId: string): Promise<void>
|
|
@@ -706,8 +896,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
706
896
|
* @param result - Step result
|
|
707
897
|
*/
|
|
708
898
|
public async setStepResult(stepId: string, result: any): Promise<void> {
|
|
709
|
-
await this.
|
|
710
|
-
|
|
899
|
+
await this.mirrored(
|
|
900
|
+
() => this.setStepResultImpl(stepId, result),
|
|
901
|
+
(mirror) => mirror.setStepResult(stepId, result)
|
|
902
|
+
)
|
|
711
903
|
}
|
|
712
904
|
|
|
713
905
|
protected abstract setStepResultImpl(
|
|
@@ -724,9 +916,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
724
916
|
stepId: string,
|
|
725
917
|
childRunId: string
|
|
726
918
|
): Promise<void> {
|
|
727
|
-
await this.
|
|
728
|
-
|
|
729
|
-
|
|
919
|
+
await this.mirrored(
|
|
920
|
+
() => this.setStepChildRunIdImpl(stepId, childRunId),
|
|
921
|
+
(mirror) => mirror.setStepChildRunId(stepId, childRunId)
|
|
730
922
|
)
|
|
731
923
|
}
|
|
732
924
|
|
|
@@ -742,14 +934,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
742
934
|
* @param error - Error object
|
|
743
935
|
*/
|
|
744
936
|
public async setStepError(stepId: string, error: Error): Promise<void> {
|
|
745
|
-
await this.
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
937
|
+
await this.mirrored(
|
|
938
|
+
() => this.setStepErrorImpl(stepId, error),
|
|
939
|
+
(mirror) => {
|
|
940
|
+
const serialized: SerializedError = {
|
|
941
|
+
message: error.message,
|
|
942
|
+
stack: error.stack,
|
|
943
|
+
code: (error as any).code,
|
|
944
|
+
expected: isExpectedError(error),
|
|
945
|
+
}
|
|
946
|
+
return mirror.setStepError(stepId, serialized)
|
|
947
|
+
}
|
|
948
|
+
)
|
|
753
949
|
}
|
|
754
950
|
|
|
755
951
|
protected abstract setStepErrorImpl(
|
|
@@ -769,15 +965,14 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
769
965
|
failedStepId: string,
|
|
770
966
|
status: 'pending' | 'running'
|
|
771
967
|
): Promise<StepState> {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
968
|
+
return this.mirrored(
|
|
969
|
+
() => this.createRetryAttemptImpl(failedStepId, status),
|
|
970
|
+
(mirror, newStep) =>
|
|
971
|
+
mirror.createRetryAttempt(failedStepId, {
|
|
972
|
+
...newStep,
|
|
973
|
+
stepName: (newStep as any).stepName ?? '',
|
|
974
|
+
})
|
|
779
975
|
)
|
|
780
|
-
return newStep
|
|
781
976
|
}
|
|
782
977
|
|
|
783
978
|
protected abstract createRetryAttemptImpl(
|
|
@@ -871,8 +1066,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
871
1066
|
stepId: string,
|
|
872
1067
|
branchKey: string
|
|
873
1068
|
): Promise<void> {
|
|
874
|
-
await this.
|
|
875
|
-
|
|
1069
|
+
await this.mirrored(
|
|
1070
|
+
() => this.setBranchTakenImpl(stepId, branchKey),
|
|
1071
|
+
(mirror) => mirror.setBranchTaken(stepId, branchKey)
|
|
1072
|
+
)
|
|
876
1073
|
}
|
|
877
1074
|
|
|
878
1075
|
protected abstract setBranchTakenImpl(
|
|
@@ -891,8 +1088,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
891
1088
|
name: string,
|
|
892
1089
|
value: unknown
|
|
893
1090
|
): Promise<void> {
|
|
894
|
-
await this.
|
|
895
|
-
|
|
1091
|
+
await this.mirrored(
|
|
1092
|
+
() => this.updateRunStateImpl(runId, name, value),
|
|
1093
|
+
(mirror) => mirror.updateRunState(runId, name, value)
|
|
1094
|
+
)
|
|
896
1095
|
}
|
|
897
1096
|
|
|
898
1097
|
protected abstract updateRunStateImpl(
|
|
@@ -915,9 +1114,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
915
1114
|
source: string,
|
|
916
1115
|
status?: WorkflowVersionStatus
|
|
917
1116
|
): Promise<void> {
|
|
918
|
-
await this.
|
|
919
|
-
|
|
920
|
-
|
|
1117
|
+
await this.mirrored(
|
|
1118
|
+
() =>
|
|
1119
|
+
this.upsertWorkflowVersionImpl(name, graphHash, graph, source, status),
|
|
1120
|
+
(mirror) =>
|
|
1121
|
+
mirror.upsertWorkflowVersion(name, graphHash, graph, source, status)
|
|
921
1122
|
)
|
|
922
1123
|
}
|
|
923
1124
|
|
|
@@ -934,9 +1135,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
934
1135
|
graphHash: string,
|
|
935
1136
|
status: WorkflowVersionStatus
|
|
936
1137
|
): Promise<void> {
|
|
937
|
-
await this.
|
|
938
|
-
|
|
939
|
-
|
|
1138
|
+
await this.mirrored(
|
|
1139
|
+
() => this.updateWorkflowVersionStatusImpl(name, graphHash, status),
|
|
1140
|
+
(mirror) => mirror.updateWorkflowVersionStatus(name, graphHash, status)
|
|
940
1141
|
)
|
|
941
1142
|
}
|
|
942
1143
|
|
|
@@ -951,10 +1152,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
951
1152
|
graphHash: string
|
|
952
1153
|
): Promise<{ graph: any; source: string } | null>
|
|
953
1154
|
|
|
954
|
-
abstract getAIGeneratedWorkflows(
|
|
955
|
-
agentName?: string
|
|
956
|
-
): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>>
|
|
957
|
-
|
|
958
1155
|
// ============================================================================
|
|
959
1156
|
// Workflow Lifecycle Methods
|
|
960
1157
|
// ============================================================================
|
|
@@ -1024,9 +1221,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1024
1221
|
const queueService = this.verifyQueueService()
|
|
1025
1222
|
await queueService.add(
|
|
1026
1223
|
this.getStepWorkerQueueName(rpcName),
|
|
1027
|
-
|
|
1028
|
-
JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
|
|
1029
|
-
),
|
|
1224
|
+
{ runId, stepName, rpcName, data, fromStepName },
|
|
1030
1225
|
{
|
|
1031
1226
|
...this.resolveStepJobOptions(stepOptions),
|
|
1032
1227
|
// Group by step function, mirroring how per-step queues split them —
|
|
@@ -1118,9 +1313,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1118
1313
|
try {
|
|
1119
1314
|
await getSingletonServices()!.queueService!.add(
|
|
1120
1315
|
this.getStepWorkerQueueName(rpcName),
|
|
1121
|
-
|
|
1122
|
-
JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
|
|
1123
|
-
),
|
|
1316
|
+
{ runId, stepName, rpcName, data, fromStepName },
|
|
1124
1317
|
{
|
|
1125
1318
|
...this.resolveStepJobOptions(stepOptions),
|
|
1126
1319
|
group: this.getJobGroup(rpcName),
|
|
@@ -1162,37 +1355,28 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1162
1355
|
return true
|
|
1163
1356
|
}
|
|
1164
1357
|
|
|
1165
|
-
/**
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
)
|
|
1179
|
-
return undefined
|
|
1180
|
-
}
|
|
1181
|
-
const actorsConfig = await metaService.getScenarioActorsMeta()
|
|
1182
|
-
if (!actorsConfig || Object.keys(actorsConfig).length === 0) {
|
|
1183
|
-
return undefined
|
|
1358
|
+
/**
|
|
1359
|
+
* Install the one extension a run may have, built from a handle onto the run
|
|
1360
|
+
* engine so that `inlineStep` and friends stay protected rather than becoming
|
|
1361
|
+
* public API. Returns the extension, so the caller keeps a typed reference to
|
|
1362
|
+
* whatever it just built.
|
|
1363
|
+
*/
|
|
1364
|
+
public setRunExtension<T extends WorkflowRunExtension>(
|
|
1365
|
+
create: (engine: WorkflowRunEngine) => T
|
|
1366
|
+
): T {
|
|
1367
|
+
const engine: WorkflowRunEngine = {
|
|
1368
|
+
inlineStep: this.inlineStep.bind(this),
|
|
1369
|
+
updateRunStatus: this.updateRunStatus.bind(this),
|
|
1370
|
+
onChildWorkflowFailed: this.onChildWorkflowFailed.bind(this),
|
|
1371
|
+
verifyStepName: this.verifyStepName.bind(this),
|
|
1184
1372
|
}
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
actors: actorsConfig,
|
|
1193
|
-
signInPath,
|
|
1194
|
-
rpcPath,
|
|
1195
|
-
})
|
|
1373
|
+
const extension = create(engine)
|
|
1374
|
+
this.runExtension = extension
|
|
1375
|
+
return extension
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
public getRunExtension(): WorkflowRunExtension | undefined {
|
|
1379
|
+
return this.runExtension
|
|
1196
1380
|
}
|
|
1197
1381
|
|
|
1198
1382
|
/**
|
|
@@ -1200,35 +1384,32 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1200
1384
|
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
1201
1385
|
* @param options.inline - If true, execute workflow directly without queue service
|
|
1202
1386
|
* @param options.startNode - Starting node ID for graph workflows (from wire config)
|
|
1387
|
+
* @param options.onRunCreated - Called with the run id the moment the run exists.
|
|
1388
|
+
* An inline run that fails throws instead of returning, so this is the only
|
|
1389
|
+
* way a caller can still read that run back — its steps, and which one failed.
|
|
1203
1390
|
*/
|
|
1204
1391
|
public async startWorkflow<I>(
|
|
1205
1392
|
name: string,
|
|
1206
1393
|
input: I,
|
|
1207
1394
|
wire: WorkflowRunWire,
|
|
1208
1395
|
rpcService: any,
|
|
1209
|
-
options?: {
|
|
1396
|
+
options?: {
|
|
1397
|
+
inline?: boolean
|
|
1398
|
+
startNode?: string
|
|
1399
|
+
actors?: ScenarioActors
|
|
1400
|
+
onRunCreated?: (runId: string) => void
|
|
1401
|
+
}
|
|
1210
1402
|
): Promise<{ runId: string }> {
|
|
1211
|
-
// Resolve workflow from static meta (root or addon namespace)
|
|
1403
|
+
// Resolve workflow from static meta (root or addon namespace)
|
|
1212
1404
|
const resolved = resolveWorkflowMeta(name)
|
|
1213
|
-
|
|
1405
|
+
const workflowMeta = resolved?.meta
|
|
1214
1406
|
const packageName = resolved?.packageName ?? null
|
|
1215
1407
|
|
|
1216
|
-
if (!workflowMeta) {
|
|
1217
|
-
const dynamicWorkflows = await this.getAIGeneratedWorkflows()
|
|
1218
|
-
const match = dynamicWorkflows.find((w) => w.workflowName === name)
|
|
1219
|
-
if (match?.graph) {
|
|
1220
|
-
workflowMeta = match.graph
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
1408
|
if (!workflowMeta) {
|
|
1225
1409
|
throw new WorkflowNotFoundError(name)
|
|
1226
1410
|
}
|
|
1227
1411
|
|
|
1228
|
-
if (
|
|
1229
|
-
workflowMeta.source === 'graph' ||
|
|
1230
|
-
workflowMeta.source === 'dynamic-workflow'
|
|
1231
|
-
) {
|
|
1412
|
+
if (workflowMeta.source === 'graph') {
|
|
1232
1413
|
const shouldInline =
|
|
1233
1414
|
options?.inline || !getSingletonServices()?.queueService
|
|
1234
1415
|
return runWorkflowGraph(
|
|
@@ -1270,17 +1451,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1270
1451
|
}
|
|
1271
1452
|
)
|
|
1272
1453
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
? await this.resolveScenarioActors()
|
|
1277
|
-
: undefined)
|
|
1278
|
-
if (actors) {
|
|
1279
|
-
this.runActors.set(runId, actors)
|
|
1280
|
-
}
|
|
1454
|
+
options?.onRunCreated?.(runId)
|
|
1455
|
+
|
|
1456
|
+
await this.runExtension?.attachRunContext(runId, workflowMeta, options)
|
|
1281
1457
|
|
|
1282
1458
|
if (shouldInline) {
|
|
1283
|
-
this.
|
|
1459
|
+
this.registerInlineRun(runId)
|
|
1284
1460
|
try {
|
|
1285
1461
|
await this.runWorkflowJob(runId, rpcService)
|
|
1286
1462
|
} catch (error: any) {
|
|
@@ -1313,8 +1489,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1313
1489
|
throw error
|
|
1314
1490
|
}
|
|
1315
1491
|
} finally {
|
|
1316
|
-
this.
|
|
1317
|
-
this.
|
|
1492
|
+
this.unregisterInlineRun(runId)
|
|
1493
|
+
this.runExtension?.detachRunContext(runId)
|
|
1318
1494
|
}
|
|
1319
1495
|
} else {
|
|
1320
1496
|
await this.resumeWorkflow(runId)
|
|
@@ -1329,7 +1505,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1329
1505
|
rpcService: any,
|
|
1330
1506
|
options?: { pollIntervalMs?: number; wire?: WorkflowRunWire }
|
|
1331
1507
|
): Promise<any> {
|
|
1332
|
-
const pollInterval = options?.pollIntervalMs ?? 1000
|
|
1333
1508
|
const { runId } = await this.startWorkflow(
|
|
1334
1509
|
name,
|
|
1335
1510
|
input,
|
|
@@ -1337,38 +1512,155 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1337
1512
|
rpcService,
|
|
1338
1513
|
{ inline: true }
|
|
1339
1514
|
)
|
|
1515
|
+
const run = await this.awaitRunEnd(runId, options?.pollIntervalMs ?? 1000)
|
|
1516
|
+
if (run.status === 'failed') {
|
|
1517
|
+
throw new WorkflowRunFailedError(run.error?.message)
|
|
1518
|
+
}
|
|
1519
|
+
if (run.status === 'cancelled') {
|
|
1520
|
+
throw new WorkflowRunCancelledError()
|
|
1521
|
+
}
|
|
1522
|
+
return run.output
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Read a run until it reaches an end state, backing off as it drags on.
|
|
1527
|
+
*
|
|
1528
|
+
* A fixed interval is wrong at both ends: it makes a workflow that finished
|
|
1529
|
+
* in milliseconds wait out the whole interval anyway, and it keeps reading a
|
|
1530
|
+
* long-running one at full rate for as long as it lasts. Starting short and
|
|
1531
|
+
* growing to `maxIntervalMs` returns quick runs promptly while a slow run's
|
|
1532
|
+
* read cost grows logarithmically rather than linearly with its duration.
|
|
1533
|
+
*/
|
|
1534
|
+
protected async awaitRunEnd(
|
|
1535
|
+
runId: string,
|
|
1536
|
+
maxIntervalMs: number
|
|
1537
|
+
): Promise<WorkflowRun> {
|
|
1538
|
+
let interval = Math.min(WORKFLOW_POLL_MIN_MS, maxIntervalMs)
|
|
1340
1539
|
while (true) {
|
|
1341
1540
|
const run = await this.getRun(runId)
|
|
1342
1541
|
if (!run) {
|
|
1343
1542
|
throw new WorkflowRunNotFoundError(runId)
|
|
1344
1543
|
}
|
|
1345
1544
|
if (WORKFLOW_END_STATES.has(run.status)) {
|
|
1346
|
-
|
|
1347
|
-
throw new WorkflowRunFailedError(run.error?.message)
|
|
1348
|
-
}
|
|
1349
|
-
if (run.status === 'cancelled') {
|
|
1350
|
-
throw new WorkflowRunCancelledError()
|
|
1351
|
-
}
|
|
1352
|
-
return run.output
|
|
1545
|
+
return run
|
|
1353
1546
|
}
|
|
1354
|
-
await
|
|
1547
|
+
await this.waitBeforeNextRead(interval)
|
|
1548
|
+
interval = Math.min(interval * WORKFLOW_POLL_FACTOR, maxIntervalMs)
|
|
1355
1549
|
}
|
|
1356
1550
|
}
|
|
1357
1551
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1552
|
+
/**
|
|
1553
|
+
* Wait between two reads of a run.
|
|
1554
|
+
*
|
|
1555
|
+
* Its own method so the backoff schedule can be asserted on directly. Timing
|
|
1556
|
+
* a poll loop by the clock measures the host's scheduler as much as the
|
|
1557
|
+
* policy — `setTimeout(40)` routinely returns late on a loaded runner — which
|
|
1558
|
+
* makes the obvious test both slow and flaky.
|
|
1559
|
+
*/
|
|
1560
|
+
protected async waitBeforeNextRead(ms: number): Promise<void> {
|
|
1561
|
+
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
1562
|
+
}
|
|
1363
1563
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1564
|
+
/**
|
|
1565
|
+
* Every step of a run in one read, or `null` if this backend has no bulk read.
|
|
1566
|
+
*
|
|
1567
|
+
* A replay walks the DSL body from the top, and each step it passes asks for
|
|
1568
|
+
* its own row — so a run of N steps costs N reads per replay and O(N^2) over
|
|
1569
|
+
* its lifetime. Backends that can answer this in a single query collapse that
|
|
1570
|
+
* to one read per replay.
|
|
1571
|
+
*/
|
|
1572
|
+
protected async listStepStates(
|
|
1573
|
+
_runId: string
|
|
1574
|
+
): Promise<Array<StepState & { stepName: string }> | null> {
|
|
1575
|
+
return null
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Begin a replay pass: fresh ordinal counters, and one read of the steps the
|
|
1580
|
+
* run has already taken so the walk back to where it left off is served from
|
|
1581
|
+
* memory. Safe because a pass reaches each step key at most once, and the
|
|
1582
|
+
* steps it replays past are `succeeded` and therefore immutable.
|
|
1583
|
+
*/
|
|
1584
|
+
private async beginReplay(runId: string): Promise<void> {
|
|
1585
|
+
const context = this.contextFor(runId)
|
|
1586
|
+
context.replay = { ordinals: new Map() }
|
|
1587
|
+
const steps = await this.listStepStates(runId)
|
|
1588
|
+
if (steps) {
|
|
1589
|
+
context.replay.steps = new Map(steps.map((step) => [step.stepName, step]))
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
private endReplay(runId: string): void {
|
|
1594
|
+
const context = this.runContexts.get(runId)
|
|
1595
|
+
if (!context) return
|
|
1596
|
+
context.replay = undefined
|
|
1597
|
+
this.releaseContext(runId)
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* The step row for `stepName`, creating it if the run has not reached it
|
|
1602
|
+
* before. Served from the replay snapshot when one is loaded.
|
|
1603
|
+
*/
|
|
1604
|
+
private async loadOrCreateStep(
|
|
1605
|
+
runId: string,
|
|
1606
|
+
stepName: string,
|
|
1607
|
+
create: () => Promise<StepState>
|
|
1608
|
+
): Promise<StepState> {
|
|
1609
|
+
const snapshot = this.runContexts.get(runId)?.replay?.steps
|
|
1610
|
+
if (snapshot) {
|
|
1611
|
+
const cached = snapshot.get(stepName)
|
|
1612
|
+
if (cached) {
|
|
1613
|
+
return cached
|
|
1614
|
+
}
|
|
1615
|
+
} else {
|
|
1616
|
+
try {
|
|
1617
|
+
return await this.getStepState(runId, stepName)
|
|
1618
|
+
} catch {
|
|
1619
|
+
// No row yet — fall through and create it.
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
let step: StepState
|
|
1624
|
+
try {
|
|
1625
|
+
step = await create()
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
// A concurrent replay of this run created the row after the snapshot was
|
|
1628
|
+
// taken. Its state is the truth; if it isn't really there, the insert
|
|
1629
|
+
// failed for its own reasons and that error is the one worth seeing.
|
|
1630
|
+
try {
|
|
1631
|
+
step = await this.getStepState(runId, stepName)
|
|
1632
|
+
} catch {
|
|
1633
|
+
throw error
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
snapshot?.set(stepName, step)
|
|
1637
|
+
return step
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* The run's immutable half — which workflow it is, the wire it was started
|
|
1642
|
+
* on, its input. `getRun` is otherwise called several times per step for
|
|
1643
|
+
* answers that were all fixed at creation, so a replay reads it once and
|
|
1644
|
+
* hands the same object to everyone who only needs that half.
|
|
1645
|
+
*
|
|
1646
|
+
* Anyone who needs `status`, `output`, `error` or `state` must call `getRun`:
|
|
1647
|
+
* those move while the run executes, and a cached copy would be a lie.
|
|
1648
|
+
*/
|
|
1649
|
+
private async getRunIdentity(runId: string): Promise<WorkflowRun | null> {
|
|
1650
|
+
const replay = this.runContexts.get(runId)?.replay
|
|
1651
|
+
if (replay?.run) {
|
|
1652
|
+
return replay.run
|
|
1653
|
+
}
|
|
1654
|
+
const run = await this.getRun(runId)
|
|
1655
|
+
if (run && replay) {
|
|
1656
|
+
replay.run = run
|
|
1657
|
+
}
|
|
1658
|
+
return run
|
|
1367
1659
|
}
|
|
1368
1660
|
|
|
1369
1661
|
/** The step the DSL walk last reached (the predecessor for the next step). */
|
|
1370
1662
|
private lastStepName(runId: string): string | undefined {
|
|
1371
|
-
return this.
|
|
1663
|
+
return this.runContexts.get(runId)?.replay?.lastStep
|
|
1372
1664
|
}
|
|
1373
1665
|
|
|
1374
1666
|
/**
|
|
@@ -1378,26 +1670,26 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1378
1670
|
* the rows clobbering. Deterministic given a deterministic DSL body.
|
|
1379
1671
|
*/
|
|
1380
1672
|
private nextStepKey(runId: string, logicalStepName: string): string {
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
perRun.set(logicalStepName, ordinal + 1)
|
|
1673
|
+
const context = this.contextFor(runId)
|
|
1674
|
+
const replay: NonNullable<RunContext['replay']> = (context.replay ??= {
|
|
1675
|
+
ordinals: new Map(),
|
|
1676
|
+
})
|
|
1677
|
+
const ordinal = replay.ordinals.get(logicalStepName) ?? 0
|
|
1678
|
+
replay.ordinals.set(logicalStepName, ordinal + 1)
|
|
1388
1679
|
const stepName =
|
|
1389
1680
|
ordinal === 0 ? logicalStepName : `${logicalStepName}#${ordinal}`
|
|
1390
|
-
|
|
1681
|
+
replay.lastStep = stepName
|
|
1391
1682
|
return stepName
|
|
1392
1683
|
}
|
|
1393
1684
|
|
|
1394
1685
|
public async runWorkflowJob(runId: string, rpcService: any): Promise<void> {
|
|
1395
|
-
// Fresh ordinal counters per replay so step keys are deterministic
|
|
1396
|
-
|
|
1686
|
+
// Fresh ordinal counters per replay so step keys are deterministic, and one
|
|
1687
|
+
// read of the steps the run has already taken.
|
|
1688
|
+
await this.beginReplay(runId)
|
|
1397
1689
|
try {
|
|
1398
1690
|
await this.runWorkflowJobInner(runId, rpcService)
|
|
1399
1691
|
} finally {
|
|
1400
|
-
this.
|
|
1692
|
+
this.endReplay(runId)
|
|
1401
1693
|
}
|
|
1402
1694
|
}
|
|
1403
1695
|
|
|
@@ -1405,7 +1697,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1405
1697
|
runId: string,
|
|
1406
1698
|
rpcService: any
|
|
1407
1699
|
): Promise<void> {
|
|
1408
|
-
|
|
1700
|
+
// Caches the run for the rest of this replay, so the steps it walks don't
|
|
1701
|
+
// each re-read the workflow name and wire it already has.
|
|
1702
|
+
const run = await this.getRunIdentity(runId)
|
|
1409
1703
|
if (!run) {
|
|
1410
1704
|
throw new WorkflowRunNotFoundError(runId)
|
|
1411
1705
|
}
|
|
@@ -1423,10 +1717,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1423
1717
|
return
|
|
1424
1718
|
}
|
|
1425
1719
|
|
|
1426
|
-
if (
|
|
1427
|
-
workflowMeta?.source === 'graph' ||
|
|
1428
|
-
workflowMeta?.source === 'dynamic-workflow'
|
|
1429
|
-
) {
|
|
1720
|
+
if (workflowMeta?.source === 'graph') {
|
|
1430
1721
|
await continueGraph(this, runId, run.workflow)
|
|
1431
1722
|
const updatedRun = await this.getRun(runId)
|
|
1432
1723
|
if (updatedRun?.status === 'completed') {
|
|
@@ -1443,29 +1734,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1443
1734
|
return
|
|
1444
1735
|
}
|
|
1445
1736
|
|
|
1446
|
-
if (!workflowMeta) {
|
|
1447
|
-
const dynamicWorkflows = await this.getAIGeneratedWorkflows()
|
|
1448
|
-
const match = dynamicWorkflows.find(
|
|
1449
|
-
(w) => w.workflowName === run.workflow
|
|
1450
|
-
)
|
|
1451
|
-
if (match?.graph) {
|
|
1452
|
-
await continueGraph(this, runId, run.workflow, match.graph)
|
|
1453
|
-
const updatedRun = await this.getRun(runId)
|
|
1454
|
-
if (updatedRun?.status === 'completed') {
|
|
1455
|
-
await this.onChildWorkflowCompleted(updatedRun, updatedRun.output)
|
|
1456
|
-
} else if (
|
|
1457
|
-
updatedRun?.status === 'failed' ||
|
|
1458
|
-
updatedRun?.status === 'cancelled'
|
|
1459
|
-
) {
|
|
1460
|
-
await this.onChildWorkflowFailed(
|
|
1461
|
-
updatedRun,
|
|
1462
|
-
new Error(updatedRun.error?.message || 'Child workflow failed')
|
|
1463
|
-
)
|
|
1464
|
-
}
|
|
1465
|
-
return
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
1737
|
const registrations = pikkuState(pkgName, 'workflows', 'registrations')
|
|
1470
1738
|
const workflow = registrations.get(resolved?.resolvedName ?? run.workflow)
|
|
1471
1739
|
if (!workflow) {
|
|
@@ -1485,15 +1753,33 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1485
1753
|
workflowWire.pikkuUserId = run.wire?.pikkuUserId
|
|
1486
1754
|
const wire: PikkuWire = {
|
|
1487
1755
|
workflow: workflowWire,
|
|
1488
|
-
scenario:
|
|
1489
|
-
workflowMeta?.source === 'scenario' ? workflowWire : undefined,
|
|
1490
1756
|
pikkuUserId: run.wire?.pikkuUserId,
|
|
1491
1757
|
session: rpcService?.wire?.session,
|
|
1492
1758
|
rpc: rpcService?.wire?.rpc,
|
|
1493
|
-
// User-flow actors registered for this run (see startWorkflow options)
|
|
1494
|
-
actors: this.runActors.get(runId),
|
|
1495
1759
|
}
|
|
1760
|
+
this.runExtension?.decorateRunWire(wire, {
|
|
1761
|
+
runId,
|
|
1762
|
+
workflowMeta,
|
|
1763
|
+
workflowWire,
|
|
1764
|
+
})
|
|
1765
|
+
|
|
1766
|
+
const lifecycle: RunLifecycleContext = {
|
|
1767
|
+
runId,
|
|
1768
|
+
run,
|
|
1769
|
+
workflowMeta,
|
|
1770
|
+
workflow,
|
|
1771
|
+
wire,
|
|
1772
|
+
packageName: pkgName,
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// `interrupted` means the run has not reached a terminal state — it is
|
|
1776
|
+
// suspended or waiting — so teardown would run while the run is still
|
|
1777
|
+
// mid-flight.
|
|
1778
|
+
let outcome: 'completed' | 'failed' | 'interrupted' = 'completed'
|
|
1779
|
+
let failure: any
|
|
1496
1780
|
try {
|
|
1781
|
+
await this.runExtension?.onBeforeRunFunc(lifecycle)
|
|
1782
|
+
|
|
1497
1783
|
const result = await runPikkuFunc(
|
|
1498
1784
|
'workflow',
|
|
1499
1785
|
workflowMeta.name,
|
|
@@ -1510,11 +1796,15 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1510
1796
|
await this.updateRunStatus(runId, 'completed', result)
|
|
1511
1797
|
await this.onChildWorkflowCompleted(run, result)
|
|
1512
1798
|
} catch (error: any) {
|
|
1799
|
+
failure = error
|
|
1800
|
+
|
|
1513
1801
|
if (error instanceof WorkflowAsyncException) {
|
|
1802
|
+
outcome = 'interrupted'
|
|
1514
1803
|
throw error
|
|
1515
1804
|
}
|
|
1516
1805
|
|
|
1517
1806
|
if (error instanceof WorkflowCancelledException) {
|
|
1807
|
+
outcome = 'failed'
|
|
1518
1808
|
await this.updateRunStatus(runId, 'cancelled', undefined, {
|
|
1519
1809
|
message: error.message || 'Workflow cancelled',
|
|
1520
1810
|
stack: '',
|
|
@@ -1525,6 +1815,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1525
1815
|
}
|
|
1526
1816
|
|
|
1527
1817
|
if (error instanceof WorkflowSuspendedException) {
|
|
1818
|
+
outcome = 'interrupted'
|
|
1528
1819
|
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
1529
1820
|
message: error.message || 'Workflow suspended',
|
|
1530
1821
|
stack: '',
|
|
@@ -1533,6 +1824,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1533
1824
|
throw error
|
|
1534
1825
|
}
|
|
1535
1826
|
|
|
1827
|
+
outcome = 'failed'
|
|
1536
1828
|
await this.updateRunStatus(runId, 'failed', undefined, {
|
|
1537
1829
|
message: error.message,
|
|
1538
1830
|
stack: error.stack,
|
|
@@ -1541,6 +1833,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1541
1833
|
await this.onChildWorkflowFailed(run, error)
|
|
1542
1834
|
|
|
1543
1835
|
throw error
|
|
1836
|
+
} finally {
|
|
1837
|
+
await this.runExtension?.onAfterRunFunc(lifecycle, outcome, failure)
|
|
1544
1838
|
}
|
|
1545
1839
|
})
|
|
1546
1840
|
}
|
|
@@ -1559,7 +1853,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1559
1853
|
await this.resumeWorkflow(parentRunId)
|
|
1560
1854
|
}
|
|
1561
1855
|
|
|
1562
|
-
|
|
1856
|
+
protected async onChildWorkflowFailed(
|
|
1563
1857
|
childRun: WorkflowRun,
|
|
1564
1858
|
error: Error
|
|
1565
1859
|
): Promise<void> {
|
|
@@ -1652,9 +1946,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1652
1946
|
const meta = pikkuState(null, 'workflows', 'meta')
|
|
1653
1947
|
const workflowMeta = meta[run.workflow]
|
|
1654
1948
|
|
|
1655
|
-
const isGraphWorkflow =
|
|
1656
|
-
workflowMeta?.source === 'graph' ||
|
|
1657
|
-
workflowMeta?.source === 'dynamic-workflow'
|
|
1949
|
+
const isGraphWorkflow = workflowMeta?.source === 'graph'
|
|
1658
1950
|
// Map the physical step key back to its logical node: a revisit instance
|
|
1659
1951
|
// is `node#N` (ordinal), which isn't a literal key in `nodes`.
|
|
1660
1952
|
let graphNodeId: string | undefined
|
|
@@ -1721,7 +2013,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1721
2013
|
stepState,
|
|
1722
2014
|
rpcName,
|
|
1723
2015
|
data,
|
|
1724
|
-
rpcService
|
|
2016
|
+
rpcService,
|
|
2017
|
+
run
|
|
1725
2018
|
)
|
|
1726
2019
|
}
|
|
1727
2020
|
}
|
|
@@ -1826,11 +2119,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1826
2119
|
stepState: StepState,
|
|
1827
2120
|
rpcName: string,
|
|
1828
2121
|
data: any,
|
|
1829
|
-
rpcService: any
|
|
2122
|
+
rpcService: any,
|
|
2123
|
+
knownRun?: WorkflowRun | null
|
|
1830
2124
|
): Promise<any> {
|
|
1831
2125
|
// Carry the run's pikkuUserId onto the step wire so authed steps rehydrate their
|
|
1832
2126
|
// session on the queued path too (the bare job wire lacks it; inline already has it).
|
|
1833
|
-
const run = await this.
|
|
2127
|
+
const run = knownRun ?? (await this.getRunIdentity(runId))
|
|
1834
2128
|
return rpcService.rpcWithWire(rpcName, data, {
|
|
1835
2129
|
...(run?.wire?.pikkuUserId ? { pikkuUserId: run.wire.pikkuUserId } : {}),
|
|
1836
2130
|
workflowStep: {
|
|
@@ -1940,13 +2234,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1940
2234
|
actor: stepOptions?.actor,
|
|
1941
2235
|
onError: stepOptions?.onError,
|
|
1942
2236
|
}
|
|
1943
|
-
//
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
stepState = await this.getStepState(runId, stepName)
|
|
1947
|
-
} catch {
|
|
1948
|
-
// Step doesn't exist - create it
|
|
1949
|
-
stepState = await this.insertStepState(
|
|
2237
|
+
// Reuse the step if the run already reached it, otherwise create it.
|
|
2238
|
+
const stepState = await this.loadOrCreateStep(runId, stepName, () =>
|
|
2239
|
+
this.insertStepState(
|
|
1950
2240
|
runId,
|
|
1951
2241
|
stepName,
|
|
1952
2242
|
rpcName,
|
|
@@ -1954,7 +2244,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1954
2244
|
resolvedStepOptions,
|
|
1955
2245
|
fromStepName
|
|
1956
2246
|
)
|
|
1957
|
-
|
|
2247
|
+
)
|
|
1958
2248
|
|
|
1959
2249
|
if (stepState.status === 'succeeded') {
|
|
1960
2250
|
// Return cached result
|
|
@@ -2048,24 +2338,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2048
2338
|
)
|
|
2049
2339
|
await this.setStepChildRunId(currentStepState.stepId, childRunId)
|
|
2050
2340
|
// Poll until child workflow completes
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
if (childRun.status === 'failed') {
|
|
2058
|
-
throw new Error(
|
|
2059
|
-
childRun.error?.message || 'Sub-workflow failed'
|
|
2060
|
-
)
|
|
2061
|
-
}
|
|
2062
|
-
if (childRun.status === 'cancelled') {
|
|
2063
|
-
throw new Error('Sub-workflow was cancelled')
|
|
2064
|
-
}
|
|
2065
|
-
return childRun.output
|
|
2066
|
-
}
|
|
2067
|
-
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
2341
|
+
const childRun = await this.awaitRunEnd(
|
|
2342
|
+
childRunId,
|
|
2343
|
+
WORKFLOW_CHILD_POLL_MAX_MS
|
|
2344
|
+
)
|
|
2345
|
+
if (childRun.status === 'failed') {
|
|
2346
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed')
|
|
2068
2347
|
}
|
|
2348
|
+
if (childRun.status === 'cancelled') {
|
|
2349
|
+
throw new Error('Sub-workflow was cancelled')
|
|
2350
|
+
}
|
|
2351
|
+
return childRun.output
|
|
2069
2352
|
}
|
|
2070
2353
|
return this.invokeStepRpc(
|
|
2071
2354
|
runId,
|
|
@@ -2088,29 +2371,40 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2088
2371
|
)
|
|
2089
2372
|
}
|
|
2090
2373
|
|
|
2091
|
-
|
|
2374
|
+
protected async inlineStep(
|
|
2092
2375
|
runId: string,
|
|
2093
2376
|
logicalStepName: string,
|
|
2094
2377
|
fn: Function,
|
|
2095
|
-
stepOptions?: WorkflowStepOptions
|
|
2378
|
+
stepOptions?: WorkflowStepOptions,
|
|
2379
|
+
/**
|
|
2380
|
+
* The input this step was called with, recorded on the run so a reporter can
|
|
2381
|
+
* name the values under test. A closure step has none; a scenario step does.
|
|
2382
|
+
*/
|
|
2383
|
+
data: any = null,
|
|
2384
|
+
/**
|
|
2385
|
+
* The name this step was dispatched by, for the kinds of inline step that
|
|
2386
|
+
* have one. A closure step has no name; a scenario step is a step RPC, so
|
|
2387
|
+
* it records the step function that ran — which is the only way to join a
|
|
2388
|
+
* step back to its declaration when its durable name was built at runtime
|
|
2389
|
+
* (a step called in a loop reaches the run as `sees @pikku/addon-todos`,
|
|
2390
|
+
* declared as `sees ${packageName}`).
|
|
2391
|
+
*/
|
|
2392
|
+
rpcName: string | null = null
|
|
2096
2393
|
): Promise<any> {
|
|
2097
2394
|
const fromStepName = this.lastStepName(runId)
|
|
2098
2395
|
const stepName = this.nextStepKey(runId, logicalStepName)
|
|
2099
|
-
//
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
} catch {
|
|
2104
|
-
// Step doesn't exist - create it (inline, no RPC)
|
|
2105
|
-
stepState = await this.insertStepState(
|
|
2396
|
+
// Reuse the step if the run already reached it, otherwise create it
|
|
2397
|
+
// (inline, so never dispatched).
|
|
2398
|
+
const stepState = await this.loadOrCreateStep(runId, stepName, () =>
|
|
2399
|
+
this.insertStepState(
|
|
2106
2400
|
runId,
|
|
2107
2401
|
stepName,
|
|
2108
|
-
|
|
2109
|
-
|
|
2402
|
+
rpcName,
|
|
2403
|
+
data,
|
|
2110
2404
|
stepOptions,
|
|
2111
2405
|
fromStepName
|
|
2112
2406
|
)
|
|
2113
|
-
|
|
2407
|
+
)
|
|
2114
2408
|
|
|
2115
2409
|
if (stepState.status === 'succeeded') {
|
|
2116
2410
|
// Return cached result
|
|
@@ -2162,13 +2456,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2162
2456
|
) {
|
|
2163
2457
|
const fromStepName = this.lastStepName(runId)
|
|
2164
2458
|
const stepName = this.nextStepKey(runId, logicalStepName)
|
|
2165
|
-
//
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
} catch {
|
|
2170
|
-
// Step doesn't exist - create it (sleep step, no RPC)
|
|
2171
|
-
stepState = await this.insertStepState(
|
|
2459
|
+
// Reuse the step if the run already reached it, otherwise create it
|
|
2460
|
+
// (sleep step, no RPC).
|
|
2461
|
+
const stepState = await this.loadOrCreateStep(runId, stepName, () =>
|
|
2462
|
+
this.insertStepState(
|
|
2172
2463
|
runId,
|
|
2173
2464
|
stepName,
|
|
2174
2465
|
null,
|
|
@@ -2176,7 +2467,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2176
2467
|
undefined,
|
|
2177
2468
|
fromStepName
|
|
2178
2469
|
)
|
|
2179
|
-
|
|
2470
|
+
)
|
|
2180
2471
|
|
|
2181
2472
|
if (stepState.status === 'succeeded') {
|
|
2182
2473
|
// Sleep already completed, return immediately
|
|
@@ -2494,8 +2785,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2494
2785
|
runId: string,
|
|
2495
2786
|
rpcService: any,
|
|
2496
2787
|
addonNamespace?: string | null
|
|
2497
|
-
):
|
|
2498
|
-
const workflowWire:
|
|
2788
|
+
): PikkuWorkflowWire {
|
|
2789
|
+
const workflowWire: PikkuWorkflowWire = {
|
|
2499
2790
|
name,
|
|
2500
2791
|
runId,
|
|
2501
2792
|
getRun: async () => (await this.getRun(runId)) as WorkflowRun,
|
|
@@ -2531,146 +2822,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2531
2822
|
}
|
|
2532
2823
|
},
|
|
2533
2824
|
|
|
2534
|
-
// Durable polling step: invoke an RPC (as an actor when options.as is
|
|
2535
|
-
// set) until the predicate passes or `within` elapses. The whole poll is
|
|
2536
|
-
// ONE recorded step, so replay returns the cached outcome.
|
|
2537
|
-
expectEventually: async (
|
|
2538
|
-
stepName: string,
|
|
2539
|
-
rpcName: string,
|
|
2540
|
-
data: any,
|
|
2541
|
-
predicate: (output: any) => boolean,
|
|
2542
|
-
options?: WorkflowExpectEventuallyOptions
|
|
2543
|
-
) => {
|
|
2544
|
-
this.verifyStepName(stepName)
|
|
2545
|
-
const resolvedRpcName =
|
|
2546
|
-
addonNamespace && !rpcName.includes(':')
|
|
2547
|
-
? `${addonNamespace}:${rpcName}`
|
|
2548
|
-
: rpcName
|
|
2549
|
-
const within = getDurationInMilliseconds(options?.within ?? '30s')
|
|
2550
|
-
const interval = getDurationInMilliseconds(options?.interval ?? '1s')
|
|
2551
|
-
return await this.inlineStep(
|
|
2552
|
-
runId,
|
|
2553
|
-
stepName,
|
|
2554
|
-
async () => {
|
|
2555
|
-
const deadline = Date.now() + within
|
|
2556
|
-
let last: any
|
|
2557
|
-
while (true) {
|
|
2558
|
-
last = options?.actor
|
|
2559
|
-
? await options.actor.invoke(resolvedRpcName, data)
|
|
2560
|
-
: await rpcService.rpcWithWire(resolvedRpcName, data, {})
|
|
2561
|
-
if (predicate(last)) return last
|
|
2562
|
-
if (Date.now() + interval > deadline) {
|
|
2563
|
-
throw new Error(
|
|
2564
|
-
`[workflow] expectEventually '${stepName}' ('${resolvedRpcName}'` +
|
|
2565
|
-
`${options?.actor ? ` as '${options.actor.name}'` : ''}) did not pass within ${within}ms; ` +
|
|
2566
|
-
`last result: ${JSON.stringify(last)?.slice(0, 300)}`
|
|
2567
|
-
)
|
|
2568
|
-
}
|
|
2569
|
-
await new Promise((resolve) => setTimeout(resolve, interval))
|
|
2570
|
-
}
|
|
2571
|
-
},
|
|
2572
|
-
options
|
|
2573
|
-
)
|
|
2574
|
-
},
|
|
2575
|
-
|
|
2576
|
-
expectError: async (
|
|
2577
|
-
stepName: string,
|
|
2578
|
-
rpcName: string,
|
|
2579
|
-
data: any,
|
|
2580
|
-
options?: WorkflowExpectErrorOptions
|
|
2581
|
-
) => {
|
|
2582
|
-
this.verifyStepName(stepName)
|
|
2583
|
-
const resolvedRpcName =
|
|
2584
|
-
addonNamespace && !rpcName.includes(':')
|
|
2585
|
-
? `${addonNamespace}:${rpcName}`
|
|
2586
|
-
: rpcName
|
|
2587
|
-
return await this.inlineStep(
|
|
2588
|
-
runId,
|
|
2589
|
-
stepName,
|
|
2590
|
-
async () => {
|
|
2591
|
-
let result: any
|
|
2592
|
-
try {
|
|
2593
|
-
result = options?.actor
|
|
2594
|
-
? await options.actor.invoke(resolvedRpcName, data)
|
|
2595
|
-
: await rpcService.rpcWithWire(resolvedRpcName, data, {})
|
|
2596
|
-
} catch (e: any) {
|
|
2597
|
-
const message = e?.message ?? String(e)
|
|
2598
|
-
if (options?.matches) {
|
|
2599
|
-
const matched =
|
|
2600
|
-
typeof options.matches === 'string'
|
|
2601
|
-
? message.includes(options.matches)
|
|
2602
|
-
: options.matches.test(message)
|
|
2603
|
-
if (!matched) {
|
|
2604
|
-
throw new Error(
|
|
2605
|
-
`[workflow] expectError '${stepName}' ('${resolvedRpcName}') threw, but the message did not match ${options.matches}: ${message}`
|
|
2606
|
-
)
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
return message
|
|
2610
|
-
}
|
|
2611
|
-
throw new Error(
|
|
2612
|
-
`[workflow] expectError '${stepName}' ('${resolvedRpcName}') expected an error but the call succeeded: ${JSON.stringify(result)?.slice(0, 300)}`
|
|
2613
|
-
)
|
|
2614
|
-
},
|
|
2615
|
-
options
|
|
2616
|
-
)
|
|
2617
|
-
},
|
|
2618
|
-
|
|
2619
|
-
expectService: async (
|
|
2620
|
-
stepName: string,
|
|
2621
|
-
serviceMethod: string,
|
|
2622
|
-
options?: WorkflowExpectServiceOptions
|
|
2623
|
-
) => {
|
|
2624
|
-
this.verifyStepName(stepName)
|
|
2625
|
-
const [service, method] = serviceMethod.split('.')
|
|
2626
|
-
if (!service || !method) {
|
|
2627
|
-
throw new Error(
|
|
2628
|
-
`[workflow] expectService '${stepName}' needs 'service.method', got '${serviceMethod}'`
|
|
2629
|
-
)
|
|
2630
|
-
}
|
|
2631
|
-
await this.inlineStep(
|
|
2632
|
-
runId,
|
|
2633
|
-
stepName,
|
|
2634
|
-
async () => {
|
|
2635
|
-
const rpcName = 'pikkuScenarioGetStubCalls'
|
|
2636
|
-
const calls: Array<{
|
|
2637
|
-
service: string
|
|
2638
|
-
method: string
|
|
2639
|
-
args: unknown[]
|
|
2640
|
-
}> = options?.actor
|
|
2641
|
-
? await options.actor.invoke(rpcName, { service })
|
|
2642
|
-
: await rpcService.rpcWithWire(rpcName, { service }, {})
|
|
2643
|
-
const matching = (calls ?? []).filter(
|
|
2644
|
-
(c) =>
|
|
2645
|
-
c.service === service &&
|
|
2646
|
-
c.method === method &&
|
|
2647
|
-
(options?.calledWith === undefined ||
|
|
2648
|
-
JSON.stringify(c.args?.[0]) ===
|
|
2649
|
-
JSON.stringify(options.calledWith))
|
|
2650
|
-
)
|
|
2651
|
-
const expected = options?.times
|
|
2652
|
-
const ok =
|
|
2653
|
-
expected === undefined
|
|
2654
|
-
? matching.length > 0
|
|
2655
|
-
: matching.length === expected
|
|
2656
|
-
if (!ok) {
|
|
2657
|
-
const seen =
|
|
2658
|
-
(calls ?? [])
|
|
2659
|
-
.map(
|
|
2660
|
-
(c) =>
|
|
2661
|
-
`${c.service}.${c.method}(${JSON.stringify(c.args?.[0])?.slice(0, 120) ?? ''})`
|
|
2662
|
-
)
|
|
2663
|
-
.join('\n ') || '(none)'
|
|
2664
|
-
throw new Error(
|
|
2665
|
-
`[workflow] expectService '${stepName}' expected ${expected ?? 'at least one'} call(s) to '${serviceMethod}'` +
|
|
2666
|
-
`${options?.calledWith !== undefined ? ` with ${JSON.stringify(options.calledWith)}` : ''}, found ${matching.length}. Recorded:\n ${seen}`
|
|
2667
|
-
)
|
|
2668
|
-
}
|
|
2669
|
-
},
|
|
2670
|
-
options
|
|
2671
|
-
)
|
|
2672
|
-
},
|
|
2673
|
-
|
|
2674
2825
|
// Implement workflow.sleep()
|
|
2675
2826
|
sleep: async (stepName: string, duration: string | number) => {
|
|
2676
2827
|
this.verifyStepName(stepName)
|
|
@@ -2689,16 +2840,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2689
2840
|
approval: (async (reason: string, options: WorkflowApprovalOptions) => {
|
|
2690
2841
|
this.verifyStepName(reason)
|
|
2691
2842
|
return await this.approvalStep(runId, reason, options)
|
|
2692
|
-
}) as
|
|
2693
|
-
|
|
2694
|
-
runScheduledTask: async (taskName: string) => {
|
|
2695
|
-
await runScheduledTask({ name: taskName })
|
|
2696
|
-
},
|
|
2843
|
+
}) as PikkuWorkflowWire['approval'],
|
|
2697
2844
|
}
|
|
2845
|
+
this.runExtension?.decorateWorkflowWire(workflowWire, {
|
|
2846
|
+
name,
|
|
2847
|
+
runId,
|
|
2848
|
+
rpcService,
|
|
2849
|
+
addonNamespace,
|
|
2850
|
+
})
|
|
2698
2851
|
return workflowWire
|
|
2699
2852
|
}
|
|
2700
2853
|
|
|
2701
|
-
|
|
2854
|
+
protected verifyStepName(stepName: string) {
|
|
2702
2855
|
if (typeof stepName !== 'string') {
|
|
2703
2856
|
throw new WorkflowStepNameNotString(stepName)
|
|
2704
2857
|
}
|