@pikku/core 0.12.4 → 0.12.5

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.
Files changed (229) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/dev/hot-reload.d.ts +10 -0
  3. package/dist/dev/hot-reload.js +158 -0
  4. package/dist/middleware-runner.d.ts +1 -0
  5. package/dist/middleware-runner.js +5 -0
  6. package/dist/permissions.d.ts +1 -0
  7. package/dist/permissions.js +5 -0
  8. package/dist/services/in-memory-workflow-service.js +7 -11
  9. package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
  10. package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
  11. package/dist/wirings/channel/channel-middleware-runner.js +5 -0
  12. package/dist/wirings/workflow/pikku-workflow-service.js +7 -8
  13. package/package.json +3 -2
  14. package/src/crypto-utils.test.ts +214 -0
  15. package/src/crypto-utils.ts +213 -0
  16. package/src/dev/hot-reload.test.ts +484 -0
  17. package/src/dev/hot-reload.ts +212 -0
  18. package/src/errors/error-handler.ts +62 -0
  19. package/src/errors/error.test.ts +195 -0
  20. package/src/errors/errors.ts +374 -0
  21. package/src/errors/index.ts +2 -0
  22. package/src/factory-functions.test.ts +109 -0
  23. package/src/function/function-runner.test.ts +536 -0
  24. package/src/function/function-runner.ts +365 -0
  25. package/src/function/functions.types.ts +304 -0
  26. package/src/function/index.ts +5 -0
  27. package/src/handle-error.test.ts +424 -0
  28. package/src/handle-error.ts +69 -0
  29. package/src/index.ts +118 -0
  30. package/src/internal.ts +6 -0
  31. package/src/middleware/auth-apikey.test.ts +363 -0
  32. package/src/middleware/auth-apikey.ts +47 -0
  33. package/src/middleware/auth-bearer.test.ts +450 -0
  34. package/src/middleware/auth-bearer.ts +72 -0
  35. package/src/middleware/auth-cookie.test.ts +528 -0
  36. package/src/middleware/auth-cookie.ts +80 -0
  37. package/src/middleware/cors.test.ts +424 -0
  38. package/src/middleware/cors.ts +104 -0
  39. package/src/middleware/index.ts +5 -0
  40. package/src/middleware/remote-auth.test.ts +488 -0
  41. package/src/middleware/remote-auth.ts +68 -0
  42. package/src/middleware/timeout.ts +15 -0
  43. package/src/middleware-runner.test.ts +418 -0
  44. package/src/middleware-runner.ts +240 -0
  45. package/src/permissions.test.ts +434 -0
  46. package/src/permissions.ts +327 -0
  47. package/src/pikku-request.ts +23 -0
  48. package/src/pikku-response.ts +5 -0
  49. package/src/pikku-state.test.ts +224 -0
  50. package/src/pikku-state.ts +216 -0
  51. package/src/run-tests-script.test.ts +49 -0
  52. package/src/schema.test.ts +249 -0
  53. package/src/schema.ts +151 -0
  54. package/src/services/ai-agent-runner-service.ts +41 -0
  55. package/src/services/ai-run-state-service.ts +20 -0
  56. package/src/services/ai-storage-service.ts +39 -0
  57. package/src/services/content-service.ts +76 -0
  58. package/src/services/deployment-service.ts +22 -0
  59. package/src/services/gateway-service.ts +20 -0
  60. package/src/services/gopass-secrets.ts +98 -0
  61. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  62. package/src/services/in-memory-trigger-service.ts +64 -0
  63. package/src/services/in-memory-workflow-service.test.ts +351 -0
  64. package/src/services/in-memory-workflow-service.ts +502 -0
  65. package/src/services/index.ts +56 -0
  66. package/src/services/jwt-service.ts +30 -0
  67. package/src/services/local-content.ts +120 -0
  68. package/src/services/local-gateway-service.ts +62 -0
  69. package/src/services/local-secrets.test.ts +80 -0
  70. package/src/services/local-secrets.ts +94 -0
  71. package/src/services/local-variables.test.ts +61 -0
  72. package/src/services/local-variables.ts +39 -0
  73. package/src/services/logger-console.test.ts +118 -0
  74. package/src/services/logger-console.ts +98 -0
  75. package/src/services/logger.ts +57 -0
  76. package/src/services/scheduler-service.ts +86 -0
  77. package/src/services/schema-service.ts +33 -0
  78. package/src/services/scoped-secret-service.test.ts +74 -0
  79. package/src/services/scoped-secret-service.ts +41 -0
  80. package/src/services/secret-service.ts +38 -0
  81. package/src/services/trigger-service.ts +17 -0
  82. package/src/services/typed-secret-service.test.ts +93 -0
  83. package/src/services/typed-secret-service.ts +70 -0
  84. package/src/services/typed-variables-service.test.ts +73 -0
  85. package/src/services/typed-variables-service.ts +81 -0
  86. package/src/services/user-session-service.test.ts +113 -0
  87. package/src/services/user-session-service.ts +86 -0
  88. package/src/services/variables-service.ts +11 -0
  89. package/src/services/workflow-service.ts +95 -0
  90. package/src/testing/index.ts +2 -0
  91. package/src/testing/service-tests.ts +873 -0
  92. package/src/time-utils.test.ts +56 -0
  93. package/src/time-utils.ts +107 -0
  94. package/src/types/core.types.ts +478 -0
  95. package/src/types/state.types.ts +188 -0
  96. package/src/utils/hash.test.ts +68 -0
  97. package/src/utils/hash.ts +26 -0
  98. package/src/utils.test.ts +350 -0
  99. package/src/utils.ts +129 -0
  100. package/src/version.test.ts +80 -0
  101. package/src/version.ts +25 -0
  102. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  103. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  104. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  105. package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
  106. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  107. package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
  108. package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
  109. package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
  110. package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
  111. package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
  112. package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
  113. package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
  114. package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
  115. package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
  116. package/src/wirings/ai-agent/index.ts +37 -0
  117. package/src/wirings/channel/channel-common.ts +90 -0
  118. package/src/wirings/channel/channel-handler.ts +250 -0
  119. package/src/wirings/channel/channel-middleware-runner.ts +126 -0
  120. package/src/wirings/channel/channel-runner.ts +166 -0
  121. package/src/wirings/channel/channel-store.ts +29 -0
  122. package/src/wirings/channel/channel.types.ts +172 -0
  123. package/src/wirings/channel/define-channel-routes.ts +25 -0
  124. package/src/wirings/channel/eventhub-service.ts +34 -0
  125. package/src/wirings/channel/eventhub-store.ts +13 -0
  126. package/src/wirings/channel/index.ts +24 -0
  127. package/src/wirings/channel/local/index.ts +3 -0
  128. package/src/wirings/channel/local/local-channel-handler.ts +81 -0
  129. package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
  130. package/src/wirings/channel/local/local-channel-runner.ts +210 -0
  131. package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
  132. package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
  133. package/src/wirings/channel/log-channels.ts +20 -0
  134. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
  135. package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
  136. package/src/wirings/channel/serverless/index.ts +5 -0
  137. package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
  138. package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
  139. package/src/wirings/cli/channel/index.ts +1 -0
  140. package/src/wirings/cli/cli-runner.test.ts +403 -0
  141. package/src/wirings/cli/cli-runner.ts +529 -0
  142. package/src/wirings/cli/cli.types.ts +358 -0
  143. package/src/wirings/cli/command-parser.test.ts +445 -0
  144. package/src/wirings/cli/command-parser.ts +504 -0
  145. package/src/wirings/cli/define-cli-commands.ts +24 -0
  146. package/src/wirings/cli/index.ts +19 -0
  147. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  148. package/src/wirings/gateway/gateway-runner.ts +411 -0
  149. package/src/wirings/gateway/gateway.types.ts +149 -0
  150. package/src/wirings/gateway/index.ts +13 -0
  151. package/src/wirings/http/http-routes.test.ts +322 -0
  152. package/src/wirings/http/http-routes.ts +197 -0
  153. package/src/wirings/http/http-runner.test.ts +144 -0
  154. package/src/wirings/http/http-runner.ts +588 -0
  155. package/src/wirings/http/http.types.ts +369 -0
  156. package/src/wirings/http/index.ts +28 -0
  157. package/src/wirings/http/log-http-routes.ts +22 -0
  158. package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
  159. package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
  160. package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
  161. package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
  162. package/src/wirings/http/routers/http-router.ts +13 -0
  163. package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
  164. package/src/wirings/http/routers/path-to-regex.ts +126 -0
  165. package/src/wirings/http/web-request.test.ts +236 -0
  166. package/src/wirings/http/web-request.ts +104 -0
  167. package/src/wirings/mcp/index.ts +27 -0
  168. package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
  169. package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
  170. package/src/wirings/mcp/mcp-runner.ts +323 -0
  171. package/src/wirings/mcp/mcp.types.ts +216 -0
  172. package/src/wirings/node/index.ts +2 -0
  173. package/src/wirings/node/node.types.ts +23 -0
  174. package/src/wirings/oauth2/index.ts +3 -0
  175. package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
  176. package/src/wirings/oauth2/oauth2-client.ts +335 -0
  177. package/src/wirings/oauth2/oauth2.types.ts +69 -0
  178. package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
  179. package/src/wirings/queue/index.ts +31 -0
  180. package/src/wirings/queue/queue-runner.test.ts +710 -0
  181. package/src/wirings/queue/queue-runner.ts +192 -0
  182. package/src/wirings/queue/queue.types.ts +189 -0
  183. package/src/wirings/queue/register-queue-helper.ts +60 -0
  184. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  185. package/src/wirings/queue/validate-worker-config.ts +116 -0
  186. package/src/wirings/rpc/index.ts +4 -0
  187. package/src/wirings/rpc/rpc-runner.ts +460 -0
  188. package/src/wirings/rpc/rpc-types.ts +56 -0
  189. package/src/wirings/rpc/wire-addon.ts +21 -0
  190. package/src/wirings/scheduler/index.ts +11 -0
  191. package/src/wirings/scheduler/log-schedulers.ts +20 -0
  192. package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
  193. package/src/wirings/scheduler/scheduler-runner.ts +133 -0
  194. package/src/wirings/scheduler/scheduler.types.ts +53 -0
  195. package/src/wirings/secret/index.ts +9 -0
  196. package/src/wirings/secret/secret.types.ts +32 -0
  197. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  198. package/src/wirings/secret/validate-secret-definitions.ts +82 -0
  199. package/src/wirings/trigger/index.ts +10 -0
  200. package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
  201. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  202. package/src/wirings/trigger/trigger-runner.ts +135 -0
  203. package/src/wirings/trigger/trigger.types.ts +178 -0
  204. package/src/wirings/variable/index.ts +8 -0
  205. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  206. package/src/wirings/variable/validate-variable-definitions.ts +69 -0
  207. package/src/wirings/variable/variable.types.ts +22 -0
  208. package/src/wirings/workflow/dsl/index.ts +31 -0
  209. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
  210. package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
  211. package/src/wirings/workflow/graph/graph-node.ts +222 -0
  212. package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
  213. package/src/wirings/workflow/graph/graph-runner.ts +816 -0
  214. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  215. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  216. package/src/wirings/workflow/graph/index.ts +19 -0
  217. package/src/wirings/workflow/graph/template.test.ts +49 -0
  218. package/src/wirings/workflow/graph/template.ts +42 -0
  219. package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
  220. package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
  221. package/src/wirings/workflow/index.ts +82 -0
  222. package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
  223. package/src/wirings/workflow/pikku-workflow-service.ts +1179 -0
  224. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  225. package/src/wirings/workflow/workflow-helpers.ts +79 -0
  226. package/src/wirings/workflow/workflow.types.ts +274 -0
  227. package/tsconfig.json +14 -0
  228. package/tsconfig.tsbuildinfo +1 -0
  229. package/lcov.info +0 -18891
@@ -0,0 +1,659 @@
1
+ import type { PikkuWire, CoreUserSession } from '../../types/core.types.js'
2
+ import type {
3
+ CoreAIAgent,
4
+ AIAgentInput,
5
+ AIAgentToolDef,
6
+ AIContentPart,
7
+ AIMessage,
8
+ AIStreamChannel,
9
+ AIStreamEvent,
10
+ PikkuAIMiddlewareHooks,
11
+ } from './ai-agent.types.js'
12
+ import type { AIAgentRunnerParams } from '../../services/ai-agent-runner-service.js'
13
+ import { PikkuError } from '../../errors/error-handler.js'
14
+ import { pikkuState, getSingletonServices } from '../../pikku-state.js'
15
+ import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
16
+ import type { SessionService } from '../../services/user-session-service.js'
17
+ import { randomUUID } from 'crypto'
18
+ import { streamAIAgent } from './ai-agent-stream.js'
19
+ import { runAIAgent } from './ai-agent-runner.js'
20
+ import {
21
+ resolveNamespace,
22
+ ContextAwareRPCService,
23
+ } from '../../wirings/rpc/rpc-runner.js'
24
+ import {
25
+ buildDynamicWorkflowInstructions,
26
+ buildWorkflowTools,
27
+ } from './agent-dynamic-workflow.js'
28
+ import {
29
+ resolveMemoryServices,
30
+ loadContextMessages,
31
+ trimMessages,
32
+ } from './ai-agent-memory.js'
33
+ import { resolveModelConfig } from './ai-agent-model-config.js'
34
+
35
+ export type RunAIAgentParams = {
36
+ sessionService?: SessionService<CoreUserSession>
37
+ }
38
+
39
+ export type StreamAIAgentOptions = {
40
+ requiresToolApproval?: 'all' | 'explicit' | false
41
+ }
42
+
43
+ export class ToolApprovalRequired extends PikkuError {
44
+ public readonly toolCallId: string
45
+ public readonly toolName: string
46
+ public readonly args: unknown
47
+ public reason?: string
48
+ public readonly displayToolName?: string
49
+ public readonly displayArgs?: unknown
50
+ public readonly agentRunId?: string
51
+
52
+ constructor(
53
+ toolCallId: string,
54
+ toolName: string,
55
+ args: unknown,
56
+ reason?: string,
57
+ displayToolName?: string,
58
+ displayArgs?: unknown,
59
+ agentRunId?: string
60
+ ) {
61
+ super(`Tool '${displayToolName ?? toolName}' requires approval`)
62
+ this.toolCallId = toolCallId
63
+ this.toolName = toolName
64
+ this.args = args
65
+ this.reason = reason
66
+ this.displayToolName = displayToolName
67
+ this.displayArgs = displayArgs
68
+ this.agentRunId = agentRunId
69
+ }
70
+ }
71
+
72
+ export type StreamContext = {
73
+ channel: AIStreamChannel
74
+ options?: StreamAIAgentOptions
75
+ delegateState?: { delegated: boolean }
76
+ }
77
+
78
+ export const resolveAgent = (
79
+ agentName: string
80
+ ): { agent: CoreAIAgent; packageName: string | null; resolvedName: string } => {
81
+ const mainAgent = pikkuState(null, 'agent', 'agents').get(agentName)
82
+ if (mainAgent) {
83
+ return { agent: mainAgent, packageName: null, resolvedName: agentName }
84
+ }
85
+
86
+ const colonIndex = agentName.indexOf(':')
87
+ if (colonIndex !== -1) {
88
+ const namespace = agentName.substring(0, colonIndex)
89
+ const localName = agentName.substring(colonIndex + 1)
90
+ const addons = pikkuState(null, 'addons', 'packages')
91
+ const pkgConfig = addons.get(namespace)
92
+ if (pkgConfig) {
93
+ const extAgent = pikkuState(pkgConfig.package, 'agent', 'agents').get(
94
+ localName
95
+ )
96
+ if (extAgent) {
97
+ return {
98
+ agent: extAgent,
99
+ packageName: pkgConfig.package,
100
+ resolvedName: localName,
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ throw new Error(`AI agent not found: ${agentName}`)
107
+ }
108
+
109
+ export async function buildInstructions(
110
+ agentName: string,
111
+ packageName: string | null
112
+ ): Promise<string> {
113
+ const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName]
114
+ const rawInstructions = meta?.instructions ?? ''
115
+ let instructions = Array.isArray(rawInstructions)
116
+ ? rawInstructions.join('\n')
117
+ : rawInstructions
118
+
119
+ if (meta?.agents?.length) {
120
+ instructions +=
121
+ '\n\nWhen calling a sub-agent, provide a short session name that describes the task. ' +
122
+ 'Use the same session name to continue a previous conversation with that agent. ' +
123
+ 'Use a new session name for a new independent task. ' +
124
+ 'When a request involves multiple actions for the same domain, combine them into a single sub-agent call rather than making separate calls.'
125
+ }
126
+
127
+ if (meta?.dynamicWorkflows && meta.tools?.length) {
128
+ instructions += buildDynamicWorkflowInstructions(
129
+ meta.tools,
130
+ meta.dynamicWorkflows
131
+ )
132
+ }
133
+
134
+ return instructions
135
+ }
136
+
137
+ export type ScopedChannel = AIStreamChannel & {
138
+ approvals: Array<{
139
+ toolCallId: string
140
+ toolName: string
141
+ args: unknown
142
+ runId: string
143
+ }>
144
+ }
145
+
146
+ export function createScopedChannel(
147
+ parent: AIStreamChannel,
148
+ agentName: string,
149
+ session: string
150
+ ): ScopedChannel {
151
+ const capturedApprovals: ScopedChannel['approvals'] = []
152
+
153
+ return {
154
+ channelId: `${parent.channelId}:${agentName}:${session}`,
155
+ openingData: parent.openingData,
156
+ get state() {
157
+ return parent.state
158
+ },
159
+ get approvals() {
160
+ return capturedApprovals
161
+ },
162
+ close: () => {},
163
+ sendBinary: (data) => parent.sendBinary(data),
164
+ send: (event: AIStreamEvent) => {
165
+ if (event.type === 'done') return
166
+ if (event.type === 'approval-request') {
167
+ capturedApprovals.push({
168
+ toolCallId: event.toolCallId,
169
+ toolName: event.toolName,
170
+ args: event.args,
171
+ runId: (event as any).runId,
172
+ })
173
+ return
174
+ }
175
+ if (
176
+ event.type === 'step-start' ||
177
+ event.type === 'text-delta' ||
178
+ event.type === 'reasoning-delta' ||
179
+ event.type === 'tool-call' ||
180
+ event.type === 'tool-result' ||
181
+ event.type === 'usage' ||
182
+ event.type === 'error' ||
183
+ event.type === 'workflow-created'
184
+ ) {
185
+ parent.send({ ...event, agent: agentName, session } as AIStreamEvent)
186
+ } else {
187
+ parent.send(event)
188
+ }
189
+ },
190
+ }
191
+ }
192
+
193
+ export async function buildToolDefs(
194
+ params: RunAIAgentParams,
195
+ agentSessionMap: Map<string, string>,
196
+ resourceId: string,
197
+ agentName: string,
198
+ packageName: string | null,
199
+ streamContext?: StreamContext,
200
+ aiMiddlewares?: PikkuAIMiddlewareHooks[],
201
+ agentMode?: 'delegate' | 'supervise'
202
+ ): Promise<{ tools: AIAgentToolDef[]; missingRpcs: string[] }> {
203
+ const singletonServices = getSingletonServices()
204
+ const tools: AIAgentToolDef[] = []
205
+ const missingRpcs: string[] = []
206
+ const approvalPolicy =
207
+ streamContext?.options?.requiresToolApproval ?? 'explicit'
208
+
209
+ const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName]
210
+ if (!meta) return { tools, missingRpcs }
211
+
212
+ const metaTools = meta.tools
213
+ const metaAgents = meta.agents
214
+
215
+ if (metaTools?.length) {
216
+ for (const toolName of metaTools) {
217
+ let fnMeta: any
218
+ let resolvedPkg: string | null = null
219
+ let schemas: Map<string, any>
220
+
221
+ const resolved = toolName.includes(':')
222
+ ? resolveNamespace(toolName)
223
+ : null
224
+
225
+ let pikkuFuncId: string | undefined
226
+
227
+ if (resolved) {
228
+ resolvedPkg = resolved.package
229
+ pikkuFuncId = resolved.function
230
+ fnMeta = pikkuState(resolvedPkg, 'function', 'meta')[pikkuFuncId]
231
+ schemas = pikkuState(resolvedPkg, 'misc', 'schemas')
232
+ } else {
233
+ const rpcMeta = pikkuState(null, 'rpc', 'meta')
234
+ pikkuFuncId = rpcMeta[toolName]
235
+ if (!pikkuFuncId) {
236
+ missingRpcs.push(toolName)
237
+ continue
238
+ }
239
+ fnMeta = pikkuState(null, 'function', 'meta')[pikkuFuncId]
240
+ schemas = pikkuState(null, 'misc', 'schemas')
241
+ }
242
+
243
+ if (!fnMeta) {
244
+ missingRpcs.push(toolName)
245
+ continue
246
+ }
247
+
248
+ const inputSchemaName = fnMeta?.inputSchemaName
249
+ let inputSchema = inputSchemaName
250
+ ? schemas.get(inputSchemaName)
251
+ : undefined
252
+ if (
253
+ !inputSchema ||
254
+ (typeof inputSchema === 'object' &&
255
+ inputSchema.type === 'object' &&
256
+ !inputSchema.properties)
257
+ ) {
258
+ inputSchema = { type: 'object', properties: {} }
259
+ }
260
+
261
+ const needsApproval =
262
+ approvalPolicy === 'all' ||
263
+ (approvalPolicy === 'explicit' && fnMeta?.approvalRequired)
264
+
265
+ // Build approvalDescriptionFn if the function has an approvalDescription configured
266
+ let approvalDescriptionFn:
267
+ | ((input: unknown) => Promise<string>)
268
+ | undefined
269
+ if (needsApproval && pikkuFuncId) {
270
+ const funcConfig = pikkuState(resolvedPkg, 'function', 'functions').get(
271
+ pikkuFuncId
272
+ )
273
+ if (funcConfig?.approvalDescription) {
274
+ const descFn = funcConfig.approvalDescription
275
+ const capturedPkg = resolvedPkg
276
+ approvalDescriptionFn = async (input: unknown) => {
277
+ let services = singletonServices
278
+ if (capturedPkg) {
279
+ const pkgServices = pikkuState(
280
+ capturedPkg,
281
+ 'package',
282
+ 'singletonServices'
283
+ )
284
+ if (pkgServices) {
285
+ services = pkgServices
286
+ }
287
+ }
288
+ return descFn(services, input)
289
+ }
290
+ }
291
+ }
292
+
293
+ tools.push({
294
+ name: toolName.replaceAll(':', '__'),
295
+ description: fnMeta?.description || fnMeta?.title || toolName,
296
+ inputSchema,
297
+ needsApproval: needsApproval || undefined,
298
+ approvalDescriptionFn,
299
+ execute: async (toolInput: unknown) => {
300
+ const wire: PikkuWire = params.sessionService
301
+ ? { ...createMiddlewareSessionWireProps(params.sessionService) }
302
+ : {}
303
+ const rpcService = new ContextAwareRPCService(
304
+ singletonServices,
305
+ wire,
306
+ { sessionService: params.sessionService }
307
+ )
308
+ return rpcService.rpc(toolName, toolInput)
309
+ },
310
+ })
311
+ }
312
+ }
313
+
314
+ if (metaAgents?.length) {
315
+ const allAgentsMeta = pikkuState(null, 'agent', 'agentsMeta')
316
+
317
+ for (const subAgentName of metaAgents) {
318
+ const subMeta = allAgentsMeta[subAgentName]
319
+ if (!subMeta) {
320
+ singletonServices.logger.warn(
321
+ `Sub-agent '${subAgentName}' not found in agent registry`
322
+ )
323
+ continue
324
+ }
325
+
326
+ tools.push({
327
+ name: subAgentName,
328
+ description: subMeta.description,
329
+ inputSchema: {
330
+ type: 'object',
331
+ properties: {
332
+ message: { type: 'string' },
333
+ session: {
334
+ type: 'string',
335
+ description: 'Short session label for thread continuity',
336
+ },
337
+ },
338
+ required: ['message', 'session'],
339
+ },
340
+ execute: async (toolInput: unknown) => {
341
+ const { message, session } = toolInput as {
342
+ message: string
343
+ session: string
344
+ }
345
+ const sessionKey = `${subAgentName}::${session}`
346
+ let threadId = agentSessionMap.get(sessionKey)
347
+ if (!threadId) {
348
+ threadId = randomUUID()
349
+ agentSessionMap.set(sessionKey, threadId)
350
+ }
351
+
352
+ if (streamContext) {
353
+ const isDelegate = agentMode !== 'supervise'
354
+ if (isDelegate && streamContext.delegateState) {
355
+ streamContext.delegateState.delegated = true
356
+ }
357
+ const { channel } = streamContext
358
+ channel.send({
359
+ type: 'agent-call',
360
+ agentName: subAgentName,
361
+ session,
362
+ input: message,
363
+ })
364
+ const subChannel = createScopedChannel(
365
+ channel,
366
+ subAgentName,
367
+ session
368
+ )
369
+ // In supervise mode, suppress sub-agent text from reaching the client.
370
+ // Approvals still flow through normally.
371
+ const effectiveChannel = isDelegate
372
+ ? subChannel
373
+ : {
374
+ ...subChannel,
375
+ send: (event: AIStreamEvent) => {
376
+ if (
377
+ event.type === 'text-delta' ||
378
+ event.type === 'reasoning-delta'
379
+ )
380
+ return
381
+ subChannel.send(event)
382
+ },
383
+ }
384
+ const resultText = await streamAIAgent(
385
+ subAgentName,
386
+ { message, threadId, resourceId },
387
+ effectiveChannel,
388
+ params,
389
+ agentSessionMap,
390
+ streamContext.options
391
+ )
392
+ if (subChannel.approvals.length > 0) {
393
+ return {
394
+ __approvalRequired: true,
395
+ toolName: subAgentName,
396
+ args: toolInput,
397
+ agentRunId: subChannel.approvals[0].runId,
398
+ subApprovals: subChannel.approvals,
399
+ }
400
+ }
401
+ channel.send({
402
+ type: 'agent-result',
403
+ agentName: subAgentName,
404
+ session,
405
+ result: resultText,
406
+ })
407
+ return resultText
408
+ }
409
+
410
+ // No stream context: sub-agent runs non-streaming
411
+ const result = await runAIAgent(
412
+ subAgentName,
413
+ { message, threadId, resourceId },
414
+ params,
415
+ agentSessionMap
416
+ )
417
+ if (
418
+ result.status === 'suspended' &&
419
+ result.pendingApprovals?.length
420
+ ) {
421
+ return {
422
+ __approvalRequired: true,
423
+ toolName: subAgentName,
424
+ args: toolInput,
425
+ agentRunId: result.runId,
426
+ subApprovals: result.pendingApprovals.map((a) => ({
427
+ toolCallId: a.toolCallId,
428
+ toolName: a.toolName,
429
+ args: a.args,
430
+ runId: a.runId,
431
+ })),
432
+ }
433
+ }
434
+ return result.object ?? result.text
435
+ },
436
+ })
437
+ }
438
+ }
439
+
440
+ if (meta.dynamicWorkflows) {
441
+ const workflowTools = buildWorkflowTools(
442
+ agentName,
443
+ packageName,
444
+ meta.tools ?? [],
445
+ meta.dynamicWorkflows,
446
+ streamContext,
447
+ params.sessionService
448
+ )
449
+ tools.push(...workflowTools)
450
+ }
451
+
452
+ const hasToolHooks = aiMiddlewares?.some(
453
+ (mw) => mw.beforeToolCall || mw.afterToolCall
454
+ )
455
+ if (hasToolHooks) {
456
+ for (const tool of tools) {
457
+ const originalExecute = tool.execute
458
+ tool.execute = async (toolInput: unknown) => {
459
+ const toolCallId = randomUUID()
460
+ let args = (toolInput ?? {}) as Record<string, unknown>
461
+
462
+ for (const mw of aiMiddlewares!) {
463
+ if (mw.beforeToolCall) {
464
+ const beforeResult = await mw.beforeToolCall(singletonServices, {
465
+ toolName: tool.name,
466
+ toolCallId,
467
+ args,
468
+ })
469
+ if (beforeResult && 'args' in beforeResult) {
470
+ args = beforeResult.args
471
+ }
472
+ }
473
+ }
474
+
475
+ const startTime = Date.now()
476
+ let result: unknown
477
+ let execError: unknown
478
+ try {
479
+ result = await originalExecute(args)
480
+ } catch (err) {
481
+ execError = err
482
+ result = err instanceof Error ? err.message : String(err)
483
+ }
484
+ const durationMs = Date.now() - startTime
485
+
486
+ for (let i = aiMiddlewares!.length - 1; i >= 0; i--) {
487
+ const mw = aiMiddlewares![i]
488
+ if (mw.afterToolCall) {
489
+ const afterResult = await mw.afterToolCall(singletonServices, {
490
+ toolName: tool.name,
491
+ toolCallId,
492
+ args,
493
+ result,
494
+ durationMs,
495
+ })
496
+ if (afterResult && 'result' in afterResult) {
497
+ result = afterResult.result
498
+ }
499
+ }
500
+ }
501
+
502
+ if (execError) throw execError
503
+ return result
504
+ }
505
+ }
506
+ }
507
+
508
+ return { tools, missingRpcs }
509
+ }
510
+
511
+ export async function prepareAgentRun(
512
+ agentName: string,
513
+ input: AIAgentInput,
514
+ params: RunAIAgentParams,
515
+ agentSessionMap: Map<string, string>,
516
+ streamContext?: StreamContext
517
+ ) {
518
+ const singletonServices = getSingletonServices()
519
+ const { agent, packageName, resolvedName } = resolveAgent(agentName)
520
+
521
+ const agentRunner = singletonServices.aiAgentRunner
522
+ if (!agentRunner) {
523
+ throw new Error('AIAgentRunnerService not available in singletonServices')
524
+ }
525
+
526
+ if (agent.dynamicWorkflows && singletonServices.workflowService) {
527
+ const persisted =
528
+ await singletonServices.workflowService.getAIGeneratedWorkflows(
529
+ resolvedName
530
+ )
531
+ const allMeta = pikkuState(null, 'workflows', 'meta')
532
+ for (const wf of persisted) {
533
+ if (!allMeta[wf.workflowName]) {
534
+ allMeta[wf.workflowName] = wf.graph
535
+ }
536
+ }
537
+ }
538
+
539
+ const { storage } = resolveMemoryServices(agent, singletonServices)
540
+ const memoryConfig = agent.memory
541
+ const threadId = input.threadId
542
+
543
+ const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
544
+ const meta = agentsMeta[resolvedName]
545
+ const outputSchemaName = meta?.outputSchema
546
+ const outputSchema = outputSchemaName
547
+ ? pikkuState(packageName, 'misc', 'schemas').get(outputSchemaName)
548
+ : undefined
549
+
550
+ const workingMemorySchemaName = meta?.workingMemorySchema ?? null
551
+ const workingMemoryJsonSchema = workingMemorySchemaName
552
+ ? pikkuState(packageName, 'misc', 'schemas').get(workingMemorySchemaName)
553
+ : undefined
554
+
555
+ if (storage) {
556
+ try {
557
+ await storage.getThread(threadId)
558
+ } catch {
559
+ await storage.createThread(input.resourceId, { threadId })
560
+ }
561
+ }
562
+
563
+ let messages: AIMessage[] = []
564
+ if (storage) {
565
+ messages = await storage.getMessages(threadId, {
566
+ lastN: memoryConfig?.lastMessages ?? 20,
567
+ })
568
+ }
569
+
570
+ const contextMessages = await loadContextMessages(
571
+ memoryConfig,
572
+ storage,
573
+ input,
574
+ workingMemoryJsonSchema
575
+ )
576
+
577
+ const userContent: AIMessage['content'] = input.attachments?.length
578
+ ? [
579
+ { type: 'text' as const, text: input.message },
580
+ ...input.attachments.map(
581
+ (a) =>
582
+ ({
583
+ type: a.type,
584
+ data: a.data,
585
+ url: a.url,
586
+ mediaType: a.mediaType,
587
+ ...(a.filename ? { filename: a.filename } : {}),
588
+ }) as AIContentPart
589
+ ),
590
+ ]
591
+ : input.message
592
+
593
+ const userMessage: AIMessage = {
594
+ id: randomUUID(),
595
+ role: 'user',
596
+ content: userContent,
597
+ createdAt: new Date(),
598
+ }
599
+
600
+ const allMessages = [...contextMessages, ...messages, userMessage]
601
+ const trimmedMessages = trimMessages(allMessages)
602
+
603
+ const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
604
+
605
+ const { tools, missingRpcs } = await buildToolDefs(
606
+ params,
607
+ agentSessionMap,
608
+ input.resourceId,
609
+ resolvedName,
610
+ packageName,
611
+ streamContext,
612
+ aiMiddlewares,
613
+ agent.agentMode
614
+ )
615
+
616
+ const instructions = await buildInstructions(resolvedName, packageName)
617
+
618
+ const resolved = resolveModelConfig(resolvedName, agent)
619
+
620
+ // Per-request overrides
621
+ if (input.model) {
622
+ resolved.model = resolveModelConfig(resolvedName, {
623
+ ...agent,
624
+ model: input.model,
625
+ }).model
626
+ }
627
+ if (input.temperature !== undefined) {
628
+ resolved.temperature = input.temperature
629
+ }
630
+
631
+ const maxSteps = resolved.maxSteps ?? 10
632
+
633
+ const runnerParams: AIAgentRunnerParams = {
634
+ model: resolved.model,
635
+ temperature: resolved.temperature,
636
+ instructions,
637
+ messages: trimmedMessages,
638
+ tools,
639
+ maxSteps: 1,
640
+ toolChoice: agent.toolChoice ?? 'auto',
641
+ outputSchema,
642
+ }
643
+
644
+ return {
645
+ agent,
646
+ packageName,
647
+ resolvedName,
648
+ agentRunner,
649
+ storage,
650
+ memoryConfig,
651
+ threadId,
652
+ userMessage,
653
+ runnerParams,
654
+ maxSteps,
655
+ missingRpcs,
656
+ workingMemoryJsonSchema,
657
+ workingMemorySchemaName,
658
+ }
659
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { approveAIAgent } from './ai-agent-registry.js'
5
+
6
+ describe('approveAIAgent', () => {
7
+ test('rejects approval when run agent does not match expected agent', async () => {
8
+ const aiRunState = {
9
+ getRun: async () => ({
10
+ runId: 'run-1',
11
+ agentName: 'internal-agent',
12
+ threadId: 'thread-1',
13
+ resourceId: 'resource-1',
14
+ status: 'suspended',
15
+ pendingApprovals: [],
16
+ usage: { inputTokens: 0, outputTokens: 0, model: 'test-model' },
17
+ createdAt: new Date(),
18
+ updatedAt: new Date(),
19
+ }),
20
+ updateRun: async () => {},
21
+ } as any
22
+
23
+ await assert.rejects(
24
+ () =>
25
+ approveAIAgent(
26
+ aiRunState,
27
+ 'run-1',
28
+ [{ toolCallId: 'call-1', approved: true }],
29
+ 'public-agent'
30
+ ),
31
+ {
32
+ message:
33
+ "Run run-1 belongs to agent 'internal-agent', not 'public-agent'",
34
+ }
35
+ )
36
+ })
37
+ })