@pikku/core 0.12.57 → 0.12.60
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 +29 -0
- package/dist/services/local-gateway-service.js +4 -3
- package/dist/wirings/gateway/gateway-runner.d.ts +2 -1
- package/dist/wirings/gateway/gateway-runner.js +31 -9
- package/dist/wirings/gateway/gateway.types.d.ts +14 -3
- package/dist/wirings/gateway/index.d.ts +2 -2
- package/dist/wirings/gateway/index.js +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +14 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +4 -0
- package/package.json +2 -2
- package/src/services/local-gateway-service.ts +7 -3
- package/src/wirings/gateway/gateway-runner.test.ts +70 -0
- package/src/wirings/gateway/gateway-runner.ts +37 -8
- package/src/wirings/gateway/gateway.types.ts +17 -2
- package/src/wirings/gateway/index.ts +6 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +134 -0
- package/src/wirings/workflow/graph/graph-runner.ts +12 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +4 -0
- package/src/wirings/workflow/workflow-step-session.test.ts +78 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -677,6 +677,140 @@ describe('graph-runner bugs', () => {
|
|
|
677
677
|
delete metaState['testInlineMetaGraph']
|
|
678
678
|
})
|
|
679
679
|
|
|
680
|
+
test('executeGraphStep dispatches an agent-name node via the agent-run path and returns its result', async () => {
|
|
681
|
+
const ws = new InMemoryWorkflowService()
|
|
682
|
+
|
|
683
|
+
const agentsMeta = pikkuState(null, 'agent', 'agentsMeta')
|
|
684
|
+
agentsMeta['summarize'] = {
|
|
685
|
+
name: 'summarize',
|
|
686
|
+
inputSchema: null,
|
|
687
|
+
outputSchema: null,
|
|
688
|
+
workingMemorySchema: null,
|
|
689
|
+
} as any
|
|
690
|
+
|
|
691
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
692
|
+
metaState['testAgentNode'] = {
|
|
693
|
+
name: 'testAgentNode',
|
|
694
|
+
pikkuFuncId: 'testAgentNode',
|
|
695
|
+
source: 'graph',
|
|
696
|
+
entryNodeIds: ['a'],
|
|
697
|
+
graphHash: 'agent-node-hash',
|
|
698
|
+
nodes: {
|
|
699
|
+
a: { nodeId: 'a', rpcName: 'summarize' },
|
|
700
|
+
},
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const runId = await ws.createRun(
|
|
704
|
+
'testAgentNode',
|
|
705
|
+
{},
|
|
706
|
+
false,
|
|
707
|
+
'agent-node-hash',
|
|
708
|
+
{ type: 'test' }
|
|
709
|
+
)
|
|
710
|
+
const agentInput = { message: 'hi', threadId: 't', resourceId: 'r' }
|
|
711
|
+
const step = await ws.insertStepState(runId, 'a', 'summarize', agentInput)
|
|
712
|
+
|
|
713
|
+
let agentRunCall: { name: string; input: any } | null = null
|
|
714
|
+
const rpcService = {
|
|
715
|
+
rpcWithWire: async () => {
|
|
716
|
+
throw new Error('should not invoke rpcWithWire for an agent node')
|
|
717
|
+
},
|
|
718
|
+
agent: {
|
|
719
|
+
run: async (name: string, input: any) => {
|
|
720
|
+
agentRunCall = { name, input }
|
|
721
|
+
return {
|
|
722
|
+
runId: 'agent-run-1',
|
|
723
|
+
result: { summary: 'done' },
|
|
724
|
+
usage: { inputTokens: 1, outputTokens: 2 },
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const result = await executeGraphStep(
|
|
731
|
+
ws,
|
|
732
|
+
rpcService,
|
|
733
|
+
runId,
|
|
734
|
+
step.stepId,
|
|
735
|
+
'a',
|
|
736
|
+
'summarize',
|
|
737
|
+
agentInput,
|
|
738
|
+
'testAgentNode'
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
assert.deepEqual(result, { summary: 'done' })
|
|
742
|
+
assert.deepEqual(agentRunCall, { name: 'summarize', input: agentInput })
|
|
743
|
+
|
|
744
|
+
delete metaState['testAgentNode']
|
|
745
|
+
delete agentsMeta['summarize']
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
test('inline graph dispatches an agent node and a downstream node consumes its result', async () => {
|
|
749
|
+
const ws = new InMemoryWorkflowService()
|
|
750
|
+
|
|
751
|
+
const agentsMeta = pikkuState(null, 'agent', 'agentsMeta')
|
|
752
|
+
agentsMeta['classifier'] = {
|
|
753
|
+
name: 'classifier',
|
|
754
|
+
inputSchema: null,
|
|
755
|
+
outputSchema: null,
|
|
756
|
+
workingMemorySchema: null,
|
|
757
|
+
} as any
|
|
758
|
+
|
|
759
|
+
let consumed: any = null
|
|
760
|
+
const rpcService = {
|
|
761
|
+
rpcWithWire: async (rpcName: string, data: any) => {
|
|
762
|
+
if (rpcName === 'consume') {
|
|
763
|
+
consumed = data
|
|
764
|
+
return { ok: true }
|
|
765
|
+
}
|
|
766
|
+
return {}
|
|
767
|
+
},
|
|
768
|
+
agent: {
|
|
769
|
+
run: async () => ({
|
|
770
|
+
runId: 'r',
|
|
771
|
+
result: { category: 'urgent' },
|
|
772
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
773
|
+
}),
|
|
774
|
+
},
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
778
|
+
metaState['testInlineAgentNode'] = {
|
|
779
|
+
name: 'testInlineAgentNode',
|
|
780
|
+
pikkuFuncId: 'testInlineAgentNode',
|
|
781
|
+
source: 'graph',
|
|
782
|
+
entryNodeIds: ['agentNode'],
|
|
783
|
+
graphHash: 'inline-agent-hash',
|
|
784
|
+
nodes: {
|
|
785
|
+
agentNode: {
|
|
786
|
+
nodeId: 'agentNode',
|
|
787
|
+
rpcName: 'classifier',
|
|
788
|
+
next: 'consume',
|
|
789
|
+
},
|
|
790
|
+
consume: {
|
|
791
|
+
nodeId: 'consume',
|
|
792
|
+
rpcName: 'consume',
|
|
793
|
+
input: { category: { $ref: 'agentNode', path: 'category' } },
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const { runId } = await runWorkflowGraph(
|
|
799
|
+
ws,
|
|
800
|
+
'testInlineAgentNode',
|
|
801
|
+
{ message: 'x', threadId: 't', resourceId: 'r' },
|
|
802
|
+
rpcService,
|
|
803
|
+
true
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
const run = await ws.getRun(runId)
|
|
807
|
+
assert.equal(run?.status, 'completed')
|
|
808
|
+
assert.deepEqual(consumed, { category: 'urgent' })
|
|
809
|
+
|
|
810
|
+
delete metaState['testInlineAgentNode']
|
|
811
|
+
delete agentsMeta['classifier']
|
|
812
|
+
})
|
|
813
|
+
|
|
680
814
|
test('queueGraphNode forwards node retries to queue attempts and backoff', async () => {
|
|
681
815
|
const ws = new InMemoryWorkflowService()
|
|
682
816
|
const enqueued: Array<{ queueName: string; data: any; options: any }> = []
|
|
@@ -621,6 +621,9 @@ export async function executeGraphStep(
|
|
|
621
621
|
let result: any
|
|
622
622
|
|
|
623
623
|
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName]
|
|
624
|
+
const agentMeta = subWorkflowMeta
|
|
625
|
+
? undefined
|
|
626
|
+
: pikkuState(null, 'agent', 'agentsMeta')[rpcName]
|
|
624
627
|
if (subWorkflowMeta) {
|
|
625
628
|
const childWire: WorkflowRunWire = {
|
|
626
629
|
type: 'workflow',
|
|
@@ -650,6 +653,9 @@ export async function executeGraphStep(
|
|
|
650
653
|
} else {
|
|
651
654
|
throw new ChildWorkflowStartedException(runId, stepId, childRunId)
|
|
652
655
|
}
|
|
656
|
+
} else if (agentMeta) {
|
|
657
|
+
const agentRun = await rpcService.agent.run(rpcName, data)
|
|
658
|
+
result = agentRun.result
|
|
653
659
|
} else {
|
|
654
660
|
result = await invokeGraphNodeRpc(
|
|
655
661
|
workflowService,
|
|
@@ -760,6 +766,9 @@ async function executeGraphNodeInline(
|
|
|
760
766
|
let result: any
|
|
761
767
|
|
|
762
768
|
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName]
|
|
769
|
+
const agentMeta = subWorkflowMeta
|
|
770
|
+
? undefined
|
|
771
|
+
: pikkuState(null, 'agent', 'agentsMeta')[rpcName]
|
|
763
772
|
if (subWorkflowMeta) {
|
|
764
773
|
const childWire: WorkflowRunWire = {
|
|
765
774
|
type: 'workflow',
|
|
@@ -783,6 +792,9 @@ async function executeGraphNodeInline(
|
|
|
783
792
|
throw new Error('Sub-workflow was cancelled')
|
|
784
793
|
}
|
|
785
794
|
result = childRun?.output
|
|
795
|
+
} else if (agentMeta) {
|
|
796
|
+
const agentRun = await rpcService.agent.run(rpcName, input)
|
|
797
|
+
result = agentRun.result
|
|
786
798
|
} else {
|
|
787
799
|
result = await invokeGraphNodeRpc(
|
|
788
800
|
workflowService,
|
|
@@ -1732,7 +1732,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1732
1732
|
data: any,
|
|
1733
1733
|
rpcService: any
|
|
1734
1734
|
): Promise<any> {
|
|
1735
|
+
// Carry the run's pikkuUserId onto the step wire so authed steps rehydrate their
|
|
1736
|
+
// session on the queued path too (the bare job wire lacks it; inline already has it).
|
|
1737
|
+
const run = await this.getRun(runId)
|
|
1735
1738
|
return rpcService.rpcWithWire(rpcName, data, {
|
|
1739
|
+
...(run?.wire?.pikkuUserId ? { pikkuUserId: run.wire.pikkuUserId } : {}),
|
|
1736
1740
|
workflowStep: {
|
|
1737
1741
|
runId,
|
|
1738
1742
|
stepId: stepState.stepId,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
|
|
5
|
+
import { pikkuState } from '../../pikku-state.js'
|
|
6
|
+
|
|
7
|
+
// A queued workflow step is executed with the bare job wire (payload is just
|
|
8
|
+
// { runId }), so its wire has no `pikkuUserId`. Without threading it from the
|
|
9
|
+
// persisted run wire, an authed step (`pikkuFunc`) sees no session and throws
|
|
10
|
+
// "Authentication required". These tests pin that `invokeStepRpc` merges the
|
|
11
|
+
// run wire's `pikkuUserId` into the step wire override so the session rehydrates.
|
|
12
|
+
|
|
13
|
+
const silentLogger = { error() {}, info() {}, warn() {}, debug() {} }
|
|
14
|
+
|
|
15
|
+
// rpcService stand-in whose only job is to capture the wire override it's handed.
|
|
16
|
+
function capturingRpcService(): {
|
|
17
|
+
service: { rpcWithWire: (n: string, d: any, w: any) => Promise<any> }
|
|
18
|
+
wire: () => any
|
|
19
|
+
} {
|
|
20
|
+
let captured: any
|
|
21
|
+
return {
|
|
22
|
+
service: {
|
|
23
|
+
rpcWithWire: async (_rpcName: string, _data: any, wire: any) => {
|
|
24
|
+
captured = wire
|
|
25
|
+
return { ok: true }
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
wire: () => captured,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('queued workflow steps carry the run wire pikkuUserId', () => {
|
|
33
|
+
test('invokeStepRpc merges run.wire.pikkuUserId into the step wire override', async () => {
|
|
34
|
+
pikkuState(null, 'package', 'singletonServices', { logger: silentLogger } as any)
|
|
35
|
+
|
|
36
|
+
const ws = new InMemoryWorkflowService()
|
|
37
|
+
const runId = await ws.createRun('flow', {}, false, 'hash', {
|
|
38
|
+
type: 'queue',
|
|
39
|
+
pikkuUserId: 'user-abc',
|
|
40
|
+
})
|
|
41
|
+
await ws.insertStepState(runId, 'deploy', 'deployByStageKind', {})
|
|
42
|
+
const stepState = await ws.getStepState(runId, 'deploy')
|
|
43
|
+
|
|
44
|
+
const rpc = capturingRpcService()
|
|
45
|
+
await (ws as any).invokeStepRpc(
|
|
46
|
+
runId,
|
|
47
|
+
'deploy',
|
|
48
|
+
stepState,
|
|
49
|
+
'deployByStageKind',
|
|
50
|
+
{},
|
|
51
|
+
rpc.service
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert.equal(
|
|
55
|
+
rpc.wire().pikkuUserId,
|
|
56
|
+
'user-abc',
|
|
57
|
+
'the step wire override carries the run wire pikkuUserId so authed steps rehydrate'
|
|
58
|
+
)
|
|
59
|
+
assert.ok(rpc.wire().workflowStep, 'workflowStep provenance is still present')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('no pikkuUserId key is injected when the run wire has none', async () => {
|
|
63
|
+
pikkuState(null, 'package', 'singletonServices', { logger: silentLogger } as any)
|
|
64
|
+
|
|
65
|
+
const ws = new InMemoryWorkflowService()
|
|
66
|
+
const runId = await ws.createRun('flow', {}, false, 'hash', { type: 'test' })
|
|
67
|
+
await ws.insertStepState(runId, 'noauth', 'someFn', {})
|
|
68
|
+
const stepState = await ws.getStepState(runId, 'noauth')
|
|
69
|
+
|
|
70
|
+
const rpc = capturingRpcService()
|
|
71
|
+
await (ws as any).invokeStepRpc(runId, 'noauth', stepState, 'someFn', {}, rpc.service)
|
|
72
|
+
|
|
73
|
+
assert.ok(
|
|
74
|
+
!('pikkuUserId' in rpc.wire()),
|
|
75
|
+
'no undefined pikkuUserId is added when the run wire lacks one'
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
})
|