@pikku/core 0.12.2 → 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 (131) hide show
  1. package/CHANGELOG.md +18 -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/pikku-state.js +3 -1
  12. package/dist/schema.js +1 -1
  13. package/dist/services/in-memory-workflow-service.d.ts +8 -2
  14. package/dist/services/in-memory-workflow-service.js +28 -1
  15. package/dist/services/workflow-service.d.ts +8 -2
  16. package/dist/testing/index.d.ts +2 -0
  17. package/dist/testing/index.js +1 -0
  18. package/dist/testing/service-tests.d.ts +31 -0
  19. package/dist/testing/service-tests.js +598 -0
  20. package/dist/types/core.types.d.ts +9 -1
  21. package/dist/types/state.types.d.ts +4 -2
  22. package/dist/utils/hash.d.ts +2 -0
  23. package/dist/utils/hash.js +23 -0
  24. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
  25. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
  26. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
  27. package/dist/wirings/ai-agent/ai-agent-memory.js +6 -5
  28. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +127 -47
  30. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
  31. package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
  32. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
  33. package/dist/wirings/ai-agent/ai-agent-stream.js +109 -46
  34. package/dist/wirings/ai-agent/ai-agent.types.d.ts +22 -1
  35. package/dist/wirings/ai-agent/index.d.ts +1 -2
  36. package/dist/wirings/ai-agent/index.js +1 -2
  37. package/dist/wirings/channel/channel-handler.js +5 -1
  38. package/dist/wirings/channel/channel.types.d.ts +1 -7
  39. package/dist/wirings/channel/local/local-channel-handler.d.ts +1 -1
  40. package/dist/wirings/channel/local/local-channel-handler.js +5 -3
  41. package/dist/wirings/channel/local/local-channel-runner.js +14 -4
  42. package/dist/wirings/channel/serverless/serverless-channel-runner.js +4 -1
  43. package/dist/wirings/cli/cli-runner.js +10 -2
  44. package/dist/wirings/cli/cli.types.d.ts +1 -0
  45. package/dist/wirings/http/http-routes.js +0 -1
  46. package/dist/wirings/http/http-runner.d.ts +1 -1
  47. package/dist/wirings/http/http-runner.js +25 -3
  48. package/dist/wirings/http/http.types.d.ts +3 -11
  49. package/dist/wirings/http/index.d.ts +2 -1
  50. package/dist/wirings/http/index.js +1 -0
  51. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
  52. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  53. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  54. package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
  55. package/dist/wirings/http/web-request.d.ts +12 -0
  56. package/dist/wirings/http/web-request.js +85 -0
  57. package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
  58. package/dist/wirings/mcp/mcp-runner.js +13 -1
  59. package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
  60. package/dist/wirings/rpc/rpc-runner.js +36 -14
  61. package/dist/wirings/rpc/rpc-types.d.ts +6 -1
  62. package/dist/wirings/rpc/wire-addon.d.ts +1 -0
  63. package/dist/wirings/rpc/wire-addon.js +1 -1
  64. package/dist/wirings/scheduler/scheduler-runner.js +1 -0
  65. package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
  66. package/dist/wirings/workflow/graph/graph-validation.js +175 -0
  67. package/dist/wirings/workflow/graph/index.d.ts +1 -0
  68. package/dist/wirings/workflow/graph/index.js +1 -0
  69. package/dist/wirings/workflow/index.d.ts +1 -1
  70. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
  71. package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
  72. package/dist/wirings/workflow/workflow.types.d.ts +10 -1
  73. package/package.json +4 -2
  74. package/src/crypto-utils.test.ts +99 -1
  75. package/src/crypto-utils.ts +114 -0
  76. package/src/function/function-runner.ts +5 -5
  77. package/src/function/functions.types.ts +38 -1
  78. package/src/handle-error.test.ts +65 -2
  79. package/src/handle-error.ts +8 -4
  80. package/src/index.ts +14 -0
  81. package/src/pikku-state.ts +3 -1
  82. package/src/schema.ts +1 -1
  83. package/src/services/in-memory-workflow-service.ts +45 -2
  84. package/src/services/workflow-service.ts +12 -1
  85. package/src/testing/index.ts +2 -0
  86. package/src/testing/service-tests.ts +873 -0
  87. package/src/types/core.types.ts +9 -1
  88. package/src/types/state.types.ts +4 -2
  89. package/src/utils/hash.test.ts +68 -0
  90. package/src/utils/hash.ts +26 -0
  91. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  92. package/src/wirings/ai-agent/ai-agent-memory.ts +7 -6
  93. package/src/wirings/ai-agent/ai-agent-prepare.ts +175 -61
  94. package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
  95. package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
  96. package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
  97. package/src/wirings/ai-agent/ai-agent-stream.ts +182 -89
  98. package/src/wirings/ai-agent/ai-agent.types.ts +23 -1
  99. package/src/wirings/ai-agent/index.ts +1 -5
  100. package/src/wirings/channel/channel-handler.ts +11 -2
  101. package/src/wirings/channel/channel.types.ts +1 -7
  102. package/src/wirings/channel/local/local-channel-handler.ts +5 -3
  103. package/src/wirings/channel/local/local-channel-runner.ts +17 -7
  104. package/src/wirings/channel/serverless/serverless-channel-runner.ts +4 -1
  105. package/src/wirings/cli/cli-runner.ts +12 -2
  106. package/src/wirings/cli/cli.types.ts +1 -0
  107. package/src/wirings/http/http-routes.ts +0 -1
  108. package/src/wirings/http/http-runner.ts +35 -10
  109. package/src/wirings/http/http.types.ts +3 -12
  110. package/src/wirings/http/index.ts +3 -0
  111. package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
  112. package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
  113. package/src/wirings/http/web-request.test.ts +236 -0
  114. package/src/wirings/http/web-request.ts +104 -0
  115. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  116. package/src/wirings/mcp/mcp-runner.ts +30 -12
  117. package/src/wirings/rpc/rpc-runner.ts +45 -24
  118. package/src/wirings/rpc/rpc-types.ts +6 -1
  119. package/src/wirings/rpc/wire-addon.ts +2 -1
  120. package/src/wirings/scheduler/scheduler-runner.ts +1 -0
  121. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  122. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  123. package/src/wirings/workflow/graph/index.ts +5 -0
  124. package/src/wirings/workflow/index.ts +1 -0
  125. package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
  126. package/src/wirings/workflow/workflow.types.ts +9 -1
  127. package/tsconfig.tsbuildinfo +1 -1
  128. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
  129. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -168
  130. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
  131. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -238
@@ -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 = '';
@@ -181,31 +180,52 @@ async function runStreamStepLoop(params) {
181
180
  }
182
181
  if (stepResult.toolCalls.length === 0)
183
182
  break;
184
- const approvalNeeded = checkForApproval(stepResult, runnerParams.tools, runId);
185
- if (approvalNeeded) {
186
- 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 };
187
198
  }
188
199
  appendStepMessages(runnerParams, stepResult);
189
200
  }
190
201
  return { outcome: 'done' };
191
202
  }
192
- function checkForApproval(stepResult, tools, runId) {
203
+ export function checkForApprovals(stepResult, tools, runId) {
204
+ const approvals = [];
193
205
  for (const tc of stepResult.toolCalls) {
194
206
  const toolDef = tools.find((t) => t.name === tc.toolName);
195
207
  if (toolDef?.needsApproval) {
196
- return new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args);
208
+ approvals.push(new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args));
209
+ continue;
197
210
  }
198
211
  const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
199
212
  if (tr?.result &&
200
213
  typeof tr.result === 'object' &&
201
214
  '__approvalRequired' in tr.result) {
202
215
  const r = tr.result;
203
- 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
+ }
204
224
  }
205
225
  }
206
- return null;
226
+ return approvals;
207
227
  }
208
- function appendStepMessages(runnerParams, stepResult) {
228
+ export function appendStepMessages(runnerParams, stepResult) {
209
229
  const assistantMsg = {
210
230
  id: randomUUID(),
211
231
  role: 'assistant',
@@ -234,10 +254,10 @@ function appendStepMessages(runnerParams, stepResult) {
234
254
  runnerParams.messages.push(toolMsg);
235
255
  }
236
256
  }
237
- function handleApproval(err, runId, channel, aiRunState, persistingChannel) {
257
+ function handleApprovals(approvals, runId, channel, aiRunState, persistingChannel) {
238
258
  return (async () => {
239
259
  await persistingChannel.flush();
240
- const pendingApproval = err.agentRunId
260
+ const pendingApprovals = approvals.map((err) => err.agentRunId
241
261
  ? {
242
262
  type: 'agent-call',
243
263
  toolCallId: err.toolCallId,
@@ -251,40 +271,32 @@ function handleApproval(err, runId, channel, aiRunState, persistingChannel) {
251
271
  toolCallId: err.toolCallId,
252
272
  toolName: err.toolName,
253
273
  args: err.args,
254
- };
274
+ });
255
275
  await aiRunState.updateRun(runId, {
256
276
  status: 'suspended',
257
277
  suspendReason: 'approval',
258
- pendingApprovals: [pendingApproval],
278
+ pendingApprovals,
259
279
  });
260
- const approvalEvent = {
261
- type: 'approval-request',
262
- toolCallId: err.toolCallId,
263
- toolName: err.displayToolName ?? err.toolName,
264
- args: err.displayArgs ?? err.args,
265
- reason: err.reason,
266
- runId,
267
- };
268
- 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
+ }
269
291
  channel.send({ type: 'done' });
270
292
  channel.close();
271
293
  })();
272
294
  }
273
295
  export async function streamAIAgent(agentName, input, channel, params, agentSessionMap, options) {
274
296
  const sessionMap = agentSessionMap ?? new Map();
275
- const { agent: resolvedAgentForProtocol } = resolveAgent(agentName);
276
- const inputRecord = input;
277
- const useUIMessageStream = resolvedAgentForProtocol.protocol === 'ui-message-stream' ||
278
- Array.isArray(inputRecord.messages);
279
- let normalizedInput;
280
- if (useUIMessageStream) {
281
- normalizedInput = parseAssistantUIInput(inputRecord);
282
- channel = createAssistantUIChannel(channel);
283
- }
284
- else {
285
- normalizedInput = input;
286
- }
297
+ const normalizedInput = input;
287
298
  const streamContext = { channel, options };
299
+ // delegateState is attached after prepareAgentRun resolves the agent config
288
300
  const { agent, packageName, resolvedName, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, normalizedInput, params, sessionMap, streamContext);
289
301
  const singletonServices = getSingletonServices();
290
302
  const { aiRunState } = singletonServices;
@@ -305,7 +317,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
305
317
  });
306
318
  channel.send({ type: 'suspended', reason: 'rpc-missing', missingRpcs });
307
319
  channel.send({ type: 'done' });
308
- return;
320
+ return '';
309
321
  }
310
322
  const aiMiddlewares = agent.aiMiddleware ?? [];
311
323
  let modifiedMessages = runnerParams.messages;
@@ -369,7 +381,28 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
369
381
  const wrappedChannel = allChannelMiddleware.length > 0
370
382
  ? wrapChannelWithMiddleware({ channel }, singletonServices, allChannelMiddleware).channel
371
383
  : channel;
372
- 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);
373
406
  try {
374
407
  const loopResult = await runStreamStepLoop({
375
408
  agent,
@@ -382,12 +415,13 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
382
415
  aiMiddlewares,
383
416
  });
384
417
  if (loopResult.outcome === 'approval') {
385
- await handleApproval(loopResult.approval, runId, channel, aiRunState, persistingChannel);
386
- return;
418
+ await handleApprovals(loopResult.approvals, runId, channel, aiRunState, persistingChannel);
419
+ return persistingChannel.fullText;
387
420
  }
388
421
  await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, runId);
389
422
  channel.send({ type: 'done' });
390
423
  channel.close();
424
+ return persistingChannel.fullText;
391
425
  }
392
426
  catch (err) {
393
427
  for (const mw of aiMiddlewares) {
@@ -404,13 +438,17 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
404
438
  }
405
439
  }
406
440
  }
407
- await aiRunState.updateRun(runId, { status: 'failed' });
441
+ await aiRunState.updateRun(runId, {
442
+ status: 'failed',
443
+ errorMessage: err instanceof Error ? err.message : String(err),
444
+ });
408
445
  channel.send({
409
446
  type: 'error',
410
447
  message: err instanceof Error ? err.message : String(err),
411
448
  });
412
449
  channel.send({ type: 'done' });
413
450
  channel.close();
451
+ return persistingChannel.fullText;
414
452
  }
415
453
  }
416
454
  export async function resumeAIAgent(input, channel, params, options) {
@@ -439,7 +477,7 @@ export async function resumeAIAgent(input, channel, params, options) {
439
477
  if (pending.type === 'agent-call') {
440
478
  await aiRunState.updateRun(pending.agentRunId, { status: 'failed' });
441
479
  }
442
- 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.';
443
481
  if (storage) {
444
482
  await storage.saveMessages(run.threadId, [
445
483
  {
@@ -458,6 +496,15 @@ export async function resumeAIAgent(input, channel, params, options) {
458
496
  },
459
497
  ]);
460
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
+ }
461
508
  await aiRunState.updateRun(run.runId, { status: 'running' });
462
509
  await continueAfterToolResult(run, agent, packageName, resolvedName, storage, memoryConfig, agentRunner, channel, params, aiRunState, options);
463
510
  return;
@@ -467,7 +514,8 @@ export async function resumeAIAgent(input, channel, params, options) {
467
514
  if (!subRun) {
468
515
  throw new Error(`Sub-agent run not found: ${pending.agentRunId}`);
469
516
  }
470
- const subPending = subRun.pendingApprovals?.[0];
517
+ const subPending = subRun.pendingApprovals?.find((p) => p.toolCallId === input.toolCallId) ??
518
+ subRun.pendingApprovals?.[0];
471
519
  if (!subPending) {
472
520
  throw new Error(`No pending approval on sub-agent run ${pending.agentRunId}`);
473
521
  }
@@ -512,18 +560,20 @@ export async function resumeAIAgent(input, channel, params, options) {
512
560
  options: { ...options, requiresToolApproval: false },
513
561
  };
514
562
  const aiMiddlewaresForResume = agent.aiMiddleware ?? [];
515
- 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);
516
564
  const matchingTool = tools.find((t) => t.name === pending.toolName);
517
565
  if (!matchingTool) {
518
566
  throw new Error(`Tool "${pending.toolName}" not found in agent definition`);
519
567
  }
520
568
  const toolArgs = typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args;
521
569
  let toolResult;
570
+ let isError = false;
522
571
  try {
523
572
  toolResult = await matchingTool.execute(toolArgs);
524
573
  }
525
574
  catch (execErr) {
526
575
  toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`;
576
+ isError = true;
527
577
  }
528
578
  const resultStr = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
529
579
  if (storage) {
@@ -547,8 +597,18 @@ export async function resumeAIAgent(input, channel, params, options) {
547
597
  toolCallId: input.toolCallId,
548
598
  toolName: pending.toolName,
549
599
  result: toolResult,
600
+ ...(isError ? { isError: true } : {}),
550
601
  });
551
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
+ }
552
612
  await aiRunState.updateRun(run.runId, { status: 'running' });
553
613
  await continueAfterToolResult(run, agent, packageName, resolvedName, storage, memoryConfig, agentRunner, channel, params, aiRunState, options);
554
614
  }
@@ -568,7 +628,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
568
628
  const contextMessages = await loadContextMessages(memoryConfig, storage, { message: '', threadId: run.threadId, resourceId: run.resourceId }, workingMemoryJsonSchema);
569
629
  const allMessages = [...contextMessages, ...messages];
570
630
  const trimmedMessages = trimMessages(allMessages);
571
- const instructions = buildInstructions(resolvedName, packageName);
631
+ const instructions = await buildInstructions(resolvedName, packageName);
572
632
  const aiMiddlewares = agent.aiMiddleware ?? [];
573
633
  let modifiedMessages = trimmedMessages;
574
634
  let modifiedInstructions = instructions;
@@ -617,7 +677,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
617
677
  : channel;
618
678
  const persistingChannel = createPersistingChannel(wrappedChannel, storage, run.threadId);
619
679
  const streamContext = { channel, options };
620
- 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;
621
681
  const resolved = resolveModelConfig(resolvedName, agent);
622
682
  const maxSteps = resolved.maxSteps ?? 10;
623
683
  const runnerParams = {
@@ -644,7 +704,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
644
704
  aiMiddlewares,
645
705
  });
646
706
  if (loopResult.outcome === 'approval') {
647
- await handleApproval(loopResult.approval, run.runId, channel, aiRunState, persistingChannel);
707
+ await handleApprovals(loopResult.approvals, run.runId, channel, aiRunState, persistingChannel);
648
708
  return;
649
709
  }
650
710
  await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, run.threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, run.runId);
@@ -666,7 +726,10 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
666
726
  }
667
727
  }
668
728
  }
669
- 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
+ });
670
733
  channel.send({
671
734
  type: 'error',
672
735
  message: err instanceof Error ? err.message : String(err),
@@ -66,6 +66,8 @@ export interface AIAgentInput {
66
66
  threadId: string;
67
67
  resourceId: string;
68
68
  attachments?: AIAgentInputAttachment[];
69
+ model?: string;
70
+ temperature?: number;
69
71
  }
70
72
  export interface AIAgentOutput {
71
73
  runId: string;
@@ -77,6 +79,14 @@ export interface AIAgentOutput {
77
79
  inputTokens: number;
78
80
  outputTokens: number;
79
81
  };
82
+ status?: 'completed' | 'suspended';
83
+ pendingApprovals?: Array<{
84
+ toolCallId: string;
85
+ toolName: string;
86
+ args: unknown;
87
+ reason?: string;
88
+ runId: string;
89
+ }>;
80
90
  }
81
91
  export interface AIAgentToolDef {
82
92
  name: string;
@@ -84,6 +94,7 @@ export interface AIAgentToolDef {
84
94
  inputSchema: Record<string, unknown>;
85
95
  execute: (input: unknown) => Promise<unknown>;
86
96
  needsApproval?: boolean;
97
+ approvalDescriptionFn?: (input: unknown) => Promise<string>;
87
98
  }
88
99
  export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> = Record<string, unknown>, Services = any> {
89
100
  modifyInput?: (services: Services, ctx: {
@@ -177,13 +188,14 @@ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMi
177
188
  temperature?: number;
178
189
  tools?: unknown[];
179
190
  agents?: unknown[];
191
+ agentMode?: 'delegate' | 'supervise';
180
192
  memory?: AIAgentMemoryConfig;
181
193
  maxSteps?: number;
182
194
  toolChoice?: 'auto' | 'required' | 'none';
195
+ dynamicWorkflows?: 'read' | 'always' | 'ask';
183
196
  input?: unknown;
184
197
  output?: unknown;
185
198
  tags?: string[];
186
- protocol?: 'ui-message-stream';
187
199
  prepareStep?: (ctx: {
188
200
  stepNumber: number;
189
201
  messages: AIMessage[];
@@ -268,6 +280,12 @@ export type AIStreamEvent = {
268
280
  type: 'audio-done';
269
281
  agent?: string;
270
282
  session?: string;
283
+ } | {
284
+ type: 'workflow-created';
285
+ workflowName: string;
286
+ graph: any;
287
+ agent?: string;
288
+ session?: string;
271
289
  } | {
272
290
  type: 'suspended';
273
291
  reason: 'rpc-missing';
@@ -296,6 +314,7 @@ export interface AgentRunState {
296
314
  threadId: string;
297
315
  resourceId: string;
298
316
  status: 'running' | 'suspended' | 'completed' | 'failed';
317
+ errorMessage?: string;
299
318
  suspendReason?: 'approval' | 'rpc-missing';
300
319
  missingRpcs?: string[];
301
320
  pendingApprovals?: PendingApproval[];
@@ -313,6 +332,7 @@ export interface AgentRunRow {
313
332
  threadId: string;
314
333
  resourceId: string;
315
334
  status: string;
335
+ errorMessage?: string;
316
336
  suspendReason?: string;
317
337
  missingRpcs?: string[];
318
338
  usageInputTokens: number;
@@ -343,4 +363,5 @@ export type AIAgentMeta = Record<string, Omit<CoreAIAgent, 'input' | 'output' |
343
363
  channelMiddleware?: MiddlewareMetadata[];
344
364
  aiMiddleware?: MiddlewareMetadata[];
345
365
  permissions?: PermissionMetadata[];
366
+ dynamicWorkflows?: 'read' | 'always' | 'ask';
346
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
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';
@@ -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,12 +85,14 @@ 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
98
  const onMessage = async (rawData) => {
@@ -1,4 +1,3 @@
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';
4
3
  export type BinaryData = ArrayBuffer | Uint8Array;
@@ -19,6 +18,7 @@ export type RunChannelParams<ChannelData> = {
19
18
  };
20
19
  export interface ChannelMessageMeta {
21
20
  pikkuFuncId: string;
21
+ packageName?: string;
22
22
  summary?: string;
23
23
  description?: string;
24
24
  errors?: string[];
@@ -71,12 +71,6 @@ export type CoreChannel<ChannelData, Channel extends string, ChannelConnect = Co
71
71
  auth?: boolean;
72
72
  binary?: boolean | null;
73
73
  onBinaryMessage?: (services: any, data: BinaryData, channel: PikkuChannel<ChannelData, any>) => Promise<BinaryData | void> | BinaryData | void;
74
- docs?: Partial<{
75
- description: string;
76
- response: string;
77
- errors: Array<typeof PikkuError>;
78
- tags: string[];
79
- }>;
80
74
  tags?: string[];
81
75
  };
82
76
  export interface PikkuChannel<OpeningData, out Out> {
@@ -4,7 +4,7 @@ export declare class PikkuLocalChannelHandler<OpeningData = unknown, Out = unkno
4
4
  private onMessageCallback?;
5
5
  private onBinaryMessageCallback?;
6
6
  private openCallBack?;
7
- private closeCallback?;
7
+ private closeCallbacks;
8
8
  private sendCallback?;
9
9
  private sendBinaryCallback?;
10
10
  registerOnOpen(callback: () => void): void;
@@ -3,7 +3,7 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
3
3
  onMessageCallback;
4
4
  onBinaryMessageCallback;
5
5
  openCallBack;
6
- closeCallback;
6
+ closeCallbacks = [];
7
7
  sendCallback;
8
8
  sendBinaryCallback;
9
9
  registerOnOpen(callback) {
@@ -28,14 +28,16 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
28
28
  return this.onBinaryMessageCallback?.(data);
29
29
  }
30
30
  registerOnClose(callback) {
31
- this.closeCallback = callback;
31
+ this.closeCallbacks.push(callback);
32
32
  }
33
33
  close() {
34
34
  if (this.getChannel().state === 'closed') {
35
35
  return;
36
36
  }
37
37
  super.close();
38
- this.closeCallback?.();
38
+ for (const cb of this.closeCallbacks) {
39
+ cb();
40
+ }
39
41
  }
40
42
  registerOnSend(send) {
41
43
  this.sendCallback = send;
@@ -75,7 +75,10 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
75
75
  }
76
76
  catch (e) {
77
77
  singletonServices.logger.error(`Error handling onConnect: ${e}`);
78
- channel.send({ error: e.message || 'Unknown error' });
78
+ channel.send({
79
+ error: e.message || 'Unknown error',
80
+ errorName: e.constructor?.name,
81
+ });
79
82
  }
80
83
  }
81
84
  });
@@ -94,7 +97,10 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
94
97
  }
95
98
  catch (e) {
96
99
  singletonServices.logger.error(`Error handling onDisconnect: ${e}`);
97
- channel.send({ error: e.message || 'Unknown error' });
100
+ channel.send({
101
+ error: e.message || 'Unknown error',
102
+ errorName: e.constructor?.name,
103
+ });
98
104
  }
99
105
  }
100
106
  await closeServices();
@@ -106,8 +112,12 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
106
112
  await channel.send(result);
107
113
  }
108
114
  catch (e) {
109
- singletonServices.logger.error(`Error handling message: ${e.message}`);
110
- channel.send({ error: e.message || 'Unknown error' });
115
+ singletonServices.logger.error(e);
116
+ channel.send({
117
+ error: e.message || 'Unknown error',
118
+ errorName: e.constructor?.name,
119
+ });
120
+ setTimeout(() => channel.close(), 200);
111
121
  }
112
122
  });
113
123
  if (onBinaryMessage) {
@@ -176,7 +176,10 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
176
176
  }
177
177
  catch (e) {
178
178
  singletonServices.logger.error(`Error processing message: ${e.message || e}`);
179
- return { error: e.message || 'Unknown error' };
179
+ return {
180
+ error: e.message || 'Unknown error',
181
+ errorName: e.constructor?.name,
182
+ };
180
183
  }
181
184
  finally {
182
185
  if (wireServices) {
@@ -130,14 +130,20 @@ function pluckCLIData(mergedData, funcName, availableOptions) {
130
130
  const funcMeta = pikkuState(null, 'function', 'meta')[funcName];
131
131
  const schemaName = funcMeta?.inputSchemaName;
132
132
  const schema = schemaName
133
- ? pikkuState(null, 'misc', 'schemas').get(schemaName)
133
+ ? pikkuState(funcMeta?.packageName ?? null, 'misc', 'schemas').get(schemaName)
134
134
  : null;
135
135
  if (schema && schema.properties) {
136
136
  // If we have a schema, only include fields that are in the schema
137
137
  const result = {};
138
138
  for (const key of Object.keys(schema.properties)) {
139
139
  if (key in mergedData) {
140
- result[key] = mergedData[key];
140
+ let value = mergedData[key];
141
+ // CLI passes all values as strings — coerce to array when schema expects one
142
+ const propSchema = schema.properties[key];
143
+ if (propSchema?.type === 'array' && !Array.isArray(value)) {
144
+ value = typeof value === 'string' ? value.split(',') : [value];
145
+ }
146
+ result[key] = value;
141
147
  }
142
148
  else if (availableOptions[key]?.default !== undefined) {
143
149
  // Apply default if not provided
@@ -237,9 +243,11 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
237
243
  wireMiddleware: allWireMiddleware,
238
244
  inheritedPermissions: currentCommand.permissions,
239
245
  wirePermissions: undefined,
246
+ coerceDataFromSchema: true,
240
247
  tags: programData?.tags,
241
248
  wire,
242
249
  sessionService: userSession,
250
+ packageName: currentCommand.packageName,
243
251
  });
244
252
  // Apply renderer one final time with the final output (if renderer exists)
245
253
  // Skip if result is undefined (void functions handle their own output)
@@ -52,6 +52,7 @@ export interface CLIProgramState {
52
52
  export interface CLICommandMeta {
53
53
  parameters?: string;
54
54
  pikkuFuncId: string;
55
+ packageName?: string;
55
56
  positionals: CLIPositional[];
56
57
  options: Record<string, CLIOption>;
57
58
  renderName?: string;
@@ -113,7 +113,6 @@ function registerRoute(route, groupConfig) {
113
113
  contentType: route.contentType,
114
114
  timeout: route.timeout,
115
115
  headers: route.headers,
116
- docs: route.docs,
117
116
  sse: route.sse,
118
117
  });
119
118
  }
@@ -136,4 +136,4 @@ export declare const pikkuFetch: <In, Out>(request: Request | PikkuHTTPRequest,
136
136
  * @param {RunHTTPWiringOptions} options - Options such as singleton services, session handling, and error configuration.
137
137
  * @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
138
138
  */
139
- export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, generateRequestId, }?: RunHTTPWiringOptions) => Promise<Out | void>;
139
+ export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, generateRequestId, }?: RunHTTPWiringOptions) => Promise<Out | void>;