@pikku/core 0.12.56 → 0.12.58
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 +23 -0
- package/dist/dev/hot-reload.d.ts +7 -3
- package/dist/dev/hot-reload.js +90 -47
- package/dist/dev/reload-meta.d.ts +24 -0
- package/dist/dev/reload-meta.js +99 -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/package.json +2 -2
- package/src/dev/hot-reload.test.ts +15 -8
- package/src/dev/hot-reload.ts +99 -54
- package/src/dev/reload-meta.test.ts +154 -0
- package/src/dev/reload-meta.ts +138 -0
- 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/tsconfig.tsbuildinfo +1 -1
|
@@ -8,6 +8,28 @@ import type {
|
|
|
8
8
|
GatewayOutboundMessage,
|
|
9
9
|
PikkuGateway,
|
|
10
10
|
} from './gateway.types.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lazily resolve a gateway's adapter. Factories are invoked once with the
|
|
14
|
+
* singleton services and cached (promise-cached, so concurrent first
|
|
15
|
+
* requests share one construction).
|
|
16
|
+
*/
|
|
17
|
+
const resolvedAdapters = new WeakMap<CoreGateway, Promise<GatewayAdapter>>()
|
|
18
|
+
|
|
19
|
+
export const resolveGatewayAdapter = (
|
|
20
|
+
config: CoreGateway,
|
|
21
|
+
services: CoreSingletonServices
|
|
22
|
+
): Promise<GatewayAdapter> => {
|
|
23
|
+
let resolved = resolvedAdapters.get(config)
|
|
24
|
+
if (!resolved) {
|
|
25
|
+
resolved =
|
|
26
|
+
typeof config.adapter === 'function'
|
|
27
|
+
? Promise.resolve(config.adapter(services))
|
|
28
|
+
: Promise.resolve(config.adapter)
|
|
29
|
+
resolvedAdapters.set(config, resolved)
|
|
30
|
+
}
|
|
31
|
+
return resolved
|
|
32
|
+
}
|
|
11
33
|
import type {
|
|
12
34
|
PikkuWire,
|
|
13
35
|
PikkuRawWire,
|
|
@@ -94,8 +116,10 @@ const wireWebhookGateway = (config: CoreGateway): void => {
|
|
|
94
116
|
} as any)
|
|
95
117
|
|
|
96
118
|
// --- GET handler (webhook verification, e.g. WhatsApp challenge) ---------
|
|
119
|
+
// Factory adapters can't be probed for verifyWebhook until first resolve,
|
|
120
|
+
// so register the GET route unconditionally for them.
|
|
97
121
|
|
|
98
|
-
if (adapter.verifyWebhook) {
|
|
122
|
+
if (typeof adapter === 'function' || adapter.verifyWebhook) {
|
|
99
123
|
const verifyFuncId = `gateway__${name}__verify`
|
|
100
124
|
|
|
101
125
|
funcMeta[verifyFuncId] = {
|
|
@@ -113,7 +137,7 @@ const wireWebhookGateway = (config: CoreGateway): void => {
|
|
|
113
137
|
|
|
114
138
|
const verifyHandler = {
|
|
115
139
|
auth: false,
|
|
116
|
-
func: createWebhookVerifyHandler(
|
|
140
|
+
func: createWebhookVerifyHandler(config),
|
|
117
141
|
}
|
|
118
142
|
|
|
119
143
|
addFunction(verifyFuncId, verifyHandler as any)
|
|
@@ -145,7 +169,7 @@ const wireWebhookGateway = (config: CoreGateway): void => {
|
|
|
145
169
|
* 6. Auto-send response via adapter if func returns outbound content
|
|
146
170
|
*/
|
|
147
171
|
const createWebhookPostHandler = (config: CoreGateway) => {
|
|
148
|
-
const { name,
|
|
172
|
+
const { name, func: userFunc, middleware: userMiddleware } = config
|
|
149
173
|
const userFuncConfig = userFunc as {
|
|
150
174
|
func: Function
|
|
151
175
|
middleware?: CorePikkuMiddleware[]
|
|
@@ -156,6 +180,8 @@ const createWebhookPostHandler = (config: CoreGateway) => {
|
|
|
156
180
|
data: unknown,
|
|
157
181
|
wire: PikkuWire
|
|
158
182
|
) => {
|
|
183
|
+
const adapter = await resolveGatewayAdapter(config, services)
|
|
184
|
+
|
|
159
185
|
// Check for POST-based webhook verification (e.g. Slack url_verification)
|
|
160
186
|
if (adapter.verifyWebhook) {
|
|
161
187
|
const verifyResult = await adapter.verifyWebhook(data, wire.http?.request)
|
|
@@ -207,12 +233,13 @@ const createWebhookPostHandler = (config: CoreGateway) => {
|
|
|
207
233
|
* Creates the GET handler for webhook verification challenges.
|
|
208
234
|
* Passes query parameters to the adapter's verifyWebhook method.
|
|
209
235
|
*/
|
|
210
|
-
const createWebhookVerifyHandler = (
|
|
236
|
+
const createWebhookVerifyHandler = (config: CoreGateway) => {
|
|
211
237
|
return async (
|
|
212
|
-
|
|
238
|
+
services: CoreSingletonServices,
|
|
213
239
|
_data: unknown,
|
|
214
240
|
wire: PikkuWire
|
|
215
241
|
) => {
|
|
242
|
+
const adapter = await resolveGatewayAdapter(config, services)
|
|
216
243
|
if (!adapter.verifyWebhook) {
|
|
217
244
|
return { error: 'Verification not supported' }
|
|
218
245
|
}
|
|
@@ -232,7 +259,7 @@ const createWebhookVerifyHandler = (adapter: GatewayAdapter) => {
|
|
|
232
259
|
// ---------------------------------------------------------------------------
|
|
233
260
|
|
|
234
261
|
const wireWebsocketGateway = (config: CoreGateway): void => {
|
|
235
|
-
const { name, route
|
|
262
|
+
const { name, route } = config
|
|
236
263
|
if (!route) {
|
|
237
264
|
throw new Error(`WebSocket gateway '${name}' requires a route`)
|
|
238
265
|
}
|
|
@@ -279,7 +306,8 @@ const wireWebsocketGateway = (config: CoreGateway): void => {
|
|
|
279
306
|
// Register onConnect
|
|
280
307
|
addFunction(connectFuncId, {
|
|
281
308
|
auth: false,
|
|
282
|
-
func: async (
|
|
309
|
+
func: async (services: any, _data: unknown, wire: PikkuWire) => {
|
|
310
|
+
const adapter = await resolveGatewayAdapter(config, services)
|
|
283
311
|
;(wire as any).gateway = {
|
|
284
312
|
gatewayName: name,
|
|
285
313
|
senderId: '',
|
|
@@ -295,6 +323,7 @@ const wireWebsocketGateway = (config: CoreGateway): void => {
|
|
|
295
323
|
addFunction(messageFuncId, {
|
|
296
324
|
auth: false,
|
|
297
325
|
func: async (services: any, data: unknown, wire: PikkuWire) => {
|
|
326
|
+
const adapter = await resolveGatewayAdapter(config, services)
|
|
298
327
|
const parsed = adapter.parse(data)
|
|
299
328
|
if (!parsed) return
|
|
300
329
|
|
|
@@ -371,7 +400,6 @@ export const createListenerMessageHandler = (
|
|
|
371
400
|
config: CoreGateway,
|
|
372
401
|
singletonServices: CoreSingletonServices
|
|
373
402
|
): ((rawData: unknown) => Promise<void>) => {
|
|
374
|
-
const { adapter } = config
|
|
375
403
|
const userFuncConfig = config.func as {
|
|
376
404
|
func: Function
|
|
377
405
|
middleware?: CorePikkuMiddleware[]
|
|
@@ -379,6 +407,7 @@ export const createListenerMessageHandler = (
|
|
|
379
407
|
const userMiddleware = config.middleware as CorePikkuMiddleware[] | undefined
|
|
380
408
|
|
|
381
409
|
return async (rawData: unknown): Promise<void> => {
|
|
410
|
+
const adapter = await resolveGatewayAdapter(config, singletonServices)
|
|
382
411
|
const parsed = adapter.parse(rawData)
|
|
383
412
|
if (!parsed) return
|
|
384
413
|
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
CommonWireMeta,
|
|
3
3
|
CorePikkuMiddleware,
|
|
4
4
|
CorePikkuMiddlewareGroup,
|
|
5
|
+
CoreSingletonServices,
|
|
5
6
|
} from '../../types/core.types.js'
|
|
6
7
|
import type {
|
|
7
8
|
CorePikkuFunctionConfig,
|
|
@@ -83,6 +84,19 @@ export interface GatewayAdapter {
|
|
|
83
84
|
): WebhookVerificationResult | Promise<WebhookVerificationResult>
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Factory that builds a GatewayAdapter from singleton services.
|
|
89
|
+
*
|
|
90
|
+
* Real platform adapters (WhatsApp Cloud API, Slack, …) need secrets or
|
|
91
|
+
* services that only exist after boot, while `wireGateway` runs at module
|
|
92
|
+
* load. Pass a factory instead of an instance and it is resolved lazily on
|
|
93
|
+
* the first inbound request (webhook/websocket) or on gateway start
|
|
94
|
+
* (listener), then cached for the lifetime of the gateway.
|
|
95
|
+
*/
|
|
96
|
+
export type GatewayAdapterFactory = (
|
|
97
|
+
services: CoreSingletonServices
|
|
98
|
+
) => GatewayAdapter | Promise<GatewayAdapter>
|
|
99
|
+
|
|
86
100
|
/**
|
|
87
101
|
* The gateway wire object available on wire.gateway inside handler functions and middleware
|
|
88
102
|
*/
|
|
@@ -122,8 +136,9 @@ export type CoreGateway<
|
|
|
122
136
|
/** HTTP route for webhook/websocket types */
|
|
123
137
|
route?: string
|
|
124
138
|
platform?: string
|
|
125
|
-
/** The gateway adapter (parse inbound, send outbound)
|
|
126
|
-
|
|
139
|
+
/** The gateway adapter (parse inbound, send outbound), or a factory
|
|
140
|
+
* resolved lazily from singleton services (for adapters needing secrets) */
|
|
141
|
+
adapter: GatewayAdapter | GatewayAdapterFactory
|
|
127
142
|
/** The handler function that processes parsed messages */
|
|
128
143
|
func: PikkuFunctionConfig
|
|
129
144
|
/** Optional middleware chain (e.g., auth) */
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
wireGateway,
|
|
3
|
+
createListenerMessageHandler,
|
|
4
|
+
resolveGatewayAdapter,
|
|
5
|
+
} from './gateway-runner.js'
|
|
2
6
|
export type {
|
|
3
7
|
GatewayAdapter,
|
|
8
|
+
GatewayAdapterFactory,
|
|
4
9
|
GatewayAttachment,
|
|
5
10
|
GatewayInboundMessage,
|
|
6
11
|
GatewayOutboundMessage,
|
|
@@ -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,
|