@pikku/core 0.12.8 → 0.12.10
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 +31 -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/function/function-runner.d.ts +3 -1
- package/dist/function/function-runner.js +5 -1
- 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/credential-service.d.ts +40 -0
- package/dist/services/credential-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +13 -0
- package/dist/services/credential-wire-service.js +31 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.js +3 -0
- package/dist/services/local-content.d.ts +7 -4
- package/dist/services/local-content.js +42 -12
- package/dist/services/local-credential-service.d.ts +10 -0
- package/dist/services/local-credential-service.js +33 -0
- package/dist/services/local-secrets.js +2 -2
- package/dist/services/typed-credential-service.d.ts +27 -0
- package/dist/services/typed-credential-service.js +40 -0
- package/dist/testing/service-tests.d.ts +8 -0
- package/dist/testing/service-tests.js +107 -1
- package/dist/types/core.types.d.ts +7 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
- 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/credential/credential.types.d.ts +24 -0
- package/dist/wirings/credential/credential.types.js +1 -0
- package/dist/wirings/credential/index.d.ts +3 -0
- package/dist/wirings/credential/index.js +2 -0
- package/dist/wirings/credential/validate-credential-definitions.d.ts +6 -0
- package/dist/wirings/credential/validate-credential-definitions.js +38 -0
- package/dist/wirings/credential/wire-credential.d.ts +48 -0
- package/dist/wirings/credential/wire-credential.js +47 -0
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
- package/dist/wirings/oauth2/index.d.ts +2 -1
- package/dist/wirings/oauth2/index.js +1 -1
- package/dist/wirings/oauth2/oauth2-client.js +4 -8
- package/dist/wirings/oauth2/oauth2-routes.d.ts +35 -0
- package/dist/wirings/oauth2/oauth2-routes.js +146 -0
- package/dist/wirings/oauth2/oauth2.types.d.ts +0 -26
- package/dist/wirings/workflow/graph/graph-runner.d.ts +8 -1
- package/dist/wirings/workflow/graph/graph-runner.js +87 -7
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +99 -24
- package/dist/wirings/workflow/workflow.types.d.ts +4 -0
- package/package.json +2 -1
- package/src/env.ts +1 -0
- package/src/errors/errors.ts +35 -0
- package/src/function/function-runner.ts +11 -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/credential-service.ts +44 -0
- package/src/services/credential-wire-service.ts +44 -0
- package/src/services/index.ts +12 -0
- package/src/services/local-content.ts +61 -13
- package/src/services/local-credential-service.ts +41 -0
- package/src/services/local-secrets.test.ts +2 -2
- package/src/services/local-secrets.ts +2 -2
- package/src/services/typed-credential-service.ts +75 -0
- package/src/testing/service-tests.ts +140 -1
- package/src/types/core.types.ts +7 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
- 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/credential/credential.types.ts +28 -0
- package/src/wirings/credential/index.ts +8 -0
- package/src/wirings/credential/validate-credential-definitions.ts +64 -0
- package/src/wirings/credential/wire-credential.ts +49 -0
- package/src/wirings/http/http-runner.ts +11 -2
- package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
- package/src/wirings/oauth2/index.ts +2 -1
- package/src/wirings/oauth2/oauth2-client.ts +4 -8
- package/src/wirings/oauth2/oauth2-routes.ts +234 -0
- package/src/wirings/oauth2/oauth2.types.ts +0 -27
- package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
- package/src/wirings/workflow/graph/graph-runner.ts +117 -8
- package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +150 -34
- package/src/wirings/workflow/workflow.types.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +0 -20
- package/dist/wirings/oauth2/wire-oauth2-credential.js +0 -21
- package/src/wirings/oauth2/wire-oauth2-credential.ts +0 -23
|
@@ -40,30 +40,3 @@ export type OAuth2Config = {
|
|
|
40
40
|
/** Additional query parameters for authorization URL */
|
|
41
41
|
additionalParams?: Record<string, string>
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Configuration for wireOAuth2Credential.
|
|
46
|
-
* Combines standard credential fields with OAuth2-specific config.
|
|
47
|
-
*/
|
|
48
|
-
export type CoreOAuth2Credential = {
|
|
49
|
-
/** Unique identifier for this credential */
|
|
50
|
-
name: string
|
|
51
|
-
/** Human-readable name for UI display */
|
|
52
|
-
displayName: string
|
|
53
|
-
/** Optional description for UI */
|
|
54
|
-
description?: string
|
|
55
|
-
/** Key used with SecretService to retrieve app credentials (clientId/clientSecret) */
|
|
56
|
-
secretId: string
|
|
57
|
-
/** Where access/refresh tokens are stored */
|
|
58
|
-
tokenSecretId: string
|
|
59
|
-
/** OAuth2 authorization URL */
|
|
60
|
-
authorizationUrl: string
|
|
61
|
-
/** OAuth2 token exchange URL */
|
|
62
|
-
tokenUrl: string
|
|
63
|
-
/** Required scopes */
|
|
64
|
-
scopes: string[]
|
|
65
|
-
/** Use PKCE flow */
|
|
66
|
-
pkce?: boolean
|
|
67
|
-
/** Additional query parameters for authorization URL */
|
|
68
|
-
additionalParams?: Record<string, string>
|
|
69
|
-
}
|
|
@@ -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,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type PikkuWorkflowService,
|
|
3
|
+
WorkflowAsyncException,
|
|
4
|
+
WorkflowSuspendedException,
|
|
5
|
+
} from '../pikku-workflow-service.js'
|
|
2
6
|
import type { GraphWireState, PikkuGraphWire } from './workflow-graph.types.js'
|
|
3
|
-
import { pikkuState } from '../../../pikku-state.js'
|
|
7
|
+
import { pikkuState, getSingletonServices } from '../../../pikku-state.js'
|
|
4
8
|
import type { WorkflowRuntimeMeta, WorkflowRunWire } from '../workflow.types.js'
|
|
5
9
|
import { RPCNotFoundError } from '../../rpc/rpc-runner.js'
|
|
6
10
|
|
|
11
|
+
export class ChildWorkflowStartedException extends Error {
|
|
12
|
+
name = 'ChildWorkflowStartedException'
|
|
13
|
+
constructor(
|
|
14
|
+
public parentRunId: string,
|
|
15
|
+
public stepId: string,
|
|
16
|
+
public childRunId: string
|
|
17
|
+
) {
|
|
18
|
+
super(`Child workflow started: ${childRunId}`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
function buildTemplateRegex(nodeId: string): RegExp | null {
|
|
8
23
|
if (!nodeId.includes('${')) return null
|
|
9
24
|
const escaped = nodeId
|
|
@@ -442,9 +457,49 @@ export async function executeGraphStep(
|
|
|
442
457
|
}
|
|
443
458
|
|
|
444
459
|
try {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
460
|
+
let result: any
|
|
461
|
+
|
|
462
|
+
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName]
|
|
463
|
+
if (subWorkflowMeta) {
|
|
464
|
+
const childWire: WorkflowRunWire = {
|
|
465
|
+
type: 'workflow',
|
|
466
|
+
id: rpcName,
|
|
467
|
+
parentRunId: runId,
|
|
468
|
+
parentStepId: stepId,
|
|
469
|
+
}
|
|
470
|
+
const shouldInline =
|
|
471
|
+
subWorkflowMeta.inline || !getSingletonServices()?.queueService
|
|
472
|
+
const { runId: childRunId } = await workflowService.startWorkflow(
|
|
473
|
+
rpcName,
|
|
474
|
+
data,
|
|
475
|
+
childWire,
|
|
476
|
+
rpcService,
|
|
477
|
+
{ inline: shouldInline }
|
|
478
|
+
)
|
|
479
|
+
await workflowService.setStepChildRunId(stepId, childRunId)
|
|
480
|
+
|
|
481
|
+
if (shouldInline) {
|
|
482
|
+
const childRun = await workflowService.getRun(childRunId)
|
|
483
|
+
if (childRun?.status === 'failed') {
|
|
484
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed')
|
|
485
|
+
}
|
|
486
|
+
if (childRun?.status === 'cancelled') {
|
|
487
|
+
throw new Error('Sub-workflow was cancelled')
|
|
488
|
+
}
|
|
489
|
+
result = childRun?.output
|
|
490
|
+
} else {
|
|
491
|
+
throw new ChildWorkflowStartedException(runId, stepId, childRunId)
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
495
|
+
graph: graphWire,
|
|
496
|
+
workflow: workflowService.createWorkflowWire(
|
|
497
|
+
graphName,
|
|
498
|
+
runId,
|
|
499
|
+
rpcService
|
|
500
|
+
),
|
|
501
|
+
})
|
|
502
|
+
}
|
|
448
503
|
|
|
449
504
|
if (wireState.branchKey) {
|
|
450
505
|
await workflowService.setBranchTaken(stepId, wireState.branchKey)
|
|
@@ -452,6 +507,15 @@ export async function executeGraphStep(
|
|
|
452
507
|
|
|
453
508
|
return result
|
|
454
509
|
} catch (error) {
|
|
510
|
+
if (
|
|
511
|
+
error instanceof WorkflowAsyncException ||
|
|
512
|
+
error instanceof WorkflowSuspendedException
|
|
513
|
+
) {
|
|
514
|
+
throw error
|
|
515
|
+
}
|
|
516
|
+
if (error instanceof ChildWorkflowStartedException) {
|
|
517
|
+
throw error
|
|
518
|
+
}
|
|
455
519
|
if (error instanceof RPCNotFoundError) {
|
|
456
520
|
await workflowService.updateRunStatus(runId, 'suspended', undefined, {
|
|
457
521
|
message: `RPC '${rpcName}' not found. Deploy the missing function and resume.`,
|
|
@@ -541,9 +605,42 @@ async function executeGraphNodeInline(
|
|
|
541
605
|
}
|
|
542
606
|
|
|
543
607
|
try {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
608
|
+
let result: any
|
|
609
|
+
|
|
610
|
+
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName]
|
|
611
|
+
if (subWorkflowMeta) {
|
|
612
|
+
const childWire: WorkflowRunWire = {
|
|
613
|
+
type: 'workflow',
|
|
614
|
+
id: rpcName,
|
|
615
|
+
parentRunId: runId,
|
|
616
|
+
parentStepId: stepState.stepId,
|
|
617
|
+
}
|
|
618
|
+
const { runId: childRunId } = await workflowService.startWorkflow(
|
|
619
|
+
rpcName,
|
|
620
|
+
input,
|
|
621
|
+
childWire,
|
|
622
|
+
rpcService,
|
|
623
|
+
{ inline: true }
|
|
624
|
+
)
|
|
625
|
+
await workflowService.setStepChildRunId(stepState.stepId, childRunId)
|
|
626
|
+
const childRun = await workflowService.getRun(childRunId)
|
|
627
|
+
if (childRun?.status === 'failed') {
|
|
628
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed')
|
|
629
|
+
}
|
|
630
|
+
if (childRun?.status === 'cancelled') {
|
|
631
|
+
throw new Error('Sub-workflow was cancelled')
|
|
632
|
+
}
|
|
633
|
+
result = childRun?.output
|
|
634
|
+
} else {
|
|
635
|
+
result = await rpcService.rpcWithWire(rpcName, input, {
|
|
636
|
+
graph: graphWire,
|
|
637
|
+
workflow: workflowService.createWorkflowWire(
|
|
638
|
+
graphName,
|
|
639
|
+
runId,
|
|
640
|
+
rpcService
|
|
641
|
+
),
|
|
642
|
+
})
|
|
643
|
+
}
|
|
547
644
|
|
|
548
645
|
if (wireState.branchKey) {
|
|
549
646
|
await workflowService.setBranchTaken(
|
|
@@ -554,6 +651,12 @@ async function executeGraphNodeInline(
|
|
|
554
651
|
|
|
555
652
|
await workflowService.setStepResult(stepState.stepId, result)
|
|
556
653
|
} catch (error) {
|
|
654
|
+
if (
|
|
655
|
+
error instanceof WorkflowAsyncException ||
|
|
656
|
+
error instanceof WorkflowSuspendedException
|
|
657
|
+
) {
|
|
658
|
+
throw error
|
|
659
|
+
}
|
|
557
660
|
if (error instanceof RPCNotFoundError) {
|
|
558
661
|
await workflowService.setStepError(stepState.stepId, error as Error)
|
|
559
662
|
await workflowService.updateRunStatus(runId, 'suspended', undefined, {
|
|
@@ -778,6 +881,12 @@ export async function runWorkflowGraph(
|
|
|
778
881
|
entryNodes
|
|
779
882
|
)
|
|
780
883
|
} catch (error) {
|
|
884
|
+
if (
|
|
885
|
+
error instanceof WorkflowAsyncException ||
|
|
886
|
+
error instanceof WorkflowSuspendedException
|
|
887
|
+
) {
|
|
888
|
+
return
|
|
889
|
+
}
|
|
781
890
|
await workflowService.updateRunStatus(runId, 'failed', undefined, {
|
|
782
891
|
message: (error as Error).message,
|
|
783
892
|
stack: (error as Error).stack || '',
|
|
@@ -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
|
|
|
@@ -698,7 +759,14 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
698
759
|
const meta = pikkuState(null, 'workflows', 'meta')
|
|
699
760
|
const workflowMeta = meta[run.workflow]
|
|
700
761
|
|
|
701
|
-
|
|
762
|
+
const isGraphWorkflow =
|
|
763
|
+
workflowMeta?.source === 'graph' ||
|
|
764
|
+
workflowMeta?.source === 'ai-agent'
|
|
765
|
+
if (
|
|
766
|
+
isGraphWorkflow &&
|
|
767
|
+
workflowMeta?.nodes &&
|
|
768
|
+
stepName in workflowMeta.nodes
|
|
769
|
+
) {
|
|
702
770
|
result = await executeGraphStep(
|
|
703
771
|
this,
|
|
704
772
|
rpcService,
|
|
@@ -710,13 +778,52 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
710
778
|
run.workflow
|
|
711
779
|
)
|
|
712
780
|
} else {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
781
|
+
// Check if rpcName refers to a sub-workflow
|
|
782
|
+
const subWorkflowMeta = meta[rpcName]
|
|
783
|
+
if (subWorkflowMeta) {
|
|
784
|
+
const childWire: WorkflowRunWire = {
|
|
785
|
+
type: 'workflow',
|
|
786
|
+
id: rpcName,
|
|
787
|
+
parentRunId: runId,
|
|
788
|
+
parentStepId: stepState.stepId,
|
|
789
|
+
}
|
|
790
|
+
const shouldInline =
|
|
791
|
+
subWorkflowMeta.inline || !getSingletonServices()?.queueService
|
|
792
|
+
const { runId: childRunId } = await this.startWorkflow(
|
|
793
|
+
rpcName,
|
|
794
|
+
data,
|
|
795
|
+
childWire,
|
|
796
|
+
rpcService,
|
|
797
|
+
{ inline: shouldInline }
|
|
798
|
+
)
|
|
799
|
+
await this.setStepChildRunId(stepState.stepId, childRunId)
|
|
800
|
+
if (shouldInline) {
|
|
801
|
+
const childRun = await this.getRun(childRunId)
|
|
802
|
+
if (childRun?.status === 'failed') {
|
|
803
|
+
throw new Error(
|
|
804
|
+
childRun.error?.message || 'Sub-workflow failed'
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
if (childRun?.status === 'cancelled') {
|
|
808
|
+
throw new Error('Sub-workflow was cancelled')
|
|
809
|
+
}
|
|
810
|
+
result = childRun?.output
|
|
811
|
+
} else {
|
|
812
|
+
throw new ChildWorkflowStartedException(
|
|
813
|
+
runId,
|
|
814
|
+
stepState.stepId,
|
|
815
|
+
childRunId
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
820
|
+
workflowStep: {
|
|
821
|
+
runId,
|
|
822
|
+
stepId: stepState.stepId,
|
|
823
|
+
attemptCount: stepState.attemptCount,
|
|
824
|
+
},
|
|
825
|
+
})
|
|
826
|
+
}
|
|
720
827
|
}
|
|
721
828
|
|
|
722
829
|
// Store result and mark succeeded
|
|
@@ -725,6 +832,13 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
725
832
|
// Resume orchestrator to continue workflow
|
|
726
833
|
await this.resumeWorkflow(runId)
|
|
727
834
|
} catch (error: any) {
|
|
835
|
+
if (error instanceof ChildWorkflowStartedException) {
|
|
836
|
+
this.logger?.debug(
|
|
837
|
+
`Workflow step '${stepName}': child workflow ${error.childRunId} started, waiting for completion`
|
|
838
|
+
)
|
|
839
|
+
return
|
|
840
|
+
}
|
|
841
|
+
|
|
728
842
|
if (error instanceof RPCNotFoundError) {
|
|
729
843
|
await this.setStepError(stepState.stepId, error)
|
|
730
844
|
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
@@ -848,12 +962,14 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
848
962
|
|
|
849
963
|
await getSingletonServices()!.queueService!.add(
|
|
850
964
|
this.getConfig().stepWorkerQueueName,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
965
|
+
JSON.parse(
|
|
966
|
+
JSON.stringify({
|
|
967
|
+
runId,
|
|
968
|
+
stepName,
|
|
969
|
+
rpcName,
|
|
970
|
+
data,
|
|
971
|
+
})
|
|
972
|
+
),
|
|
857
973
|
{
|
|
858
974
|
// attempts includes initial attempt, retries doesn't
|
|
859
975
|
attempts: retries + 1,
|
|
@@ -1153,7 +1269,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1153
1269
|
})
|
|
1154
1270
|
}
|
|
1155
1271
|
|
|
1156
|
-
|
|
1272
|
+
public createWorkflowWire(
|
|
1157
1273
|
name: string,
|
|
1158
1274
|
runId: string,
|
|
1159
1275
|
rpcService: any
|
|
@@ -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) */
|