@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.
@@ -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
+ })