@cogitator-ai/channels 0.2.5 → 0.3.0

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 (129) hide show
  1. package/README.md +141 -39
  2. package/dist/assistant-config.d.ts +1 -0
  3. package/dist/assistant-config.d.ts.map +1 -1
  4. package/dist/assistant-config.js +1 -0
  5. package/dist/assistant-config.js.map +1 -1
  6. package/dist/channels/discord.d.ts +4 -0
  7. package/dist/channels/discord.d.ts.map +1 -1
  8. package/dist/channels/discord.js +123 -59
  9. package/dist/channels/discord.js.map +1 -1
  10. package/dist/channels/slack.d.ts +9 -0
  11. package/dist/channels/slack.d.ts.map +1 -1
  12. package/dist/channels/slack.js +153 -34
  13. package/dist/channels/slack.js.map +1 -1
  14. package/dist/channels/telegram.d.ts +13 -0
  15. package/dist/channels/telegram.d.ts.map +1 -1
  16. package/dist/channels/telegram.js +206 -111
  17. package/dist/channels/telegram.js.map +1 -1
  18. package/dist/channels/terminal.d.ts +10 -0
  19. package/dist/channels/terminal.d.ts.map +1 -1
  20. package/dist/channels/terminal.js +46 -10
  21. package/dist/channels/terminal.js.map +1 -1
  22. package/dist/channels/webchat.d.ts +7 -2
  23. package/dist/channels/webchat.d.ts.map +1 -1
  24. package/dist/channels/webchat.js +112 -50
  25. package/dist/channels/webchat.js.map +1 -1
  26. package/dist/channels/whatsapp.d.ts +19 -3
  27. package/dist/channels/whatsapp.d.ts.map +1 -1
  28. package/dist/channels/whatsapp.js +204 -66
  29. package/dist/channels/whatsapp.js.map +1 -1
  30. package/dist/cron.d.ts +2 -0
  31. package/dist/cron.d.ts.map +1 -0
  32. package/dist/cron.js +9 -0
  33. package/dist/cron.js.map +1 -0
  34. package/dist/envelope.d.ts.map +1 -1
  35. package/dist/envelope.js +7 -6
  36. package/dist/envelope.js.map +1 -1
  37. package/dist/formatters/code-protect.d.ts +2 -0
  38. package/dist/formatters/code-protect.d.ts.map +1 -0
  39. package/dist/formatters/code-protect.js +12 -0
  40. package/dist/formatters/code-protect.js.map +1 -0
  41. package/dist/formatters/markdown.d.ts +1 -1
  42. package/dist/formatters/markdown.d.ts.map +1 -1
  43. package/dist/formatters/markdown.js +79 -21
  44. package/dist/formatters/markdown.js.map +1 -1
  45. package/dist/formatters/whatsapp-markdown.d.ts.map +1 -1
  46. package/dist/formatters/whatsapp-markdown.js +7 -21
  47. package/dist/formatters/whatsapp-markdown.js.map +1 -1
  48. package/dist/gateway.d.ts +32 -3
  49. package/dist/gateway.d.ts.map +1 -1
  50. package/dist/gateway.js +275 -174
  51. package/dist/gateway.js.map +1 -1
  52. package/dist/heartbeat.d.ts +9 -2
  53. package/dist/heartbeat.d.ts.map +1 -1
  54. package/dist/heartbeat.js +121 -84
  55. package/dist/heartbeat.js.map +1 -1
  56. package/dist/inbound-debounce.js +2 -2
  57. package/dist/inbound-debounce.js.map +1 -1
  58. package/dist/index.d.ts +6 -3
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +2 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/media/audio-format.d.ts +3 -0
  63. package/dist/media/audio-format.d.ts.map +1 -0
  64. package/dist/media/audio-format.js +20 -0
  65. package/dist/media/audio-format.js.map +1 -0
  66. package/dist/media/deepgram-stt.d.ts +1 -1
  67. package/dist/media/deepgram-stt.d.ts.map +1 -1
  68. package/dist/media/deepgram-stt.js +4 -3
  69. package/dist/media/deepgram-stt.js.map +1 -1
  70. package/dist/media/media-processor.d.ts +12 -1
  71. package/dist/media/media-processor.d.ts.map +1 -1
  72. package/dist/media/media-processor.js +75 -18
  73. package/dist/media/media-processor.js.map +1 -1
  74. package/dist/media/whisper-api.d.ts.map +1 -1
  75. package/dist/media/whisper-api.js +20 -32
  76. package/dist/media/whisper-api.js.map +1 -1
  77. package/dist/media/whisper-local.d.ts +1 -0
  78. package/dist/media/whisper-local.d.ts.map +1 -1
  79. package/dist/media/whisper-local.js +100 -45
  80. package/dist/media/whisper-local.js.map +1 -1
  81. package/dist/message-queue.d.ts +1 -0
  82. package/dist/message-queue.d.ts.map +1 -1
  83. package/dist/message-queue.js +17 -10
  84. package/dist/message-queue.js.map +1 -1
  85. package/dist/middleware/auto-extract.d.ts +2 -0
  86. package/dist/middleware/auto-extract.d.ts.map +1 -1
  87. package/dist/middleware/auto-extract.js +94 -30
  88. package/dist/middleware/auto-extract.js.map +1 -1
  89. package/dist/middleware/dm-policy.d.ts +2 -1
  90. package/dist/middleware/dm-policy.d.ts.map +1 -1
  91. package/dist/middleware/dm-policy.js +26 -27
  92. package/dist/middleware/dm-policy.js.map +1 -1
  93. package/dist/middleware/owner-commands.d.ts +1 -1
  94. package/dist/middleware/owner-commands.d.ts.map +1 -1
  95. package/dist/middleware/owner-commands.js +8 -7
  96. package/dist/middleware/owner-commands.js.map +1 -1
  97. package/dist/middleware/pairing-codes.d.ts +12 -0
  98. package/dist/middleware/pairing-codes.d.ts.map +1 -0
  99. package/dist/middleware/pairing-codes.js +26 -0
  100. package/dist/middleware/pairing-codes.js.map +1 -0
  101. package/dist/middleware/pairing.d.ts +3 -3
  102. package/dist/middleware/pairing.d.ts.map +1 -1
  103. package/dist/middleware/pairing.js +18 -25
  104. package/dist/middleware/pairing.js.map +1 -1
  105. package/dist/middleware/rate-limit.d.ts +3 -1
  106. package/dist/middleware/rate-limit.d.ts.map +1 -1
  107. package/dist/middleware/rate-limit.js +21 -6
  108. package/dist/middleware/rate-limit.js.map +1 -1
  109. package/dist/runtime-builder.d.ts +11 -0
  110. package/dist/runtime-builder.d.ts.map +1 -1
  111. package/dist/runtime-builder.js +212 -62
  112. package/dist/runtime-builder.js.map +1 -1
  113. package/dist/simple-timer-store.d.ts +6 -0
  114. package/dist/simple-timer-store.d.ts.map +1 -1
  115. package/dist/simple-timer-store.js +8 -1
  116. package/dist/simple-timer-store.js.map +1 -1
  117. package/dist/stream-buffer.d.ts +14 -7
  118. package/dist/stream-buffer.d.ts.map +1 -1
  119. package/dist/stream-buffer.js +129 -84
  120. package/dist/stream-buffer.js.map +1 -1
  121. package/dist/tools/path-guard.d.ts +4 -0
  122. package/dist/tools/path-guard.d.ts.map +1 -0
  123. package/dist/tools/path-guard.js +53 -0
  124. package/dist/tools/path-guard.js.map +1 -0
  125. package/dist/tools/self-config.d.ts +12 -2
  126. package/dist/tools/self-config.d.ts.map +1 -1
  127. package/dist/tools/self-config.js +53 -16
  128. package/dist/tools/self-config.js.map +1 -1
  129. package/package.json +19 -12
package/dist/gateway.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseModel } from '@cogitator-ai/core';
1
2
  import { SessionManager, CompactionService } from '@cogitator-ai/memory';
2
3
  import { StreamBuffer } from './stream-buffer.js';
3
4
  import { adaptMarkdown, chunkMessage, getPlatformLimit } from './formatters/markdown.js';
@@ -5,46 +6,56 @@ import { StatusReactionTracker } from './status-reactions.js';
5
6
  import { InboundDebouncer } from './inbound-debounce.js';
6
7
  import { formatEnvelope } from './envelope.js';
7
8
  import { MessageQueue } from './message-queue.js';
9
+ const DEFAULT_SUMMARY_PROMPT = 'Summarize the following conversation between a user and an assistant. ' +
10
+ 'Preserve facts, decisions, user preferences, open tasks and any commitments. ' +
11
+ 'Write a concise summary in the language of the conversation.';
12
+ const TYPING_INTERVAL_MS = 4000;
13
+ function messageText(message) {
14
+ if (typeof message.content === 'string')
15
+ return message.content;
16
+ return message.content
17
+ .map((part) => (part.type === 'text' ? part.text : `[${part.type}]`))
18
+ .join(' ');
19
+ }
20
+ function isScheduled(msg) {
21
+ const raw = msg.raw;
22
+ return typeof raw === 'object' && raw !== null && 'scheduled' in raw && raw.scheduled === true;
23
+ }
24
+ function dayKey(timestamp) {
25
+ const date = new Date(timestamp);
26
+ return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
27
+ }
8
28
  export class Gateway {
9
29
  config;
10
30
  channels;
11
31
  sessionManager;
12
- compactionService;
13
32
  middlewares;
14
33
  streamConfig;
15
34
  debouncer;
16
35
  messageQueue;
17
36
  hooks;
18
37
  lastMessageTime = new Map();
38
+ threads = new Map();
39
+ inFlight = new Map();
19
40
  running = false;
20
41
  startedAt = null;
21
42
  messageCount = 0;
43
+ messageCountDay = dayKey(Date.now());
22
44
  constructor(config) {
23
45
  this.config = config;
24
46
  this.channels = config.channels;
25
47
  this.middlewares = config.middleware ?? [];
26
48
  this.streamConfig = config.stream ?? { flushInterval: 500, minChunkSize: 20 };
27
- if (config.memory) {
28
- this.sessionManager = config.sessionManager ? null : new SessionManager(config.memory);
29
- this.compactionService = config.session?.compaction
30
- ? new CompactionService({
31
- adapter: config.memory,
32
- summarize: this.createSummarizeFn(),
33
- })
34
- : null;
35
- }
36
- else {
37
- this.sessionManager = null;
38
- this.compactionService = null;
39
- }
40
49
  this.hooks = config.hooks ?? null;
41
- this.debouncer = config.debounce?.enabled
42
- ? new InboundDebouncer(config.debounce, (merged) => this.processMessage(merged))
43
- : null;
50
+ this.sessionManager =
51
+ config.sessionManager ?? (config.memory ? new SessionManager(config.memory) : null);
44
52
  this.messageQueue =
45
53
  config.queueMode && config.queueMode !== 'parallel'
46
- ? new MessageQueue(config.queueMode, (msg) => this.processMessage(msg))
54
+ ? new MessageQueue(config.queueMode, (msg, signal) => this.processMessage(msg, signal))
47
55
  : null;
56
+ this.debouncer = config.debounce?.enabled
57
+ ? new InboundDebouncer(config.debounce, (merged) => this.dispatch(merged))
58
+ : null;
48
59
  for (const channel of this.channels) {
49
60
  channel.onMessage((msg) => this.handleIncoming(msg));
50
61
  }
@@ -54,7 +65,14 @@ export class Gateway {
54
65
  return;
55
66
  this.running = true;
56
67
  this.startedAt = Date.now();
57
- await Promise.all(this.channels.map((ch) => ch.start()));
68
+ const results = await Promise.allSettled(this.channels.map((ch) => ch.start()));
69
+ const failure = results.find((r) => r.status === 'rejected');
70
+ if (failure) {
71
+ this.running = false;
72
+ this.startedAt = null;
73
+ await Promise.allSettled(this.channels.map((ch) => ch.stop()));
74
+ throw failure.reason instanceof Error ? failure.reason : new Error(String(failure.reason));
75
+ }
58
76
  }
59
77
  async stop() {
60
78
  if (!this.running)
@@ -64,130 +82,201 @@ export class Gateway {
64
82
  await this.debouncer.flushAll();
65
83
  this.messageQueue?.dispose();
66
84
  await Promise.all(this.channels.map((ch) => ch.stop()));
85
+ this.startedAt = null;
67
86
  }
68
87
  get stats() {
88
+ this.rollMessageCounter();
69
89
  return {
70
90
  uptime: this.startedAt ? Date.now() - this.startedAt : 0,
71
- activeSessions: 0,
72
- totalSessions: 0,
91
+ activeSessions: this.inFlight.size,
92
+ totalSessions: this.threads.size,
73
93
  messagesToday: this.messageCount,
74
94
  connectedChannels: this.channels.map((ch) => ch.type),
75
95
  };
76
96
  }
97
+ getSessions() {
98
+ return [...this.threads.entries()]
99
+ .map(([threadId, info]) => ({ threadId, ...info, active: this.inFlight.has(threadId) }))
100
+ .sort((a, b) => b.lastActiveAt - a.lastActiveAt);
101
+ }
77
102
  async injectMessage(msg) {
78
103
  return this.handleIncoming(msg);
79
104
  }
105
+ /**
106
+ * Summarize older messages of a conversation thread, keeping `keepRecent` messages intact.
107
+ * Runs regardless of the configured message threshold.
108
+ */
109
+ async compactThread(threadId, agent) {
110
+ const compaction = this.config.session?.compaction;
111
+ if (!this.config.memory || !compaction) {
112
+ return 'Compaction is not configured';
113
+ }
114
+ const targetAgent = agent ?? (await this.resolveAgent({ id: 'system', channelType: 'system' }));
115
+ const service = new CompactionService({
116
+ adapter: this.config.memory,
117
+ summarize: this.createSummarizeFn(targetAgent, compaction),
118
+ });
119
+ const result = await service.compact(threadId, { ...compaction, threshold: 0 });
120
+ if (result.compactedMessages < result.originalMessages) {
121
+ await this.hooks?.emit('session:compacted', { threadId, result });
122
+ return `Compacted ${result.originalMessages} messages into ${result.compactedMessages}`;
123
+ }
124
+ return `Nothing to compact (${result.originalMessages} messages)`;
125
+ }
126
+ async compactIfNeeded(threadId, agent) {
127
+ const memory = this.config.memory;
128
+ const compaction = this.config.session?.compaction;
129
+ if (!memory || !compaction)
130
+ return;
131
+ const entries = await memory.getEntries({ threadId });
132
+ if (!entries.success || entries.data.length < compaction.threshold)
133
+ return;
134
+ await this.compactThread(threadId, agent);
135
+ }
80
136
  async handleIncoming(msg) {
81
137
  if (this.debouncer) {
82
138
  this.debouncer.enqueue(msg);
139
+ return;
83
140
  }
84
- else if (this.messageQueue) {
85
- const threadId = this.getThreadId(msg);
86
- this.messageQueue.push(msg, threadId);
141
+ await this.dispatch(msg);
142
+ }
143
+ async dispatch(msg) {
144
+ if (this.messageQueue) {
145
+ this.messageQueue.push(msg, this.getThreadId(msg));
146
+ return;
87
147
  }
88
- else {
89
- await this.processMessage(msg);
148
+ await this.processMessage(msg);
149
+ }
150
+ rollMessageCounter() {
151
+ const today = dayKey(Date.now());
152
+ if (today !== this.messageCountDay) {
153
+ this.messageCountDay = today;
154
+ this.messageCount = 0;
90
155
  }
91
156
  }
92
- async processMessage(msg) {
93
- try {
94
- const channel = this.channels.find((ch) => ch.type === msg.channelType);
95
- if (!channel)
157
+ async runMiddleware(msg, ctx) {
158
+ let reachedEnd = false;
159
+ const runAll = async (index) => {
160
+ if (index >= this.middlewares.length) {
161
+ reachedEnd = true;
96
162
  return;
97
- const user = {
98
- id: msg.userId,
99
- channelType: msg.channelType,
100
- name: msg.userName,
101
- };
102
- const threadId = this.getThreadId(msg);
103
- const ctx = this.createMiddlewareContext(threadId, user, channel);
104
- let middlewarePassed = true;
105
- if (this.middlewares.length > 0) {
106
- let nextCalled = false;
107
- const runAll = async (index) => {
108
- if (index >= this.middlewares.length) {
109
- nextCalled = true;
110
- return;
111
- }
112
- await this.middlewares[index].handle(msg, ctx, () => runAll(index + 1));
113
- };
114
- await runAll(0);
115
- middlewarePassed = nextCalled;
116
163
  }
117
- if (!middlewarePassed)
164
+ await this.middlewares[index].handle(msg, ctx, () => runAll(index + 1));
165
+ };
166
+ await runAll(0);
167
+ return reachedEnd;
168
+ }
169
+ async processMessage(msg, signal) {
170
+ const channel = this.channels.find((ch) => ch.type === msg.channelType);
171
+ if (!channel)
172
+ return;
173
+ const threadId = this.getThreadId(msg);
174
+ this.inFlight.set(threadId, (this.inFlight.get(threadId) ?? 0) + 1);
175
+ try {
176
+ await this.processWithChannel(msg, channel, threadId, signal);
177
+ }
178
+ catch (error) {
179
+ if (signal?.aborted)
118
180
  return;
119
- await this.hooks?.emit('message:received', { msg, threadId, user });
120
- this.messageCount++;
121
- if (this.config.envelope?.enabled) {
122
- const prevTime = this.lastMessageTime.get(threadId);
123
- msg = { ...msg, text: formatEnvelope(msg, this.config.envelope, prevTime) };
124
- this.lastMessageTime.set(threadId, Date.now());
125
- }
126
- if (this.sessionManager) {
127
- const session = await this.sessionManager.getOrCreate({
128
- userId: msg.userId,
129
- channelType: msg.channelType,
130
- channelId: msg.channelId,
131
- agentId: this.getAgentName(),
132
- });
133
- await this.sessionManager.incrementMessageCount(session.id);
134
- if (this.compactionService && this.config.session?.compaction) {
135
- const compaction = this.config.session.compaction;
136
- if (session.messageCount >= compaction.threshold) {
137
- await this.compactionService.compact(session.id, compaction);
138
- }
139
- }
140
- }
141
- const agent = await this.resolveAgent(user);
142
- const reactionsEnabled = this.config.reactions?.enabled && channel.setReaction;
143
- let tracker;
144
- if (reactionsEnabled) {
145
- tracker = new StatusReactionTracker(channel, msg.channelId, msg.id, this.config.reactions);
146
- tracker.setPhase('queued');
181
+ this.config.onError?.(error instanceof Error ? error : new Error(String(error)), msg);
182
+ }
183
+ finally {
184
+ const remaining = (this.inFlight.get(threadId) ?? 1) - 1;
185
+ if (remaining <= 0)
186
+ this.inFlight.delete(threadId);
187
+ else
188
+ this.inFlight.set(threadId, remaining);
189
+ }
190
+ }
191
+ async processWithChannel(msg, channel, threadId, signal) {
192
+ const user = {
193
+ id: msg.userId,
194
+ channelType: msg.channelType,
195
+ name: msg.userName,
196
+ };
197
+ const ctx = this.createMiddlewareContext(threadId, user, channel);
198
+ if (!(await this.runMiddleware(msg, ctx)))
199
+ return;
200
+ await this.handleAccepted(msg, channel, threadId, user, signal);
201
+ }
202
+ async handleAccepted(originalMsg, channel, threadId, user, signal) {
203
+ let msg = originalMsg;
204
+ await this.hooks?.emit('message:received', { msg, threadId, user });
205
+ this.rollMessageCounter();
206
+ this.messageCount++;
207
+ const known = this.threads.get(threadId);
208
+ this.threads.set(threadId, {
209
+ channelType: msg.channelType,
210
+ userId: msg.userId,
211
+ ...(msg.userName || known?.userName ? { userName: msg.userName ?? known?.userName } : {}),
212
+ messageCount: (known?.messageCount ?? 0) + 1,
213
+ lastActiveAt: Date.now(),
214
+ });
215
+ if (this.config.envelope?.enabled) {
216
+ const prevTime = this.lastMessageTime.get(threadId);
217
+ msg = { ...msg, text: formatEnvelope(msg, this.config.envelope, prevTime) };
218
+ this.lastMessageTime.set(threadId, Date.now());
219
+ }
220
+ const agent = await this.resolveAgent(user);
221
+ if (this.sessionManager) {
222
+ const session = await this.sessionManager.getOrCreate({
223
+ userId: msg.userId,
224
+ channelType: msg.channelType,
225
+ channelId: msg.channelId,
226
+ agentId: agent.name,
227
+ });
228
+ if (session.messageCount === 0) {
229
+ await this.hooks?.emit('session:created', { session, threadId });
147
230
  }
148
- await channel.sendTyping(msg.channelId);
149
- const typingInterval = setInterval(() => {
150
- channel.sendTyping(msg.channelId).catch(() => { });
151
- }, 4000);
231
+ await this.sessionManager.incrementMessageCount?.(session.id);
232
+ }
233
+ if (this.config.memory && this.config.session?.compaction) {
152
234
  try {
153
- tracker?.setPhase('thinking');
154
- if (this.config.stream) {
155
- await this.runStreaming(agent, msg, channel, threadId);
156
- }
157
- else {
158
- await this.runDirect(agent, msg, channel, threadId);
159
- }
160
- tracker?.setPhase('done');
235
+ await this.compactIfNeeded(threadId, agent);
161
236
  }
162
237
  catch (error) {
163
- tracker?.setPhase('error');
164
- throw error;
238
+ this.config.onError?.(error instanceof Error ? error : new Error(String(error)), originalMsg);
239
+ }
240
+ }
241
+ const tracker = this.config.reactions?.enabled && channel.setReaction
242
+ ? new StatusReactionTracker(channel, msg.channelId, msg.id, this.config.reactions)
243
+ : undefined;
244
+ tracker?.setPhase('queued');
245
+ const typingInterval = setInterval(() => {
246
+ channel.sendTyping(msg.channelId).catch(() => { });
247
+ }, TYPING_INTERVAL_MS);
248
+ try {
249
+ await channel.sendTyping(msg.channelId).catch(() => { });
250
+ tracker?.setPhase('thinking');
251
+ if (this.config.stream) {
252
+ await this.runStreaming(agent, msg, channel, threadId, tracker, signal);
165
253
  }
166
- finally {
167
- clearInterval(typingInterval);
168
- tracker?.dispose();
254
+ else {
255
+ await this.runDirect(agent, msg, channel, threadId, tracker, signal);
169
256
  }
257
+ tracker?.setPhase('done');
170
258
  }
171
259
  catch (error) {
172
- if (this.config.onError) {
173
- this.config.onError(error instanceof Error ? error : new Error(String(error)), msg);
174
- }
260
+ tracker?.setPhase('error');
261
+ throw error;
262
+ }
263
+ finally {
264
+ clearInterval(typingInterval);
265
+ tracker?.dispose();
175
266
  }
176
267
  }
177
268
  async extractMedia(msg, agent) {
178
269
  let input = msg.text;
179
270
  let images;
180
271
  if (this.config.mediaProcessor && msg.attachments?.length) {
181
- console.log(`[gateway] Processing ${msg.attachments.length} attachment(s), types: ${msg.attachments.map((a) => a.type).join(', ')}`);
182
272
  const result = await this.config.mediaProcessor.process(msg.attachments, agent.model);
183
- console.log(`[gateway] Media result: images=${result.images.length}, text=${!!result.transcribedText}, notes=${result.systemNotes.length}`);
184
273
  if (result.images.length > 0)
185
274
  images = result.images;
186
275
  if (result.transcribedText) {
187
276
  input = result.transcribedText + (input ? `\n${input}` : '');
188
277
  }
189
278
  if (result.systemNotes.length > 0) {
190
- input = (input || '') + '\n' + result.systemNotes.join('\n');
279
+ input = (input ? `${input}\n` : '') + result.systemNotes.join('\n');
191
280
  }
192
281
  }
193
282
  if (!input && !images?.length) {
@@ -195,43 +284,63 @@ export class Gateway {
195
284
  }
196
285
  return { input, images };
197
286
  }
198
- async runDirect(agent, msg, channel, threadId) {
287
+ buildRunOptions(msg, threadId, input, images, tracker, signal) {
288
+ return {
289
+ input,
290
+ threadId,
291
+ useMemory: !!this.config.memory,
292
+ userId: msg.userId,
293
+ channelType: msg.channelType,
294
+ channelId: msg.channelId,
295
+ ...(images ? { images } : {}),
296
+ ...(signal ? { signal } : {}),
297
+ ...(this.config.runTimeout !== undefined ? { timeout: this.config.runTimeout } : {}),
298
+ ...(tracker
299
+ ? {
300
+ onToolCall: () => tracker.setPhase('tool'),
301
+ onToolResult: () => tracker.setPhase('thinking'),
302
+ }
303
+ : {}),
304
+ };
305
+ }
306
+ async sendChunked(channel, msg, output, replyTo) {
307
+ const chunks = chunkMessage(output, getPlatformLimit(msg.channelType));
308
+ let sentId = '';
309
+ for (let i = 0; i < chunks.length; i++) {
310
+ const id = await channel.sendText(msg.channelId, chunks[i], {
311
+ ...(i === 0 && replyTo ? { replyTo } : {}),
312
+ format: 'markdown',
313
+ });
314
+ if (i === 0)
315
+ sentId = id;
316
+ }
317
+ return sentId;
318
+ }
319
+ async runDirect(agent, msg, channel, threadId, tracker, signal) {
199
320
  const { input, images } = await this.extractMedia(msg, agent);
200
- const replyTo = msg.raw?.scheduled ? undefined : msg.id;
321
+ const replyTo = isScheduled(msg) ? undefined : msg.id;
201
322
  await this.hooks?.emit('agent:before_run', { msg, threadId, agent: agent.name });
202
323
  let result;
203
324
  try {
204
- result = await this.config.cogitator.run(agent, {
205
- input,
206
- threadId,
207
- useMemory: !!this.config.memory,
208
- userId: msg.userId,
209
- channelType: msg.channelType,
210
- channelId: msg.channelId,
211
- ...(images ? { images } : {}),
212
- });
325
+ result = await this.config.cogitator.run(agent, this.buildRunOptions(msg, threadId, input, images, tracker, signal));
213
326
  }
214
327
  catch (error) {
215
328
  await this.hooks?.emit('agent:error', { msg, threadId, error });
216
329
  throw error;
217
330
  }
218
331
  await this.hooks?.emit('agent:after_run', { msg, threadId, output: result.output });
332
+ if (signal?.aborted)
333
+ return;
219
334
  const output = adaptMarkdown(result.output, msg.channelType);
335
+ if (!output)
336
+ return;
220
337
  await this.hooks?.emit('message:sending', {
221
338
  msg,
222
339
  threadId,
223
340
  text: output,
224
341
  channelId: msg.channelId,
225
342
  });
226
- const limit = getPlatformLimit(msg.channelType);
227
- const chunks = chunkMessage(output, limit);
228
- let sentId = '';
229
- for (let i = 0; i < chunks.length; i++) {
230
- sentId = await channel.sendText(msg.channelId, chunks[i], {
231
- ...(i === 0 && replyTo ? { replyTo } : {}),
232
- format: 'markdown',
233
- });
234
- }
343
+ const sentId = await this.sendChunked(channel, msg, output, replyTo);
235
344
  await this.hooks?.emit('message:sent', {
236
345
  msg,
237
346
  threadId,
@@ -239,67 +348,53 @@ export class Gateway {
239
348
  messageId: sentId,
240
349
  });
241
350
  }
242
- async runStreaming(agent, msg, channel, threadId) {
351
+ async runStreaming(agent, msg, channel, threadId, tracker, signal) {
243
352
  const { input, images } = await this.extractMedia(msg, agent);
244
- console.log(`[gateway] Running LLM, input: "${input.slice(0, 200)}", images: ${images?.length ?? 0}`);
245
- const replyTo = msg.raw?.scheduled ? undefined : msg.id;
246
- const useDraft = !!channel.sendDraft;
353
+ const replyTo = isScheduled(msg) ? undefined : msg.id;
247
354
  const streamCfg = {
248
355
  ...this.streamConfig,
249
356
  maxMessageChars: this.streamConfig.maxMessageChars ?? getPlatformLimit(msg.channelType),
250
357
  };
251
- const stream = new StreamBuffer(channel, msg.channelId, streamCfg, replyTo, useDraft);
358
+ const stream = new StreamBuffer(channel, msg.channelId, streamCfg, replyTo, !!channel.sendDraft, (text) => adaptMarkdown(text, msg.channelType));
252
359
  stream.start();
253
360
  await this.hooks?.emit('agent:before_run', { msg, threadId, agent: agent.name });
254
361
  await this.hooks?.emit('stream:started', { msg, threadId });
362
+ let tokenCount = 0;
363
+ let result;
255
364
  try {
256
- let tokenCount = 0;
257
- const runPromise = this.config.cogitator.run(agent, {
258
- input,
259
- threadId,
260
- useMemory: !!this.config.memory,
261
- userId: msg.userId,
262
- channelType: msg.channelType,
263
- channelId: msg.channelId,
365
+ result = await this.config.cogitator.run(agent, {
366
+ ...this.buildRunOptions(msg, threadId, input, images, tracker, signal),
264
367
  stream: true,
265
368
  onToken: (token) => {
266
369
  tokenCount++;
267
370
  stream.append(token);
268
371
  },
269
- ...(images ? { images } : {}),
270
- });
271
- const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('LLM response timeout (90s)')), 90_000));
272
- const result = await Promise.race([runPromise, timeout]);
273
- if (tokenCount > 0) {
274
- await stream.finish();
275
- }
276
- else {
277
- await stream.abort();
278
- if (result.output) {
279
- const output = adaptMarkdown(result.output, msg.channelType);
280
- const limit = getPlatformLimit(msg.channelType);
281
- const chunks = chunkMessage(output, limit);
282
- for (let i = 0; i < chunks.length; i++) {
283
- await channel.sendText(msg.channelId, chunks[i], {
284
- ...(i === 0 && replyTo ? { replyTo } : {}),
285
- format: 'markdown',
286
- });
287
- }
288
- }
289
- }
290
- await this.hooks?.emit('agent:after_run', { msg, threadId, output: result.output });
291
- await this.hooks?.emit('stream:finished', {
292
- msg,
293
- threadId,
294
- messageIds: stream.getMessageIds(),
295
372
  });
296
373
  }
297
374
  catch (error) {
298
- console.error('[gateway] LLM run error:', error.message);
299
375
  await stream.abort();
300
376
  await this.hooks?.emit('agent:error', { msg, threadId, error });
301
377
  throw error;
302
378
  }
379
+ if (signal?.aborted) {
380
+ await stream.abort();
381
+ return;
382
+ }
383
+ if (tokenCount > 0) {
384
+ await stream.finish();
385
+ }
386
+ else {
387
+ await stream.abort();
388
+ const output = adaptMarkdown(result.output, msg.channelType);
389
+ if (output)
390
+ await this.sendChunked(channel, msg, output, replyTo);
391
+ }
392
+ await this.hooks?.emit('agent:after_run', { msg, threadId, output: result.output });
393
+ await this.hooks?.emit('stream:finished', {
394
+ msg,
395
+ threadId,
396
+ messageIds: stream.getMessageIds(),
397
+ });
303
398
  }
304
399
  getThreadId(msg) {
305
400
  if (this.config.session?.threadKey) {
@@ -310,17 +405,10 @@ export class Gateway {
310
405
  async resolveAgent(user) {
311
406
  const agentOrFactory = this.config.agent;
312
407
  if (typeof agentOrFactory === 'function') {
313
- return agentOrFactory(user);
408
+ return (await agentOrFactory(user));
314
409
  }
315
410
  return agentOrFactory;
316
411
  }
317
- getAgentName() {
318
- const agentOrFactory = this.config.agent;
319
- if (typeof agentOrFactory === 'function') {
320
- return 'dynamic';
321
- }
322
- return agentOrFactory.name;
323
- }
324
412
  createMiddlewareContext(threadId, user, channel) {
325
413
  const store = new Map();
326
414
  return {
@@ -335,12 +423,25 @@ export class Gateway {
335
423
  },
336
424
  };
337
425
  }
338
- createSummarizeFn() {
426
+ createSummarizeFn(agent, compaction) {
427
+ const modelId = compaction.summaryModel ?? agent.model;
428
+ const explicitProvider = compaction.summaryModel ? undefined : agent.config?.provider;
429
+ const prompt = compaction.summaryPrompt ?? DEFAULT_SUMMARY_PROMPT;
339
430
  return async (messages) => {
340
- const text = messages
341
- .map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`)
342
- .join('\n');
343
- return `[Summary of ${messages.length} messages]: ${text.slice(0, 500)}`;
431
+ const backend = this.config.cogitator.getLLMBackend(modelId, explicitProvider);
432
+ const model = explicitProvider ? modelId : parseModel(modelId).model;
433
+ const transcript = messages.map((m) => `${m.role}: ${messageText(m)}`).join('\n');
434
+ const response = await backend.chat({
435
+ model,
436
+ messages: [
437
+ { role: 'system', content: prompt },
438
+ { role: 'user', content: transcript },
439
+ ],
440
+ });
441
+ const summary = response.content.trim();
442
+ if (!summary)
443
+ throw new Error('Summarization model returned an empty summary');
444
+ return summary;
344
445
  };
345
446
  }
346
447
  }