@pikku/core 0.12.1 → 0.12.3

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 (165) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/crypto-utils.d.ts +10 -0
  3. package/dist/crypto-utils.js +62 -0
  4. package/dist/function/function-runner.js +4 -4
  5. package/dist/function/functions.types.d.ts +26 -1
  6. package/dist/function/functions.types.js +16 -0
  7. package/dist/handle-error.d.ts +1 -1
  8. package/dist/handle-error.js +7 -3
  9. package/dist/index.d.ts +14 -2
  10. package/dist/index.js +1 -1
  11. package/dist/middleware-runner.js +1 -0
  12. package/dist/permissions.js +1 -0
  13. package/dist/pikku-state.js +7 -1
  14. package/dist/schema.js +1 -1
  15. package/dist/services/gateway-service.d.ts +19 -0
  16. package/dist/services/gateway-service.js +1 -0
  17. package/dist/services/in-memory-workflow-service.d.ts +8 -2
  18. package/dist/services/in-memory-workflow-service.js +28 -1
  19. package/dist/services/index.d.ts +2 -0
  20. package/dist/services/index.js +1 -0
  21. package/dist/services/local-gateway-service.d.ts +22 -0
  22. package/dist/services/local-gateway-service.js +51 -0
  23. package/dist/services/workflow-service.d.ts +8 -2
  24. package/dist/testing/index.d.ts +2 -0
  25. package/dist/testing/index.js +1 -0
  26. package/dist/testing/service-tests.d.ts +31 -0
  27. package/dist/testing/service-tests.js +598 -0
  28. package/dist/types/core.types.d.ts +12 -2
  29. package/dist/types/state.types.d.ts +9 -2
  30. package/dist/utils/hash.d.ts +2 -0
  31. package/dist/utils/hash.js +23 -0
  32. package/dist/utils.js +1 -1
  33. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
  34. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
  35. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
  36. package/dist/wirings/ai-agent/ai-agent-memory.js +19 -7
  37. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
  38. package/dist/wirings/ai-agent/ai-agent-prepare.js +141 -48
  39. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
  40. package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
  41. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
  42. package/dist/wirings/ai-agent/ai-agent-stream.js +126 -48
  43. package/dist/wirings/ai-agent/ai-agent.types.d.ts +57 -3
  44. package/dist/wirings/ai-agent/index.d.ts +2 -3
  45. package/dist/wirings/ai-agent/index.js +1 -2
  46. package/dist/wirings/channel/channel-handler.d.ts +5 -2
  47. package/dist/wirings/channel/channel-handler.js +16 -2
  48. package/dist/wirings/channel/channel-middleware-runner.js +1 -0
  49. package/dist/wirings/channel/channel.types.d.ts +7 -7
  50. package/dist/wirings/channel/index.d.ts +1 -1
  51. package/dist/wirings/channel/local/local-channel-handler.d.ts +8 -1
  52. package/dist/wirings/channel/local/local-channel-handler.js +22 -3
  53. package/dist/wirings/channel/local/local-channel-runner.js +28 -5
  54. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
  55. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  56. package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
  57. package/dist/wirings/cli/cli-runner.js +13 -2
  58. package/dist/wirings/cli/cli.types.d.ts +1 -0
  59. package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
  60. package/dist/wirings/gateway/gateway-runner.js +325 -0
  61. package/dist/wirings/gateway/gateway.types.d.ts +127 -0
  62. package/dist/wirings/gateway/gateway.types.js +1 -0
  63. package/dist/wirings/gateway/index.d.ts +2 -0
  64. package/dist/wirings/gateway/index.js +1 -0
  65. package/dist/wirings/http/http-routes.js +0 -1
  66. package/dist/wirings/http/http-runner.d.ts +1 -1
  67. package/dist/wirings/http/http-runner.js +28 -3
  68. package/dist/wirings/http/http.types.d.ts +3 -11
  69. package/dist/wirings/http/index.d.ts +2 -1
  70. package/dist/wirings/http/index.js +1 -0
  71. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
  72. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  73. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  74. package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
  75. package/dist/wirings/http/web-request.d.ts +12 -0
  76. package/dist/wirings/http/web-request.js +85 -0
  77. package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
  78. package/dist/wirings/mcp/mcp-runner.js +13 -1
  79. package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
  80. package/dist/wirings/rpc/rpc-runner.js +36 -14
  81. package/dist/wirings/rpc/rpc-types.d.ts +6 -1
  82. package/dist/wirings/rpc/wire-addon.d.ts +1 -0
  83. package/dist/wirings/rpc/wire-addon.js +1 -1
  84. package/dist/wirings/scheduler/scheduler-runner.js +1 -0
  85. package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
  86. package/dist/wirings/workflow/graph/graph-validation.js +175 -0
  87. package/dist/wirings/workflow/graph/index.d.ts +1 -0
  88. package/dist/wirings/workflow/graph/index.js +1 -0
  89. package/dist/wirings/workflow/index.d.ts +1 -1
  90. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
  91. package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
  92. package/dist/wirings/workflow/workflow.types.d.ts +10 -1
  93. package/package.json +6 -3
  94. package/src/crypto-utils.test.ts +99 -1
  95. package/src/crypto-utils.ts +114 -0
  96. package/src/function/function-runner.ts +5 -5
  97. package/src/function/functions.types.ts +38 -1
  98. package/src/handle-error.test.ts +65 -2
  99. package/src/handle-error.ts +8 -4
  100. package/src/index.ts +14 -0
  101. package/src/middleware-runner.ts +1 -0
  102. package/src/permissions.ts +1 -0
  103. package/src/pikku-state.ts +8 -1
  104. package/src/schema.ts +1 -1
  105. package/src/services/gateway-service.ts +20 -0
  106. package/src/services/in-memory-workflow-service.ts +45 -2
  107. package/src/services/index.ts +2 -0
  108. package/src/services/local-gateway-service.ts +62 -0
  109. package/src/services/workflow-service.ts +12 -1
  110. package/src/testing/index.ts +2 -0
  111. package/src/testing/service-tests.ts +873 -0
  112. package/src/types/core.types.ts +12 -1
  113. package/src/types/state.types.ts +12 -2
  114. package/src/utils/hash.test.ts +68 -0
  115. package/src/utils/hash.ts +26 -0
  116. package/src/utils.ts +1 -1
  117. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  118. package/src/wirings/ai-agent/ai-agent-memory.ts +17 -7
  119. package/src/wirings/ai-agent/ai-agent-prepare.ts +194 -62
  120. package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
  121. package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
  122. package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
  123. package/src/wirings/ai-agent/ai-agent-stream.ts +195 -91
  124. package/src/wirings/ai-agent/ai-agent.types.ts +57 -3
  125. package/src/wirings/ai-agent/index.ts +3 -5
  126. package/src/wirings/channel/channel-handler.ts +41 -4
  127. package/src/wirings/channel/channel-middleware-runner.ts +1 -0
  128. package/src/wirings/channel/channel.types.ts +12 -7
  129. package/src/wirings/channel/index.ts +1 -0
  130. package/src/wirings/channel/local/local-channel-handler.ts +31 -3
  131. package/src/wirings/channel/local/local-channel-runner.ts +32 -8
  132. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
  133. package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
  134. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
  135. package/src/wirings/cli/cli-runner.ts +15 -2
  136. package/src/wirings/cli/cli.types.ts +1 -0
  137. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  138. package/src/wirings/gateway/gateway-runner.ts +411 -0
  139. package/src/wirings/gateway/gateway.types.ts +149 -0
  140. package/src/wirings/gateway/index.ts +13 -0
  141. package/src/wirings/http/http-routes.ts +0 -1
  142. package/src/wirings/http/http-runner.ts +38 -10
  143. package/src/wirings/http/http.types.ts +3 -12
  144. package/src/wirings/http/index.ts +3 -0
  145. package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
  146. package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
  147. package/src/wirings/http/web-request.test.ts +236 -0
  148. package/src/wirings/http/web-request.ts +104 -0
  149. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  150. package/src/wirings/mcp/mcp-runner.ts +30 -12
  151. package/src/wirings/rpc/rpc-runner.ts +45 -24
  152. package/src/wirings/rpc/rpc-types.ts +6 -1
  153. package/src/wirings/rpc/wire-addon.ts +2 -1
  154. package/src/wirings/scheduler/scheduler-runner.ts +1 -0
  155. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  156. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  157. package/src/wirings/workflow/graph/index.ts +5 -0
  158. package/src/wirings/workflow/index.ts +1 -0
  159. package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
  160. package/src/wirings/workflow/workflow.types.ts +9 -1
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
  163. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -146
  164. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
  165. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -214
@@ -112,7 +112,7 @@ export async function saveMessages(
112
112
  threadId: string,
113
113
  resourceId: string,
114
114
  memoryConfig: AIAgentMemoryConfig | undefined,
115
- userMessage: AIMessage,
115
+ userMessage: AIMessage | null | undefined,
116
116
  result: {
117
117
  text: string
118
118
  steps: {
@@ -133,15 +133,16 @@ export async function saveMessages(
133
133
  let responseText = result.text
134
134
 
135
135
  if (storage) {
136
- const newMessages: AIMessage[] = [userMessage]
136
+ const newMessages: AIMessage[] = userMessage ? [userMessage] : []
137
137
 
138
138
  for (const step of result.steps) {
139
139
  if (step.toolCalls?.length) {
140
+ const toolCallIds = step.toolCalls.map(() => randomUUID())
140
141
  newMessages.push({
141
142
  id: randomUUID(),
142
143
  role: 'assistant',
143
- toolCalls: step.toolCalls.map((tc) => ({
144
- id: randomUUID(),
144
+ toolCalls: step.toolCalls.map((tc, i) => ({
145
+ id: toolCallIds[i],
145
146
  name: tc.name,
146
147
  args: tc.args,
147
148
  })),
@@ -150,8 +151,8 @@ export async function saveMessages(
150
151
  newMessages.push({
151
152
  id: randomUUID(),
152
153
  role: 'tool',
153
- toolResults: step.toolCalls.map((tc) => ({
154
- id: randomUUID(),
154
+ toolResults: step.toolCalls.map((tc, i) => ({
155
+ id: toolCallIds[i],
155
156
  name: tc.name,
156
157
  result: tc.result,
157
158
  })),
@@ -277,7 +278,16 @@ function sanitizeToolMessages(messages: AIMessage[]): AIMessage[] {
277
278
 
278
279
  function estimateTokens(msg: AIMessage): number {
279
280
  let chars = 0
280
- if (msg.content) chars += msg.content.length
281
+ if (msg.content) {
282
+ if (typeof msg.content === 'string') {
283
+ chars += msg.content.length
284
+ } else {
285
+ for (const part of msg.content) {
286
+ if (part.type === 'text') chars += part.text.length
287
+ else chars += 1000
288
+ }
289
+ }
290
+ }
281
291
  if (msg.toolCalls) chars += JSON.stringify(msg.toolCalls).length
282
292
  if (msg.toolResults) chars += JSON.stringify(msg.toolResults).length
283
293
  return Math.ceil(chars / 4)
@@ -3,6 +3,7 @@ import type {
3
3
  CoreAIAgent,
4
4
  AIAgentInput,
5
5
  AIAgentToolDef,
6
+ AIContentPart,
6
7
  AIMessage,
7
8
  AIStreamChannel,
8
9
  AIStreamEvent,
@@ -10,16 +11,17 @@ import type {
10
11
  } from './ai-agent.types.js'
11
12
  import type { AIAgentRunnerParams } from '../../services/ai-agent-runner-service.js'
12
13
  import { PikkuError } from '../../errors/error-handler.js'
13
- import {
14
- pikkuState,
15
- getSingletonServices,
16
- getCreateWireServices,
17
- } from '../../pikku-state.js'
18
- import { runPikkuFunc } from '../../function/function-runner.js'
14
+ import { pikkuState, getSingletonServices } from '../../pikku-state.js'
19
15
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
20
16
  import type { SessionService } from '../../services/user-session-service.js'
21
17
  import { randomUUID } from 'crypto'
22
-
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 { buildDynamicWorkflowInstructions, buildWorkflowTools } from './agent-dynamic-workflow.js'
23
25
  import {
24
26
  resolveMemoryServices,
25
27
  loadContextMessages,
@@ -39,7 +41,7 @@ export class ToolApprovalRequired extends PikkuError {
39
41
  public readonly toolCallId: string
40
42
  public readonly toolName: string
41
43
  public readonly args: unknown
42
- public readonly reason?: string
44
+ public reason?: string
43
45
  public readonly displayToolName?: string
44
46
  public readonly displayArgs?: unknown
45
47
  public readonly agentRunId?: string
@@ -67,6 +69,7 @@ export class ToolApprovalRequired extends PikkuError {
67
69
  export type StreamContext = {
68
70
  channel: AIStreamChannel
69
71
  options?: StreamAIAgentOptions
72
+ delegateState?: { delegated: boolean }
70
73
  }
71
74
 
72
75
  export const resolveAgent = (
@@ -81,7 +84,7 @@ export const resolveAgent = (
81
84
  if (colonIndex !== -1) {
82
85
  const namespace = agentName.substring(0, colonIndex)
83
86
  const localName = agentName.substring(colonIndex + 1)
84
- const addons = pikkuState(null, 'rpc', 'addons')
87
+ const addons = pikkuState(null, 'addons', 'packages')
85
88
  const pkgConfig = addons.get(namespace)
86
89
  if (pkgConfig) {
87
90
  const extAgent = pikkuState(pkgConfig.package, 'agent', 'agents').get(
@@ -100,31 +103,38 @@ export const resolveAgent = (
100
103
  throw new Error(`AI agent not found: ${agentName}`)
101
104
  }
102
105
 
103
- export function buildInstructions(
106
+ export async function buildInstructions(
104
107
  agentName: string,
105
108
  packageName: string | null
106
- ): string {
109
+ ): Promise<string> {
107
110
  const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName]
108
- const instructions = meta?.instructions ?? ''
109
- const baseInstructions = Array.isArray(instructions)
110
- ? instructions.join('\n')
111
- : instructions
112
-
113
- return meta?.agents?.length
114
- ? baseInstructions +
115
- '\n\nWhen calling a sub-agent, provide a short session name that describes the task. ' +
116
- 'Use the same session name to continue a previous conversation with that agent. ' +
117
- 'Use a new session name for a new independent task.'
118
- : baseInstructions
111
+ const rawInstructions = meta?.instructions ?? ''
112
+ let instructions = Array.isArray(rawInstructions)
113
+ ? rawInstructions.join('\n')
114
+ : rawInstructions
115
+
116
+ if (meta?.agents?.length) {
117
+ instructions +=
118
+ '\n\nWhen calling a sub-agent, provide a short session name that describes the task. ' +
119
+ 'Use the same session name to continue a previous conversation with that agent. ' +
120
+ 'Use a new session name for a new independent task. ' +
121
+ 'When a request involves multiple actions for the same domain, combine them into a single sub-agent call rather than making separate calls.'
122
+ }
123
+
124
+ if (meta?.dynamicWorkflows && meta.tools?.length) {
125
+ instructions += buildDynamicWorkflowInstructions(meta.tools, meta.dynamicWorkflows)
126
+ }
127
+
128
+ return instructions
119
129
  }
120
130
 
121
131
  export type ScopedChannel = AIStreamChannel & {
122
- approval: {
132
+ approvals: Array<{
123
133
  toolCallId: string
124
134
  toolName: string
125
135
  args: unknown
126
136
  runId: string
127
- } | null
137
+ }>
128
138
  }
129
139
 
130
140
  export function createScopedChannel(
@@ -132,7 +142,7 @@ export function createScopedChannel(
132
142
  agentName: string,
133
143
  session: string
134
144
  ): ScopedChannel {
135
- let capturedApproval: ScopedChannel['approval'] = null
145
+ const capturedApprovals: ScopedChannel['approvals'] = []
136
146
 
137
147
  return {
138
148
  channelId: `${parent.channelId}:${agentName}:${session}`,
@@ -140,19 +150,20 @@ export function createScopedChannel(
140
150
  get state() {
141
151
  return parent.state
142
152
  },
143
- get approval() {
144
- return capturedApproval
153
+ get approvals() {
154
+ return capturedApprovals
145
155
  },
146
156
  close: () => {},
157
+ sendBinary: (data) => parent.sendBinary(data),
147
158
  send: (event: AIStreamEvent) => {
148
159
  if (event.type === 'done') return
149
160
  if (event.type === 'approval-request') {
150
- capturedApproval = {
161
+ capturedApprovals.push({
151
162
  toolCallId: event.toolCallId,
152
163
  toolName: event.toolName,
153
164
  args: event.args,
154
165
  runId: (event as any).runId,
155
- }
166
+ })
156
167
  return
157
168
  }
158
169
  if (
@@ -162,7 +173,8 @@ export function createScopedChannel(
162
173
  event.type === 'tool-call' ||
163
174
  event.type === 'tool-result' ||
164
175
  event.type === 'usage' ||
165
- event.type === 'error'
176
+ event.type === 'error' ||
177
+ event.type === 'workflow-created'
166
178
  ) {
167
179
  parent.send({ ...event, agent: agentName, session } as AIStreamEvent)
168
180
  } else {
@@ -172,15 +184,16 @@ export function createScopedChannel(
172
184
  }
173
185
  }
174
186
 
175
- export function buildToolDefs(
187
+ export async function buildToolDefs(
176
188
  params: RunAIAgentParams,
177
189
  agentSessionMap: Map<string, string>,
178
190
  resourceId: string,
179
191
  agentName: string,
180
192
  packageName: string | null,
181
193
  streamContext?: StreamContext,
182
- aiMiddlewares?: PikkuAIMiddlewareHooks[]
183
- ): { tools: AIAgentToolDef[]; missingRpcs: string[] } {
194
+ aiMiddlewares?: PikkuAIMiddlewareHooks[],
195
+ agentMode?: 'delegate' | 'supervise'
196
+ ): Promise<{ tools: AIAgentToolDef[]; missingRpcs: string[] }> {
184
197
  const singletonServices = getSingletonServices()
185
198
  const tools: AIAgentToolDef[] = []
186
199
  const missingRpcs: string[] = []
@@ -194,18 +207,38 @@ export function buildToolDefs(
194
207
  const metaAgents = meta.agents
195
208
 
196
209
  if (metaTools?.length) {
197
- const functionMeta = pikkuState(null, 'function', 'meta')
198
- const schemas = pikkuState(null, 'misc', 'schemas')
199
-
200
210
  for (const toolName of metaTools) {
201
- const rpcMeta = pikkuState(null, 'rpc', 'meta')
202
- const pikkuFuncId = rpcMeta[toolName]
203
- if (!pikkuFuncId) {
211
+ let fnMeta: any
212
+ let resolvedPkg: string | null = null
213
+ let schemas: Map<string, any>
214
+
215
+ const resolved = toolName.includes(':')
216
+ ? resolveNamespace(toolName)
217
+ : null
218
+
219
+ let pikkuFuncId: string | undefined
220
+
221
+ if (resolved) {
222
+ resolvedPkg = resolved.package
223
+ pikkuFuncId = resolved.function
224
+ fnMeta = pikkuState(resolvedPkg, 'function', 'meta')[pikkuFuncId]
225
+ schemas = pikkuState(resolvedPkg, 'misc', 'schemas')
226
+ } else {
227
+ const rpcMeta = pikkuState(null, 'rpc', 'meta')
228
+ pikkuFuncId = rpcMeta[toolName]
229
+ if (!pikkuFuncId) {
230
+ missingRpcs.push(toolName)
231
+ continue
232
+ }
233
+ fnMeta = pikkuState(null, 'function', 'meta')[pikkuFuncId]
234
+ schemas = pikkuState(null, 'misc', 'schemas')
235
+ }
236
+
237
+ if (!fnMeta) {
204
238
  missingRpcs.push(toolName)
205
239
  continue
206
240
  }
207
241
 
208
- const fnMeta = functionMeta[pikkuFuncId]
209
242
  const inputSchemaName = fnMeta?.inputSchemaName
210
243
  let inputSchema = inputSchemaName
211
244
  ? schemas.get(inputSchemaName)
@@ -221,24 +254,52 @@ export function buildToolDefs(
221
254
 
222
255
  const needsApproval =
223
256
  approvalPolicy === 'all' ||
224
- (approvalPolicy === 'explicit' && fnMeta?.requiresApproval)
257
+ (approvalPolicy === 'explicit' && fnMeta?.approvalRequired)
258
+
259
+ // Build approvalDescriptionFn if the function has an approvalDescription configured
260
+ let approvalDescriptionFn:
261
+ | ((input: unknown) => Promise<string>)
262
+ | undefined
263
+ if (needsApproval && pikkuFuncId) {
264
+ const funcConfig = pikkuState(resolvedPkg, 'function', 'functions').get(
265
+ pikkuFuncId
266
+ )
267
+ if (funcConfig?.approvalDescription) {
268
+ const descFn = funcConfig.approvalDescription
269
+ const capturedPkg = resolvedPkg
270
+ approvalDescriptionFn = async (input: unknown) => {
271
+ let services = singletonServices
272
+ if (capturedPkg) {
273
+ const pkgServices = pikkuState(
274
+ capturedPkg,
275
+ 'package',
276
+ 'singletonServices'
277
+ )
278
+ if (pkgServices) {
279
+ services = pkgServices
280
+ }
281
+ }
282
+ return descFn(services, input)
283
+ }
284
+ }
285
+ }
225
286
 
226
287
  tools.push({
227
- name: pikkuFuncId,
288
+ name: toolName.replaceAll(':', '__'),
228
289
  description: fnMeta?.description || fnMeta?.title || toolName,
229
290
  inputSchema,
230
291
  needsApproval: needsApproval || undefined,
292
+ approvalDescriptionFn,
231
293
  execute: async (toolInput: unknown) => {
232
294
  const wire: PikkuWire = params.sessionService
233
295
  ? { ...createMiddlewareSessionWireProps(params.sessionService) }
234
296
  : {}
235
- return runPikkuFunc('agent', `tool:${pikkuFuncId}`, pikkuFuncId, {
297
+ const rpcService = new ContextAwareRPCService(
236
298
  singletonServices,
237
- createWireServices: getCreateWireServices(),
238
- data: () => toolInput,
239
299
  wire,
240
- sessionService: params.sessionService,
241
- })
300
+ { sessionService: params.sessionService }
301
+ )
302
+ return rpcService.rpc(toolName, toolInput)
242
303
  },
243
304
  })
244
305
  }
@@ -278,12 +339,15 @@ export function buildToolDefs(
278
339
  const sessionKey = `${subAgentName}::${session}`
279
340
  let threadId = agentSessionMap.get(sessionKey)
280
341
  if (!threadId) {
281
- threadId = `${subAgentName}-${session}-${Date.now()}`
342
+ threadId = randomUUID()
282
343
  agentSessionMap.set(sessionKey, threadId)
283
344
  }
284
345
 
285
346
  if (streamContext) {
286
- const { streamAIAgent } = await import('./ai-agent-stream.js')
347
+ const isDelegate = agentMode !== 'supervise'
348
+ if (isDelegate && streamContext.delegateState) {
349
+ streamContext.delegateState.delegated = true
350
+ }
287
351
  const { channel } = streamContext
288
352
  channel.send({
289
353
  type: 'agent-call',
@@ -296,34 +360,48 @@ export function buildToolDefs(
296
360
  subAgentName,
297
361
  session
298
362
  )
299
- await streamAIAgent(
363
+ // In supervise mode, suppress sub-agent text from reaching the client.
364
+ // Approvals still flow through normally.
365
+ const effectiveChannel = isDelegate
366
+ ? subChannel
367
+ : {
368
+ ...subChannel,
369
+ send: (event: AIStreamEvent) => {
370
+ if (
371
+ event.type === 'text-delta' ||
372
+ event.type === 'reasoning-delta'
373
+ )
374
+ return
375
+ subChannel.send(event)
376
+ },
377
+ }
378
+ const resultText = await streamAIAgent(
300
379
  subAgentName,
301
380
  { message, threadId, resourceId },
302
- subChannel,
381
+ effectiveChannel,
303
382
  params,
304
383
  agentSessionMap,
305
384
  streamContext.options
306
385
  )
307
- if (subChannel.approval) {
386
+ if (subChannel.approvals.length > 0) {
308
387
  return {
309
388
  __approvalRequired: true,
310
389
  toolName: subAgentName,
311
390
  args: toolInput,
312
- displayToolName: subChannel.approval.toolName,
313
- displayArgs: subChannel.approval.args,
314
- agentRunId: subChannel.approval.runId,
391
+ agentRunId: subChannel.approvals[0].runId,
392
+ subApprovals: subChannel.approvals,
315
393
  }
316
394
  }
317
395
  channel.send({
318
396
  type: 'agent-result',
319
397
  agentName: subAgentName,
320
398
  session,
321
- result: null,
399
+ result: resultText,
322
400
  })
323
- return null
401
+ return resultText
324
402
  }
325
403
 
326
- const { runAIAgent } = await import('./ai-agent-runner.js')
404
+ // No stream context: sub-agent runs non-streaming
327
405
  const result = await runAIAgent(
328
406
  subAgentName,
329
407
  { message, threadId, resourceId },
@@ -336,6 +414,18 @@ export function buildToolDefs(
336
414
  }
337
415
  }
338
416
 
417
+ if (meta.dynamicWorkflows) {
418
+ const workflowTools = buildWorkflowTools(
419
+ agentName,
420
+ packageName,
421
+ meta.tools ?? [],
422
+ meta.dynamicWorkflows,
423
+ streamContext,
424
+ params.sessionService
425
+ )
426
+ tools.push(...workflowTools)
427
+ }
428
+
339
429
  const hasToolHooks = aiMiddlewares?.some(
340
430
  (mw) => mw.beforeToolCall || mw.afterToolCall
341
431
  )
@@ -410,6 +500,19 @@ export async function prepareAgentRun(
410
500
  throw new Error('AIAgentRunnerService not available in singletonServices')
411
501
  }
412
502
 
503
+ if (agent.dynamicWorkflows && singletonServices.workflowService) {
504
+ const persisted =
505
+ await singletonServices.workflowService.getAIGeneratedWorkflows(
506
+ resolvedName
507
+ )
508
+ const allMeta = pikkuState(null, 'workflows', 'meta')
509
+ for (const wf of persisted) {
510
+ if (!allMeta[wf.workflowName]) {
511
+ allMeta[wf.workflowName] = wf.graph
512
+ }
513
+ }
514
+ }
515
+
413
516
  const { storage } = resolveMemoryServices(agent, singletonServices)
414
517
  const memoryConfig = agent.memory
415
518
  const threadId = input.threadId
@@ -448,10 +551,26 @@ export async function prepareAgentRun(
448
551
  workingMemoryJsonSchema
449
552
  )
450
553
 
554
+ const userContent: AIMessage['content'] = input.attachments?.length
555
+ ? [
556
+ { type: 'text' as const, text: input.message },
557
+ ...input.attachments.map(
558
+ (a) =>
559
+ ({
560
+ type: a.type,
561
+ data: a.data,
562
+ url: a.url,
563
+ mediaType: a.mediaType,
564
+ ...(a.filename ? { filename: a.filename } : {}),
565
+ }) as AIContentPart
566
+ ),
567
+ ]
568
+ : input.message
569
+
451
570
  const userMessage: AIMessage = {
452
571
  id: randomUUID(),
453
572
  role: 'user',
454
- content: input.message,
573
+ content: userContent,
455
574
  createdAt: new Date(),
456
575
  }
457
576
 
@@ -460,19 +579,32 @@ export async function prepareAgentRun(
460
579
 
461
580
  const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
462
581
 
463
- const { tools, missingRpcs } = buildToolDefs(
582
+ const { tools, missingRpcs } = await buildToolDefs(
464
583
  params,
465
584
  agentSessionMap,
466
585
  input.resourceId,
467
586
  resolvedName,
468
587
  packageName,
469
588
  streamContext,
470
- aiMiddlewares
589
+ aiMiddlewares,
590
+ agent.agentMode
471
591
  )
472
592
 
473
- const instructions = buildInstructions(resolvedName, packageName)
593
+ const instructions = await buildInstructions(resolvedName, packageName)
474
594
 
475
595
  const resolved = resolveModelConfig(resolvedName, agent)
596
+
597
+ // Per-request overrides
598
+ if (input.model) {
599
+ resolved.model = resolveModelConfig(resolvedName, {
600
+ ...agent,
601
+ model: input.model,
602
+ }).model
603
+ }
604
+ if (input.temperature !== undefined) {
605
+ resolved.temperature = input.temperature
606
+ }
607
+
476
608
  const maxSteps = resolved.maxSteps ?? 10
477
609
 
478
610
  const runnerParams: AIAgentRunnerParams = {
@@ -84,7 +84,9 @@ describe('runAIAgent', () => {
84
84
  expectedError
85
85
  )
86
86
 
87
- assert.deepEqual(updates, [{ status: 'failed' }])
87
+ assert.deepEqual(updates, [
88
+ { status: 'failed', errorMessage: 'runner failed' },
89
+ ])
88
90
  })
89
91
 
90
92
  test('loops through multiple steps and accumulates usage', async () => {