@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
@@ -3,7 +3,6 @@ import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../channel
3
3
  import { randomUUID } from 'crypto';
4
4
  import { parseWorkingMemory, deepMergeWorkingMemory, resolveMemoryServices, loadContextMessages, trimMessages, } from './ai-agent-memory.js';
5
5
  import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, } from './ai-agent-prepare.js';
6
- import { createAssistantUIChannel, parseAssistantUIInput, } from './ai-agent-assistant-ui.js';
7
6
  import { resolveModelConfig } from './ai-agent-model-config.js';
8
7
  function createPersistingChannel(parent, storage, threadId) {
9
8
  let fullText = '';
@@ -57,6 +56,7 @@ function createPersistingChannel(parent, storage, threadId) {
57
56
  },
58
57
  flush: flushStep,
59
58
  close: () => parent.close(),
59
+ sendBinary: (data) => parent.sendBinary(data),
60
60
  send: (event) => {
61
61
  if (storage) {
62
62
  switch (event.type) {
@@ -180,31 +180,52 @@ async function runStreamStepLoop(params) {
180
180
  }
181
181
  if (stepResult.toolCalls.length === 0)
182
182
  break;
183
- const approvalNeeded = checkForApproval(stepResult, runnerParams.tools, runId);
184
- if (approvalNeeded) {
185
- return { outcome: 'approval', approval: approvalNeeded };
183
+ const approvalsNeeded = checkForApprovals(stepResult, runnerParams.tools, runId);
184
+ if (approvalsNeeded.length > 0) {
185
+ // For each approval, call approvalDescriptionFn if available
186
+ for (const approval of approvalsNeeded) {
187
+ const toolDef = runnerParams.tools.find((t) => t.name === approval.toolName);
188
+ if (toolDef?.approvalDescriptionFn && !approval.reason) {
189
+ try {
190
+ approval.reason = await toolDef.approvalDescriptionFn(approval.args);
191
+ }
192
+ catch {
193
+ // If description generation fails, continue without it
194
+ }
195
+ }
196
+ }
197
+ return { outcome: 'approval', approvals: approvalsNeeded };
186
198
  }
187
199
  appendStepMessages(runnerParams, stepResult);
188
200
  }
189
201
  return { outcome: 'done' };
190
202
  }
191
- function checkForApproval(stepResult, tools, runId) {
203
+ export function checkForApprovals(stepResult, tools, runId) {
204
+ const approvals = [];
192
205
  for (const tc of stepResult.toolCalls) {
193
206
  const toolDef = tools.find((t) => t.name === tc.toolName);
194
207
  if (toolDef?.needsApproval) {
195
- return new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args);
208
+ approvals.push(new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args));
209
+ continue;
196
210
  }
197
211
  const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
198
212
  if (tr?.result &&
199
213
  typeof tr.result === 'object' &&
200
214
  '__approvalRequired' in tr.result) {
201
215
  const r = tr.result;
202
- return new ToolApprovalRequired(tc.toolCallId, r.toolName, r.args, r.reason, r.displayToolName, r.displayArgs, r.agentRunId);
216
+ if (r.subApprovals?.length) {
217
+ for (const sub of r.subApprovals) {
218
+ approvals.push(new ToolApprovalRequired(sub.toolCallId, r.toolName, r.args, undefined, sub.toolName, sub.args, r.agentRunId));
219
+ }
220
+ }
221
+ else {
222
+ approvals.push(new ToolApprovalRequired(tc.toolCallId, r.toolName, r.args, r.reason, r.displayToolName, r.displayArgs, r.agentRunId));
223
+ }
203
224
  }
204
225
  }
205
- return null;
226
+ return approvals;
206
227
  }
207
- function appendStepMessages(runnerParams, stepResult) {
228
+ export function appendStepMessages(runnerParams, stepResult) {
208
229
  const assistantMsg = {
209
230
  id: randomUUID(),
210
231
  role: 'assistant',
@@ -233,10 +254,10 @@ function appendStepMessages(runnerParams, stepResult) {
233
254
  runnerParams.messages.push(toolMsg);
234
255
  }
235
256
  }
236
- function handleApproval(err, runId, channel, aiRunState, persistingChannel) {
257
+ function handleApprovals(approvals, runId, channel, aiRunState, persistingChannel) {
237
258
  return (async () => {
238
259
  await persistingChannel.flush();
239
- const pendingApproval = err.agentRunId
260
+ const pendingApprovals = approvals.map((err) => err.agentRunId
240
261
  ? {
241
262
  type: 'agent-call',
242
263
  toolCallId: err.toolCallId,
@@ -250,40 +271,32 @@ function handleApproval(err, runId, channel, aiRunState, persistingChannel) {
250
271
  toolCallId: err.toolCallId,
251
272
  toolName: err.toolName,
252
273
  args: err.args,
253
- };
274
+ });
254
275
  await aiRunState.updateRun(runId, {
255
276
  status: 'suspended',
256
277
  suspendReason: 'approval',
257
- pendingApprovals: [pendingApproval],
278
+ pendingApprovals,
258
279
  });
259
- const approvalEvent = {
260
- type: 'approval-request',
261
- toolCallId: err.toolCallId,
262
- toolName: err.displayToolName ?? err.toolName,
263
- args: err.displayArgs ?? err.args,
264
- reason: err.reason,
265
- runId,
266
- };
267
- channel.send(approvalEvent);
280
+ for (const err of approvals) {
281
+ const approvalEvent = {
282
+ type: 'approval-request',
283
+ toolCallId: err.toolCallId,
284
+ toolName: err.displayToolName ?? err.toolName,
285
+ args: err.displayArgs ?? err.args,
286
+ reason: err.reason,
287
+ runId,
288
+ };
289
+ channel.send(approvalEvent);
290
+ }
268
291
  channel.send({ type: 'done' });
269
292
  channel.close();
270
293
  })();
271
294
  }
272
295
  export async function streamAIAgent(agentName, input, channel, params, agentSessionMap, options) {
273
296
  const sessionMap = agentSessionMap ?? new Map();
274
- const { agent: resolvedAgentForProtocol } = resolveAgent(agentName);
275
- const inputRecord = input;
276
- const useUIMessageStream = resolvedAgentForProtocol.protocol === 'ui-message-stream' ||
277
- Array.isArray(inputRecord.messages);
278
- let normalizedInput;
279
- if (useUIMessageStream) {
280
- normalizedInput = parseAssistantUIInput(inputRecord);
281
- channel = createAssistantUIChannel(channel);
282
- }
283
- else {
284
- normalizedInput = input;
285
- }
297
+ const normalizedInput = input;
286
298
  const streamContext = { channel, options };
299
+ // delegateState is attached after prepareAgentRun resolves the agent config
287
300
  const { agent, packageName, resolvedName, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, normalizedInput, params, sessionMap, streamContext);
288
301
  const singletonServices = getSingletonServices();
289
302
  const { aiRunState } = singletonServices;
@@ -304,7 +317,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
304
317
  });
305
318
  channel.send({ type: 'suspended', reason: 'rpc-missing', missingRpcs });
306
319
  channel.send({ type: 'done' });
307
- return;
320
+ return '';
308
321
  }
309
322
  const aiMiddlewares = agent.aiMiddleware ?? [];
310
323
  let modifiedMessages = runnerParams.messages;
@@ -345,8 +358,15 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
345
358
  allEvents,
346
359
  state,
347
360
  });
348
- if (result != null)
361
+ if (result == null)
362
+ return;
363
+ if (Array.isArray(result)) {
364
+ for (const r of result)
365
+ await next(r);
366
+ }
367
+ else {
349
368
  await next(result);
369
+ }
350
370
  };
351
371
  });
352
372
  const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta');
@@ -361,7 +381,28 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
361
381
  const wrappedChannel = allChannelMiddleware.length > 0
362
382
  ? wrapChannelWithMiddleware({ channel }, singletonServices, allChannelMiddleware).channel
363
383
  : channel;
364
- const persistingChannel = createPersistingChannel(wrappedChannel, storage, threadId);
384
+ // In delegate mode (default), suppress parent's text from reaching the client
385
+ // AFTER a sub-agent has been called. If the parent responds directly (no delegation),
386
+ // its text goes through normally. Sub-agent text bypasses this path entirely
387
+ // (goes through subChannel → channel directly).
388
+ const isDelegateMode = agent.agentMode !== 'supervise' && meta.agents?.length;
389
+ const delegateState = { delegated: false };
390
+ if (isDelegateMode) {
391
+ streamContext.delegateState = delegateState;
392
+ }
393
+ const outputChannel = isDelegateMode
394
+ ? {
395
+ ...wrappedChannel,
396
+ send: (event) => {
397
+ if (delegateState.delegated &&
398
+ (event.type === 'text-delta' || event.type === 'reasoning-delta'))
399
+ return;
400
+ wrappedChannel.send(event);
401
+ },
402
+ delegateState,
403
+ }
404
+ : wrappedChannel;
405
+ const persistingChannel = createPersistingChannel(outputChannel, storage, threadId);
365
406
  try {
366
407
  const loopResult = await runStreamStepLoop({
367
408
  agent,
@@ -374,12 +415,13 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
374
415
  aiMiddlewares,
375
416
  });
376
417
  if (loopResult.outcome === 'approval') {
377
- await handleApproval(loopResult.approval, runId, channel, aiRunState, persistingChannel);
378
- return;
418
+ await handleApprovals(loopResult.approvals, runId, channel, aiRunState, persistingChannel);
419
+ return persistingChannel.fullText;
379
420
  }
380
421
  await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, runId);
381
422
  channel.send({ type: 'done' });
382
423
  channel.close();
424
+ return persistingChannel.fullText;
383
425
  }
384
426
  catch (err) {
385
427
  for (const mw of aiMiddlewares) {
@@ -396,13 +438,17 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
396
438
  }
397
439
  }
398
440
  }
399
- await aiRunState.updateRun(runId, { status: 'failed' });
441
+ await aiRunState.updateRun(runId, {
442
+ status: 'failed',
443
+ errorMessage: err instanceof Error ? err.message : String(err),
444
+ });
400
445
  channel.send({
401
446
  type: 'error',
402
447
  message: err instanceof Error ? err.message : String(err),
403
448
  });
404
449
  channel.send({ type: 'done' });
405
450
  channel.close();
451
+ return persistingChannel.fullText;
406
452
  }
407
453
  }
408
454
  export async function resumeAIAgent(input, channel, params, options) {
@@ -431,7 +477,7 @@ export async function resumeAIAgent(input, channel, params, options) {
431
477
  if (pending.type === 'agent-call') {
432
478
  await aiRunState.updateRun(pending.agentRunId, { status: 'failed' });
433
479
  }
434
- const denialResult = 'Tool call was not approved by the user';
480
+ const denialResult = 'The user explicitly declined this action. Inform them that it was declined and do not retry.';
435
481
  if (storage) {
436
482
  await storage.saveMessages(run.threadId, [
437
483
  {
@@ -450,6 +496,15 @@ export async function resumeAIAgent(input, channel, params, options) {
450
496
  },
451
497
  ]);
452
498
  }
499
+ // Check remaining pending approvals
500
+ const updatedRun = await aiRunState.getRun(run.runId);
501
+ const remaining = updatedRun?.pendingApprovals ?? [];
502
+ if (remaining.length > 0) {
503
+ // Still waiting for more approvals - don't continue step loop
504
+ channel.send({ type: 'done' });
505
+ channel.close();
506
+ return;
507
+ }
453
508
  await aiRunState.updateRun(run.runId, { status: 'running' });
454
509
  await continueAfterToolResult(run, agent, packageName, resolvedName, storage, memoryConfig, agentRunner, channel, params, aiRunState, options);
455
510
  return;
@@ -459,7 +514,8 @@ export async function resumeAIAgent(input, channel, params, options) {
459
514
  if (!subRun) {
460
515
  throw new Error(`Sub-agent run not found: ${pending.agentRunId}`);
461
516
  }
462
- const subPending = subRun.pendingApprovals?.[0];
517
+ const subPending = subRun.pendingApprovals?.find((p) => p.toolCallId === input.toolCallId) ??
518
+ subRun.pendingApprovals?.[0];
463
519
  if (!subPending) {
464
520
  throw new Error(`No pending approval on sub-agent run ${pending.agentRunId}`);
465
521
  }
@@ -504,18 +560,20 @@ export async function resumeAIAgent(input, channel, params, options) {
504
560
  options: { ...options, requiresToolApproval: false },
505
561
  };
506
562
  const aiMiddlewaresForResume = agent.aiMiddleware ?? [];
507
- const { tools } = buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, streamContext, aiMiddlewaresForResume);
563
+ const { tools } = await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, streamContext, aiMiddlewaresForResume);
508
564
  const matchingTool = tools.find((t) => t.name === pending.toolName);
509
565
  if (!matchingTool) {
510
566
  throw new Error(`Tool "${pending.toolName}" not found in agent definition`);
511
567
  }
512
568
  const toolArgs = typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args;
513
569
  let toolResult;
570
+ let isError = false;
514
571
  try {
515
572
  toolResult = await matchingTool.execute(toolArgs);
516
573
  }
517
574
  catch (execErr) {
518
575
  toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`;
576
+ isError = true;
519
577
  }
520
578
  const resultStr = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
521
579
  if (storage) {
@@ -539,8 +597,18 @@ export async function resumeAIAgent(input, channel, params, options) {
539
597
  toolCallId: input.toolCallId,
540
598
  toolName: pending.toolName,
541
599
  result: toolResult,
600
+ ...(isError ? { isError: true } : {}),
542
601
  });
543
602
  }
603
+ // Check remaining pending approvals after processing this one
604
+ const updatedRun = await aiRunState.getRun(run.runId);
605
+ const remaining = updatedRun?.pendingApprovals ?? [];
606
+ if (remaining.length > 0) {
607
+ // Still waiting for more approvals - don't continue step loop
608
+ channel.send({ type: 'done' });
609
+ channel.close();
610
+ return;
611
+ }
544
612
  await aiRunState.updateRun(run.runId, { status: 'running' });
545
613
  await continueAfterToolResult(run, agent, packageName, resolvedName, storage, memoryConfig, agentRunner, channel, params, aiRunState, options);
546
614
  }
@@ -560,7 +628,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
560
628
  const contextMessages = await loadContextMessages(memoryConfig, storage, { message: '', threadId: run.threadId, resourceId: run.resourceId }, workingMemoryJsonSchema);
561
629
  const allMessages = [...contextMessages, ...messages];
562
630
  const trimmedMessages = trimMessages(allMessages);
563
- const instructions = buildInstructions(resolvedName, packageName);
631
+ const instructions = await buildInstructions(resolvedName, packageName);
564
632
  const aiMiddlewares = agent.aiMiddleware ?? [];
565
633
  let modifiedMessages = trimmedMessages;
566
634
  let modifiedInstructions = instructions;
@@ -586,8 +654,15 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
586
654
  allEvents,
587
655
  state,
588
656
  });
589
- if (result != null)
657
+ if (result == null)
658
+ return;
659
+ if (Array.isArray(result)) {
660
+ for (const r of result)
661
+ await next(r);
662
+ }
663
+ else {
590
664
  await next(result);
665
+ }
591
666
  };
592
667
  });
593
668
  const allChannelMiddleware = combineChannelMiddleware('agent', `stream:${run.agentName}`, {
@@ -602,7 +677,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
602
677
  : channel;
603
678
  const persistingChannel = createPersistingChannel(wrappedChannel, storage, run.threadId);
604
679
  const streamContext = { channel, options };
605
- const resumeTools = buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, streamContext, aiMiddlewares).tools;
680
+ const resumeTools = (await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, streamContext, aiMiddlewares)).tools;
606
681
  const resolved = resolveModelConfig(resolvedName, agent);
607
682
  const maxSteps = resolved.maxSteps ?? 10;
608
683
  const runnerParams = {
@@ -629,7 +704,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
629
704
  aiMiddlewares,
630
705
  });
631
706
  if (loopResult.outcome === 'approval') {
632
- await handleApproval(loopResult.approval, run.runId, channel, aiRunState, persistingChannel);
707
+ await handleApprovals(loopResult.approvals, run.runId, channel, aiRunState, persistingChannel);
633
708
  return;
634
709
  }
635
710
  await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, run.threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, run.runId);
@@ -651,7 +726,10 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
651
726
  }
652
727
  }
653
728
  }
654
- await aiRunState.updateRun(run.runId, { status: 'failed' });
729
+ await aiRunState.updateRun(run.runId, {
730
+ status: 'failed',
731
+ errorMessage: err instanceof Error ? err.message : String(err),
732
+ });
655
733
  channel.send({
656
734
  type: 'error',
657
735
  message: err instanceof Error ? err.message : String(err),
@@ -10,6 +10,21 @@ export interface AIThread {
10
10
  createdAt: Date;
11
11
  updatedAt: Date;
12
12
  }
13
+ export type AIContentPart = {
14
+ type: 'text';
15
+ text: string;
16
+ } | {
17
+ type: 'image';
18
+ data?: string;
19
+ url?: string;
20
+ mediaType?: string;
21
+ } | {
22
+ type: 'file';
23
+ data?: string;
24
+ url?: string;
25
+ mediaType: string;
26
+ filename?: string;
27
+ };
13
28
  export interface AIToolCall {
14
29
  id: string;
15
30
  name: string;
@@ -23,7 +38,7 @@ export interface AIToolResult {
23
38
  export interface AIMessage {
24
39
  id: string;
25
40
  role: 'system' | 'user' | 'assistant' | 'tool';
26
- content?: string;
41
+ content?: string | AIContentPart[];
27
42
  toolCalls?: AIToolCall[];
28
43
  toolResults?: AIToolResult[];
29
44
  createdAt: Date;
@@ -39,10 +54,20 @@ export interface AIAgentStep {
39
54
  result: string;
40
55
  }[];
41
56
  }
57
+ export interface AIAgentInputAttachment {
58
+ type: 'image' | 'file';
59
+ data?: string;
60
+ url?: string;
61
+ mediaType?: string;
62
+ filename?: string;
63
+ }
42
64
  export interface AIAgentInput {
43
65
  message: string;
44
66
  threadId: string;
45
67
  resourceId: string;
68
+ attachments?: AIAgentInputAttachment[];
69
+ model?: string;
70
+ temperature?: number;
46
71
  }
47
72
  export interface AIAgentOutput {
48
73
  runId: string;
@@ -54,6 +79,14 @@ export interface AIAgentOutput {
54
79
  inputTokens: number;
55
80
  outputTokens: number;
56
81
  };
82
+ status?: 'completed' | 'suspended';
83
+ pendingApprovals?: Array<{
84
+ toolCallId: string;
85
+ toolName: string;
86
+ args: unknown;
87
+ reason?: string;
88
+ runId: string;
89
+ }>;
57
90
  }
58
91
  export interface AIAgentToolDef {
59
92
  name: string;
@@ -61,6 +94,7 @@ export interface AIAgentToolDef {
61
94
  inputSchema: Record<string, unknown>;
62
95
  execute: (input: unknown) => Promise<unknown>;
63
96
  needsApproval?: boolean;
97
+ approvalDescriptionFn?: (input: unknown) => Promise<string>;
64
98
  }
65
99
  export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> = Record<string, unknown>, Services = any> {
66
100
  modifyInput?: (services: Services, ctx: {
@@ -77,7 +111,7 @@ export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> =
77
111
  event: AIStreamEvent;
78
112
  allEvents: readonly AIStreamEvent[];
79
113
  state: State;
80
- }) => Promise<AIStreamEvent | null> | AIStreamEvent | null;
114
+ }) => Promise<AIStreamEvent | AIStreamEvent[] | null> | AIStreamEvent | AIStreamEvent[] | null;
81
115
  modifyOutput?: (services: Services, ctx: {
82
116
  text: string;
83
117
  messages: AIMessage[];
@@ -154,13 +188,14 @@ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMi
154
188
  temperature?: number;
155
189
  tools?: unknown[];
156
190
  agents?: unknown[];
191
+ agentMode?: 'delegate' | 'supervise';
157
192
  memory?: AIAgentMemoryConfig;
158
193
  maxSteps?: number;
159
194
  toolChoice?: 'auto' | 'required' | 'none';
195
+ dynamicWorkflows?: 'read' | 'always' | 'ask';
160
196
  input?: unknown;
161
197
  output?: unknown;
162
198
  tags?: string[];
163
- protocol?: 'ui-message-stream';
164
199
  prepareStep?: (ctx: {
165
200
  stepNumber: number;
166
201
  messages: AIMessage[];
@@ -235,6 +270,22 @@ export type AIStreamEvent = {
235
270
  message: string;
236
271
  agent?: string;
237
272
  session?: string;
273
+ } | {
274
+ type: 'audio-delta';
275
+ data: string;
276
+ format: string;
277
+ agent?: string;
278
+ session?: string;
279
+ } | {
280
+ type: 'audio-done';
281
+ agent?: string;
282
+ session?: string;
283
+ } | {
284
+ type: 'workflow-created';
285
+ workflowName: string;
286
+ graph: any;
287
+ agent?: string;
288
+ session?: string;
238
289
  } | {
239
290
  type: 'suspended';
240
291
  reason: 'rpc-missing';
@@ -263,6 +314,7 @@ export interface AgentRunState {
263
314
  threadId: string;
264
315
  resourceId: string;
265
316
  status: 'running' | 'suspended' | 'completed' | 'failed';
317
+ errorMessage?: string;
266
318
  suspendReason?: 'approval' | 'rpc-missing';
267
319
  missingRpcs?: string[];
268
320
  pendingApprovals?: PendingApproval[];
@@ -280,6 +332,7 @@ export interface AgentRunRow {
280
332
  threadId: string;
281
333
  resourceId: string;
282
334
  status: string;
335
+ errorMessage?: string;
283
336
  suspendReason?: string;
284
337
  missingRpcs?: string[];
285
338
  usageInputTokens: number;
@@ -310,4 +363,5 @@ export type AIAgentMeta = Record<string, Omit<CoreAIAgent, 'input' | 'output' |
310
363
  channelMiddleware?: MiddlewareMetadata[];
311
364
  aiMiddleware?: MiddlewareMetadata[];
312
365
  permissions?: PermissionMetadata[];
366
+ dynamicWorkflows?: 'read' | 'always' | 'ask';
313
367
  }>;
@@ -1,7 +1,6 @@
1
1
  export { agent, agentStream, agentResume, agentApprove, } from './ai-agent-helpers.js';
2
- export { runAIAgent } from './ai-agent-runner.js';
2
+ export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
3
3
  export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
4
- export { createAssistantUIChannel, parseAssistantUIInput, } from './ai-agent-assistant-ui.js';
5
4
  export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, } from './ai-agent-prepare.js';
6
5
  export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
7
- export type { AIAgentInput, AIAgentMeta, AIAgentMemoryConfig, AIAgentStep, AgentRunRow, AgentRunService, AgentRunState, AIMessage, AIStreamChannel, AIStreamEvent, AIThread, CoreAIAgent, PendingApproval, PikkuAIMiddlewareHooks, } from './ai-agent.types.js';
6
+ export type { AIAgentInput, AIAgentInputAttachment, AIAgentMeta, AIAgentMemoryConfig, AIAgentStep, AIContentPart, AgentRunRow, AgentRunService, AgentRunState, AIMessage, AIStreamChannel, AIStreamEvent, AIThread, CoreAIAgent, PendingApproval, PikkuAIMiddlewareHooks, } from './ai-agent.types.js';
@@ -1,6 +1,5 @@
1
1
  export { agent, agentStream, agentResume, agentApprove, } from './ai-agent-helpers.js';
2
- export { runAIAgent } from './ai-agent-runner.js';
2
+ export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
3
3
  export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
4
- export { createAssistantUIChannel, parseAssistantUIInput, } from './ai-agent-assistant-ui.js';
5
4
  export { ToolApprovalRequired, } from './ai-agent-prepare.js';
6
5
  export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
@@ -1,4 +1,7 @@
1
1
  import type { CoreServices, CoreUserSession } from '../../types/core.types.js';
2
- import type { CoreChannel, PikkuChannelHandler } from './channel.types.js';
2
+ import type { BinaryData, CoreChannel, PikkuChannelHandler } from './channel.types.js';
3
3
  import type { SessionService } from '../../services/user-session-service.js';
4
- export declare const processMessageHandlers: (services: CoreServices, channelConfig: CoreChannel<any, any>, channelHandler: PikkuChannelHandler, userSession?: SessionService<CoreUserSession>) => (rawData: any) => Promise<unknown>;
4
+ export declare const processMessageHandlers: (services: CoreServices, channelConfig: CoreChannel<any, any>, channelHandler: PikkuChannelHandler, userSession?: SessionService<CoreUserSession>) => {
5
+ onMessage: (rawData: unknown) => Promise<unknown>;
6
+ onBinaryMessage: ((data: BinaryData) => Promise<BinaryData | void>) | undefined;
7
+ };
@@ -48,7 +48,9 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
48
48
  channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
49
49
  return;
50
50
  }
51
- const { pikkuFuncId, middleware: routeInheritedMiddleware, permissions: inheritedPermissions, } = getRouteMeta(channelConfig.name, routingProperty, routerValue);
51
+ const routeMeta = getRouteMeta(channelConfig.name, routingProperty, routerValue);
52
+ const { packageName, middleware: routeInheritedMiddleware, permissions: inheritedPermissions, } = routeMeta;
53
+ const pikkuFuncId = routeMeta.pikkuFuncId;
52
54
  // Get wire middleware: channel-level middleware + message-specific middleware
53
55
  const channelWireMiddleware = channelConfig.middleware || [];
54
56
  // Check if onMessage is a wrapper object vs direct function config:
@@ -83,15 +85,17 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
83
85
  wireChannelMiddleware: wireChannelMiddleware,
84
86
  inheritedPermissions,
85
87
  wirePermissions,
88
+ coerceDataFromSchema: true,
86
89
  tags: channelConfig.tags,
87
90
  sessionService: userSession,
88
91
  wire: {
89
92
  channel: channelHandler.getChannel(),
90
93
  ...(userSession && createMiddlewareSessionWireProps(userSession)),
91
94
  },
95
+ packageName,
92
96
  });
93
97
  };
94
- return async (rawData) => {
98
+ const onMessage = async (rawData) => {
95
99
  let result;
96
100
  let processed = false;
97
101
  // Route-specific handling
@@ -134,4 +138,14 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
134
138
  }
135
139
  return result;
136
140
  };
141
+ const onBinaryMessage = channelConfig.onBinaryMessage
142
+ ? async (data) => {
143
+ if (!validateAuth(requiresSession, userSession?.get(), channelConfig)) {
144
+ logger.error(`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for binary message`);
145
+ return;
146
+ }
147
+ return channelConfig.onBinaryMessage(services, data, channelHandler.getChannel());
148
+ }
149
+ : undefined;
150
+ return { onMessage, onBinaryMessage };
137
151
  };
@@ -68,6 +68,7 @@ export function wrapChannelWithMiddleware(wire, services, middlewares) {
68
68
  channel: {
69
69
  ...channel,
70
70
  send: ((data) => dispatch(0, data)),
71
+ sendBinary: channel.sendBinary,
71
72
  },
72
73
  };
73
74
  }
@@ -1,6 +1,6 @@
1
- import type { PikkuError } from '../../errors/error-handler.js';
2
1
  import type { HTTPFunctionMetaInputTypes, PikkuHTTPRequest, PikkuHTTPResponse } from '../http/http.types.js';
3
2
  import type { CorePikkuMiddleware, MiddlewareMetadata, PermissionMetadata } from '../../types/core.types.js';
3
+ export type BinaryData = ArrayBuffer | Uint8Array;
4
4
  export type CorePikkuChannelMiddleware<Services = any, Event = unknown> = (services: Services, event: Event, next: (event: Event | Event[] | null) => Promise<void> | void) => Promise<void> | void;
5
5
  export type CorePikkuChannelMiddlewareFactory<In = any, Services = any, Event = unknown> = (input: In) => CorePikkuChannelMiddleware<Services, Event>;
6
6
  import type { CorePermissionGroup, CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuFunctionSessionless, CorePikkuPermission } from '../../function/functions.types.js';
@@ -18,6 +18,7 @@ export type RunChannelParams<ChannelData> = {
18
18
  };
19
19
  export interface ChannelMessageMeta {
20
20
  pikkuFuncId: string;
21
+ packageName?: string;
21
22
  summary?: string;
22
23
  description?: string;
23
24
  errors?: string[];
@@ -36,6 +37,7 @@ export interface ChannelMeta {
36
37
  disconnect: ChannelMessageMeta | null;
37
38
  message: ChannelMessageMeta | null;
38
39
  messageWirings: Record<string, Record<string, ChannelMessageMeta>>;
40
+ binary?: boolean | null;
39
41
  summary?: string;
40
42
  description?: string;
41
43
  errors?: string[];
@@ -67,23 +69,21 @@ export type CoreChannel<ChannelData, Channel extends string, ChannelConnect = Co
67
69
  channelMiddleware?: Array<CorePikkuChannelMiddleware | CorePikkuChannelMiddlewareFactory>;
68
70
  permissions?: CorePermissionGroup<PikkuPermission>;
69
71
  auth?: boolean;
70
- docs?: Partial<{
71
- description: string;
72
- response: string;
73
- errors: Array<typeof PikkuError>;
74
- tags: string[];
75
- }>;
72
+ binary?: boolean | null;
73
+ onBinaryMessage?: (services: any, data: BinaryData, channel: PikkuChannel<ChannelData, any>) => Promise<BinaryData | void> | BinaryData | void;
76
74
  tags?: string[];
77
75
  };
78
76
  export interface PikkuChannel<OpeningData, out Out> {
79
77
  channelId: string;
80
78
  openingData: OpeningData;
81
79
  send(data: Out, isBinary?: boolean): Promise<void> | void;
80
+ sendBinary(data: BinaryData): Promise<void> | void;
82
81
  close(): Promise<void> | void;
83
82
  state: 'initial' | 'open' | 'closed';
84
83
  }
85
84
  export interface PikkuChannelHandler<OpeningData = unknown, Out = unknown> {
86
85
  send(message: Out, isBinary?: boolean): Promise<void> | void;
86
+ sendBinary(data: BinaryData): Promise<void> | void;
87
87
  getChannel(): PikkuChannel<OpeningData, Out>;
88
88
  }
89
89
  export type PikkuChannelHandlerFactory<OpeningData = unknown, Out = unknown> = (channelId: string, channelName: string, openingData: OpeningData) => PikkuChannelHandler<OpeningData, Out>;
@@ -6,5 +6,5 @@ export type { EventHubService } from './eventhub-service.js';
6
6
  export { ChannelStore } from './channel-store.js';
7
7
  export type { Channel } from './channel-store.js';
8
8
  export { EventHubStore } from './eventhub-store.js';
9
- export type { ChannelsMeta, CoreChannel, CorePikkuChannelMiddleware, CorePikkuChannelMiddlewareFactory, ChannelMessageMeta, ChannelMeta, PikkuChannel, PikkuChannelHandlerFactory, } from './channel.types.js';
9
+ export type { BinaryData, ChannelsMeta, CoreChannel, CorePikkuChannelMiddleware, CorePikkuChannelMiddlewareFactory, ChannelMessageMeta, ChannelMeta, PikkuChannel, PikkuChannelHandlerFactory, } from './channel.types.js';
10
10
  export { defineChannelRoutes } from './define-channel-routes.js';