@dobby.ai/dobby 0.1.0 → 0.1.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 (156) hide show
  1. package/README.md +84 -39
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/commands/doctor.js +81 -2
  5. package/dist/src/cli/commands/extension.js +3 -1
  6. package/dist/src/cli/commands/init.js +43 -173
  7. package/dist/src/cli/commands/topology.js +38 -14
  8. package/dist/src/cli/program.js +15 -137
  9. package/dist/src/cli/shared/config-io.js +3 -31
  10. package/dist/src/cli/shared/config-mutators.js +33 -9
  11. package/dist/src/cli/shared/configure-sections.js +52 -12
  12. package/dist/src/cli/shared/init-catalog.js +89 -46
  13. package/dist/src/cli/shared/local-extension-specs.js +85 -0
  14. package/dist/src/cli/shared/schema-prompts.js +26 -2
  15. package/dist/src/core/gateway.js +3 -1
  16. package/dist/src/core/routing.js +53 -38
  17. package/dist/src/core/types.js +2 -0
  18. package/dist/src/cron/config.js +2 -2
  19. package/dist/src/cron/service.js +87 -23
  20. package/dist/src/cron/store.js +1 -1
  21. package/dist/src/main.js +0 -0
  22. package/dist/src/shared/dobby-repo.js +40 -0
  23. package/package.json +11 -4
  24. package/.env.example +0 -9
  25. package/AGENTS.md +0 -267
  26. package/ROADMAP.md +0 -34
  27. package/config/cron.example.json +0 -9
  28. package/config/gateway.example.json +0 -128
  29. package/config/models.custom.example.json +0 -27
  30. package/dist/src/agent/tests/event-forwarder.test.js +0 -113
  31. package/dist/src/cli/shared/config-path.js +0 -207
  32. package/dist/src/cli/shared/init-models-file.js +0 -65
  33. package/dist/src/cli/shared/presets.js +0 -86
  34. package/dist/src/cli/tests/config-command.test.js +0 -42
  35. package/dist/src/cli/tests/config-io.test.js +0 -64
  36. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  37. package/dist/src/cli/tests/config-path.test.js +0 -21
  38. package/dist/src/cli/tests/discord-config.test.js +0 -23
  39. package/dist/src/cli/tests/doctor.test.js +0 -107
  40. package/dist/src/cli/tests/init-catalog.test.js +0 -87
  41. package/dist/src/cli/tests/presets.test.js +0 -41
  42. package/dist/src/cli/tests/program-options.test.js +0 -92
  43. package/dist/src/cli/tests/routing-config.test.js +0 -199
  44. package/dist/src/cli/tests/routing-legacy.test.js +0 -191
  45. package/dist/src/core/tests/control-command.test.js +0 -17
  46. package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
  47. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  48. package/dist/src/core/tests/typing-controller.test.js +0 -103
  49. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  50. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  51. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  52. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  53. package/docs/MVP.md +0 -135
  54. package/docs/RUNBOOK.md +0 -242
  55. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  56. package/plugins/connector-discord/dobby.manifest.json +0 -18
  57. package/plugins/connector-discord/index.js +0 -1
  58. package/plugins/connector-discord/package-lock.json +0 -360
  59. package/plugins/connector-discord/package.json +0 -38
  60. package/plugins/connector-discord/src/connector.ts +0 -350
  61. package/plugins/connector-discord/src/contribution.ts +0 -21
  62. package/plugins/connector-discord/src/mapper.ts +0 -102
  63. package/plugins/connector-discord/tsconfig.json +0 -19
  64. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  65. package/plugins/connector-feishu/index.js +0 -1
  66. package/plugins/connector-feishu/package-lock.json +0 -618
  67. package/plugins/connector-feishu/package.json +0 -38
  68. package/plugins/connector-feishu/src/connector.ts +0 -343
  69. package/plugins/connector-feishu/src/contribution.ts +0 -26
  70. package/plugins/connector-feishu/src/mapper.ts +0 -401
  71. package/plugins/connector-feishu/tsconfig.json +0 -19
  72. package/plugins/plugin-sdk/index.d.ts +0 -261
  73. package/plugins/plugin-sdk/index.js +0 -1
  74. package/plugins/plugin-sdk/package-lock.json +0 -12
  75. package/plugins/plugin-sdk/package.json +0 -22
  76. package/plugins/provider-claude/dobby.manifest.json +0 -17
  77. package/plugins/provider-claude/index.js +0 -1
  78. package/plugins/provider-claude/package-lock.json +0 -3398
  79. package/plugins/provider-claude/package.json +0 -39
  80. package/plugins/provider-claude/src/contribution.ts +0 -1018
  81. package/plugins/provider-claude/tsconfig.json +0 -19
  82. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  83. package/plugins/provider-claude-cli/index.js +0 -1
  84. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  85. package/plugins/provider-claude-cli/package.json +0 -38
  86. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  87. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  88. package/plugins/provider-pi/dobby.manifest.json +0 -17
  89. package/plugins/provider-pi/index.js +0 -1
  90. package/plugins/provider-pi/package-lock.json +0 -3877
  91. package/plugins/provider-pi/package.json +0 -40
  92. package/plugins/provider-pi/src/contribution.ts +0 -476
  93. package/plugins/provider-pi/tsconfig.json +0 -19
  94. package/plugins/sandbox-core/boxlite.js +0 -1
  95. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  96. package/plugins/sandbox-core/docker.js +0 -1
  97. package/plugins/sandbox-core/package-lock.json +0 -136
  98. package/plugins/sandbox-core/package.json +0 -39
  99. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  100. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  101. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  102. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  103. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  104. package/plugins/sandbox-core/tsconfig.json +0 -19
  105. package/scripts/local-extensions.mjs +0 -168
  106. package/src/agent/event-forwarder.ts +0 -414
  107. package/src/cli/commands/config.ts +0 -328
  108. package/src/cli/commands/configure.ts +0 -92
  109. package/src/cli/commands/cron.ts +0 -410
  110. package/src/cli/commands/doctor.ts +0 -230
  111. package/src/cli/commands/extension.ts +0 -205
  112. package/src/cli/commands/init.ts +0 -396
  113. package/src/cli/commands/start.ts +0 -223
  114. package/src/cli/commands/topology.ts +0 -383
  115. package/src/cli/index.ts +0 -9
  116. package/src/cli/program.ts +0 -465
  117. package/src/cli/shared/config-io.ts +0 -277
  118. package/src/cli/shared/config-mutators.ts +0 -440
  119. package/src/cli/shared/config-schema.ts +0 -228
  120. package/src/cli/shared/config-types.ts +0 -121
  121. package/src/cli/shared/configure-sections.ts +0 -551
  122. package/src/cli/shared/discord-config.ts +0 -14
  123. package/src/cli/shared/init-catalog.ts +0 -189
  124. package/src/cli/shared/init-models-file.ts +0 -77
  125. package/src/cli/shared/runtime.ts +0 -33
  126. package/src/cli/shared/schema-prompts.ts +0 -414
  127. package/src/cli/tests/config-command.test.ts +0 -56
  128. package/src/cli/tests/config-io.test.ts +0 -92
  129. package/src/cli/tests/config-mutators.test.ts +0 -59
  130. package/src/cli/tests/doctor.test.ts +0 -120
  131. package/src/cli/tests/init-catalog.test.ts +0 -96
  132. package/src/cli/tests/program-options.test.ts +0 -113
  133. package/src/cli/tests/routing-config.test.ts +0 -209
  134. package/src/core/control-command.ts +0 -12
  135. package/src/core/dedup-store.ts +0 -103
  136. package/src/core/gateway.ts +0 -607
  137. package/src/core/routing.ts +0 -379
  138. package/src/core/runtime-registry.ts +0 -141
  139. package/src/core/tests/control-command.test.ts +0 -20
  140. package/src/core/tests/runtime-registry.test.ts +0 -140
  141. package/src/core/tests/typing-controller.test.ts +0 -129
  142. package/src/core/types.ts +0 -318
  143. package/src/core/typing-controller.ts +0 -119
  144. package/src/cron/config.ts +0 -154
  145. package/src/cron/schedule.ts +0 -61
  146. package/src/cron/service.ts +0 -249
  147. package/src/cron/store.ts +0 -155
  148. package/src/cron/types.ts +0 -60
  149. package/src/extension/loader.ts +0 -145
  150. package/src/extension/manager.ts +0 -355
  151. package/src/extension/manifest.ts +0 -26
  152. package/src/extension/registry.ts +0 -229
  153. package/src/main.ts +0 -8
  154. package/src/sandbox/executor.ts +0 -44
  155. package/src/sandbox/host-executor.ts +0 -118
  156. package/tsconfig.json +0 -18
@@ -1,607 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import type { ImageContent } from "@mariozechner/pi-ai";
3
- import { EventForwarder } from "../agent/event-forwarder.js";
4
- import type { Executor } from "../sandbox/executor.js";
5
- import { parseControlCommand, type ControlCommand } from "./control-command.js";
6
- import type { DedupStore } from "./dedup-store.js";
7
- import { BindingResolver, RouteResolver } from "./routing.js";
8
- import { RuntimeRegistry } from "./runtime-registry.js";
9
- import { createTypingKeepAliveController } from "./typing-controller.js";
10
- import type {
11
- BindingResolution,
12
- ConnectorPlugin,
13
- GatewayAgentRuntime,
14
- GatewayConfig,
15
- GatewayLogger,
16
- InboundAttachment,
17
- InboundEnvelope,
18
- Platform,
19
- PromptPayload,
20
- ProviderInstance,
21
- RouteResolution,
22
- } from "./types.js";
23
-
24
- interface GatewayOptions {
25
- config: GatewayConfig;
26
- connectors: ConnectorPlugin[];
27
- providers: Map<string, ProviderInstance>;
28
- executors: Map<string, Executor>;
29
- routeResolver: RouteResolver;
30
- bindingResolver: BindingResolver;
31
- dedupStore: DedupStore;
32
- runtimeRegistry: RuntimeRegistry;
33
- logger: GatewayLogger;
34
- }
35
-
36
- interface MessageHandlingOptions {
37
- origin: "connector" | "scheduled";
38
- useDedup: boolean;
39
- stateless: boolean;
40
- includeReplyTo: boolean;
41
- conversationKeyOverride?: string;
42
- routeIdOverride?: string;
43
- sessionPolicy?: "shared-session" | "ephemeral";
44
- timeoutMs?: number;
45
- }
46
-
47
- interface StopControlEvent {
48
- type: "stop";
49
- connectorId: string;
50
- platform: Platform;
51
- accountId: string;
52
- chatId: string;
53
- threadId?: string;
54
- }
55
-
56
- interface ResolvedMessageRoute {
57
- route: RouteResolution;
58
- routeId: string;
59
- binding?: BindingResolution;
60
- }
61
-
62
- export interface ScheduledExecutionRequest {
63
- jobId: string;
64
- runId: string;
65
- connectorId: string;
66
- routeId: string;
67
- channelId: string;
68
- threadId?: string;
69
- prompt: string;
70
- timeoutMs?: number;
71
- }
72
-
73
- function isImageAttachment(attachment: InboundAttachment): boolean {
74
- return Boolean(attachment.mimeType?.startsWith("image/") && attachment.localPath);
75
- }
76
-
77
- function dedupKey(message: InboundEnvelope): string {
78
- return `${message.connectorId}:${message.platform}:${message.accountId}:${message.chatId}:${message.messageId}`;
79
- }
80
-
81
- function conversationKey(message: InboundEnvelope): string {
82
- return `${message.connectorId}:${message.platform}:${message.accountId}:${message.chatId}:${message.threadId ?? "root"}`;
83
- }
84
-
85
- export class Gateway {
86
- private readonly connectorsById = new Map<string, ConnectorPlugin>();
87
- private started = false;
88
-
89
- constructor(private readonly options: GatewayOptions) {
90
- for (const connector of options.connectors) {
91
- this.connectorsById.set(connector.id, connector);
92
- }
93
- }
94
-
95
- async start(): Promise<void> {
96
- if (this.started) return;
97
-
98
- await this.options.dedupStore.load();
99
- this.options.dedupStore.startAutoFlush();
100
-
101
- for (const connector of this.options.connectors) {
102
- await connector.start({
103
- emitInbound: async (message) => this.handleInbound(message),
104
- emitControl: async (event) => this.handleControl(event),
105
- });
106
- }
107
-
108
- this.started = true;
109
- }
110
-
111
- async stop(): Promise<void> {
112
- if (!this.started) return;
113
-
114
- for (const connector of this.options.connectors) {
115
- await connector.stop();
116
- }
117
-
118
- this.options.dedupStore.stopAutoFlush();
119
- await this.options.dedupStore.flush();
120
- await this.options.runtimeRegistry.closeAll();
121
-
122
- this.started = false;
123
- }
124
-
125
- async handleScheduled(request: ScheduledExecutionRequest): Promise<void> {
126
- const connector = this.connectorsById.get(request.connectorId);
127
- if (!connector) {
128
- throw new Error(`No connector found for scheduled run '${request.runId}' (${request.connectorId})`);
129
- }
130
-
131
- const syntheticInbound: InboundEnvelope = {
132
- connectorId: request.connectorId,
133
- platform: connector.platform,
134
- accountId: request.connectorId,
135
- source: {
136
- type: "chat",
137
- id: request.channelId,
138
- },
139
- chatId: request.channelId,
140
- ...(request.threadId ? { threadId: request.threadId } : {}),
141
- messageId: `cron:${request.runId}`,
142
- userId: "cron",
143
- userName: "cron",
144
- text: request.prompt,
145
- attachments: [],
146
- timestampMs: Date.now(),
147
- raw: {
148
- type: "cron",
149
- jobId: request.jobId,
150
- runId: request.runId,
151
- },
152
- isDirectMessage: false,
153
- mentionedBot: true,
154
- };
155
-
156
- await this.handleMessage(syntheticInbound, {
157
- origin: "scheduled",
158
- useDedup: true,
159
- stateless: true,
160
- includeReplyTo: false,
161
- conversationKeyOverride: `cron:${request.runId}`,
162
- routeIdOverride: request.routeId,
163
- sessionPolicy: "ephemeral",
164
- ...(request.timeoutMs !== undefined ? { timeoutMs: request.timeoutMs } : {}),
165
- });
166
- }
167
-
168
- private outboundBaseFromInbound(message: InboundEnvelope): {
169
- platform: Platform;
170
- accountId: string;
171
- chatId: string;
172
- threadId?: string;
173
- } {
174
- return {
175
- platform: message.platform,
176
- accountId: message.accountId,
177
- chatId: message.chatId,
178
- ...(message.threadId ? { threadId: message.threadId } : {}),
179
- };
180
- }
181
-
182
- private outboundBaseFromControl(event: StopControlEvent): {
183
- platform: Platform;
184
- accountId: string;
185
- chatId: string;
186
- threadId?: string;
187
- } {
188
- return {
189
- platform: event.platform,
190
- accountId: event.accountId,
191
- chatId: event.chatId,
192
- ...(event.threadId ? { threadId: event.threadId } : {}),
193
- };
194
- }
195
-
196
- private async sendCommandReply(
197
- connector: ConnectorPlugin,
198
- message: InboundEnvelope,
199
- text: string,
200
- ): Promise<void> {
201
- await connector.send({
202
- ...this.outboundBaseFromInbound(message),
203
- mode: "create",
204
- text,
205
- });
206
- }
207
-
208
- private async handleInbound(message: InboundEnvelope): Promise<void> {
209
- await this.handleMessage(message, {
210
- origin: "connector",
211
- useDedup: true,
212
- stateless: false,
213
- includeReplyTo: true,
214
- sessionPolicy: "shared-session",
215
- });
216
- }
217
-
218
- private resolveMessageRoute(message: InboundEnvelope, handling: MessageHandlingOptions): ResolvedMessageRoute | null {
219
- const overrideRouteId = handling.routeIdOverride?.trim();
220
- if (overrideRouteId) {
221
- const route = this.options.routeResolver.resolve(overrideRouteId);
222
- if (!route) {
223
- return null;
224
- }
225
-
226
- return {
227
- routeId: overrideRouteId,
228
- route,
229
- };
230
- }
231
-
232
- const binding = this.options.bindingResolver.resolve(message.connectorId, message.source);
233
- if (!binding) {
234
- if (handling.origin === "connector") {
235
- this.options.logger.debug(
236
- {
237
- connectorId: message.connectorId,
238
- sourceType: message.source.type,
239
- sourceId: message.source.id,
240
- },
241
- "Ignoring inbound message from unbound source",
242
- );
243
- }
244
- return null;
245
- }
246
-
247
- const route = this.options.routeResolver.resolve(binding.config.route);
248
- if (!route) {
249
- return null;
250
- }
251
-
252
- return {
253
- routeId: binding.config.route,
254
- route,
255
- binding,
256
- };
257
- }
258
-
259
- private async handleMessage(message: InboundEnvelope, handling: MessageHandlingOptions): Promise<void> {
260
- const connector = this.connectorsById.get(message.connectorId);
261
- if (!connector) {
262
- this.options.logger.warn({ connectorId: message.connectorId }, "No connector found for inbound message");
263
- return;
264
- }
265
-
266
- if (handling.useDedup) {
267
- const key = dedupKey(message);
268
- if (this.options.dedupStore.has(key)) {
269
- this.options.logger.debug({ dedupKey: key }, "Skipping duplicate message");
270
- return;
271
- }
272
- this.options.dedupStore.add(key);
273
- }
274
-
275
- const resolvedRoute = this.resolveMessageRoute(message, handling);
276
- if (!resolvedRoute) {
277
- if (handling.routeIdOverride) {
278
- await connector.send({
279
- ...this.outboundBaseFromInbound(message),
280
- mode: "create",
281
- ...(handling.includeReplyTo ? { replyToMessageId: message.messageId } : {}),
282
- text: `No route configured for route '${handling.routeIdOverride}'.`,
283
- });
284
- }
285
- return;
286
- }
287
-
288
- if (handling.origin === "connector") {
289
- const command = parseControlCommand(message.text);
290
- if (command) {
291
- try {
292
- await this.handleCommand(connector, message, command, resolvedRoute.route);
293
- } catch (error) {
294
- this.options.logger.error({ err: error, messageId: message.messageId }, "Failed to handle control command");
295
- await this.sendCommandReply(
296
- connector,
297
- message,
298
- `Error: ${error instanceof Error ? error.message : String(error)}`,
299
- );
300
- }
301
- return;
302
- }
303
- }
304
-
305
- const { route } = resolvedRoute;
306
- if (route.profile.mentions === "required" && !message.isDirectMessage && !message.mentionedBot) {
307
- this.options.logger.debug(
308
- {
309
- sourceType: message.source.type,
310
- sourceId: message.source.id,
311
- routeId: route.routeId,
312
- },
313
- "Ignoring non-mention message",
314
- );
315
- return;
316
- }
317
-
318
- const providerId = route.profile.provider;
319
- const sandboxId = route.profile.sandbox;
320
- const provider = this.options.providers.get(providerId);
321
- const executor = this.options.executors.get(sandboxId);
322
-
323
- if (!provider || !executor) {
324
- await connector.send({
325
- ...this.outboundBaseFromInbound(message),
326
- mode: "create",
327
- ...(handling.includeReplyTo ? { replyToMessageId: message.messageId } : {}),
328
- text: `Route runtime not available (provider='${providerId}', sandbox='${sandboxId}')`,
329
- });
330
- return;
331
- }
332
-
333
- const convKey = handling.conversationKeyOverride ?? conversationKey(message);
334
-
335
- if (handling.stateless) {
336
- const runtime = await provider.createRuntime({
337
- conversationKey: convKey,
338
- route,
339
- inbound: message,
340
- executor,
341
- ...(handling.sessionPolicy ? { sessionPolicy: handling.sessionPolicy } : {}),
342
- });
343
- try {
344
- await this.processMessage(connector, runtime, route, message, {
345
- includeReplyTo: handling.includeReplyTo,
346
- ...(handling.timeoutMs !== undefined ? { timeoutMs: handling.timeoutMs } : {}),
347
- });
348
- } finally {
349
- runtime.dispose();
350
- }
351
- return;
352
- }
353
-
354
- await this.options.runtimeRegistry.run(
355
- convKey,
356
- async () => {
357
- const runtime = await provider.createRuntime({
358
- conversationKey: convKey,
359
- route,
360
- inbound: message,
361
- executor,
362
- ...(handling.sessionPolicy ? { sessionPolicy: handling.sessionPolicy } : {}),
363
- });
364
-
365
- return {
366
- key: convKey,
367
- routeId: route.routeId,
368
- route: route.profile,
369
- providerId,
370
- sandboxId,
371
- runtime,
372
- close: async () => {
373
- runtime.dispose();
374
- },
375
- };
376
- },
377
- async (runtime) => {
378
- await this.processMessage(connector, runtime.runtime, route, message, {
379
- includeReplyTo: handling.includeReplyTo,
380
- });
381
- },
382
- );
383
- }
384
-
385
- private async processMessage(
386
- connector: ConnectorPlugin,
387
- runtime: GatewayAgentRuntime,
388
- route: RouteResolution,
389
- message: InboundEnvelope,
390
- options: {
391
- includeReplyTo: boolean;
392
- timeoutMs?: number;
393
- },
394
- ): Promise<void> {
395
- this.options.logger.info(
396
- {
397
- connectorId: message.connectorId,
398
- routeId: route.routeId,
399
- sourceType: message.source.type,
400
- sourceId: message.source.id,
401
- chatId: message.chatId,
402
- threadId: message.threadId,
403
- messageId: message.messageId,
404
- },
405
- "Processing inbound message",
406
- );
407
-
408
- const typingController = createTypingKeepAliveController(connector, message, this.options.logger);
409
- let unsubscribe: (() => void) | null = null;
410
- const forwarder = new EventForwarder(connector, message, null, this.options.logger, {
411
- onOutboundActivity: typingController.markVisibleOutput,
412
- });
413
- try {
414
- await typingController.prime();
415
- unsubscribe = runtime.subscribe(forwarder.handleEvent);
416
-
417
- const payload = await this.buildPromptPayload(message);
418
- this.options.logger.info(
419
- {
420
- routeId: route.routeId,
421
- messageId: message.messageId,
422
- rootMessageId: forwarder.primaryMessageId() ?? null,
423
- hasImages: payload.images.length > 0,
424
- textLength: payload.text.length,
425
- },
426
- "Starting provider prompt",
427
- );
428
- await this.promptWithOptionalTimeout(runtime, payload, options.timeoutMs);
429
- this.options.logger.info(
430
- {
431
- routeId: route.routeId,
432
- messageId: message.messageId,
433
- rootMessageId: forwarder.primaryMessageId() ?? null,
434
- },
435
- "Provider prompt finished",
436
- );
437
- await forwarder.finalize();
438
- this.options.logger.info({ routeId: route.routeId, messageId: message.messageId }, "Inbound message processed");
439
- } catch (error) {
440
- this.options.logger.error({ err: error, routeId: route.routeId }, "Failed to process inbound message");
441
- const rootMessageId = forwarder.primaryMessageId();
442
- const canEditExisting = connector.capabilities.updateStrategy === "edit" && rootMessageId !== null;
443
- await connector.send(
444
- canEditExisting
445
- ? {
446
- ...this.outboundBaseFromInbound(message),
447
- mode: "update",
448
- targetMessageId: rootMessageId,
449
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
450
- }
451
- : {
452
- ...this.outboundBaseFromInbound(message),
453
- mode: "create",
454
- ...(
455
- rootMessageId
456
- ? { replyToMessageId: rootMessageId }
457
- : options.includeReplyTo
458
- ? { replyToMessageId: message.messageId }
459
- : {}
460
- ),
461
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
462
- },
463
- );
464
- } finally {
465
- unsubscribe?.();
466
- typingController.stop();
467
- }
468
- }
469
-
470
- private async promptWithOptionalTimeout(
471
- runtime: GatewayAgentRuntime,
472
- payload: PromptPayload,
473
- timeoutMs?: number,
474
- ): Promise<void> {
475
- if (timeoutMs === undefined || timeoutMs <= 0) {
476
- await runtime.prompt(payload.text, payload.images.length > 0 ? { images: payload.images } : undefined);
477
- return;
478
- }
479
-
480
- let timer: NodeJS.Timeout | null = null;
481
- try {
482
- await Promise.race([
483
- runtime.prompt(payload.text, payload.images.length > 0 ? { images: payload.images } : undefined),
484
- new Promise<never>((_, reject) => {
485
- timer = setTimeout(() => {
486
- void runtime.abort().catch(() => {
487
- // Best-effort abort on timeout.
488
- });
489
- reject(new Error(`Cron run timed out after ${timeoutMs}ms`));
490
- }, timeoutMs);
491
- }),
492
- ]);
493
- } finally {
494
- if (timer) {
495
- clearTimeout(timer);
496
- }
497
- }
498
- }
499
-
500
- private async buildPromptPayload(message: InboundEnvelope): Promise<PromptPayload> {
501
- const textParts: string[] = [];
502
- const baseText = message.text.trim();
503
- textParts.push(baseText.length > 0 ? baseText : "(empty message)");
504
-
505
- const images: ImageContent[] = [];
506
- const otherAttachments: string[] = [];
507
-
508
- for (const attachment of message.attachments) {
509
- if (isImageAttachment(attachment) && attachment.localPath && attachment.mimeType) {
510
- try {
511
- const buffer = await readFile(attachment.localPath);
512
- images.push({
513
- type: "image",
514
- mimeType: attachment.mimeType,
515
- data: buffer.toString("base64"),
516
- });
517
- continue;
518
- } catch (error) {
519
- this.options.logger.warn({ err: error, attachment: attachment.localPath }, "Failed to read image attachment");
520
- }
521
- }
522
-
523
- if (attachment.localPath) {
524
- otherAttachments.push(attachment.localPath);
525
- } else if (attachment.remoteUrl) {
526
- otherAttachments.push(attachment.remoteUrl);
527
- }
528
- }
529
-
530
- if (otherAttachments.length > 0) {
531
- textParts.push(`<attachments>\n${otherAttachments.join("\n")}\n</attachments>`);
532
- }
533
-
534
- return {
535
- text: textParts.join("\n\n"),
536
- images,
537
- };
538
- }
539
-
540
- private async handleCommand(
541
- connector: ConnectorPlugin,
542
- message: InboundEnvelope,
543
- command: ControlCommand,
544
- route: RouteResolution,
545
- ): Promise<void> {
546
- const convKey = conversationKey(message);
547
- if (command === "cancel") {
548
- const cancelled = await this.options.runtimeRegistry.cancel(convKey);
549
- this.options.logger.info({ conversationKey: convKey, cancelled }, "Conversation cancel requested");
550
- await this.sendCommandReply(
551
- connector,
552
- message,
553
- cancelled ? "_Cancelled current session tasks._" : "_No active or queued session tasks to cancel._",
554
- );
555
- return;
556
- }
557
-
558
- const providerId = route.profile.provider;
559
- const provider = this.options.providers.get(providerId);
560
- if (!provider) {
561
- throw new Error(`Route provider not available (provider='${providerId}')`);
562
- }
563
- if (!provider.archiveSession) {
564
- throw new Error(`Provider '${providerId}' does not support session archival`);
565
- }
566
-
567
- const hadRuntime = await this.options.runtimeRegistry.reset(convKey);
568
- const archiveResult = await provider.archiveSession({
569
- conversationKey: convKey,
570
- inbound: message,
571
- sessionPolicy: "shared-session",
572
- archivedAtMs: message.timestampMs,
573
- });
574
-
575
- this.options.logger.info(
576
- {
577
- conversationKey: convKey,
578
- providerId,
579
- hadRuntime,
580
- archived: archiveResult.archived,
581
- archivePath: archiveResult.archivePath ?? null,
582
- },
583
- "New session requested",
584
- );
585
-
586
- await this.sendCommandReply(connector, message, "_Started a new session._");
587
- }
588
-
589
- private async handleControl(event: StopControlEvent): Promise<void> {
590
- const convKey = `${event.connectorId}:${event.platform}:${event.accountId}:${event.chatId}:${event.threadId ?? "root"}`;
591
- const connector = this.connectorsById.get(event.connectorId);
592
- const cancelled = await this.options.runtimeRegistry.cancel(convKey);
593
-
594
- this.options.logger.info({ conversationKey: convKey, cancelled }, "Stop requested");
595
- if (!connector) return;
596
-
597
- try {
598
- await connector.send({
599
- ...this.outboundBaseFromControl(event),
600
- mode: "create",
601
- text: cancelled ? "_Cancelled current session tasks._" : "_No active or queued session tasks to cancel._",
602
- });
603
- } catch (error) {
604
- this.options.logger.warn({ err: error, conversationKey: convKey }, "Failed to send stop acknowledgement");
605
- }
606
- }
607
- }