@pikku/core 0.12.137 → 0.12.138

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 (52) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/dev/hot-reload.js +5 -2
  3. package/dist/env.d.ts +18 -0
  4. package/dist/env.js +60 -1
  5. package/dist/function/abort-scope.d.ts +6 -5
  6. package/dist/function/abort-scope.js +6 -17
  7. package/dist/function/function-runner.js +2 -1
  8. package/dist/middleware/auth-apikey.js +1 -1
  9. package/dist/middleware/auth-bearer.js +1 -1
  10. package/dist/middleware/auth-cookie.js +1 -1
  11. package/dist/schema.js +3 -1
  12. package/dist/services/in-memory-queue-service.js +4 -2
  13. package/dist/services/in-memory-workflow-service.js +7 -6
  14. package/dist/services/index.d.ts +0 -1
  15. package/dist/services/index.js +0 -1
  16. package/dist/services/local-variables.d.ts +5 -0
  17. package/dist/services/local-variables.js +7 -2
  18. package/dist/services/queue-webhook-service.js +1 -1
  19. package/dist/services/webhook-service.d.ts +2 -2
  20. package/dist/services/webhook-service.js +6 -4
  21. package/dist/types/core.types.d.ts +10 -1
  22. package/dist/utils/hmac.d.ts +25 -10
  23. package/dist/utils/hmac.js +202 -24
  24. package/dist/wirings/agent/agent-agui.js +65 -62
  25. package/dist/wirings/agent/agent-interrupt.d.ts +4 -1
  26. package/dist/wirings/agent/agent-interrupt.js +3 -3
  27. package/dist/wirings/agent/agent-memory.js +5 -6
  28. package/dist/wirings/agent/agent-model-config.js +2 -1
  29. package/dist/wirings/agent/agent-prepare.js +21 -13
  30. package/dist/wirings/agent/agent-stream.js +38 -34
  31. package/dist/wirings/agent/agent.types.d.ts +8 -1
  32. package/dist/wirings/channel/channel-handler.js +1 -1
  33. package/dist/wirings/channel/local/local-channel-handler.js +5 -1
  34. package/dist/wirings/channel/local/local-channel-runner.js +6 -4
  35. package/dist/wirings/channel/local/local-eventhub-service.d.ts +1 -0
  36. package/dist/wirings/channel/local/local-eventhub-service.js +4 -1
  37. package/dist/wirings/cli/channel/cli-channel-runner.js +7 -2
  38. package/dist/wirings/cli/channel/cli-raw-client-runner.js +8 -4
  39. package/dist/wirings/cli/cli-runner.js +1 -1
  40. package/dist/wirings/gateway/gateway-runner.js +3 -3
  41. package/dist/wirings/http/http-runner.js +6 -6
  42. package/dist/wirings/scheduler/scheduler-runner.js +1 -1
  43. package/dist/wirings/scope/define-scope.d.ts +19 -0
  44. package/dist/wirings/scope/define-scope.js +19 -0
  45. package/dist/wirings/scope/index.d.ts +1 -1
  46. package/dist/wirings/scope/index.js +1 -1
  47. package/dist/wirings/trigger/trigger-runner.js +3 -1
  48. package/dist/wirings/trigger/webhook-source-runner.js +5 -2
  49. package/dist/wirings/virtual-user/virtual-user-scaffold.js +3 -2
  50. package/dist/wirings/workflow/workflow-invocation-id.js +85 -8
  51. package/package.json +3 -1
  52. package/src/public-surface.json +6 -3
@@ -247,7 +247,7 @@ export function createScopedChannel(parent, agentName, session) {
247
247
  },
248
248
  close: () => { },
249
249
  sendBinary: (data) => parent.sendBinary(data),
250
- send: (event) => {
250
+ send: async (event) => {
251
251
  if (event.type === 'done')
252
252
  return;
253
253
  if (event.type === 'approval-request') {
@@ -267,10 +267,14 @@ export function createScopedChannel(parent, agentName, session) {
267
267
  event.type === 'tool-result' ||
268
268
  event.type === 'usage' ||
269
269
  event.type === 'error') {
270
- parent.send({ ...event, agent: agentName, session });
270
+ await parent.send({
271
+ ...event,
272
+ agent: agentName,
273
+ session,
274
+ });
271
275
  }
272
276
  else {
273
- parent.send(event);
277
+ await parent.send(event);
274
278
  }
275
279
  },
276
280
  setState: (s) => parent.setState(s),
@@ -402,10 +406,12 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
402
406
  needsApproval: needsApproval || undefined,
403
407
  approvalDescriptionFn,
404
408
  readonly: fnMeta?.readonly || undefined,
405
- execute: async (toolInput) => {
409
+ execute: async (toolInput, options) => {
406
410
  const wire = params.sessionService
407
411
  ? { ...createMiddlewareSessionWireProps(params.sessionService) }
408
412
  : {};
413
+ if (options?.abortScope)
414
+ wire.abortScope = options.abortScope;
409
415
  const rpcService = new ContextAwareRPCService(singletonServices, wire, { sessionService: params.sessionService });
410
416
  return rpcService.rpc(toolName, toolInput);
411
417
  },
@@ -457,7 +463,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
457
463
  streamContext.delegateState.delegated = true;
458
464
  }
459
465
  const { channel } = streamContext;
460
- channel.send({
466
+ await channel.send({
461
467
  type: 'agent-call',
462
468
  agentName: subAgentName,
463
469
  session,
@@ -468,11 +474,11 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
468
474
  ? subChannel
469
475
  : {
470
476
  ...subChannel,
471
- send: (event) => {
477
+ send: async (event) => {
472
478
  if (event.type === 'text-delta' ||
473
479
  event.type === 'reasoning-delta')
474
480
  return;
475
- subChannel.send(event);
481
+ await subChannel.send(event);
476
482
  },
477
483
  };
478
484
  const resultText = await streamAgent(subAgentName, buildSubAgentRunInput(message, threadId, resourceId, parentContext), effectiveChannel, params, agentSessionMap, streamContext.options);
@@ -486,7 +492,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
486
492
  subApprovals: subChannel.approvals,
487
493
  };
488
494
  }
489
- channel.send({
495
+ await channel.send({
490
496
  type: 'agent-result',
491
497
  agentName: subAgentName,
492
498
  session,
@@ -544,7 +550,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
544
550
  name: workflowName,
545
551
  description: wfMeta.description || workflowName,
546
552
  inputSchema,
547
- execute: async (toolInput) => {
553
+ execute: async (toolInput, options) => {
548
554
  const workflowService = singletonServices.workflowService;
549
555
  if (!workflowService) {
550
556
  throw new Error(`workflowService is not configured — cannot run workflow tool '${workflowName}'`);
@@ -552,6 +558,8 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
552
558
  const wire = params.sessionService
553
559
  ? { ...createMiddlewareSessionWireProps(params.sessionService) }
554
560
  : {};
561
+ if (options?.abortScope)
562
+ wire.abortScope = options.abortScope;
555
563
  const rpcService = new ContextAwareRPCService(singletonServices, wire, { sessionService: params.sessionService });
556
564
  return workflowService.runToCompletion(workflowName, toolInput, rpcService, { wire: { type: 'internal' } });
557
565
  },
@@ -560,9 +568,9 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
560
568
  }
561
569
  for (const tool of tools) {
562
570
  const originalExecute = tool.execute;
563
- tool.execute = async (toolInput) => {
571
+ tool.execute = async (toolInput, options) => {
564
572
  try {
565
- return await originalExecute(toolInput);
573
+ return await originalExecute(toolInput, options);
566
574
  }
567
575
  catch (err) {
568
576
  if (err?.payload?.error === 'missing_credential') {
@@ -581,7 +589,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
581
589
  if (hasToolHooks) {
582
590
  for (const tool of tools) {
583
591
  const originalExecute = tool.execute;
584
- tool.execute = async (toolInput) => {
592
+ tool.execute = async (toolInput, options) => {
585
593
  const toolCallId = randomUUID();
586
594
  let args = (toolInput ?? {});
587
595
  for (const mw of agentMiddlewares) {
@@ -600,7 +608,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
600
608
  let result;
601
609
  let execError;
602
610
  try {
603
- result = await originalExecute(args);
611
+ result = await originalExecute(args, options);
604
612
  }
605
613
  catch (err) {
606
614
  execError = err;
@@ -86,7 +86,7 @@ function createPersistingChannel(parent, storage, threadId, logger) {
86
86
  flush: flushStep,
87
87
  close: () => parent.close(),
88
88
  sendBinary: (data) => parent.sendBinary(data),
89
- send: (event) => {
89
+ send: async (event) => {
90
90
  // Accumulated whether or not storage is configured: `fullText` is what
91
91
  // the client was streamed, and an interrupted run has to be able to
92
92
  // report the fragment it got through even with persistence turned off.
@@ -149,7 +149,7 @@ function createPersistingChannel(parent, storage, threadId, logger) {
149
149
  break;
150
150
  }
151
151
  }
152
- parent.send(event);
152
+ await parent.send(event);
153
153
  },
154
154
  setState: (s) => parent.setState(s),
155
155
  getState: () => parent.getState(),
@@ -267,7 +267,7 @@ async function runStreamStepLoop(params) {
267
267
  if (stopped)
268
268
  break;
269
269
  }
270
- channel.send({ type: 'step-start', stepNumber: step });
270
+ await channel.send({ type: 'step-start', stepNumber: step });
271
271
  const stepResult = await agentRunner.stream(runnerParams, streamChannel);
272
272
  for (const mw of agentMiddlewares) {
273
273
  if (mw.afterStep) {
@@ -422,10 +422,10 @@ function handleApprovals(approvals, runId, channel, agentRunState, persistingCha
422
422
  reason: err.reason,
423
423
  runId,
424
424
  };
425
- channel.send(approvalEvent);
425
+ await channel.send(approvalEvent);
426
426
  }
427
- channel.send({ type: 'done' });
428
- channel.close();
427
+ await channel.send({ type: 'done' });
428
+ await channel.close();
429
429
  })();
430
430
  }
431
431
  function handleCredentialRequests(requests, runId, channel, agentRunState, persistingChannel) {
@@ -446,7 +446,7 @@ function handleCredentialRequests(requests, runId, channel, agentRunState, persi
446
446
  pendingApprovals,
447
447
  });
448
448
  for (const req of requests) {
449
- channel.send({
449
+ await channel.send({
450
450
  type: 'credential-request',
451
451
  toolCallId: req.toolCallId,
452
452
  toolName: req.toolName,
@@ -457,8 +457,8 @@ function handleCredentialRequests(requests, runId, channel, agentRunState, persi
457
457
  runId,
458
458
  });
459
459
  }
460
- channel.send({ type: 'done' });
461
- channel.close();
460
+ await channel.send({ type: 'done' });
461
+ await channel.close();
462
462
  })();
463
463
  }
464
464
  export async function streamAgent(agentName, input, channel, params, agentSessionMap, options) {
@@ -487,8 +487,12 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
487
487
  createdAt: new Date(),
488
488
  updatedAt: new Date(),
489
489
  });
490
- channel.send({ type: 'suspended', reason: 'rpc-missing', missingRpcs });
491
- channel.send({ type: 'done' });
490
+ await channel.send({
491
+ type: 'suspended',
492
+ reason: 'rpc-missing',
493
+ missingRpcs,
494
+ });
495
+ await channel.send({ type: 'done' });
492
496
  return '';
493
497
  }
494
498
  const workingMemoryMiddleware = getWorkingMemoryMiddleware(memoryConfig, storage, {
@@ -512,7 +516,7 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
512
516
  // knowledge: decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md
513
517
  const transcript = sharedNotes[SPOKEN_TRANSCRIPT];
514
518
  if (typeof transcript === 'string') {
515
- channel.send({ type: 'transcript', text: transcript });
519
+ await channel.send({ type: 'transcript', text: transcript });
516
520
  }
517
521
  // knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
518
522
  const lastModified = modifiedMessages[modifiedMessages.length - 1];
@@ -615,7 +619,7 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
615
619
  });
616
620
  // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
617
621
  await outputChannel.send({ type: 'done' });
618
- channel.close();
622
+ await channel.close();
619
623
  return persistingChannel.fullText;
620
624
  }
621
625
  catch (err) {
@@ -631,14 +635,14 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
631
635
  if (storage) {
632
636
  trackInterruptNote(threadId, persistOrphanedToolResults(interruptHandle, storage, threadId));
633
637
  }
634
- channel.send({
638
+ await channel.send({
635
639
  type: 'interrupted',
636
640
  runId,
637
641
  text: persistingChannel.fullText,
638
642
  reason: interruption?.reason ?? 'user',
639
643
  });
640
- channel.send({ type: 'done' });
641
- channel.close();
644
+ await channel.send({ type: 'done' });
645
+ await channel.close();
642
646
  return persistingChannel.fullText;
643
647
  }
644
648
  for (const mw of agentMiddlewares) {
@@ -657,12 +661,12 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
657
661
  status: 'failed',
658
662
  errorMessage: err instanceof Error ? err.message : String(err),
659
663
  });
660
- channel.send({
664
+ await channel.send({
661
665
  type: 'error',
662
666
  message: err instanceof Error ? err.message : String(err),
663
667
  });
664
- channel.send({ type: 'done' });
665
- channel.close();
668
+ await channel.send({ type: 'done' });
669
+ await channel.close();
666
670
  return persistingChannel.fullText;
667
671
  }
668
672
  finally {
@@ -763,7 +767,7 @@ export async function resumeAgent(input, channel, params, options) {
763
767
  },
764
768
  ]);
765
769
  }
766
- channel.send({
770
+ await channel.send({
767
771
  type: 'tool-result',
768
772
  toolCallId: input.toolCallId,
769
773
  toolName: pending.type === 'tool-call' || pending.type === 'credential-request'
@@ -774,8 +778,8 @@ export async function resumeAgent(input, channel, params, options) {
774
778
  const updatedRun = await agentRunState.getRun(run.runId);
775
779
  const remaining = updatedRun?.pendingApprovals ?? [];
776
780
  if (remaining.length > 0) {
777
- channel.send({ type: 'done' });
778
- channel.close();
781
+ await channel.send({ type: 'done' });
782
+ await channel.close();
779
783
  return;
780
784
  }
781
785
  await agentRunState.updateRun(run.runId, { status: 'running' });
@@ -793,7 +797,7 @@ export async function resumeAgent(input, channel, params, options) {
793
797
  throw new Error(`No pending approval on sub-agent run ${pending.agentRunId}`);
794
798
  }
795
799
  const subChannel = createScopedChannel(channel, subRun.agentName, 'resume');
796
- channel.send({
800
+ await channel.send({
797
801
  type: 'agent-call',
798
802
  agentName: subRun.agentName,
799
803
  session: 'resume',
@@ -804,7 +808,7 @@ export async function resumeAgent(input, channel, params, options) {
804
808
  toolCallId: subPending.toolCallId,
805
809
  approved: true,
806
810
  }, subChannel, params, options);
807
- channel.send({
811
+ await channel.send({
808
812
  type: 'agent-result',
809
813
  agentName: subRun.agentName,
810
814
  session: 'resume',
@@ -871,7 +875,7 @@ export async function resumeAgent(input, channel, params, options) {
871
875
  },
872
876
  ]);
873
877
  }
874
- channel.send({
878
+ await channel.send({
875
879
  type: 'tool-result',
876
880
  toolCallId: input.toolCallId,
877
881
  toolName: pending.toolName,
@@ -882,8 +886,8 @@ export async function resumeAgent(input, channel, params, options) {
882
886
  const updatedRun = await agentRunState.getRun(run.runId);
883
887
  const remaining = updatedRun?.pendingApprovals ?? [];
884
888
  if (remaining.length > 0) {
885
- channel.send({ type: 'done' });
886
- channel.close();
889
+ await channel.send({ type: 'done' });
890
+ await channel.close();
887
891
  return;
888
892
  }
889
893
  await agentRunState.updateRun(run.runId, { status: 'running' });
@@ -1002,7 +1006,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
1002
1006
  });
1003
1007
  // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
1004
1008
  await wrappedChannel.send({ type: 'done' });
1005
- channel.close();
1009
+ await channel.close();
1006
1010
  }
1007
1011
  catch (err) {
1008
1012
  // Same reasoning as the first turn: an interrupt is not a failure, and the
@@ -1014,14 +1018,14 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
1014
1018
  if (storage) {
1015
1019
  trackInterruptNote(run.threadId, persistOrphanedToolResults(interruptHandle, storage, run.threadId));
1016
1020
  }
1017
- channel.send({
1021
+ await channel.send({
1018
1022
  type: 'interrupted',
1019
1023
  runId: run.runId,
1020
1024
  text: persistingChannel.fullText,
1021
1025
  reason: interruption?.reason ?? 'user',
1022
1026
  });
1023
- channel.send({ type: 'done' });
1024
- channel.close();
1027
+ await channel.send({ type: 'done' });
1028
+ await channel.close();
1025
1029
  return;
1026
1030
  }
1027
1031
  for (const mw of agentMiddlewares) {
@@ -1040,12 +1044,12 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
1040
1044
  status: 'failed',
1041
1045
  errorMessage: err instanceof Error ? err.message : String(err),
1042
1046
  });
1043
- channel.send({
1047
+ await channel.send({
1044
1048
  type: 'error',
1045
1049
  message: err instanceof Error ? err.message : String(err),
1046
1050
  });
1047
- channel.send({ type: 'done' });
1048
- channel.close();
1051
+ await channel.send({ type: 'done' });
1052
+ await channel.close();
1049
1053
  }
1050
1054
  finally {
1051
1055
  interruptHandle.release();
@@ -1,5 +1,6 @@
1
1
  import type { CorePermissionGroup, CorePikkuPermission } from '../../function/functions.types.js';
2
2
  import type { CorePikkuMiddleware, MiddlewareMetadata } from '../../middleware/middleware.types.js';
3
+ import type { AbortScope } from '../../function/abort-scope.js';
3
4
  import type { CoreSingletonServices } from '../../types/core.types.js';
4
5
  import type { PermissionMetadata } from '../../function/function-meta.types.js';
5
6
  import type { AIProviderOptions } from '../../services/agent-runner-service.js';
@@ -166,7 +167,13 @@ export interface AgentToolDef extends Partial<ApprovalPolicy> {
166
167
  name: string;
167
168
  description: string;
168
169
  inputSchema: Record<string, unknown>;
169
- execute: (input: unknown) => Promise<unknown>;
170
+ /**
171
+ * `options.abortScope` is supplied by the interrupt tracker and must be put
172
+ * on the wire of whatever this runs, so `beginChanges()` can observe it.
173
+ */
174
+ execute: (input: unknown, options?: {
175
+ abortScope?: AbortScope;
176
+ }) => Promise<unknown>;
170
177
  /**
171
178
  * Mirrors the pikku function's `readonly` flag. A read that gets interrupted
172
179
  * is discarded rather than reported: nothing changed, and by the next turn
@@ -47,7 +47,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
47
47
  : 'the default message route';
48
48
  logger.error(`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}. No session is attached to this websocket connection. Ensure auth middleware establishes a session during websocket upgrade, and configure sessionStore when the runtime needs persisted sessions.`);
49
49
  // knowledge: questions/unauthorized-channel-replies-escape-the-declared-out-type.md
50
- channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
50
+ await channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
51
51
  return;
52
52
  }
53
53
  const routeMeta = getRouteMeta(channelConfig.name, routingProperty, routerValue);
@@ -14,6 +14,10 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
14
14
  registerOnOpen(callback) {
15
15
  this.openCallBack = callback;
16
16
  }
17
+ // The base class declares `open(): void`; this override is intentionally
18
+ // async (callers already await it). Widening the exported base signature is
19
+ // a public API change, so the rule is suppressed for this one override.
20
+ // oxlint-disable-next-line typescript/no-misused-promises
17
21
  async open() {
18
22
  this.getChannel().state = 'open';
19
23
  if (this.openCallBack) {
@@ -39,7 +43,7 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
39
43
  if (this.getChannel().state === 'closed') {
40
44
  return;
41
45
  }
42
- super.close();
46
+ await super.close();
43
47
  const results = await Promise.allSettled(this.closeCallbacks.map((cb) => cb()));
44
48
  for (const result of results) {
45
49
  if (result.status === 'rejected') {
@@ -98,7 +98,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
98
98
  catch (e) {
99
99
  singletonServices.logger.error(`Error handling onConnect: ${e}`);
100
100
  const errorResponse = getErrorResponse(e);
101
- channel.send({
101
+ await channel.send({
102
102
  error: errorResponse?.message ??
103
103
  (isProduction() ? 'Internal server error' : e.message),
104
104
  ...(!isProduction() && { errorName: e.constructor?.name }),
@@ -124,7 +124,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
124
124
  catch (e) {
125
125
  singletonServices.logger.error(`Error handling onDisconnect: ${e}`);
126
126
  const errorResponse = getErrorResponse(e);
127
- channel.send({
127
+ await channel.send({
128
128
  error: errorResponse?.message ??
129
129
  (isProduction() ? 'Internal server error' : e.message),
130
130
  ...(!isProduction() && { errorName: e.constructor?.name }),
@@ -149,12 +149,14 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
149
149
  catch (e) {
150
150
  singletonServices.logger.error(e);
151
151
  const errorResponse = getErrorResponse(e);
152
- channel.send({
152
+ await channel.send({
153
153
  error: errorResponse?.message ??
154
154
  (isProduction() ? 'Internal server error' : e.message),
155
155
  ...(!isProduction() && { errorName: e.constructor?.name }),
156
156
  });
157
- setTimeout(() => channel.close(), 200);
157
+ setTimeout(() => {
158
+ Promise.resolve(channel.close()).catch((closeError) => singletonServices.logger.error(closeError));
159
+ }, 200);
158
160
  }
159
161
  });
160
162
  if (onBinaryMessage) {
@@ -6,6 +6,7 @@ export declare class LocalEventHubService<Data extends Record<string, any> = {}>
6
6
  subscribe<T extends keyof Data>(topic: T, channelId: string): void | Promise<void>;
7
7
  unsubscribe<T extends keyof Data>(topic: T, channelId: string): void | Promise<void>;
8
8
  publish<T extends keyof Data>(topic: T, channelId: string | null, data: Data[T], isBinary?: boolean): void | Promise<void>;
9
+ private deliver;
9
10
  onChannelOpened(channelHandler: PikkuChannelHandler): void;
10
11
  onChannelClosed(channelId: string): void;
11
12
  }
@@ -17,6 +17,9 @@ export class LocalEventHubService {
17
17
  }
18
18
  }
19
19
  publish(topic, channelId, data, isBinary) {
20
+ return this.deliver(topic, channelId, data, isBinary);
21
+ }
22
+ async deliver(topic, channelId, data, isBinary) {
20
23
  const subscribedChannelIds = this.subscriptions.get(topic);
21
24
  if (!subscribedChannelIds) {
22
25
  return;
@@ -27,7 +30,7 @@ export class LocalEventHubService {
27
30
  const channel = this.channels.get(toChannelId);
28
31
  if (channel) {
29
32
  try {
30
- channel.send(data, isBinary);
33
+ await channel.send(data, isBinary);
31
34
  }
32
35
  catch {
33
36
  this.onChannelClosed(toChannelId);
@@ -42,7 +42,7 @@ export async function executeCLIViaChannel({ programName, pikkuWS, args = proces
42
42
  const renderer = renderers[commandId] || defaultRenderer || defaultJSONRenderer;
43
43
  return new Promise((resolve, reject) => {
44
44
  const commandRoute = pikkuWS.getRoute('command');
45
- const responseHandler = (response) => {
45
+ const responseHandler = async (response) => {
46
46
  if (response?.action === 'cli-control' &&
47
47
  response?.event === 'complete') {
48
48
  commandRoute.unsubscribe(commandId, responseHandler);
@@ -52,7 +52,12 @@ export async function executeCLIViaChannel({ programName, pikkuWS, args = proces
52
52
  }
53
53
  // A CLI channel response is rendered off-process: there are no services
54
54
  // to hand the renderer, and every CLI renderer ignores the argument.
55
- renderer(null, response, undefined);
55
+ try {
56
+ await renderer(null, response, undefined);
57
+ }
58
+ catch (error) {
59
+ reject(error);
60
+ }
56
61
  };
57
62
  commandRoute.subscribe(commandId, responseHandler);
58
63
  pikkuWS.ws.addEventListener('close', () => {
@@ -35,11 +35,11 @@ export async function executeRawCLIViaChannel({ pikkuWS, args = process.argv.sli
35
35
  // event loop open.
36
36
  const runEnded = new AbortController();
37
37
  const approver = approve ?? approverForMode(mode, { signal: runEnded.signal });
38
- const render = (commandId, data) => {
38
+ const render = async (commandId, data) => {
39
39
  const renderer = (commandId ? renderers[commandId] : undefined) ||
40
40
  defaultRenderer ||
41
41
  defaultJSONRenderer;
42
- renderer(clientServices, data, undefined);
42
+ await renderer(clientServices, data, undefined);
43
43
  };
44
44
  return new Promise((resolve) => {
45
45
  const commandRoute = pikkuWS.getRoute('command');
@@ -88,10 +88,14 @@ export async function executeRawCLIViaChannel({ pikkuWS, args = process.argv.sli
88
88
  // echo, and rendering that would print noise between real output.
89
89
  switch (message?.action) {
90
90
  case 'cli-output':
91
- render(message.commandId, message.data);
91
+ render(message.commandId, message.data).catch((e) => {
92
+ console.error(e);
93
+ });
92
94
  break;
93
95
  case 'cli-result':
94
- render(message.commandId, message.result);
96
+ render(message.commandId, message.result).catch((e) => {
97
+ console.error(e);
98
+ });
95
99
  break;
96
100
  case 'cli-help':
97
101
  console.log(message.help);
@@ -255,7 +255,7 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
255
255
  return result;
256
256
  }
257
257
  finally {
258
- channel.close();
258
+ await channel.close();
259
259
  }
260
260
  }
261
261
  export const pikkuCLIRender = (renderer) => {
@@ -232,7 +232,7 @@ const wireWebsocketGateway = (config) => {
232
232
  senderId: '',
233
233
  platform: adapter.name,
234
234
  send: async (msg) => {
235
- wire.channel?.send(msg);
235
+ await wire.channel?.send(msg);
236
236
  },
237
237
  };
238
238
  },
@@ -249,7 +249,7 @@ const wireWebsocketGateway = (config) => {
249
249
  senderId: parsed.senderId,
250
250
  platform: adapter.name,
251
251
  send: async (msg) => {
252
- wire.channel?.send(msg);
252
+ await wire.channel?.send(msg);
253
253
  },
254
254
  };
255
255
  wire.gateway = gateway;
@@ -268,7 +268,7 @@ const wireWebsocketGateway = (config) => {
268
268
  ? await runMiddleware(services, wire, gatewayMiddleware, invoke)
269
269
  : await invoke();
270
270
  if (result && (result.text || result.richContent || result.attachments)) {
271
- wire.channel?.send(result);
271
+ await wire.channel?.send(result);
272
272
  }
273
273
  },
274
274
  });
@@ -149,19 +149,19 @@ const executeRoute = async (services, matchedRoute, http, options) => {
149
149
  const channelRef = channel;
150
150
  const channelHandler = {
151
151
  getChannel: () => channelRef,
152
- send: (data, isBinary) => {
152
+ send: async (data, isBinary) => {
153
153
  if (isBinary)
154
- channelRef.sendBinary(data);
154
+ await channelRef.sendBinary(data);
155
155
  else
156
- channelRef.send(data);
156
+ await channelRef.send(data);
157
157
  },
158
158
  sendBinary: (data) => channelRef.sendBinary(data),
159
159
  };
160
160
  singletonServices.eventHub.onChannelOpened(channelHandler);
161
161
  const originalClose = channel.close;
162
- channel.close = () => {
162
+ channel.close = async () => {
163
163
  singletonServices.eventHub.onChannelClosed(channelId);
164
- originalClose();
164
+ await originalClose();
165
165
  };
166
166
  }
167
167
  }
@@ -201,7 +201,7 @@ const executeRoute = async (services, matchedRoute, http, options) => {
201
201
  }
202
202
  }
203
203
  catch { }
204
- channel?.close();
204
+ await channel?.close();
205
205
  return { result };
206
206
  }
207
207
  throw e;
@@ -43,7 +43,7 @@ export async function runScheduledTask({ name, session, traceId, }) {
43
43
  const meta = pikkuState(null, 'scheduler', 'meta')[name];
44
44
  const userSession = new PikkuSessionService(singletonServices.sessionStore);
45
45
  if (session) {
46
- userSession.set(session);
46
+ await userSession.set(session);
47
47
  }
48
48
  if (!task) {
49
49
  throw new ScheduledTaskNotFoundError(`Scheduled task not found: ${name}`);
@@ -30,3 +30,22 @@ import type { CoreScopes } from './scope.types.js';
30
30
  * ```
31
31
  */
32
32
  export declare const defineScope: (_config: CoreScopes) => void;
33
+ /**
34
+ * Declares scopes by id without describing them, so an app can grant a scope
35
+ * (`defineSystemRole`, a function's `scopes`) whose full tree belongs to an
36
+ * addon it has not wired. `wireAddon({ scopes })` does not declare anything: it
37
+ * only requires the scope of the addon's functions, and the tree itself arrives
38
+ * with the addon's metadata. Declaring by id lets an app grant
39
+ * `pikku:console` without wiring `@pikku/addon-console`, which would otherwise
40
+ * be the only way to make that grant valid.
41
+ *
42
+ * Each id declares that node and its ancestors. When the addon IS wired, its
43
+ * own description wins and this call adds nothing it does not already have; the
44
+ * two never conflict, unlike a second `defineScope` for the same root.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * declareScopes(['pikku:console'])
49
+ * ```
50
+ */
51
+ export declare const declareScopes: (_ids: string[]) => void;
@@ -29,3 +29,22 @@
29
29
  * ```
30
30
  */
31
31
  export const defineScope = (_config) => { };
32
+ /**
33
+ * Declares scopes by id without describing them, so an app can grant a scope
34
+ * (`defineSystemRole`, a function's `scopes`) whose full tree belongs to an
35
+ * addon it has not wired. `wireAddon({ scopes })` does not declare anything: it
36
+ * only requires the scope of the addon's functions, and the tree itself arrives
37
+ * with the addon's metadata. Declaring by id lets an app grant
38
+ * `pikku:console` without wiring `@pikku/addon-console`, which would otherwise
39
+ * be the only way to make that grant valid.
40
+ *
41
+ * Each id declares that node and its ancestors. When the addon IS wired, its
42
+ * own description wins and this call adds nothing it does not already have; the
43
+ * two never conflict, unlike a second `defineScope` for the same root.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * declareScopes(['pikku:console'])
48
+ * ```
49
+ */
50
+ export const declareScopes = (_ids) => { };
@@ -1,4 +1,4 @@
1
- export { defineScope } from './define-scope.js';
1
+ export { defineScope, declareScopes } from './define-scope.js';
2
2
  export { flattenScopeDefinitions, validateAndBuildScopeDefinitionsMeta, } from './validate-scope-definitions.js';
3
3
  export type { CoreScopes, CoreScopeNode, FlatScope, ScopeDefinitionMeta, ScopeDefinitionsMeta, ScopeDefinitions, ScopeNodeMeta, ScopeOrigin, } from './scope.types.js';
4
4
  export { hasScopes, verifyScopes } from '../../scopes.js';
@@ -1,3 +1,3 @@
1
- export { defineScope } from './define-scope.js';
1
+ export { defineScope, declareScopes } from './define-scope.js';
2
2
  export { flattenScopeDefinitions, validateAndBuildScopeDefinitionsMeta, } from './validate-scope-definitions.js';
3
3
  export { hasScopes, verifyScopes } from '../../scopes.js';