@pikku/core 0.12.1 → 0.12.2

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 (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/middleware-runner.js +1 -0
  3. package/dist/permissions.js +1 -0
  4. package/dist/pikku-state.js +4 -0
  5. package/dist/services/gateway-service.d.ts +19 -0
  6. package/dist/services/gateway-service.js +1 -0
  7. package/dist/services/index.d.ts +2 -0
  8. package/dist/services/index.js +1 -0
  9. package/dist/services/local-gateway-service.d.ts +22 -0
  10. package/dist/services/local-gateway-service.js +51 -0
  11. package/dist/types/core.types.d.ts +3 -1
  12. package/dist/types/state.types.d.ts +5 -0
  13. package/dist/utils.js +1 -1
  14. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +27 -5
  15. package/dist/wirings/ai-agent/ai-agent-memory.js +13 -2
  16. package/dist/wirings/ai-agent/ai-agent-prepare.js +14 -1
  17. package/dist/wirings/ai-agent/ai-agent-stream.js +17 -2
  18. package/dist/wirings/ai-agent/ai-agent.types.d.ts +35 -2
  19. package/dist/wirings/ai-agent/index.d.ts +1 -1
  20. package/dist/wirings/channel/channel-handler.d.ts +5 -2
  21. package/dist/wirings/channel/channel-handler.js +11 -1
  22. package/dist/wirings/channel/channel-middleware-runner.js +1 -0
  23. package/dist/wirings/channel/channel.types.d.ts +6 -0
  24. package/dist/wirings/channel/index.d.ts +1 -1
  25. package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -0
  26. package/dist/wirings/channel/local/local-channel-handler.js +17 -0
  27. package/dist/wirings/channel/local/local-channel-runner.js +14 -1
  28. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
  29. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  30. package/dist/wirings/channel/serverless/serverless-channel-runner.js +1 -1
  31. package/dist/wirings/cli/cli-runner.js +3 -0
  32. package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
  33. package/dist/wirings/gateway/gateway-runner.js +325 -0
  34. package/dist/wirings/gateway/gateway.types.d.ts +127 -0
  35. package/dist/wirings/gateway/gateway.types.js +1 -0
  36. package/dist/wirings/gateway/index.d.ts +2 -0
  37. package/dist/wirings/gateway/index.js +1 -0
  38. package/dist/wirings/http/http-runner.js +3 -0
  39. package/package.json +3 -2
  40. package/src/middleware-runner.ts +1 -0
  41. package/src/permissions.ts +1 -0
  42. package/src/pikku-state.ts +5 -0
  43. package/src/services/gateway-service.ts +20 -0
  44. package/src/services/index.ts +2 -0
  45. package/src/services/local-gateway-service.ts +62 -0
  46. package/src/types/core.types.ts +3 -0
  47. package/src/types/state.types.ts +8 -0
  48. package/src/utils.ts +1 -1
  49. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +33 -9
  50. package/src/wirings/ai-agent/ai-agent-memory.ts +10 -1
  51. package/src/wirings/ai-agent/ai-agent-prepare.ts +19 -1
  52. package/src/wirings/ai-agent/ai-agent-stream.ts +13 -2
  53. package/src/wirings/ai-agent/ai-agent.types.ts +34 -2
  54. package/src/wirings/ai-agent/index.ts +2 -0
  55. package/src/wirings/channel/channel-handler.ts +30 -2
  56. package/src/wirings/channel/channel-middleware-runner.ts +1 -0
  57. package/src/wirings/channel/channel.types.ts +11 -0
  58. package/src/wirings/channel/index.ts +1 -0
  59. package/src/wirings/channel/local/local-channel-handler.ts +26 -0
  60. package/src/wirings/channel/local/local-channel-runner.ts +15 -1
  61. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
  62. package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
  63. package/src/wirings/channel/serverless/serverless-channel-runner.ts +1 -1
  64. package/src/wirings/cli/cli-runner.ts +3 -0
  65. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  66. package/src/wirings/gateway/gateway-runner.ts +411 -0
  67. package/src/wirings/gateway/gateway.types.ts +149 -0
  68. package/src/wirings/gateway/index.ts +13 -0
  69. package/src/wirings/http/http-runner.ts +3 -0
  70. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.2
4
+
5
+ ### Patch Changes
6
+
7
+ - cc4c9e9: Add gateway meta-wiring for messaging platforms:
8
+
9
+ - New `wireGateway()` API with three transport types: webhook, websocket, listener
10
+ - `GatewayAdapter` interface for platform-specific parse/send logic
11
+ - `PikkuGateway` wire object (`wire.gateway`) with senderId, platform, and send()
12
+ - `GatewayService` interface and `LocalGatewayService` for listener gateway lifecycle
13
+ - `createListenerMessageHandler()` helper for building listener message callbacks
14
+ - Add `'gateway'` to `PikkuWiringTypes` and `gateway` to `PikkuWire`
15
+ - Add `gateway` state block to `PikkuPackageState`
16
+
17
+ - 3e04565: chore: update dependencies to latest minor/patch versions
18
+
3
19
  ## 0.12.1
4
20
 
5
21
  ### Patch Changes
@@ -96,6 +96,7 @@ const middlewareCache = {
96
96
  agent: {},
97
97
  cli: {},
98
98
  workflow: {},
99
+ gateway: {},
99
100
  };
100
101
  /**
101
102
  * Combines wiring-specific middleware with function-level middleware.
@@ -99,6 +99,7 @@ const combinedPermissionsCache = {
99
99
  agent: {},
100
100
  cli: {},
101
101
  workflow: {},
102
+ gateway: {},
102
103
  };
103
104
  /**
104
105
  * Combines wiring-specific permissions with function-level permissions.
@@ -92,6 +92,10 @@ const createEmptyPackageState = () => ({
92
92
  agents: new Map(),
93
93
  agentsMeta: {},
94
94
  },
95
+ gateway: {
96
+ gateways: new Map(),
97
+ meta: {},
98
+ },
95
99
  cli: {
96
100
  meta: { programs: {}, renderers: {} },
97
101
  programs: {},
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Abstract GatewayService interface.
3
+ *
4
+ * Implementations manage listener gateway lifecycle — initializing adapters
5
+ * and delivering messages to handler functions.
6
+ *
7
+ * A `LocalGatewayService` starts all listeners unconditionally;
8
+ * a distributed implementation could check leader election first.
9
+ */
10
+ export interface GatewayService {
11
+ /**
12
+ * Start all listener gateways
13
+ */
14
+ start(): Promise<void>;
15
+ /**
16
+ * Stop all listener gateways
17
+ */
18
+ stop(): Promise<void>;
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -12,6 +12,7 @@ export { ConsoleLogger } from './logger-console.js';
12
12
  export { InMemoryWorkflowService } from './in-memory-workflow-service.js';
13
13
  export { InMemoryTriggerService } from './in-memory-trigger-service.js';
14
14
  export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
15
+ export { LocalGatewayService } from './local-gateway-service.js';
15
16
  export type { ContentService } from './content-service.js';
16
17
  export type { JWTService } from './jwt-service.js';
17
18
  export type { Logger } from './logger.js';
@@ -21,6 +22,7 @@ export type { SchemaService } from './schema-service.js';
21
22
  export type { SessionService } from './user-session-service.js';
22
23
  export type { ScheduledTaskSummary, ScheduledTaskInfo, SchedulerService, } from './scheduler-service.js';
23
24
  export type { TriggerService } from './trigger-service.js';
25
+ export type { GatewayService } from './gateway-service.js';
24
26
  export type { DeploymentService, DeploymentConfig, DeploymentInfo, DeploymentServiceConfig, } from './deployment-service.js';
25
27
  export type { AIStorageService } from './ai-storage-service.js';
26
28
  export type { AIAgentRunnerParams, AIAgentRunnerResult, AIAgentStepResult, AIAgentRunnerService, } from './ai-agent-runner-service.js';
@@ -12,3 +12,4 @@ export { ConsoleLogger } from './logger-console.js';
12
12
  export { InMemoryWorkflowService } from './in-memory-workflow-service.js';
13
13
  export { InMemoryTriggerService } from './in-memory-trigger-service.js';
14
14
  export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
15
+ export { LocalGatewayService } from './local-gateway-service.js';
@@ -0,0 +1,22 @@
1
+ import type { GatewayService } from './gateway-service.js';
2
+ /**
3
+ * Local GatewayService implementation.
4
+ *
5
+ * Starts all registered listener gateways unconditionally (single-process).
6
+ * For distributed deployments, implement `GatewayService` with leader
7
+ * election or similar coordination.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const gatewayService = new LocalGatewayService()
12
+ * await gatewayService.start()
13
+ *
14
+ * // On shutdown
15
+ * await gatewayService.stop()
16
+ * ```
17
+ */
18
+ export declare class LocalGatewayService implements GatewayService {
19
+ private activeAdapters;
20
+ start(): Promise<void>;
21
+ stop(): Promise<void>;
22
+ }
@@ -0,0 +1,51 @@
1
+ import { pikkuState, getSingletonServices } from '../pikku-state.js';
2
+ import { createListenerMessageHandler } from '../wirings/gateway/gateway-runner.js';
3
+ /**
4
+ * Local GatewayService implementation.
5
+ *
6
+ * Starts all registered listener gateways unconditionally (single-process).
7
+ * For distributed deployments, implement `GatewayService` with leader
8
+ * election or similar coordination.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const gatewayService = new LocalGatewayService()
13
+ * await gatewayService.start()
14
+ *
15
+ * // On shutdown
16
+ * await gatewayService.stop()
17
+ * ```
18
+ */
19
+ export class LocalGatewayService {
20
+ activeAdapters = new Map();
21
+ async start() {
22
+ const singletonServices = getSingletonServices();
23
+ const gateways = pikkuState(null, 'gateway', 'gateways');
24
+ for (const [name, config] of gateways) {
25
+ if (config.type !== 'listener')
26
+ continue;
27
+ if (this.activeAdapters.has(name))
28
+ continue;
29
+ const handleMessage = createListenerMessageHandler(name, config, singletonServices);
30
+ await config.adapter.init(handleMessage);
31
+ this.activeAdapters.set(name, config.adapter);
32
+ singletonServices.logger.info(`Started listener gateway: ${name}`);
33
+ }
34
+ }
35
+ async stop() {
36
+ for (const [name, adapter] of this.activeAdapters) {
37
+ try {
38
+ await adapter.close();
39
+ }
40
+ catch (e) {
41
+ try {
42
+ getSingletonServices().logger.error(`Error closing listener gateway '${name}':`, e);
43
+ }
44
+ catch {
45
+ // logger may not be available during shutdown
46
+ }
47
+ }
48
+ }
49
+ this.activeAdapters.clear();
50
+ }
51
+ }
@@ -13,6 +13,7 @@ import type { PikkuCLI } from '../wirings/cli/cli.types.js';
13
13
  import type { PikkuWorkflowWire, WorkflowService, WorkflowServiceConfig, WorkflowStepWire } from '../wirings/workflow/workflow.types.js';
14
14
  import type { PikkuGraphWire } from '../wirings/workflow/graph/workflow-graph.types.js';
15
15
  import type { PikkuTrigger } from '../wirings/trigger/trigger.types.js';
16
+ import type { PikkuGateway } from '../wirings/gateway/gateway.types.js';
16
17
  import type { SchedulerService } from '../services/scheduler-service.js';
17
18
  import type { DeploymentService } from '../services/deployment-service.js';
18
19
  import type { AIStorageService } from '../services/ai-storage-service.js';
@@ -20,7 +21,7 @@ import type { ContentService } from '../services/content-service.js';
20
21
  import type { AIAgentRunnerService } from '../services/ai-agent-runner-service.js';
21
22
  import type { AIRunStateService } from '../services/ai-run-state-service.js';
22
23
  import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js';
23
- export type PikkuWiringTypes = 'http' | 'scheduler' | 'trigger' | 'channel' | 'rpc' | 'queue' | 'mcp' | 'cli' | 'workflow' | 'agent';
24
+ export type PikkuWiringTypes = 'http' | 'scheduler' | 'trigger' | 'channel' | 'rpc' | 'queue' | 'mcp' | 'cli' | 'workflow' | 'agent' | 'gateway';
24
25
  export interface FunctionServicesMeta {
25
26
  optimized: boolean;
26
27
  services: string[];
@@ -182,6 +183,7 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
182
183
  workflowStep: WorkflowStepWire;
183
184
  graph: PikkuGraphWire;
184
185
  trigger: PikkuTrigger<TriggerOutput>;
186
+ gateway: PikkuGateway;
185
187
  session: HasInitialSession extends true ? UserSession : UserSession | undefined;
186
188
  /** Update and persist the current session */
187
189
  setSession: (session: CoreUserSession) => Promise<void> | void;
@@ -6,6 +6,7 @@ import type { CLIMeta, CLIProgramState } from '../wirings/cli/cli.types.js';
6
6
  import type { HTTPMethod, CoreHTTPFunctionWiring, HTTPWiringsMeta } from '../wirings/http/http.types.js';
7
7
  import type { CoreMCPResource, MCPResourceMeta, MCPToolMeta, CoreMCPPrompt, MCPPromptMeta } from '../wirings/mcp/mcp.types.js';
8
8
  import type { CoreAIAgent, AIAgentMeta } from '../wirings/ai-agent/ai-agent.types.js';
9
+ import type { CoreGateway, GatewaysMeta } from '../wirings/gateway/gateway.types.js';
9
10
  import type { CoreQueueWorker, QueueWorkersMeta } from '../wirings/queue/queue.types.js';
10
11
  import type { CoreScheduledTask, ScheduledTasksMeta } from '../wirings/scheduler/scheduler.types.js';
11
12
  import type { CoreWorkflow, WorkflowsRuntimeMeta } from '../wirings/workflow/workflow.types.js';
@@ -74,6 +75,10 @@ export interface PikkuPackageState {
74
75
  agents: Map<string, CoreAIAgent>;
75
76
  agentsMeta: AIAgentMeta;
76
77
  };
78
+ gateway: {
79
+ gateways: Map<string, CoreGateway>;
80
+ meta: GatewaysMeta;
81
+ };
77
82
  cli: {
78
83
  meta: CLIMeta | Record<string, any>;
79
84
  programs: Record<string, CLIProgramState>;
package/dist/utils.js CHANGED
@@ -82,7 +82,7 @@ const stopService = async (logger, name, service) => {
82
82
  export const stopSingletonServices = async () => {
83
83
  const singletonServices = getSingletonServices();
84
84
  const logger = singletonServices.logger;
85
- // First, stop all addon package singleton services
85
+ // Stop all addon package singleton services
86
86
  const stateMap = getAllPackageStates();
87
87
  if (stateMap.size > 0) {
88
88
  for (const [packageName, packageState] of stateMap) {
@@ -29,6 +29,7 @@ export function createAssistantUIChannel(parent) {
29
29
  return parent.state;
30
30
  },
31
31
  close: () => parent.close(),
32
+ sendBinary: (data) => parent.sendBinary(data),
32
33
  send: (event) => {
33
34
  switch (event.type) {
34
35
  case 'text-delta':
@@ -39,6 +40,14 @@ export function createAssistantUIChannel(parent) {
39
40
  ensureStarted();
40
41
  sendChunk({ type: 'reasoning-delta', delta: event.text });
41
42
  break;
43
+ case 'audio-delta':
44
+ ensureStarted();
45
+ sendChunk({ type: 'audio-delta', data: event });
46
+ break;
47
+ case 'audio-done':
48
+ ensureStarted();
49
+ sendChunk({ type: 'audio-done', data: event });
50
+ break;
42
51
  case 'tool-call':
43
52
  ensureStarted();
44
53
  hasToolCalls = true;
@@ -126,21 +135,34 @@ export function parseAssistantUIInput(input, defaults) {
126
135
  throw new Error('No user message found in assistant-ui input');
127
136
  }
128
137
  let messageText;
138
+ let attachments;
139
+ const extractFromParts = (parts) => {
140
+ const textParts = parts.filter((p) => p.type === 'text');
141
+ messageText = textParts.map((p) => String(p.text)).join(' ') || '';
142
+ const nonTextParts = parts.filter((p) => p.type === 'image' || p.type === 'file');
143
+ if (nonTextParts.length > 0) {
144
+ attachments = nonTextParts.map((p) => ({
145
+ type: p.type,
146
+ data: p.data,
147
+ url: p.url,
148
+ mediaType: p.mediaType,
149
+ filename: p.filename,
150
+ }));
151
+ }
152
+ };
129
153
  if (typeof lastUserMessage.content === 'string') {
130
154
  messageText = lastUserMessage.content;
131
155
  }
132
156
  else if (Array.isArray(lastUserMessage.parts)) {
133
- const textPart = lastUserMessage.parts.find((p) => p.type === 'text');
134
- messageText = textPart ? String(textPart.text) : '';
157
+ extractFromParts(lastUserMessage.parts);
135
158
  }
136
159
  else if (Array.isArray(lastUserMessage.content)) {
137
- const textPart = lastUserMessage.content.find((p) => p.type === 'text');
138
- messageText = textPart ? String(textPart.text) : '';
160
+ extractFromParts(lastUserMessage.content);
139
161
  }
140
162
  else {
141
163
  messageText = '';
142
164
  }
143
165
  const threadId = input.threadId ?? input.id ?? randomUUID();
144
166
  const resourceId = defaults?.resourceId ?? 'default';
145
- return { message: messageText, threadId, resourceId };
167
+ return { message: messageText, threadId, resourceId, attachments };
146
168
  }
@@ -175,8 +175,19 @@ function sanitizeToolMessages(messages) {
175
175
  }
176
176
  function estimateTokens(msg) {
177
177
  let chars = 0;
178
- if (msg.content)
179
- chars += msg.content.length;
178
+ if (msg.content) {
179
+ if (typeof msg.content === 'string') {
180
+ chars += msg.content.length;
181
+ }
182
+ else {
183
+ for (const part of msg.content) {
184
+ if (part.type === 'text')
185
+ chars += part.text.length;
186
+ else
187
+ chars += 1000;
188
+ }
189
+ }
190
+ }
180
191
  if (msg.toolCalls)
181
192
  chars += JSON.stringify(msg.toolCalls).length;
182
193
  if (msg.toolResults)
@@ -73,6 +73,7 @@ export function createScopedChannel(parent, agentName, session) {
73
73
  return capturedApproval;
74
74
  },
75
75
  close: () => { },
76
+ sendBinary: (data) => parent.sendBinary(data),
76
77
  send: (event) => {
77
78
  if (event.type === 'done')
78
79
  return;
@@ -307,10 +308,22 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
307
308
  });
308
309
  }
309
310
  const contextMessages = await loadContextMessages(memoryConfig, storage, input, workingMemoryJsonSchema);
311
+ const userContent = input.attachments?.length
312
+ ? [
313
+ { type: 'text', text: input.message },
314
+ ...input.attachments.map((a) => ({
315
+ type: a.type,
316
+ data: a.data,
317
+ url: a.url,
318
+ mediaType: a.mediaType,
319
+ ...(a.filename ? { filename: a.filename } : {}),
320
+ })),
321
+ ]
322
+ : input.message;
310
323
  const userMessage = {
311
324
  id: randomUUID(),
312
325
  role: 'user',
313
- content: input.message,
326
+ content: userContent,
314
327
  createdAt: new Date(),
315
328
  };
316
329
  const allMessages = [...contextMessages, ...messages, userMessage];
@@ -57,6 +57,7 @@ function createPersistingChannel(parent, storage, threadId) {
57
57
  },
58
58
  flush: flushStep,
59
59
  close: () => parent.close(),
60
+ sendBinary: (data) => parent.sendBinary(data),
60
61
  send: (event) => {
61
62
  if (storage) {
62
63
  switch (event.type) {
@@ -345,8 +346,15 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
345
346
  allEvents,
346
347
  state,
347
348
  });
348
- if (result != null)
349
+ if (result == null)
350
+ return;
351
+ if (Array.isArray(result)) {
352
+ for (const r of result)
353
+ await next(r);
354
+ }
355
+ else {
349
356
  await next(result);
357
+ }
350
358
  };
351
359
  });
352
360
  const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta');
@@ -586,8 +594,15 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
586
594
  allEvents,
587
595
  state,
588
596
  });
589
- if (result != null)
597
+ if (result == null)
598
+ return;
599
+ if (Array.isArray(result)) {
600
+ for (const r of result)
601
+ await next(r);
602
+ }
603
+ else {
590
604
  await next(result);
605
+ }
591
606
  };
592
607
  });
593
608
  const allChannelMiddleware = combineChannelMiddleware('agent', `stream:${run.agentName}`, {
@@ -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,18 @@ 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[];
46
69
  }
47
70
  export interface AIAgentOutput {
48
71
  runId: string;
@@ -77,7 +100,7 @@ export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> =
77
100
  event: AIStreamEvent;
78
101
  allEvents: readonly AIStreamEvent[];
79
102
  state: State;
80
- }) => Promise<AIStreamEvent | null> | AIStreamEvent | null;
103
+ }) => Promise<AIStreamEvent | AIStreamEvent[] | null> | AIStreamEvent | AIStreamEvent[] | null;
81
104
  modifyOutput?: (services: Services, ctx: {
82
105
  text: string;
83
106
  messages: AIMessage[];
@@ -235,6 +258,16 @@ export type AIStreamEvent = {
235
258
  message: string;
236
259
  agent?: string;
237
260
  session?: string;
261
+ } | {
262
+ type: 'audio-delta';
263
+ data: string;
264
+ format: string;
265
+ agent?: string;
266
+ session?: string;
267
+ } | {
268
+ type: 'audio-done';
269
+ agent?: string;
270
+ session?: string;
238
271
  } | {
239
272
  type: 'suspended';
240
273
  reason: 'rpc-missing';
@@ -4,4 +4,4 @@ export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
4
4
  export { createAssistantUIChannel, parseAssistantUIInput, } from './ai-agent-assistant-ui.js';
5
5
  export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, } from './ai-agent-prepare.js';
6
6
  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';
7
+ export type { AIAgentInput, AIAgentInputAttachment, AIAgentMeta, AIAgentMemoryConfig, AIAgentStep, AIContentPart, AgentRunRow, AgentRunService, AgentRunState, AIMessage, AIStreamChannel, AIStreamEvent, AIThread, CoreAIAgent, PendingApproval, PikkuAIMiddlewareHooks, } from './ai-agent.types.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
+ };
@@ -91,7 +91,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
91
91
  },
92
92
  });
93
93
  };
94
- return async (rawData) => {
94
+ const onMessage = async (rawData) => {
95
95
  let result;
96
96
  let processed = false;
97
97
  // Route-specific handling
@@ -134,4 +134,14 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
134
134
  }
135
135
  return result;
136
136
  };
137
+ const onBinaryMessage = channelConfig.onBinaryMessage
138
+ ? async (data) => {
139
+ if (!validateAuth(requiresSession, userSession?.get(), channelConfig)) {
140
+ logger.error(`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for binary message`);
141
+ return;
142
+ }
143
+ return channelConfig.onBinaryMessage(services, data, channelHandler.getChannel());
144
+ }
145
+ : undefined;
146
+ return { onMessage, onBinaryMessage };
137
147
  };
@@ -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,7 @@
1
1
  import type { PikkuError } from '../../errors/error-handler.js';
2
2
  import type { HTTPFunctionMetaInputTypes, PikkuHTTPRequest, PikkuHTTPResponse } from '../http/http.types.js';
3
3
  import type { CorePikkuMiddleware, MiddlewareMetadata, PermissionMetadata } from '../../types/core.types.js';
4
+ export type BinaryData = ArrayBuffer | Uint8Array;
4
5
  export type CorePikkuChannelMiddleware<Services = any, Event = unknown> = (services: Services, event: Event, next: (event: Event | Event[] | null) => Promise<void> | void) => Promise<void> | void;
5
6
  export type CorePikkuChannelMiddlewareFactory<In = any, Services = any, Event = unknown> = (input: In) => CorePikkuChannelMiddleware<Services, Event>;
6
7
  import type { CorePermissionGroup, CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuFunctionSessionless, CorePikkuPermission } from '../../function/functions.types.js';
@@ -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,6 +69,8 @@ export type CoreChannel<ChannelData, Channel extends string, ChannelConnect = Co
67
69
  channelMiddleware?: Array<CorePikkuChannelMiddleware | CorePikkuChannelMiddlewareFactory>;
68
70
  permissions?: CorePermissionGroup<PikkuPermission>;
69
71
  auth?: boolean;
72
+ binary?: boolean | null;
73
+ onBinaryMessage?: (services: any, data: BinaryData, channel: PikkuChannel<ChannelData, any>) => Promise<BinaryData | void> | BinaryData | void;
70
74
  docs?: Partial<{
71
75
  description: string;
72
76
  response: string;
@@ -79,11 +83,13 @@ export interface PikkuChannel<OpeningData, out Out> {
79
83
  channelId: string;
80
84
  openingData: OpeningData;
81
85
  send(data: Out, isBinary?: boolean): Promise<void> | void;
86
+ sendBinary(data: BinaryData): Promise<void> | void;
82
87
  close(): Promise<void> | void;
83
88
  state: 'initial' | 'open' | 'closed';
84
89
  }
85
90
  export interface PikkuChannelHandler<OpeningData = unknown, Out = unknown> {
86
91
  send(message: Out, isBinary?: boolean): Promise<void> | void;
92
+ sendBinary(data: BinaryData): Promise<void> | void;
87
93
  getChannel(): PikkuChannel<OpeningData, Out>;
88
94
  }
89
95
  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';
@@ -1,15 +1,22 @@
1
+ import type { BinaryData } from '../channel.types.js';
1
2
  import { PikkuAbstractChannelHandler } from '../pikku-abstract-channel-handler.js';
2
3
  export declare class PikkuLocalChannelHandler<OpeningData = unknown, Out = unknown> extends PikkuAbstractChannelHandler<OpeningData, Out> {
3
4
  private onMessageCallback?;
5
+ private onBinaryMessageCallback?;
4
6
  private openCallBack?;
5
7
  private closeCallback?;
6
8
  private sendCallback?;
9
+ private sendBinaryCallback?;
7
10
  registerOnOpen(callback: () => void): void;
8
11
  open(): void;
9
12
  registerOnMessage(callback: (data: any) => Promise<unknown>): void;
10
13
  message(data: unknown): Promise<unknown>;
14
+ registerOnBinaryMessage(callback: (data: BinaryData) => Promise<BinaryData | void> | BinaryData | void): void;
15
+ binaryMessage(data: BinaryData): Promise<BinaryData | void>;
11
16
  registerOnClose(callback: () => void): void;
12
17
  close(): void;
13
18
  registerOnSend(send: (message: Out) => void): void;
14
19
  send(message: Out, isBinary?: boolean): void;
20
+ registerOnSendBinary(send: (data: BinaryData) => void): void;
21
+ sendBinary(data: BinaryData): void;
15
22
  }
@@ -1,9 +1,11 @@
1
1
  import { PikkuAbstractChannelHandler } from '../pikku-abstract-channel-handler.js';
2
2
  export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
3
3
  onMessageCallback;
4
+ onBinaryMessageCallback;
4
5
  openCallBack;
5
6
  closeCallback;
6
7
  sendCallback;
8
+ sendBinaryCallback;
7
9
  registerOnOpen(callback) {
8
10
  this.openCallBack = callback;
9
11
  }
@@ -19,6 +21,12 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
19
21
  async message(data) {
20
22
  return this.onMessageCallback?.(data);
21
23
  }
24
+ registerOnBinaryMessage(callback) {
25
+ this.onBinaryMessageCallback = callback;
26
+ }
27
+ async binaryMessage(data) {
28
+ return this.onBinaryMessageCallback?.(data);
29
+ }
22
30
  registerOnClose(callback) {
23
31
  this.closeCallback = callback;
24
32
  }
@@ -38,4 +46,13 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
38
46
  }
39
47
  return this.sendCallback?.(message, isBinary);
40
48
  }
49
+ registerOnSendBinary(send) {
50
+ this.sendBinaryCallback = send;
51
+ }
52
+ sendBinary(data) {
53
+ if (!this.sendBinaryCallback) {
54
+ throw new Error('No sendBinary callback registered');
55
+ }
56
+ this.sendBinaryCallback(data);
57
+ }
41
58
  }
@@ -99,7 +99,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
99
99
  }
100
100
  await closeServices();
101
101
  });
102
- const onMessage = processMessageHandlers(services, channelConfig, channelHandler, userSession);
102
+ const { onMessage, onBinaryMessage } = processMessageHandlers(services, channelConfig, channelHandler, userSession);
103
103
  channelHandler.registerOnMessage(async (data) => {
104
104
  try {
105
105
  const result = await onMessage(data);
@@ -110,6 +110,19 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
110
110
  channel.send({ error: e.message || 'Unknown error' });
111
111
  }
112
112
  });
113
+ if (onBinaryMessage) {
114
+ channelHandler.registerOnBinaryMessage(async (data) => {
115
+ try {
116
+ const result = await onBinaryMessage(data);
117
+ if (result) {
118
+ await channel.sendBinary(result);
119
+ }
120
+ }
121
+ catch (e) {
122
+ singletonServices.logger.error(`Error handling binary message: ${e.message}`);
123
+ }
124
+ });
125
+ }
113
126
  }
114
127
  catch (e) {
115
128
  handleHTTPError(e, http, channelId, singletonServices.logger, logWarningsForStatusCodes, respondWith404, bubbleErrors);