@pikku/core 0.12.8 → 0.12.9
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 +24 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/errors/errors.d.ts +14 -0
- package/dist/errors/errors.js +27 -0
- package/dist/handle-error.js +2 -1
- package/dist/middleware/auth-bearer.js +10 -1
- package/dist/middleware/auth-cookie.js +34 -25
- package/dist/middleware/timeout.js +11 -5
- package/dist/pikku-state.js +1 -1
- package/dist/services/local-content.d.ts +7 -4
- package/dist/services/local-content.js +42 -12
- package/dist/services/local-secrets.js +2 -2
- package/dist/testing/service-tests.js +1 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
- package/dist/wirings/channel/local/local-channel-runner.js +11 -6
- package/dist/wirings/http/http-runner.js +4 -2
- package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
- package/dist/wirings/workflow/graph/graph-runner.d.ts +7 -0
- package/dist/wirings/workflow/graph/graph-runner.js +72 -7
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +3 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +59 -16
- package/dist/wirings/workflow/workflow.types.d.ts +4 -0
- package/package.json +1 -1
- package/src/env.ts +1 -0
- package/src/errors/errors.ts +35 -0
- package/src/handle-error.ts +2 -1
- package/src/middleware/auth-bearer.ts +10 -1
- package/src/middleware/auth-cookie.ts +11 -4
- package/src/middleware/timeout.ts +14 -7
- package/src/pikku-state.ts +1 -1
- package/src/services/local-content.ts +61 -13
- package/src/services/local-secrets.test.ts +2 -2
- package/src/services/local-secrets.ts +2 -2
- package/src/testing/service-tests.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +34 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +2 -1
- package/src/wirings/channel/local/local-channel-runner.ts +11 -6
- package/src/wirings/http/http-runner.ts +4 -2
- package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
- package/src/wirings/workflow/graph/graph-runner.ts +84 -7
- package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +95 -25
- package/src/wirings/workflow/workflow.types.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -5,6 +5,8 @@ import { processMessageHandlers } from '../channel-handler.js'
|
|
|
5
5
|
import type { CoreChannel, RunChannelOptions } from '../channel.types.js'
|
|
6
6
|
import { PikkuLocalChannelHandler } from './local-channel-handler.js'
|
|
7
7
|
import type { PikkuWire, WireServices } from '../../../types/core.types.js'
|
|
8
|
+
import { isProduction } from '../../../env.js'
|
|
9
|
+
import { getErrorResponse } from '../../../errors/error-handler.js'
|
|
8
10
|
import { handleHTTPError } from '../../../handle-error.js'
|
|
9
11
|
import {
|
|
10
12
|
PikkuSessionService,
|
|
@@ -121,9 +123,10 @@ export const runLocalChannel = async ({
|
|
|
121
123
|
}
|
|
122
124
|
} catch (e: any) {
|
|
123
125
|
singletonServices.logger.error(`Error handling onConnect: ${e}`)
|
|
126
|
+
const errorResponse = getErrorResponse(e)
|
|
124
127
|
channel.send({
|
|
125
|
-
error:
|
|
126
|
-
errorName: e.constructor?.name,
|
|
128
|
+
error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
|
|
129
|
+
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
127
130
|
})
|
|
128
131
|
}
|
|
129
132
|
}
|
|
@@ -143,9 +146,10 @@ export const runLocalChannel = async ({
|
|
|
143
146
|
})
|
|
144
147
|
} catch (e: any) {
|
|
145
148
|
singletonServices.logger.error(`Error handling onDisconnect: ${e}`)
|
|
149
|
+
const errorResponse = getErrorResponse(e)
|
|
146
150
|
channel.send({
|
|
147
|
-
error:
|
|
148
|
-
errorName: e.constructor?.name,
|
|
151
|
+
error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
|
|
152
|
+
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
149
153
|
})
|
|
150
154
|
}
|
|
151
155
|
}
|
|
@@ -165,9 +169,10 @@ export const runLocalChannel = async ({
|
|
|
165
169
|
await channel.send(result)
|
|
166
170
|
} catch (e: any) {
|
|
167
171
|
singletonServices.logger.error(e)
|
|
172
|
+
const errorResponse = getErrorResponse(e)
|
|
168
173
|
channel.send({
|
|
169
|
-
error:
|
|
170
|
-
errorName: e.constructor?.name,
|
|
174
|
+
error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
|
|
175
|
+
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
171
176
|
})
|
|
172
177
|
setTimeout(() => channel.close(), 200)
|
|
173
178
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
getCreateWireServices,
|
|
33
33
|
} from '../../pikku-state.js'
|
|
34
34
|
import { PikkuSessionService } from '../../services/user-session-service.js'
|
|
35
|
+
import { getErrorResponse } from '../../errors/error-handler.js'
|
|
35
36
|
import { handleHTTPError } from '../../handle-error.js'
|
|
36
37
|
import { pikkuState } from '../../pikku-state.js'
|
|
37
38
|
import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
|
|
@@ -530,7 +531,7 @@ export const fetchData = async <In, Out>(
|
|
|
530
531
|
apiRoute,
|
|
531
532
|
apiType,
|
|
532
533
|
})
|
|
533
|
-
throw new NotFoundError(
|
|
534
|
+
throw new NotFoundError()
|
|
534
535
|
}
|
|
535
536
|
|
|
536
537
|
// Execute the matched route along with its middleware and session management
|
|
@@ -552,10 +553,11 @@ export const fetchData = async <In, Out>(
|
|
|
552
553
|
// For SSE routes, send error through the stream since the response is already in stream mode
|
|
553
554
|
singletonServices.logger.error(e instanceof Error ? e.message : e)
|
|
554
555
|
try {
|
|
556
|
+
const errorResponse = getErrorResponse(e)
|
|
555
557
|
response.arrayBuffer(
|
|
556
558
|
JSON.stringify({
|
|
557
559
|
type: 'error',
|
|
558
|
-
errorText:
|
|
560
|
+
errorText: errorResponse?.message ?? 'Internal server error',
|
|
559
561
|
})
|
|
560
562
|
)
|
|
561
563
|
response.arrayBuffer(JSON.stringify({ type: 'done' }))
|
|
@@ -105,6 +105,9 @@ export class PikkuFetchHTTPRequest<In = unknown>
|
|
|
105
105
|
const merged: Record<string, unknown> = {}
|
|
106
106
|
for (const part of parts) {
|
|
107
107
|
for (const [key, value] of Object.entries(part)) {
|
|
108
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
108
111
|
if (key in merged && !valuesAreEquivalent(merged[key], value)) {
|
|
109
112
|
throw new UnprocessableContentError(
|
|
110
113
|
`Conflicting values for key "${key}": "${merged[key]}" vs "${value}"`
|
|
@@ -141,7 +144,14 @@ export class PikkuFetchHTTPRequest<In = unknown>
|
|
|
141
144
|
body = { data: buffer }
|
|
142
145
|
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
143
146
|
const text = await this.request.text()
|
|
144
|
-
|
|
147
|
+
const params = new URLSearchParams(text)
|
|
148
|
+
let count = 0
|
|
149
|
+
for (const _ of params) {
|
|
150
|
+
if (++count > 256) {
|
|
151
|
+
throw new UnprocessableContentError('Too many form parameters')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
body = Object.fromEntries(params)
|
|
145
155
|
} else {
|
|
146
156
|
throw new UnprocessableContentError(
|
|
147
157
|
`Unsupported content type ${contentType}`
|
|
@@ -484,4 +484,46 @@ describe('graph-runner bugs', () => {
|
|
|
484
484
|
|
|
485
485
|
delete metaState['testInlineRpcMissing']
|
|
486
486
|
})
|
|
487
|
+
|
|
488
|
+
test('graph workflow with inline: true in meta should run inline', async () => {
|
|
489
|
+
const ws = new InMemoryWorkflowService()
|
|
490
|
+
let executed = false
|
|
491
|
+
|
|
492
|
+
const mockRpcService = {
|
|
493
|
+
rpcWithWire: async () => {
|
|
494
|
+
executed = true
|
|
495
|
+
return { done: true }
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Set up queue service to verify nothing is queued
|
|
500
|
+
let queued = false
|
|
501
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
502
|
+
queueService: {
|
|
503
|
+
add: async () => {
|
|
504
|
+
queued = true
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
} as any)
|
|
508
|
+
|
|
509
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
510
|
+
metaState['testInlineMetaGraph'] = {
|
|
511
|
+
name: 'testInlineMetaGraph',
|
|
512
|
+
pikkuFuncId: 'testInlineMetaGraph',
|
|
513
|
+
source: 'graph',
|
|
514
|
+
inline: true,
|
|
515
|
+
entryNodeIds: ['a'],
|
|
516
|
+
graphHash: 'inline-meta-graph-hash',
|
|
517
|
+
nodes: {
|
|
518
|
+
a: { nodeId: 'a', rpcName: 'doA' },
|
|
519
|
+
},
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
await runWorkflowGraph(ws, 'testInlineMetaGraph', {}, mockRpcService, true)
|
|
523
|
+
|
|
524
|
+
assert.equal(executed, true, 'RPC should have been executed inline')
|
|
525
|
+
assert.equal(queued, false, 'nothing should have been queued')
|
|
526
|
+
|
|
527
|
+
delete metaState['testInlineMetaGraph']
|
|
528
|
+
})
|
|
487
529
|
})
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { PikkuWorkflowService } from '../pikku-workflow-service.js'
|
|
2
2
|
import type { GraphWireState, PikkuGraphWire } from './workflow-graph.types.js'
|
|
3
|
-
import { pikkuState } from '../../../pikku-state.js'
|
|
3
|
+
import { pikkuState, getSingletonServices } from '../../../pikku-state.js'
|
|
4
4
|
import type { WorkflowRuntimeMeta, WorkflowRunWire } from '../workflow.types.js'
|
|
5
5
|
import { RPCNotFoundError } from '../../rpc/rpc-runner.js'
|
|
6
6
|
|
|
7
|
+
export class ChildWorkflowStartedException extends Error {
|
|
8
|
+
name = 'ChildWorkflowStartedException'
|
|
9
|
+
constructor(
|
|
10
|
+
public parentRunId: string,
|
|
11
|
+
public stepId: string,
|
|
12
|
+
public childRunId: string
|
|
13
|
+
) {
|
|
14
|
+
super(`Child workflow started: ${childRunId}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
function buildTemplateRegex(nodeId: string): RegExp | null {
|
|
8
19
|
if (!nodeId.includes('${')) return null
|
|
9
20
|
const escaped = nodeId
|
|
@@ -442,9 +453,44 @@ export async function executeGraphStep(
|
|
|
442
453
|
}
|
|
443
454
|
|
|
444
455
|
try {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
456
|
+
let result: any
|
|
457
|
+
|
|
458
|
+
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName]
|
|
459
|
+
if (subWorkflowMeta) {
|
|
460
|
+
const childWire: WorkflowRunWire = {
|
|
461
|
+
type: 'workflow',
|
|
462
|
+
id: rpcName,
|
|
463
|
+
parentRunId: runId,
|
|
464
|
+
parentStepId: stepId,
|
|
465
|
+
}
|
|
466
|
+
const shouldInline =
|
|
467
|
+
subWorkflowMeta.inline || !getSingletonServices()?.queueService
|
|
468
|
+
const { runId: childRunId } = await workflowService.startWorkflow(
|
|
469
|
+
rpcName,
|
|
470
|
+
data,
|
|
471
|
+
childWire,
|
|
472
|
+
rpcService,
|
|
473
|
+
{ inline: shouldInline }
|
|
474
|
+
)
|
|
475
|
+
await workflowService.setStepChildRunId(stepId, childRunId)
|
|
476
|
+
|
|
477
|
+
if (shouldInline) {
|
|
478
|
+
const childRun = await workflowService.getRun(childRunId)
|
|
479
|
+
if (childRun?.status === 'failed') {
|
|
480
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed')
|
|
481
|
+
}
|
|
482
|
+
if (childRun?.status === 'cancelled') {
|
|
483
|
+
throw new Error('Sub-workflow was cancelled')
|
|
484
|
+
}
|
|
485
|
+
result = childRun?.output
|
|
486
|
+
} else {
|
|
487
|
+
throw new ChildWorkflowStartedException(runId, stepId, childRunId)
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
491
|
+
graph: graphWire,
|
|
492
|
+
})
|
|
493
|
+
}
|
|
448
494
|
|
|
449
495
|
if (wireState.branchKey) {
|
|
450
496
|
await workflowService.setBranchTaken(stepId, wireState.branchKey)
|
|
@@ -452,6 +498,9 @@ export async function executeGraphStep(
|
|
|
452
498
|
|
|
453
499
|
return result
|
|
454
500
|
} catch (error) {
|
|
501
|
+
if (error instanceof ChildWorkflowStartedException) {
|
|
502
|
+
throw error
|
|
503
|
+
}
|
|
455
504
|
if (error instanceof RPCNotFoundError) {
|
|
456
505
|
await workflowService.updateRunStatus(runId, 'suspended', undefined, {
|
|
457
506
|
message: `RPC '${rpcName}' not found. Deploy the missing function and resume.`,
|
|
@@ -541,9 +590,37 @@ async function executeGraphNodeInline(
|
|
|
541
590
|
}
|
|
542
591
|
|
|
543
592
|
try {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
593
|
+
let result: any
|
|
594
|
+
|
|
595
|
+
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName]
|
|
596
|
+
if (subWorkflowMeta) {
|
|
597
|
+
const childWire: WorkflowRunWire = {
|
|
598
|
+
type: 'workflow',
|
|
599
|
+
id: rpcName,
|
|
600
|
+
parentRunId: runId,
|
|
601
|
+
parentStepId: stepState.stepId,
|
|
602
|
+
}
|
|
603
|
+
const { runId: childRunId } = await workflowService.startWorkflow(
|
|
604
|
+
rpcName,
|
|
605
|
+
input,
|
|
606
|
+
childWire,
|
|
607
|
+
rpcService,
|
|
608
|
+
{ inline: true }
|
|
609
|
+
)
|
|
610
|
+
await workflowService.setStepChildRunId(stepState.stepId, childRunId)
|
|
611
|
+
const childRun = await workflowService.getRun(childRunId)
|
|
612
|
+
if (childRun?.status === 'failed') {
|
|
613
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed')
|
|
614
|
+
}
|
|
615
|
+
if (childRun?.status === 'cancelled') {
|
|
616
|
+
throw new Error('Sub-workflow was cancelled')
|
|
617
|
+
}
|
|
618
|
+
result = childRun?.output
|
|
619
|
+
} else {
|
|
620
|
+
result = await rpcService.rpcWithWire(rpcName, input, {
|
|
621
|
+
graph: graphWire,
|
|
622
|
+
})
|
|
623
|
+
}
|
|
547
624
|
|
|
548
625
|
if (wireState.branchKey) {
|
|
549
626
|
await workflowService.setBranchTaken(
|
|
@@ -9,6 +9,115 @@ import {
|
|
|
9
9
|
type PikkuWorkflowWire,
|
|
10
10
|
} from './pikku-workflow-service.js'
|
|
11
11
|
|
|
12
|
+
describe('pikku-workflow-service inline meta flag', () => {
|
|
13
|
+
test('workflow with inline: true in meta should create inline run and not queue', async () => {
|
|
14
|
+
const ws = new InMemoryWorkflowService()
|
|
15
|
+
const workflowName = 'testInlineMetaFlag'
|
|
16
|
+
const graphHash = 'inline-meta-hash'
|
|
17
|
+
|
|
18
|
+
// Set up a queue service that tracks if anything was queued
|
|
19
|
+
let queued = false
|
|
20
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
21
|
+
logger: {
|
|
22
|
+
error: () => {},
|
|
23
|
+
info: () => {},
|
|
24
|
+
warn: () => {},
|
|
25
|
+
debug: () => {},
|
|
26
|
+
},
|
|
27
|
+
queueService: {
|
|
28
|
+
add: async () => {
|
|
29
|
+
queued = true
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} as any)
|
|
33
|
+
|
|
34
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
35
|
+
metaState[workflowName] = {
|
|
36
|
+
name: workflowName,
|
|
37
|
+
pikkuFuncId: workflowName,
|
|
38
|
+
source: 'dsl',
|
|
39
|
+
graphHash,
|
|
40
|
+
inline: true,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const functionMetaState = pikkuState(null, 'function', 'meta')
|
|
44
|
+
functionMetaState[workflowName] = {
|
|
45
|
+
name: workflowName,
|
|
46
|
+
sessionless: true,
|
|
47
|
+
permissions: [],
|
|
48
|
+
} as any
|
|
49
|
+
|
|
50
|
+
addWorkflow(workflowName, {
|
|
51
|
+
func: async () => {
|
|
52
|
+
return { ok: true }
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const { runId } = await ws.startWorkflow(workflowName, {}, {})
|
|
57
|
+
|
|
58
|
+
// The run should be created as inline
|
|
59
|
+
const run = await ws.getRun(runId)
|
|
60
|
+
assert.equal(run?.inline, true, 'run should be marked as inline')
|
|
61
|
+
assert.equal(
|
|
62
|
+
queued,
|
|
63
|
+
false,
|
|
64
|
+
'nothing should have been queued to the queue service'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
delete metaState[workflowName]
|
|
68
|
+
delete functionMetaState[workflowName]
|
|
69
|
+
pikkuState(null, 'workflows', 'registrations').delete(workflowName)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('workflow without inline flag should use queue when queueService exists', async () => {
|
|
73
|
+
const ws = new InMemoryWorkflowService()
|
|
74
|
+
const workflowName = 'testNonInlineMetaFlag'
|
|
75
|
+
const graphHash = 'non-inline-meta-hash'
|
|
76
|
+
|
|
77
|
+
let queued = false
|
|
78
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
79
|
+
queueService: {
|
|
80
|
+
add: async () => {
|
|
81
|
+
queued = true
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
} as any)
|
|
85
|
+
|
|
86
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
87
|
+
metaState[workflowName] = {
|
|
88
|
+
name: workflowName,
|
|
89
|
+
pikkuFuncId: workflowName,
|
|
90
|
+
source: 'dsl',
|
|
91
|
+
graphHash,
|
|
92
|
+
// no inline flag
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const functionMetaState = pikkuState(null, 'function', 'meta')
|
|
96
|
+
functionMetaState[workflowName] = {
|
|
97
|
+
name: workflowName,
|
|
98
|
+
sessionless: true,
|
|
99
|
+
permissions: [],
|
|
100
|
+
} as any
|
|
101
|
+
|
|
102
|
+
addWorkflow(workflowName, {
|
|
103
|
+
func: async () => {
|
|
104
|
+
return { ok: true }
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await ws.startWorkflow(workflowName, {}, {})
|
|
109
|
+
|
|
110
|
+
// Wait a tick
|
|
111
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
112
|
+
|
|
113
|
+
assert.equal(queued, true, 'workflow should have been queued')
|
|
114
|
+
|
|
115
|
+
delete metaState[workflowName]
|
|
116
|
+
delete functionMetaState[workflowName]
|
|
117
|
+
pikkuState(null, 'workflows', 'registrations').delete(workflowName)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
12
121
|
describe('pikku-workflow-service version mismatch fallback', () => {
|
|
13
122
|
test('should fall back to stored graph for dsl workflow version mismatch', async () => {
|
|
14
123
|
const ws = new InMemoryWorkflowService()
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import type { WorkflowService } from '../../services/workflow-service.js'
|
|
27
27
|
import { PikkuError, addError } from '../../errors/error-handler.js'
|
|
28
28
|
import { RPCNotFoundError } from '../rpc/rpc-runner.js'
|
|
29
|
+
import { ChildWorkflowStartedException } from './graph/graph-runner.js'
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Exception thrown when workflow needs to pause for async step
|
|
@@ -111,6 +112,10 @@ const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
|
|
|
111
112
|
export abstract class PikkuWorkflowService implements WorkflowService {
|
|
112
113
|
private inlineRuns = new Set<string>()
|
|
113
114
|
|
|
115
|
+
protected get logger() {
|
|
116
|
+
return getSingletonServices()?.logger
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
constructor() {}
|
|
115
120
|
|
|
116
121
|
/**
|
|
@@ -386,12 +391,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
386
391
|
data: any
|
|
387
392
|
): Promise<void> {
|
|
388
393
|
const queueService = this.verifyQueueService()
|
|
389
|
-
await queueService.add(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
394
|
+
await queueService.add(
|
|
395
|
+
this.getConfig().stepWorkerQueueName,
|
|
396
|
+
JSON.parse(
|
|
397
|
+
JSON.stringify({
|
|
398
|
+
runId,
|
|
399
|
+
stepName,
|
|
400
|
+
rpcName,
|
|
401
|
+
data,
|
|
402
|
+
})
|
|
403
|
+
)
|
|
404
|
+
)
|
|
395
405
|
}
|
|
396
406
|
|
|
397
407
|
/**
|
|
@@ -447,7 +457,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
447
457
|
|
|
448
458
|
if (workflowMeta.source === 'graph' || workflowMeta.source === 'ai-agent') {
|
|
449
459
|
const shouldInline =
|
|
450
|
-
options?.inline ||
|
|
460
|
+
options?.inline ||
|
|
461
|
+
workflowMeta.inline ||
|
|
462
|
+
!getSingletonServices()?.queueService
|
|
451
463
|
return runWorkflowGraph(
|
|
452
464
|
this,
|
|
453
465
|
name,
|
|
@@ -472,7 +484,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
472
484
|
}
|
|
473
485
|
|
|
474
486
|
const shouldInline =
|
|
475
|
-
options?.inline ||
|
|
487
|
+
options?.inline ||
|
|
488
|
+
workflowMeta.inline ||
|
|
489
|
+
!getSingletonServices()?.queueService
|
|
476
490
|
|
|
477
491
|
const runId = await this.createRun(
|
|
478
492
|
name,
|
|
@@ -484,16 +498,22 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
484
498
|
|
|
485
499
|
if (shouldInline) {
|
|
486
500
|
this.inlineRuns.add(runId)
|
|
487
|
-
|
|
488
|
-
.
|
|
501
|
+
try {
|
|
502
|
+
await this.runWorkflowJob(runId, rpcService)
|
|
503
|
+
} catch (error: any) {
|
|
504
|
+
if (
|
|
505
|
+
error.name !== 'WorkflowAsyncException' &&
|
|
506
|
+
error.name !== 'WorkflowCancelledException' &&
|
|
507
|
+
error.name !== 'WorkflowSuspendedException'
|
|
508
|
+
) {
|
|
489
509
|
getSingletonServices()!.logger.error(
|
|
490
510
|
`Workflow ${name} (run ${runId}) failed:`,
|
|
491
|
-
|
|
511
|
+
error
|
|
492
512
|
)
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
513
|
+
}
|
|
514
|
+
} finally {
|
|
515
|
+
this.inlineRuns.delete(runId)
|
|
516
|
+
}
|
|
497
517
|
} else {
|
|
498
518
|
await this.resumeWorkflow(runId)
|
|
499
519
|
}
|
|
@@ -553,6 +573,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
553
573
|
|
|
554
574
|
if (workflowMeta?.source === 'graph') {
|
|
555
575
|
await continueGraph(this, runId, run.workflow)
|
|
576
|
+
const updatedRun = await this.getRun(runId)
|
|
577
|
+
if (updatedRun?.status === 'completed') {
|
|
578
|
+
await this.onChildWorkflowCompleted(updatedRun, updatedRun.output)
|
|
579
|
+
} else if (
|
|
580
|
+
updatedRun?.status === 'failed' ||
|
|
581
|
+
updatedRun?.status === 'cancelled'
|
|
582
|
+
) {
|
|
583
|
+
await this.onChildWorkflowFailed(
|
|
584
|
+
updatedRun,
|
|
585
|
+
new Error(updatedRun.error?.message || 'Child workflow failed')
|
|
586
|
+
)
|
|
587
|
+
}
|
|
556
588
|
return
|
|
557
589
|
}
|
|
558
590
|
|
|
@@ -587,6 +619,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
587
619
|
)
|
|
588
620
|
|
|
589
621
|
await this.updateRunStatus(runId, 'completed', result)
|
|
622
|
+
await this.onChildWorkflowCompleted(run, result)
|
|
590
623
|
} catch (error: any) {
|
|
591
624
|
if (error instanceof WorkflowAsyncException) {
|
|
592
625
|
throw error
|
|
@@ -598,6 +631,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
598
631
|
stack: '',
|
|
599
632
|
code: 'WORKFLOW_CANCELLED',
|
|
600
633
|
})
|
|
634
|
+
await this.onChildWorkflowFailed(run, error)
|
|
601
635
|
throw error
|
|
602
636
|
}
|
|
603
637
|
|
|
@@ -615,12 +649,41 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
615
649
|
stack: error.stack,
|
|
616
650
|
code: error.code,
|
|
617
651
|
})
|
|
652
|
+
await this.onChildWorkflowFailed(run, error)
|
|
618
653
|
|
|
619
654
|
throw error
|
|
620
655
|
}
|
|
621
656
|
})
|
|
622
657
|
}
|
|
623
658
|
|
|
659
|
+
private async onChildWorkflowCompleted(
|
|
660
|
+
childRun: WorkflowRun,
|
|
661
|
+
result: any
|
|
662
|
+
): Promise<void> {
|
|
663
|
+
const { parentRunId, parentStepId } = childRun.wire ?? {}
|
|
664
|
+
if (!parentRunId || !parentStepId) return
|
|
665
|
+
|
|
666
|
+
this.logger?.debug(
|
|
667
|
+
`Child workflow ${childRun.id} completed, updating parent step ${parentStepId}`
|
|
668
|
+
)
|
|
669
|
+
await this.setStepResult(parentStepId, result)
|
|
670
|
+
await this.resumeWorkflow(parentRunId)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private async onChildWorkflowFailed(
|
|
674
|
+
childRun: WorkflowRun,
|
|
675
|
+
error: Error
|
|
676
|
+
): Promise<void> {
|
|
677
|
+
const { parentRunId, parentStepId } = childRun.wire ?? {}
|
|
678
|
+
if (!parentRunId || !parentStepId) return
|
|
679
|
+
|
|
680
|
+
this.logger?.debug(
|
|
681
|
+
`Child workflow ${childRun.id} failed, updating parent step ${parentStepId}`
|
|
682
|
+
)
|
|
683
|
+
await this.setStepError(parentStepId, error)
|
|
684
|
+
await this.resumeWorkflow(parentRunId)
|
|
685
|
+
}
|
|
686
|
+
|
|
624
687
|
private async runVersionMismatchFallback(
|
|
625
688
|
run: WorkflowRun,
|
|
626
689
|
currentMeta: { source: string },
|
|
@@ -666,9 +729,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
666
729
|
// Get step state
|
|
667
730
|
let stepState = await this.getStepState(runId, stepName)
|
|
668
731
|
|
|
669
|
-
// Idempotency - if already succeeded,
|
|
732
|
+
// Idempotency - if already succeeded, nothing to do
|
|
670
733
|
if (stepState.status === 'succeeded') {
|
|
671
|
-
await this.resumeWorkflow(runId)
|
|
672
734
|
return
|
|
673
735
|
}
|
|
674
736
|
|
|
@@ -682,8 +744,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
682
744
|
stepState = await this.createRetryAttempt(stepState.stepId, 'running')
|
|
683
745
|
}
|
|
684
746
|
|
|
685
|
-
if (stepState.status === 'pending') {
|
|
686
|
-
// Mark pending step as running before execution
|
|
747
|
+
if (stepState.status === 'pending' || stepState.status === 'scheduled') {
|
|
687
748
|
await this.setStepRunning(stepState.stepId)
|
|
688
749
|
}
|
|
689
750
|
|
|
@@ -725,6 +786,13 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
725
786
|
// Resume orchestrator to continue workflow
|
|
726
787
|
await this.resumeWorkflow(runId)
|
|
727
788
|
} catch (error: any) {
|
|
789
|
+
if (error instanceof ChildWorkflowStartedException) {
|
|
790
|
+
this.logger?.debug(
|
|
791
|
+
`Workflow step '${stepName}': child workflow ${error.childRunId} started, waiting for completion`
|
|
792
|
+
)
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
|
|
728
796
|
if (error instanceof RPCNotFoundError) {
|
|
729
797
|
await this.setStepError(stepState.stepId, error)
|
|
730
798
|
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
@@ -848,12 +916,14 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
848
916
|
|
|
849
917
|
await getSingletonServices()!.queueService!.add(
|
|
850
918
|
this.getConfig().stepWorkerQueueName,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
919
|
+
JSON.parse(
|
|
920
|
+
JSON.stringify({
|
|
921
|
+
runId,
|
|
922
|
+
stepName,
|
|
923
|
+
rpcName,
|
|
924
|
+
data,
|
|
925
|
+
})
|
|
926
|
+
),
|
|
857
927
|
{
|
|
858
928
|
// attempts includes initial attempt, retries doesn't
|
|
859
929
|
attempts: retries + 1,
|
|
@@ -40,6 +40,7 @@ export interface WorkflowRunWire {
|
|
|
40
40
|
type: string
|
|
41
41
|
id?: string
|
|
42
42
|
parentRunId?: string
|
|
43
|
+
parentStepId?: string
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface WorkflowServiceConfig {
|
|
@@ -224,6 +225,7 @@ export type WorkflowsMeta = Record<
|
|
|
224
225
|
steps: WorkflowStepMeta[]
|
|
225
226
|
context?: WorkflowContext
|
|
226
227
|
dsl?: boolean
|
|
228
|
+
inline?: boolean
|
|
227
229
|
}
|
|
228
230
|
>
|
|
229
231
|
|
|
@@ -243,6 +245,8 @@ export interface WorkflowRuntimeMeta {
|
|
|
243
245
|
description?: string
|
|
244
246
|
/** Tags for organization */
|
|
245
247
|
tags?: string[]
|
|
248
|
+
/** If true, workflow always executes inline without queues */
|
|
249
|
+
inline?: boolean
|
|
246
250
|
/** Serialized nodes */
|
|
247
251
|
nodes?: Record<string, any>
|
|
248
252
|
/** Entry node IDs for graph workflows (computed at build time) */
|