@pikku/core 0.12.3 → 0.12.4
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 +6 -0
- package/dist/services/in-memory-workflow-service.d.ts +15 -2
- package/dist/services/in-memory-workflow-service.js +49 -0
- package/lcov.info +18891 -0
- package/package.json +1 -1
- package/src/crypto-utils.test.ts +0 -214
- package/src/crypto-utils.ts +0 -213
- package/src/errors/error-handler.ts +0 -62
- package/src/errors/error.test.ts +0 -195
- package/src/errors/errors.ts +0 -374
- package/src/errors/index.ts +0 -2
- package/src/factory-functions.test.ts +0 -109
- package/src/function/function-runner.test.ts +0 -536
- package/src/function/function-runner.ts +0 -365
- package/src/function/functions.types.ts +0 -304
- package/src/function/index.ts +0 -5
- package/src/handle-error.test.ts +0 -424
- package/src/handle-error.ts +0 -69
- package/src/index.ts +0 -118
- package/src/internal.ts +0 -6
- package/src/middleware/auth-apikey.test.ts +0 -363
- package/src/middleware/auth-apikey.ts +0 -47
- package/src/middleware/auth-bearer.test.ts +0 -450
- package/src/middleware/auth-bearer.ts +0 -72
- package/src/middleware/auth-cookie.test.ts +0 -528
- package/src/middleware/auth-cookie.ts +0 -80
- package/src/middleware/cors.test.ts +0 -424
- package/src/middleware/cors.ts +0 -104
- package/src/middleware/index.ts +0 -5
- package/src/middleware/remote-auth.test.ts +0 -488
- package/src/middleware/remote-auth.ts +0 -68
- package/src/middleware/timeout.ts +0 -15
- package/src/middleware-runner.test.ts +0 -418
- package/src/middleware-runner.ts +0 -234
- package/src/permissions.test.ts +0 -434
- package/src/permissions.ts +0 -319
- package/src/pikku-request.ts +0 -23
- package/src/pikku-response.ts +0 -5
- package/src/pikku-state.test.ts +0 -224
- package/src/pikku-state.ts +0 -216
- package/src/run-tests-script.test.ts +0 -49
- package/src/schema.test.ts +0 -249
- package/src/schema.ts +0 -151
- package/src/services/ai-agent-runner-service.ts +0 -41
- package/src/services/ai-run-state-service.ts +0 -20
- package/src/services/ai-storage-service.ts +0 -39
- package/src/services/content-service.ts +0 -76
- package/src/services/deployment-service.ts +0 -22
- package/src/services/gateway-service.ts +0 -20
- package/src/services/gopass-secrets.ts +0 -98
- package/src/services/in-memory-ai-run-state-service.ts +0 -73
- package/src/services/in-memory-trigger-service.ts +0 -64
- package/src/services/in-memory-workflow-service.test.ts +0 -351
- package/src/services/in-memory-workflow-service.ts +0 -438
- package/src/services/index.ts +0 -56
- package/src/services/jwt-service.ts +0 -30
- package/src/services/local-content.ts +0 -120
- package/src/services/local-gateway-service.ts +0 -62
- package/src/services/local-secrets.test.ts +0 -80
- package/src/services/local-secrets.ts +0 -94
- package/src/services/local-variables.test.ts +0 -61
- package/src/services/local-variables.ts +0 -39
- package/src/services/logger-console.test.ts +0 -118
- package/src/services/logger-console.ts +0 -98
- package/src/services/logger.ts +0 -57
- package/src/services/scheduler-service.ts +0 -86
- package/src/services/schema-service.ts +0 -33
- package/src/services/scoped-secret-service.test.ts +0 -74
- package/src/services/scoped-secret-service.ts +0 -41
- package/src/services/secret-service.ts +0 -38
- package/src/services/trigger-service.ts +0 -17
- package/src/services/typed-secret-service.test.ts +0 -93
- package/src/services/typed-secret-service.ts +0 -70
- package/src/services/typed-variables-service.test.ts +0 -73
- package/src/services/typed-variables-service.ts +0 -81
- package/src/services/user-session-service.test.ts +0 -113
- package/src/services/user-session-service.ts +0 -86
- package/src/services/variables-service.ts +0 -11
- package/src/services/workflow-service.ts +0 -95
- package/src/testing/index.ts +0 -2
- package/src/testing/service-tests.ts +0 -873
- package/src/time-utils.test.ts +0 -56
- package/src/time-utils.ts +0 -107
- package/src/types/core.types.ts +0 -478
- package/src/types/state.types.ts +0 -188
- package/src/utils/hash.test.ts +0 -68
- package/src/utils/hash.ts +0 -26
- package/src/utils.test.ts +0 -350
- package/src/utils.ts +0 -129
- package/src/version.test.ts +0 -80
- package/src/version.ts +0 -25
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -469
- package/src/wirings/ai-agent/ai-agent-helpers.test.ts +0 -152
- package/src/wirings/ai-agent/ai-agent-helpers.ts +0 -76
- package/src/wirings/ai-agent/ai-agent-memory.ts +0 -310
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +0 -115
- package/src/wirings/ai-agent/ai-agent-model-config.ts +0 -43
- package/src/wirings/ai-agent/ai-agent-prepare.ts +0 -636
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +0 -37
- package/src/wirings/ai-agent/ai-agent-registry.ts +0 -82
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +0 -319
- package/src/wirings/ai-agent/ai-agent-runner.ts +0 -773
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +0 -859
- package/src/wirings/ai-agent/ai-agent-stream.ts +0 -1153
- package/src/wirings/ai-agent/ai-agent.types.ts +0 -382
- package/src/wirings/ai-agent/index.ts +0 -37
- package/src/wirings/channel/channel-common.ts +0 -90
- package/src/wirings/channel/channel-handler.ts +0 -250
- package/src/wirings/channel/channel-middleware-runner.ts +0 -120
- package/src/wirings/channel/channel-runner.ts +0 -166
- package/src/wirings/channel/channel-store.ts +0 -29
- package/src/wirings/channel/channel.types.ts +0 -172
- package/src/wirings/channel/define-channel-routes.ts +0 -25
- package/src/wirings/channel/eventhub-service.ts +0 -34
- package/src/wirings/channel/eventhub-store.ts +0 -13
- package/src/wirings/channel/index.ts +0 -24
- package/src/wirings/channel/local/index.ts +0 -3
- package/src/wirings/channel/local/local-channel-handler.ts +0 -81
- package/src/wirings/channel/local/local-channel-runner.test.ts +0 -215
- package/src/wirings/channel/local/local-channel-runner.ts +0 -210
- package/src/wirings/channel/local/local-eventhub-service.test.ts +0 -95
- package/src/wirings/channel/local/local-eventhub-service.ts +0 -101
- package/src/wirings/channel/log-channels.ts +0 -20
- package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +0 -54
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +0 -45
- package/src/wirings/channel/serverless/index.ts +0 -5
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +0 -289
- package/src/wirings/cli/channel/cli-channel-runner.ts +0 -136
- package/src/wirings/cli/channel/index.ts +0 -1
- package/src/wirings/cli/cli-runner.test.ts +0 -403
- package/src/wirings/cli/cli-runner.ts +0 -529
- package/src/wirings/cli/cli.types.ts +0 -358
- package/src/wirings/cli/command-parser.test.ts +0 -445
- package/src/wirings/cli/command-parser.ts +0 -504
- package/src/wirings/cli/define-cli-commands.ts +0 -24
- package/src/wirings/cli/index.ts +0 -19
- package/src/wirings/gateway/gateway-runner.test.ts +0 -474
- package/src/wirings/gateway/gateway-runner.ts +0 -411
- package/src/wirings/gateway/gateway.types.ts +0 -149
- package/src/wirings/gateway/index.ts +0 -13
- package/src/wirings/http/http-routes.test.ts +0 -322
- package/src/wirings/http/http-routes.ts +0 -197
- package/src/wirings/http/http-runner.test.ts +0 -144
- package/src/wirings/http/http-runner.ts +0 -588
- package/src/wirings/http/http.types.ts +0 -369
- package/src/wirings/http/index.ts +0 -28
- package/src/wirings/http/log-http-routes.ts +0 -22
- package/src/wirings/http/pikku-fetch-http-request.test.ts +0 -237
- package/src/wirings/http/pikku-fetch-http-request.ts +0 -170
- package/src/wirings/http/pikku-fetch-http-response.test.ts +0 -82
- package/src/wirings/http/pikku-fetch-http-response.ts +0 -147
- package/src/wirings/http/routers/http-router.ts +0 -13
- package/src/wirings/http/routers/path-to-regex.test.ts +0 -319
- package/src/wirings/http/routers/path-to-regex.ts +0 -126
- package/src/wirings/http/web-request.test.ts +0 -236
- package/src/wirings/http/web-request.ts +0 -104
- package/src/wirings/mcp/index.ts +0 -27
- package/src/wirings/mcp/mcp-endpoint-registry.test.ts +0 -389
- package/src/wirings/mcp/mcp-endpoint-registry.ts +0 -159
- package/src/wirings/mcp/mcp-runner.ts +0 -323
- package/src/wirings/mcp/mcp.types.ts +0 -216
- package/src/wirings/node/index.ts +0 -2
- package/src/wirings/node/node.types.ts +0 -23
- package/src/wirings/oauth2/index.ts +0 -3
- package/src/wirings/oauth2/oauth2-client.test.ts +0 -929
- package/src/wirings/oauth2/oauth2-client.ts +0 -335
- package/src/wirings/oauth2/oauth2.types.ts +0 -69
- package/src/wirings/oauth2/wire-oauth2-credential.ts +0 -23
- package/src/wirings/queue/index.ts +0 -31
- package/src/wirings/queue/queue-runner.test.ts +0 -710
- package/src/wirings/queue/queue-runner.ts +0 -192
- package/src/wirings/queue/queue.types.ts +0 -189
- package/src/wirings/queue/register-queue-helper.ts +0 -60
- package/src/wirings/queue/validate-worker-config.test.ts +0 -108
- package/src/wirings/queue/validate-worker-config.ts +0 -116
- package/src/wirings/rpc/index.ts +0 -4
- package/src/wirings/rpc/rpc-runner.ts +0 -460
- package/src/wirings/rpc/rpc-types.ts +0 -56
- package/src/wirings/rpc/wire-addon.ts +0 -21
- package/src/wirings/scheduler/index.ts +0 -11
- package/src/wirings/scheduler/log-schedulers.ts +0 -20
- package/src/wirings/scheduler/scheduler-runner.test.ts +0 -660
- package/src/wirings/scheduler/scheduler-runner.ts +0 -133
- package/src/wirings/scheduler/scheduler.types.ts +0 -53
- package/src/wirings/secret/index.ts +0 -9
- package/src/wirings/secret/secret.types.ts +0 -32
- package/src/wirings/secret/validate-secret-definitions.test.ts +0 -140
- package/src/wirings/secret/validate-secret-definitions.ts +0 -82
- package/src/wirings/trigger/index.ts +0 -10
- package/src/wirings/trigger/pikku-trigger-service.ts +0 -112
- package/src/wirings/trigger/trigger-runner.test.ts +0 -79
- package/src/wirings/trigger/trigger-runner.ts +0 -135
- package/src/wirings/trigger/trigger.types.ts +0 -178
- package/src/wirings/variable/index.ts +0 -8
- package/src/wirings/variable/validate-variable-definitions.test.ts +0 -91
- package/src/wirings/variable/validate-variable-definitions.ts +0 -69
- package/src/wirings/variable/variable.types.ts +0 -22
- package/src/wirings/workflow/dsl/index.ts +0 -31
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +0 -320
- package/src/wirings/workflow/dsl/workflow-runner.ts +0 -28
- package/src/wirings/workflow/graph/graph-node.ts +0 -222
- package/src/wirings/workflow/graph/graph-runner.test.ts +0 -487
- package/src/wirings/workflow/graph/graph-runner.ts +0 -816
- package/src/wirings/workflow/graph/graph-validation.test.ts +0 -170
- package/src/wirings/workflow/graph/graph-validation.ts +0 -237
- package/src/wirings/workflow/graph/index.ts +0 -19
- package/src/wirings/workflow/graph/template.test.ts +0 -49
- package/src/wirings/workflow/graph/template.ts +0 -42
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +0 -29
- package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -104
- package/src/wirings/workflow/index.ts +0 -82
- package/src/wirings/workflow/pikku-workflow-service.test.ts +0 -200
- package/src/wirings/workflow/pikku-workflow-service.ts +0 -1176
- package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
- package/src/wirings/workflow/workflow-helpers.ts +0 -79
- package/src/wirings/workflow/workflow.types.ts +0 -274
- package/tsconfig.json +0 -14
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,1176 +0,0 @@
|
|
|
1
|
-
import { runPikkuFunc } from '../../function/function-runner.js'
|
|
2
|
-
import {
|
|
3
|
-
getSingletonServices,
|
|
4
|
-
getCreateWireServices,
|
|
5
|
-
pikkuState,
|
|
6
|
-
} from '../../pikku-state.js'
|
|
7
|
-
import { getDurationInMilliseconds } from '../../time-utils.js'
|
|
8
|
-
import type { PikkuWire, SerializedError } from '../../types/core.types.js'
|
|
9
|
-
import type { QueueService } from '../queue/queue.types.js'
|
|
10
|
-
import type {
|
|
11
|
-
PikkuWorkflowWire,
|
|
12
|
-
StepState,
|
|
13
|
-
WorkflowRun,
|
|
14
|
-
WorkflowRunWire,
|
|
15
|
-
WorkflowStatus,
|
|
16
|
-
WorkflowVersionStatus,
|
|
17
|
-
WorkflowServiceConfig,
|
|
18
|
-
WorkflowStepOptions,
|
|
19
|
-
} from './workflow.types.js'
|
|
20
|
-
import {
|
|
21
|
-
continueGraph,
|
|
22
|
-
executeGraphStep,
|
|
23
|
-
runWorkflowGraph,
|
|
24
|
-
runFromMeta,
|
|
25
|
-
} from './graph/graph-runner.js'
|
|
26
|
-
import type { WorkflowService } from '../../services/workflow-service.js'
|
|
27
|
-
import { PikkuError, addError } from '../../errors/error-handler.js'
|
|
28
|
-
import { RPCNotFoundError } from '../rpc/rpc-runner.js'
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Exception thrown when workflow needs to pause for async step
|
|
32
|
-
*/
|
|
33
|
-
export class WorkflowAsyncException extends Error {
|
|
34
|
-
constructor(
|
|
35
|
-
public readonly runId: string,
|
|
36
|
-
public readonly stepName: string
|
|
37
|
-
) {
|
|
38
|
-
super(`Workflow paused at step: ${stepName}`)
|
|
39
|
-
this.name = 'WorkflowAsyncException'
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Exception thrown when workflow is cancelled
|
|
45
|
-
*/
|
|
46
|
-
export class WorkflowCancelledException extends Error {
|
|
47
|
-
constructor(
|
|
48
|
-
public readonly runId: string,
|
|
49
|
-
public readonly reason?: string
|
|
50
|
-
) {
|
|
51
|
-
super(reason || 'Workflow cancelled')
|
|
52
|
-
this.name = 'WorkflowCancelledException'
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Exception thrown when workflow is suspended
|
|
58
|
-
*/
|
|
59
|
-
export class WorkflowSuspendedException extends Error {
|
|
60
|
-
constructor(
|
|
61
|
-
public readonly runId: string,
|
|
62
|
-
public readonly reason: string
|
|
63
|
-
) {
|
|
64
|
-
super(reason || 'Workflow suspended')
|
|
65
|
-
this.name = 'WorkflowSuspendedException'
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Error class for workflow not found
|
|
71
|
-
*/
|
|
72
|
-
export class WorkflowNotFoundError extends PikkuError {
|
|
73
|
-
constructor(name: string) {
|
|
74
|
-
super(`Workflow not found: ${name}`)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
addError(WorkflowNotFoundError, {
|
|
78
|
-
status: 404,
|
|
79
|
-
message: 'Workflow not found.',
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
export class WorkflowRunNotFoundError extends PikkuError {
|
|
83
|
-
constructor(runId: string) {
|
|
84
|
-
super(`Workflow run not found: ${runId}`)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
addError(WorkflowRunNotFoundError, {
|
|
88
|
-
status: 404,
|
|
89
|
-
message: 'Workflow run not found.',
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
export class WorkflowServiceNotInitialized extends Error {}
|
|
93
|
-
export class WorkflowStepNameNotString extends Error {
|
|
94
|
-
constructor(stepName: any) {
|
|
95
|
-
super(`Workflow step name must be a string. Received: ${typeof stepName}`)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
|
|
100
|
-
'completed',
|
|
101
|
-
'failed',
|
|
102
|
-
'cancelled',
|
|
103
|
-
'suspended',
|
|
104
|
-
])
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Abstract workflow state service
|
|
108
|
-
* Implementations provide pluggable storage backends (SQLite, PostgreSQL, etc.)
|
|
109
|
-
* Combines orchestration and step execution
|
|
110
|
-
*/
|
|
111
|
-
export abstract class PikkuWorkflowService implements WorkflowService {
|
|
112
|
-
private inlineRuns = new Set<string>()
|
|
113
|
-
|
|
114
|
-
constructor() {}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Check if a run is executing inline (without queues)
|
|
118
|
-
*/
|
|
119
|
-
protected isInline(runId: string): boolean {
|
|
120
|
-
return this.inlineRuns.has(runId)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Register a run as inline (for graph-runner to use)
|
|
125
|
-
*/
|
|
126
|
-
public registerInlineRun(runId: string): void {
|
|
127
|
-
this.inlineRuns.add(runId)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Unregister a run from inline tracking
|
|
132
|
-
*/
|
|
133
|
-
public unregisterInlineRun(runId: string): void {
|
|
134
|
-
this.inlineRuns.delete(runId)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
public async registerWorkflowVersions(): Promise<void> {
|
|
138
|
-
const allMeta = pikkuState(null, 'workflows', 'meta')
|
|
139
|
-
for (const [name, meta] of Object.entries(allMeta)) {
|
|
140
|
-
if (!meta.graphHash) continue
|
|
141
|
-
await this.upsertWorkflowVersion(name, meta.graphHash, meta, meta.source)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
abstract createRun(
|
|
146
|
-
workflowName: string,
|
|
147
|
-
input: any,
|
|
148
|
-
inline: boolean,
|
|
149
|
-
graphHash: string,
|
|
150
|
-
wire: WorkflowRunWire
|
|
151
|
-
): Promise<string>
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Get a workflow run by ID
|
|
155
|
-
* @param id - Run ID
|
|
156
|
-
* @returns Workflow run or null if not found
|
|
157
|
-
*/
|
|
158
|
-
abstract getRun(id: string): Promise<WorkflowRun | null>
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get workflow run history (all step attempts in chronological order)
|
|
162
|
-
* @param runId - Run ID
|
|
163
|
-
* @returns Array of step states with step names, ordered oldest to newest
|
|
164
|
-
*/
|
|
165
|
-
abstract getRunHistory(
|
|
166
|
-
runId: string
|
|
167
|
-
): Promise<Array<StepState & { stepName: string }>>
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Update workflow run status
|
|
171
|
-
* @param id - Run ID
|
|
172
|
-
* @param status - New status
|
|
173
|
-
*/
|
|
174
|
-
abstract updateRunStatus(
|
|
175
|
-
id: string,
|
|
176
|
-
status: WorkflowStatus,
|
|
177
|
-
output?: any,
|
|
178
|
-
error?: SerializedError
|
|
179
|
-
): Promise<void>
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Insert initial step state (called by orchestrator)
|
|
183
|
-
* Creates pending step in both workflow_step and workflow_step_history
|
|
184
|
-
* @param runId - Run ID
|
|
185
|
-
* @param stepName - Step cache key
|
|
186
|
-
* @param rpcName - RPC function name
|
|
187
|
-
* @param data - Step input data
|
|
188
|
-
* @param stepOptions - Step options (retries, retryDelay)
|
|
189
|
-
* @returns Step state with generated stepId
|
|
190
|
-
*/
|
|
191
|
-
abstract insertStepState(
|
|
192
|
-
runId: string,
|
|
193
|
-
stepName: string,
|
|
194
|
-
rpcName: string | null,
|
|
195
|
-
data: any,
|
|
196
|
-
stepOptions?: WorkflowStepOptions
|
|
197
|
-
): Promise<StepState>
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Get step state by cache key (read-only)
|
|
201
|
-
* @param runId - Run ID
|
|
202
|
-
* @param stepName - Step cache key (from workflow.do)
|
|
203
|
-
* @returns Step state with attemptCount calculated from history
|
|
204
|
-
*/
|
|
205
|
-
abstract getStepState(runId: string, stepName: string): Promise<StepState>
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Mark step as running
|
|
209
|
-
* Updates both workflow_step and workflow_step_history
|
|
210
|
-
* @param stepId - Step ID
|
|
211
|
-
*/
|
|
212
|
-
abstract setStepRunning(stepId: string): Promise<void>
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Mark step as scheduled (queued for execution)
|
|
216
|
-
* Updates both workflow_step and workflow_step_history
|
|
217
|
-
* @param stepId - Step ID
|
|
218
|
-
*/
|
|
219
|
-
abstract setStepScheduled(stepId: string): Promise<void>
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Store step result and mark as succeeded
|
|
223
|
-
* Updates both workflow_step and workflow_step_history
|
|
224
|
-
* @param stepId - Step ID
|
|
225
|
-
* @param result - Step result
|
|
226
|
-
*/
|
|
227
|
-
abstract setStepResult(stepId: string, result: any): Promise<void>
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Store step error and mark as failed
|
|
231
|
-
* Updates both workflow_step and workflow_step_history
|
|
232
|
-
* @param stepId - Step ID
|
|
233
|
-
* @param error - Error object
|
|
234
|
-
*/
|
|
235
|
-
abstract setStepError(stepId: string, error: Error): Promise<void>
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Create a new retry attempt for a failed step
|
|
239
|
-
* Inserts new pending step in both workflow_step and workflow_step_history
|
|
240
|
-
* Resets status to 'pending' with new stepId
|
|
241
|
-
* Copies metadata (rpcName, data, retries, retryDelay) from failed attempt
|
|
242
|
-
* @param failedStepId - Failed step ID to copy from
|
|
243
|
-
* @returns New step state for the retry attempt
|
|
244
|
-
*/
|
|
245
|
-
abstract createRetryAttempt(
|
|
246
|
-
failedStepId: string,
|
|
247
|
-
status: 'pending' | 'running'
|
|
248
|
-
): Promise<StepState>
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Execute function within a run lock to prevent concurrent modifications
|
|
252
|
-
* @param id - Run ID
|
|
253
|
-
* @param fn - Function to execute
|
|
254
|
-
* @returns Function result
|
|
255
|
-
*/
|
|
256
|
-
abstract withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Execute function within a step lock to prevent concurrent step execution
|
|
260
|
-
* @param runId - Run ID
|
|
261
|
-
* @param stepName - Step name
|
|
262
|
-
* @param fn - Function to execute
|
|
263
|
-
* @returns Function result
|
|
264
|
-
*/
|
|
265
|
-
abstract withStepLock<T>(
|
|
266
|
-
runId: string,
|
|
267
|
-
stepName: string,
|
|
268
|
-
fn: () => Promise<T>
|
|
269
|
-
): Promise<T>
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Close any open connections
|
|
273
|
-
*/
|
|
274
|
-
abstract close(): Promise<void>
|
|
275
|
-
|
|
276
|
-
// ============================================================================
|
|
277
|
-
// Workflow Graph Methods
|
|
278
|
-
// ============================================================================
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Get completed graph state (lightweight - no results)
|
|
282
|
-
* @param runId - Run ID
|
|
283
|
-
* @returns Completed node IDs and their branch keys
|
|
284
|
-
*/
|
|
285
|
-
abstract getCompletedGraphState(runId: string): Promise<{
|
|
286
|
-
completedNodeIds: string[]
|
|
287
|
-
failedNodeIds: string[]
|
|
288
|
-
branchKeys: Record<string, string>
|
|
289
|
-
}>
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Filter candidate nodes to only those without existing steps
|
|
293
|
-
* @param runId - Run ID
|
|
294
|
-
* @param nodeIds - Candidate node IDs to check
|
|
295
|
-
* @returns Node IDs that don't have a step yet
|
|
296
|
-
*/
|
|
297
|
-
abstract getNodesWithoutSteps(
|
|
298
|
-
runId: string,
|
|
299
|
-
nodeIds: string[]
|
|
300
|
-
): Promise<string[]>
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Get results for specific nodes
|
|
304
|
-
* @param runId - Run ID
|
|
305
|
-
* @param nodeIds - Node IDs to fetch results for
|
|
306
|
-
* @returns Map of nodeId to result
|
|
307
|
-
*/
|
|
308
|
-
abstract getNodeResults(
|
|
309
|
-
runId: string,
|
|
310
|
-
nodeIds: string[]
|
|
311
|
-
): Promise<Record<string, any>>
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Set the branch key for a graph node step
|
|
315
|
-
* @param stepId - Step ID
|
|
316
|
-
* @param branchKey - Branch key selected by graph.branch()
|
|
317
|
-
*/
|
|
318
|
-
abstract setBranchTaken(stepId: string, branchKey: string): Promise<void>
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Update a state variable in the workflow run's state
|
|
322
|
-
* @param runId - Run ID
|
|
323
|
-
* @param name - Variable name
|
|
324
|
-
* @param value - Value to store
|
|
325
|
-
*/
|
|
326
|
-
abstract updateRunState(
|
|
327
|
-
runId: string,
|
|
328
|
-
name: string,
|
|
329
|
-
value: unknown
|
|
330
|
-
): Promise<void>
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Get the entire state object for a workflow run
|
|
334
|
-
* @param runId - Run ID
|
|
335
|
-
* @returns The state object with all variables
|
|
336
|
-
*/
|
|
337
|
-
abstract getRunState(runId: string): Promise<Record<string, unknown>>
|
|
338
|
-
|
|
339
|
-
abstract upsertWorkflowVersion(
|
|
340
|
-
name: string,
|
|
341
|
-
graphHash: string,
|
|
342
|
-
graph: any,
|
|
343
|
-
source: string,
|
|
344
|
-
status?: WorkflowVersionStatus
|
|
345
|
-
): Promise<void>
|
|
346
|
-
|
|
347
|
-
abstract updateWorkflowVersionStatus(
|
|
348
|
-
name: string,
|
|
349
|
-
graphHash: string,
|
|
350
|
-
status: WorkflowVersionStatus
|
|
351
|
-
): Promise<void>
|
|
352
|
-
|
|
353
|
-
abstract getWorkflowVersion(
|
|
354
|
-
name: string,
|
|
355
|
-
graphHash: string
|
|
356
|
-
): Promise<{ graph: any; source: string } | null>
|
|
357
|
-
|
|
358
|
-
abstract getAIGeneratedWorkflows(
|
|
359
|
-
agentName?: string
|
|
360
|
-
): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>>
|
|
361
|
-
|
|
362
|
-
// ============================================================================
|
|
363
|
-
// Workflow Lifecycle Methods
|
|
364
|
-
// ============================================================================
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Resume a paused workflow by triggering the orchestrator
|
|
368
|
-
* @param runId - Run ID
|
|
369
|
-
*/
|
|
370
|
-
public async resumeWorkflow(runId: string): Promise<void> {
|
|
371
|
-
const queueService = this.verifyQueueService()
|
|
372
|
-
await queueService.add(this.getConfig().orchestratorQueueName, { runId })
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
public async queueStepWorker(
|
|
376
|
-
runId: string,
|
|
377
|
-
stepName: string,
|
|
378
|
-
rpcName: string,
|
|
379
|
-
data: any
|
|
380
|
-
): Promise<void> {
|
|
381
|
-
const queueService = this.verifyQueueService()
|
|
382
|
-
await queueService.add(this.getConfig().stepWorkerQueueName, {
|
|
383
|
-
runId,
|
|
384
|
-
stepName,
|
|
385
|
-
rpcName,
|
|
386
|
-
data,
|
|
387
|
-
})
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Execute a workflow sleep step completion
|
|
392
|
-
* Sets the step result to null and resumes the workflow
|
|
393
|
-
* @param data - Sleeper input data
|
|
394
|
-
*/
|
|
395
|
-
public async executeWorkflowSleepCompleted(
|
|
396
|
-
runId: string,
|
|
397
|
-
stepId: string
|
|
398
|
-
): Promise<void> {
|
|
399
|
-
await this.setStepResult(stepId, null)
|
|
400
|
-
await this.resumeWorkflow(runId)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Schedule orchestrator retry with delay
|
|
405
|
-
* @param runId - Run ID
|
|
406
|
-
* @param retryDelay - Delay in milliseconds or duration string (optional)
|
|
407
|
-
*/
|
|
408
|
-
protected async scheduleOrchestratorRetry(
|
|
409
|
-
runId: string,
|
|
410
|
-
retryDelay?: number | string
|
|
411
|
-
): Promise<void> {
|
|
412
|
-
const queueService = this.verifyQueueService()
|
|
413
|
-
await queueService.add(
|
|
414
|
-
this.getConfig().orchestratorQueueName,
|
|
415
|
-
{ runId },
|
|
416
|
-
retryDelay ? { delay: getDurationInMilliseconds(retryDelay) } : undefined
|
|
417
|
-
)
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Start a new workflow run
|
|
422
|
-
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
423
|
-
* @param options.inline - If true, execute workflow directly without queue service
|
|
424
|
-
* @param options.startNode - Starting node ID for graph workflows (from wire config)
|
|
425
|
-
*/
|
|
426
|
-
public async startWorkflow<I>(
|
|
427
|
-
name: string,
|
|
428
|
-
input: I,
|
|
429
|
-
wire: WorkflowRunWire,
|
|
430
|
-
rpcService: any,
|
|
431
|
-
options?: { inline?: boolean; startNode?: string }
|
|
432
|
-
): Promise<{ runId: string }> {
|
|
433
|
-
// Check meta to determine workflow type
|
|
434
|
-
const meta = pikkuState(null, 'workflows', 'meta')
|
|
435
|
-
const workflowMeta = meta[name]
|
|
436
|
-
|
|
437
|
-
if (!workflowMeta) {
|
|
438
|
-
throw new WorkflowNotFoundError(name)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (workflowMeta.source === 'graph' || workflowMeta.source === 'ai-agent') {
|
|
442
|
-
const shouldInline =
|
|
443
|
-
options?.inline || !getSingletonServices()?.queueService
|
|
444
|
-
return runWorkflowGraph(
|
|
445
|
-
this,
|
|
446
|
-
name,
|
|
447
|
-
input,
|
|
448
|
-
rpcService,
|
|
449
|
-
shouldInline,
|
|
450
|
-
options?.startNode,
|
|
451
|
-
wire
|
|
452
|
-
)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// DSL workflow - check registration exists
|
|
456
|
-
const registrations = pikkuState(null, 'workflows', 'registrations')
|
|
457
|
-
const workflow = registrations.get(name)
|
|
458
|
-
|
|
459
|
-
if (!workflow) {
|
|
460
|
-
throw new WorkflowNotFoundError(name)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (!workflowMeta.graphHash) {
|
|
464
|
-
throw new Error(`Missing workflow graphHash for '${name}'`)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const runId = await this.createRun(
|
|
468
|
-
name,
|
|
469
|
-
input,
|
|
470
|
-
options?.inline ?? false,
|
|
471
|
-
workflowMeta.graphHash,
|
|
472
|
-
wire
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
if (options?.inline) {
|
|
476
|
-
this.inlineRuns.add(runId)
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (options?.inline || !getSingletonServices()?.queueService) {
|
|
480
|
-
this.runWorkflowJob(runId, rpcService)
|
|
481
|
-
.catch(() => {})
|
|
482
|
-
.finally(() => {
|
|
483
|
-
if (options?.inline) {
|
|
484
|
-
this.inlineRuns.delete(runId)
|
|
485
|
-
}
|
|
486
|
-
})
|
|
487
|
-
} else {
|
|
488
|
-
await this.resumeWorkflow(runId)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return { runId }
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
public async runToCompletion<I>(
|
|
495
|
-
name: string,
|
|
496
|
-
input: I,
|
|
497
|
-
rpcService: any,
|
|
498
|
-
options?: { pollIntervalMs?: number; wire?: WorkflowRunWire }
|
|
499
|
-
): Promise<any> {
|
|
500
|
-
const pollInterval = options?.pollIntervalMs ?? 1000
|
|
501
|
-
const { runId } = await this.startWorkflow(
|
|
502
|
-
name,
|
|
503
|
-
input,
|
|
504
|
-
options?.wire ?? { type: 'internal' },
|
|
505
|
-
rpcService,
|
|
506
|
-
{ inline: true }
|
|
507
|
-
)
|
|
508
|
-
while (true) {
|
|
509
|
-
const run = await this.getRun(runId)
|
|
510
|
-
if (!run) {
|
|
511
|
-
throw new WorkflowRunNotFoundError(runId)
|
|
512
|
-
}
|
|
513
|
-
if (WORKFLOW_END_STATES.has(run.status)) {
|
|
514
|
-
if (run.status === 'failed') {
|
|
515
|
-
throw new Error(run.error?.message || 'Workflow failed')
|
|
516
|
-
}
|
|
517
|
-
if (run.status === 'cancelled') {
|
|
518
|
-
throw new Error('Workflow was cancelled')
|
|
519
|
-
}
|
|
520
|
-
return run.output
|
|
521
|
-
}
|
|
522
|
-
await new Promise((resolve) => setTimeout(resolve, pollInterval))
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
public async runWorkflowJob(runId: string, rpcService: any): Promise<void> {
|
|
527
|
-
const run = await this.getRun(runId)
|
|
528
|
-
if (!run) {
|
|
529
|
-
throw new WorkflowRunNotFoundError(runId)
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const meta = pikkuState(null, 'workflows', 'meta')
|
|
533
|
-
const workflowMeta = meta[run.workflow]
|
|
534
|
-
|
|
535
|
-
if (
|
|
536
|
-
run.graphHash &&
|
|
537
|
-
workflowMeta?.graphHash &&
|
|
538
|
-
run.graphHash !== workflowMeta.graphHash
|
|
539
|
-
) {
|
|
540
|
-
await this.runVersionMismatchFallback(run, workflowMeta, rpcService)
|
|
541
|
-
return
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (workflowMeta?.source === 'graph') {
|
|
545
|
-
await continueGraph(this, runId, run.workflow)
|
|
546
|
-
return
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const registrations = pikkuState(null, 'workflows', 'registrations')
|
|
550
|
-
const workflow = registrations.get(run.workflow)
|
|
551
|
-
if (!workflow) {
|
|
552
|
-
throw new WorkflowNotFoundError(run.workflow)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
await this.withRunLock(runId, async () => {
|
|
556
|
-
const workflowWire = this.createWorkflowWire(
|
|
557
|
-
run.workflow,
|
|
558
|
-
runId,
|
|
559
|
-
rpcService
|
|
560
|
-
)
|
|
561
|
-
const wire: PikkuWire = { workflow: workflowWire }
|
|
562
|
-
try {
|
|
563
|
-
const result = await runPikkuFunc(
|
|
564
|
-
'workflow',
|
|
565
|
-
workflowMeta.name,
|
|
566
|
-
workflowMeta.pikkuFuncId,
|
|
567
|
-
{
|
|
568
|
-
singletonServices: getSingletonServices()!,
|
|
569
|
-
wire,
|
|
570
|
-
createWireServices: getCreateWireServices(),
|
|
571
|
-
data: () => run.input,
|
|
572
|
-
}
|
|
573
|
-
)
|
|
574
|
-
|
|
575
|
-
await this.updateRunStatus(runId, 'completed', result)
|
|
576
|
-
} catch (error: any) {
|
|
577
|
-
if (error instanceof WorkflowAsyncException) {
|
|
578
|
-
throw error
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
if (error instanceof WorkflowCancelledException) {
|
|
582
|
-
await this.updateRunStatus(runId, 'cancelled', undefined, {
|
|
583
|
-
message: error.message || 'Workflow cancelled',
|
|
584
|
-
stack: '',
|
|
585
|
-
code: 'WORKFLOW_CANCELLED',
|
|
586
|
-
})
|
|
587
|
-
throw error
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (error instanceof WorkflowSuspendedException) {
|
|
591
|
-
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
592
|
-
message: error.message || 'Workflow suspended',
|
|
593
|
-
stack: '',
|
|
594
|
-
code: 'WORKFLOW_SUSPENDED',
|
|
595
|
-
})
|
|
596
|
-
throw error
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
await this.updateRunStatus(runId, 'failed', undefined, {
|
|
600
|
-
message: error.message,
|
|
601
|
-
stack: error.stack,
|
|
602
|
-
code: error.code,
|
|
603
|
-
})
|
|
604
|
-
|
|
605
|
-
throw error
|
|
606
|
-
}
|
|
607
|
-
})
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private async runVersionMismatchFallback(
|
|
611
|
-
run: WorkflowRun,
|
|
612
|
-
currentMeta: { source: string },
|
|
613
|
-
rpcService: any
|
|
614
|
-
): Promise<void> {
|
|
615
|
-
const source = currentMeta.source
|
|
616
|
-
|
|
617
|
-
if (source === 'complex') {
|
|
618
|
-
await this.updateRunStatus(run.id, 'failed', undefined, {
|
|
619
|
-
message: `Workflow '${run.workflow}' definition changed. Complex workflows with inline steps cannot be migrated.`,
|
|
620
|
-
stack: '',
|
|
621
|
-
code: 'VERSION_CONFLICT',
|
|
622
|
-
})
|
|
623
|
-
return
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const version = await this.getWorkflowVersion(run.workflow, run.graphHash!)
|
|
627
|
-
if (!version) {
|
|
628
|
-
await this.updateRunStatus(run.id, 'failed', undefined, {
|
|
629
|
-
message: `Workflow '${run.workflow}' version '${run.graphHash}' not found. Cannot resume with changed definition.`,
|
|
630
|
-
stack: '',
|
|
631
|
-
code: 'VERSION_NOT_FOUND',
|
|
632
|
-
})
|
|
633
|
-
return
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
await runFromMeta(this, run.id, version.graph, rpcService)
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Execute a single workflow step (called by worker)
|
|
641
|
-
* Handles idempotency, RPC execution, result storage, retry logic, and orchestrator triggering
|
|
642
|
-
*/
|
|
643
|
-
public async executeWorkflowStep(
|
|
644
|
-
runId: string,
|
|
645
|
-
stepName: string,
|
|
646
|
-
rpcName: string,
|
|
647
|
-
data: any,
|
|
648
|
-
rpcService: any
|
|
649
|
-
): Promise<void> {
|
|
650
|
-
// Use step-level lock to prevent concurrent execution of same step
|
|
651
|
-
await this.withStepLock(runId, stepName, async () => {
|
|
652
|
-
// Get step state
|
|
653
|
-
let stepState = await this.getStepState(runId, stepName)
|
|
654
|
-
|
|
655
|
-
// Idempotency - if already succeeded, resume orchestrator and return
|
|
656
|
-
if (stepState.status === 'succeeded') {
|
|
657
|
-
await this.resumeWorkflow(runId)
|
|
658
|
-
return
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Log warning if already running (race condition)
|
|
662
|
-
if (stepState.status === 'running') {
|
|
663
|
-
return
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// If status is 'failed', this is a retry - create new attempt history
|
|
667
|
-
if (stepState.status === 'failed') {
|
|
668
|
-
stepState = await this.createRetryAttempt(stepState.stepId, 'running')
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (stepState.status === 'pending') {
|
|
672
|
-
// Mark pending step as running before execution
|
|
673
|
-
await this.setStepRunning(stepState.stepId)
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
try {
|
|
677
|
-
let result: any
|
|
678
|
-
|
|
679
|
-
const run = await this.getRun(runId)
|
|
680
|
-
if (!run) {
|
|
681
|
-
throw new Error(`Workflow run not found: ${runId}`)
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const meta = pikkuState(null, 'workflows', 'meta')
|
|
685
|
-
const workflowMeta = meta[run.workflow]
|
|
686
|
-
|
|
687
|
-
if (workflowMeta?.nodes && stepName in workflowMeta.nodes) {
|
|
688
|
-
result = await executeGraphStep(
|
|
689
|
-
this,
|
|
690
|
-
rpcService,
|
|
691
|
-
runId,
|
|
692
|
-
stepState.stepId,
|
|
693
|
-
stepName,
|
|
694
|
-
rpcName,
|
|
695
|
-
data,
|
|
696
|
-
run.workflow
|
|
697
|
-
)
|
|
698
|
-
} else {
|
|
699
|
-
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
700
|
-
workflowStep: {
|
|
701
|
-
runId,
|
|
702
|
-
stepId: stepState.stepId,
|
|
703
|
-
attemptCount: stepState.attemptCount,
|
|
704
|
-
},
|
|
705
|
-
})
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// Store result and mark succeeded
|
|
709
|
-
await this.setStepResult(stepState.stepId, result)
|
|
710
|
-
|
|
711
|
-
// Resume orchestrator to continue workflow
|
|
712
|
-
await this.resumeWorkflow(runId)
|
|
713
|
-
} catch (error: any) {
|
|
714
|
-
if (error instanceof RPCNotFoundError) {
|
|
715
|
-
await this.setStepError(stepState.stepId, error)
|
|
716
|
-
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
717
|
-
message: `RPC '${rpcName}' not found. Deploy the missing function and resume.`,
|
|
718
|
-
code: 'RPC_NOT_FOUND',
|
|
719
|
-
})
|
|
720
|
-
return
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Store error and mark failed
|
|
724
|
-
await this.setStepError(stepState.stepId, error)
|
|
725
|
-
|
|
726
|
-
const maxAttempts = (stepState.retries ?? 0) + 1
|
|
727
|
-
const retriesExhausted = stepState.attemptCount >= maxAttempts
|
|
728
|
-
|
|
729
|
-
if (retriesExhausted) {
|
|
730
|
-
// No more retries - resume orchestrator to mark workflow as failed
|
|
731
|
-
await this.resumeWorkflow(runId)
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Always throw so queue knows the job failed and can retry if needed
|
|
735
|
-
throw error
|
|
736
|
-
}
|
|
737
|
-
})
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Orchestrate workflow execution (called by orchestrator)
|
|
742
|
-
* Runs workflow job and handles async exceptions
|
|
743
|
-
*/
|
|
744
|
-
public async orchestrateWorkflow(
|
|
745
|
-
runId: string,
|
|
746
|
-
rpcService: any
|
|
747
|
-
): Promise<void> {
|
|
748
|
-
try {
|
|
749
|
-
// Run workflow job (replays with caching)
|
|
750
|
-
await this.runWorkflowJob(runId, rpcService)
|
|
751
|
-
} catch (error: any) {
|
|
752
|
-
if (
|
|
753
|
-
error.name === 'WorkflowAsyncException' ||
|
|
754
|
-
error.name === 'WorkflowCancelledException' ||
|
|
755
|
-
error.name === 'WorkflowSuspendedException'
|
|
756
|
-
) {
|
|
757
|
-
return
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
await this.updateRunStatus(runId, 'failed', undefined, {
|
|
761
|
-
message: error.message,
|
|
762
|
-
stack: error.stack,
|
|
763
|
-
code: error.code,
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
throw error
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
private verifyQueueService(): QueueService {
|
|
771
|
-
if (!getSingletonServices()?.queueService) {
|
|
772
|
-
throw new Error(
|
|
773
|
-
'QueueService not configured. Remote workflows require a queue service.'
|
|
774
|
-
)
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return getSingletonServices()!.queueService!
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
private async rpcStep(
|
|
781
|
-
runId: string,
|
|
782
|
-
stepName: string,
|
|
783
|
-
rpcName: string,
|
|
784
|
-
data: any,
|
|
785
|
-
rpcService: any,
|
|
786
|
-
stepOptions?: WorkflowStepOptions
|
|
787
|
-
): Promise<any> {
|
|
788
|
-
// Check if step already exists
|
|
789
|
-
let stepState: StepState
|
|
790
|
-
try {
|
|
791
|
-
stepState = await this.getStepState(runId, stepName)
|
|
792
|
-
} catch {
|
|
793
|
-
// Step doesn't exist - create it
|
|
794
|
-
stepState = await this.insertStepState(
|
|
795
|
-
runId,
|
|
796
|
-
stepName,
|
|
797
|
-
rpcName,
|
|
798
|
-
data,
|
|
799
|
-
stepOptions
|
|
800
|
-
)
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (stepState.status === 'succeeded') {
|
|
804
|
-
// Return cached result
|
|
805
|
-
return stepState.result
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (stepState.status === 'failed') {
|
|
809
|
-
// Step failed with retries exhausted - throw error to fail the workflow
|
|
810
|
-
const error = new Error(
|
|
811
|
-
stepState.error?.message ||
|
|
812
|
-
`Step '${stepName}' failed after exhausting all retries`
|
|
813
|
-
)
|
|
814
|
-
// Preserve original error properties if available
|
|
815
|
-
if (stepState.error) {
|
|
816
|
-
Object.assign(error, stepState.error)
|
|
817
|
-
}
|
|
818
|
-
throw error
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (stepState.status === 'scheduled') {
|
|
822
|
-
// Step is already scheduled, pause workflow
|
|
823
|
-
throw new WorkflowAsyncException(runId, stepName)
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// Step is pending - schedule it
|
|
827
|
-
await this.setStepScheduled(stepState.stepId)
|
|
828
|
-
|
|
829
|
-
// Enqueue step worker (unless inline mode)
|
|
830
|
-
if (!this.isInline(runId) && getSingletonServices()!.queueService) {
|
|
831
|
-
// Map step retry options to queue job options
|
|
832
|
-
const retries = stepOptions?.retries ?? 0
|
|
833
|
-
const retryDelay = stepOptions?.retryDelay
|
|
834
|
-
|
|
835
|
-
await getSingletonServices()!.queueService!.add(
|
|
836
|
-
this.getConfig().stepWorkerQueueName,
|
|
837
|
-
{
|
|
838
|
-
runId,
|
|
839
|
-
stepName,
|
|
840
|
-
rpcName,
|
|
841
|
-
data,
|
|
842
|
-
},
|
|
843
|
-
{
|
|
844
|
-
// attempts includes initial attempt, retries doesn't
|
|
845
|
-
attempts: retries + 1,
|
|
846
|
-
// Map retry delay to backoff
|
|
847
|
-
backoff:
|
|
848
|
-
typeof retryDelay === 'number'
|
|
849
|
-
? { type: 'fixed', delay: retryDelay }
|
|
850
|
-
: retryDelay === 'exponential'
|
|
851
|
-
? 'exponential'
|
|
852
|
-
: undefined,
|
|
853
|
-
}
|
|
854
|
-
)
|
|
855
|
-
// Pause workflow - step will callback when done
|
|
856
|
-
throw new WorkflowAsyncException(runId, stepName)
|
|
857
|
-
} else {
|
|
858
|
-
// Inline or no queue service - execute locally with retry loop
|
|
859
|
-
const retries = stepOptions?.retries ?? 0
|
|
860
|
-
const retryDelay = stepOptions?.retryDelay
|
|
861
|
-
let currentStepState = stepState
|
|
862
|
-
|
|
863
|
-
while (true) {
|
|
864
|
-
try {
|
|
865
|
-
await this.setStepRunning(currentStepState.stepId)
|
|
866
|
-
const result = await rpcService.rpcWithWire(rpcName, data, {
|
|
867
|
-
workflowStep: {
|
|
868
|
-
runId,
|
|
869
|
-
stepId: currentStepState.stepId,
|
|
870
|
-
attemptCount: currentStepState.attemptCount,
|
|
871
|
-
},
|
|
872
|
-
})
|
|
873
|
-
await this.setStepResult(currentStepState.stepId, result)
|
|
874
|
-
return result
|
|
875
|
-
} catch (error: any) {
|
|
876
|
-
if (error instanceof RPCNotFoundError) {
|
|
877
|
-
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
878
|
-
message: `RPC '${rpcName}' not found. Deploy the missing function and resume.`,
|
|
879
|
-
code: 'RPC_NOT_FOUND',
|
|
880
|
-
})
|
|
881
|
-
throw error
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Record the error (marks step as failed)
|
|
885
|
-
await this.setStepError(currentStepState.stepId, error)
|
|
886
|
-
|
|
887
|
-
// Check if we should retry
|
|
888
|
-
if (currentStepState.attemptCount < retries) {
|
|
889
|
-
// Create a new pending retry attempt
|
|
890
|
-
currentStepState = await this.createRetryAttempt(
|
|
891
|
-
currentStepState.stepId,
|
|
892
|
-
'pending'
|
|
893
|
-
)
|
|
894
|
-
|
|
895
|
-
// Wait for retry delay if specified
|
|
896
|
-
if (retryDelay) {
|
|
897
|
-
await new Promise((resolve) =>
|
|
898
|
-
setTimeout(resolve, getDurationInMilliseconds(retryDelay))
|
|
899
|
-
)
|
|
900
|
-
}
|
|
901
|
-
// Continue loop to retry
|
|
902
|
-
} else {
|
|
903
|
-
// No more retries, fail the workflow
|
|
904
|
-
throw error
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
private async inlineStep(
|
|
912
|
-
runId: string,
|
|
913
|
-
stepName: string,
|
|
914
|
-
fn: Function,
|
|
915
|
-
stepOptions?: WorkflowStepOptions
|
|
916
|
-
): Promise<any> {
|
|
917
|
-
// Check if step already exists
|
|
918
|
-
let stepState: StepState
|
|
919
|
-
try {
|
|
920
|
-
stepState = await this.getStepState(runId, stepName)
|
|
921
|
-
} catch {
|
|
922
|
-
// Step doesn't exist - create it (inline, no RPC)
|
|
923
|
-
stepState = await this.insertStepState(
|
|
924
|
-
runId,
|
|
925
|
-
stepName,
|
|
926
|
-
null,
|
|
927
|
-
null,
|
|
928
|
-
stepOptions
|
|
929
|
-
)
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
if (stepState.status === 'succeeded') {
|
|
933
|
-
// Return cached result
|
|
934
|
-
return stepState.result
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// Execute inline function
|
|
938
|
-
const retries = stepOptions?.retries ?? this.getConfig().retries
|
|
939
|
-
const retryDelay = stepOptions?.retryDelay ?? this.getConfig().retryDelay
|
|
940
|
-
let currentStepState = stepState
|
|
941
|
-
|
|
942
|
-
// Check if we're running inline (in-memory) or remote (queue-based)
|
|
943
|
-
if (this.isInline(runId)) {
|
|
944
|
-
// Inline mode - execute with retry loop
|
|
945
|
-
while (true) {
|
|
946
|
-
try {
|
|
947
|
-
await this.setStepRunning(currentStepState.stepId)
|
|
948
|
-
const result = await fn()
|
|
949
|
-
await this.setStepResult(currentStepState.stepId, result)
|
|
950
|
-
return result
|
|
951
|
-
} catch (error: any) {
|
|
952
|
-
// Record the error (marks step as failed)
|
|
953
|
-
await this.setStepError(currentStepState.stepId, error)
|
|
954
|
-
|
|
955
|
-
// Check if we should retry
|
|
956
|
-
if (currentStepState.attemptCount < retries) {
|
|
957
|
-
// Create a new pending retry attempt
|
|
958
|
-
currentStepState = await this.createRetryAttempt(
|
|
959
|
-
currentStepState.stepId,
|
|
960
|
-
'pending'
|
|
961
|
-
)
|
|
962
|
-
|
|
963
|
-
// Wait for retry delay if specified
|
|
964
|
-
if (retryDelay) {
|
|
965
|
-
await new Promise((resolve) =>
|
|
966
|
-
setTimeout(resolve, getDurationInMilliseconds(retryDelay))
|
|
967
|
-
)
|
|
968
|
-
}
|
|
969
|
-
// Continue loop to retry
|
|
970
|
-
} else {
|
|
971
|
-
// No more retries, fail the workflow
|
|
972
|
-
throw error
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
} else {
|
|
977
|
-
// Remote mode - use queue-based retry
|
|
978
|
-
try {
|
|
979
|
-
await this.setStepRunning(currentStepState.stepId)
|
|
980
|
-
const result = await fn()
|
|
981
|
-
await this.setStepResult(currentStepState.stepId, result)
|
|
982
|
-
return result
|
|
983
|
-
} catch (error: any) {
|
|
984
|
-
// Record the error (marks step as failed)
|
|
985
|
-
await this.setStepError(currentStepState.stepId, error)
|
|
986
|
-
|
|
987
|
-
// Check if we should retry
|
|
988
|
-
if (currentStepState.attemptCount < retries) {
|
|
989
|
-
// Create a new pending retry attempt (copies metadata from failed step)
|
|
990
|
-
await this.createRetryAttempt(currentStepState.stepId, 'pending')
|
|
991
|
-
|
|
992
|
-
// Schedule orchestrator to retry after delay
|
|
993
|
-
await this.scheduleOrchestratorRetry(runId, retryDelay)
|
|
994
|
-
|
|
995
|
-
// Pause workflow - orchestrator will replay and pick up new attempt
|
|
996
|
-
throw new WorkflowAsyncException(runId, stepName)
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// No more retries, fail the workflow
|
|
1000
|
-
throw error
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
private async sleepStep(runId: string, stepName: string, duration: number) {
|
|
1006
|
-
// Check if step already exists
|
|
1007
|
-
let stepState: StepState
|
|
1008
|
-
try {
|
|
1009
|
-
stepState = await this.getStepState(runId, stepName)
|
|
1010
|
-
} catch {
|
|
1011
|
-
// Step doesn't exist - create it (sleep step, no RPC)
|
|
1012
|
-
stepState = await this.insertStepState(runId, stepName, null, {
|
|
1013
|
-
duration,
|
|
1014
|
-
})
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (stepState.status === 'succeeded') {
|
|
1018
|
-
// Sleep already completed, return immediately
|
|
1019
|
-
return
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
if (stepState.status === 'scheduled') {
|
|
1023
|
-
// Sleep is already scheduled, pause workflow
|
|
1024
|
-
throw new WorkflowAsyncException(runId, stepName)
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
// Step is pending - schedule it
|
|
1028
|
-
await this.setStepScheduled(stepState.stepId)
|
|
1029
|
-
|
|
1030
|
-
// Check if inline mode or no scheduler service
|
|
1031
|
-
if (!this.isInline(runId) && getSingletonServices()!.schedulerService) {
|
|
1032
|
-
// Remote mode - schedule sleep via scheduler service
|
|
1033
|
-
await getSingletonServices()!.schedulerService!.scheduleRPC(
|
|
1034
|
-
duration,
|
|
1035
|
-
this.getConfig().sleeperRPCName,
|
|
1036
|
-
{
|
|
1037
|
-
runId,
|
|
1038
|
-
stepId: stepState.stepId,
|
|
1039
|
-
}
|
|
1040
|
-
)
|
|
1041
|
-
// Pause workflow - sleep will callback when done
|
|
1042
|
-
throw new WorkflowAsyncException(runId, stepName)
|
|
1043
|
-
} else {
|
|
1044
|
-
// Inline mode - use setTimeout with actual duration
|
|
1045
|
-
await new Promise((resolve) =>
|
|
1046
|
-
setTimeout(resolve, getDurationInMilliseconds(duration))
|
|
1047
|
-
)
|
|
1048
|
-
await this.setStepResult(stepState.stepId, null)
|
|
1049
|
-
return
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
private getSuspendStepName(reason: string): string {
|
|
1054
|
-
if (!reason) {
|
|
1055
|
-
return '__workflow_suspend'
|
|
1056
|
-
}
|
|
1057
|
-
return '__workflow_suspend'
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
private async suspendStep(runId: string, reason: string): Promise<void> {
|
|
1061
|
-
const stepName = this.getSuspendStepName(reason)
|
|
1062
|
-
await this.withStepLock(runId, stepName, async () => {
|
|
1063
|
-
let stepState: StepState
|
|
1064
|
-
try {
|
|
1065
|
-
stepState = await this.getStepState(runId, stepName)
|
|
1066
|
-
} catch {
|
|
1067
|
-
stepState = await this.insertStepState(
|
|
1068
|
-
runId,
|
|
1069
|
-
stepName,
|
|
1070
|
-
'pikkuWorkflowSuspend',
|
|
1071
|
-
{
|
|
1072
|
-
reason,
|
|
1073
|
-
}
|
|
1074
|
-
)
|
|
1075
|
-
}
|
|
1076
|
-
if (!stepState.stepId) {
|
|
1077
|
-
stepState = await this.insertStepState(
|
|
1078
|
-
runId,
|
|
1079
|
-
stepName,
|
|
1080
|
-
'pikkuWorkflowSuspend',
|
|
1081
|
-
{
|
|
1082
|
-
reason,
|
|
1083
|
-
}
|
|
1084
|
-
)
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
if (stepState.status === 'succeeded') {
|
|
1088
|
-
return
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
if (stepState.status === 'pending') {
|
|
1092
|
-
await this.setStepRunning(stepState.stepId)
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
await this.setStepResult(stepState.stepId, {
|
|
1096
|
-
reason,
|
|
1097
|
-
suspendedAt: new Date().toISOString(),
|
|
1098
|
-
})
|
|
1099
|
-
throw new WorkflowSuspendedException(runId, reason)
|
|
1100
|
-
})
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
private createWorkflowWire(
|
|
1104
|
-
name: string,
|
|
1105
|
-
runId: string,
|
|
1106
|
-
rpcService: any
|
|
1107
|
-
): PikkuWorkflowWire {
|
|
1108
|
-
const workflowWire: PikkuWorkflowWire = {
|
|
1109
|
-
name,
|
|
1110
|
-
runId,
|
|
1111
|
-
getRun: async () => (await this.getRun(runId)) as WorkflowRun,
|
|
1112
|
-
|
|
1113
|
-
// Implement workflow.do() - RPC form
|
|
1114
|
-
do: async (
|
|
1115
|
-
stepName: string,
|
|
1116
|
-
rpcNameOrFn: any,
|
|
1117
|
-
dataOrOptions?: any,
|
|
1118
|
-
options?: any
|
|
1119
|
-
) => {
|
|
1120
|
-
this.verifyStepName(stepName)
|
|
1121
|
-
if (typeof rpcNameOrFn === 'string') {
|
|
1122
|
-
return await this.rpcStep(
|
|
1123
|
-
runId,
|
|
1124
|
-
stepName,
|
|
1125
|
-
rpcNameOrFn,
|
|
1126
|
-
dataOrOptions,
|
|
1127
|
-
rpcService,
|
|
1128
|
-
options
|
|
1129
|
-
)
|
|
1130
|
-
} else {
|
|
1131
|
-
return await this.inlineStep(
|
|
1132
|
-
runId,
|
|
1133
|
-
stepName,
|
|
1134
|
-
rpcNameOrFn,
|
|
1135
|
-
dataOrOptions
|
|
1136
|
-
)
|
|
1137
|
-
}
|
|
1138
|
-
},
|
|
1139
|
-
|
|
1140
|
-
// Implement workflow.sleep()
|
|
1141
|
-
sleep: async (stepName: string, duration: string | number) => {
|
|
1142
|
-
this.verifyStepName(stepName)
|
|
1143
|
-
await this.sleepStep(
|
|
1144
|
-
runId,
|
|
1145
|
-
stepName,
|
|
1146
|
-
getDurationInMilliseconds(duration)
|
|
1147
|
-
)
|
|
1148
|
-
},
|
|
1149
|
-
|
|
1150
|
-
suspend: async (reason: string) => {
|
|
1151
|
-
await this.suspendStep(runId, reason || 'Workflow suspended')
|
|
1152
|
-
},
|
|
1153
|
-
}
|
|
1154
|
-
return workflowWire
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
private verifyStepName(stepName: string) {
|
|
1158
|
-
if (typeof stepName !== 'string') {
|
|
1159
|
-
throw new WorkflowStepNameNotString(stepName)
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
private getConfig(): WorkflowServiceConfig {
|
|
1164
|
-
const singletonServices = getSingletonServices()
|
|
1165
|
-
const workflow = singletonServices.config?.workflow
|
|
1166
|
-
return {
|
|
1167
|
-
retries: workflow?.retries ?? 0,
|
|
1168
|
-
retryDelay: workflow?.retryDelay ?? 0,
|
|
1169
|
-
orchestratorQueueName:
|
|
1170
|
-
workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
|
|
1171
|
-
stepWorkerQueueName:
|
|
1172
|
-
workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
|
|
1173
|
-
sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
}
|