@cogitator-ai/channels 0.2.5 → 0.4.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.
- package/README.md +142 -40
- package/dist/assistant-config.d.ts +1 -0
- package/dist/assistant-config.d.ts.map +1 -1
- package/dist/assistant-config.js +1 -0
- package/dist/assistant-config.js.map +1 -1
- package/dist/channels/discord.d.ts +4 -0
- package/dist/channels/discord.d.ts.map +1 -1
- package/dist/channels/discord.js +123 -59
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/slack.d.ts +9 -0
- package/dist/channels/slack.d.ts.map +1 -1
- package/dist/channels/slack.js +153 -34
- package/dist/channels/slack.js.map +1 -1
- package/dist/channels/telegram.d.ts +13 -0
- package/dist/channels/telegram.d.ts.map +1 -1
- package/dist/channels/telegram.js +206 -111
- package/dist/channels/telegram.js.map +1 -1
- package/dist/channels/terminal.d.ts +10 -0
- package/dist/channels/terminal.d.ts.map +1 -1
- package/dist/channels/terminal.js +46 -10
- package/dist/channels/terminal.js.map +1 -1
- package/dist/channels/webchat.d.ts +7 -2
- package/dist/channels/webchat.d.ts.map +1 -1
- package/dist/channels/webchat.js +112 -50
- package/dist/channels/webchat.js.map +1 -1
- package/dist/channels/whatsapp.d.ts +19 -3
- package/dist/channels/whatsapp.d.ts.map +1 -1
- package/dist/channels/whatsapp.js +204 -66
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/cron.d.ts +2 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +9 -0
- package/dist/cron.js.map +1 -0
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +7 -6
- package/dist/envelope.js.map +1 -1
- package/dist/formatters/code-protect.d.ts +2 -0
- package/dist/formatters/code-protect.d.ts.map +1 -0
- package/dist/formatters/code-protect.js +12 -0
- package/dist/formatters/code-protect.js.map +1 -0
- package/dist/formatters/markdown.d.ts +1 -1
- package/dist/formatters/markdown.d.ts.map +1 -1
- package/dist/formatters/markdown.js +79 -21
- package/dist/formatters/markdown.js.map +1 -1
- package/dist/formatters/whatsapp-markdown.d.ts.map +1 -1
- package/dist/formatters/whatsapp-markdown.js +7 -21
- package/dist/formatters/whatsapp-markdown.js.map +1 -1
- package/dist/gateway.d.ts +32 -3
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +275 -174
- package/dist/gateway.js.map +1 -1
- package/dist/heartbeat.d.ts +9 -2
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +121 -84
- package/dist/heartbeat.js.map +1 -1
- package/dist/inbound-debounce.js +2 -2
- package/dist/inbound-debounce.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/media/audio-format.d.ts +3 -0
- package/dist/media/audio-format.d.ts.map +1 -0
- package/dist/media/audio-format.js +20 -0
- package/dist/media/audio-format.js.map +1 -0
- package/dist/media/deepgram-stt.d.ts +1 -1
- package/dist/media/deepgram-stt.d.ts.map +1 -1
- package/dist/media/deepgram-stt.js +4 -3
- package/dist/media/deepgram-stt.js.map +1 -1
- package/dist/media/media-processor.d.ts +12 -1
- package/dist/media/media-processor.d.ts.map +1 -1
- package/dist/media/media-processor.js +75 -18
- package/dist/media/media-processor.js.map +1 -1
- package/dist/media/whisper-api.d.ts.map +1 -1
- package/dist/media/whisper-api.js +21 -33
- package/dist/media/whisper-api.js.map +1 -1
- package/dist/media/whisper-local.d.ts +1 -0
- package/dist/media/whisper-local.d.ts.map +1 -1
- package/dist/media/whisper-local.js +106 -50
- package/dist/media/whisper-local.js.map +1 -1
- package/dist/message-queue.d.ts +1 -0
- package/dist/message-queue.d.ts.map +1 -1
- package/dist/message-queue.js +17 -10
- package/dist/message-queue.js.map +1 -1
- package/dist/middleware/auto-extract.d.ts +2 -0
- package/dist/middleware/auto-extract.d.ts.map +1 -1
- package/dist/middleware/auto-extract.js +94 -30
- package/dist/middleware/auto-extract.js.map +1 -1
- package/dist/middleware/dm-policy.d.ts +2 -1
- package/dist/middleware/dm-policy.d.ts.map +1 -1
- package/dist/middleware/dm-policy.js +26 -27
- package/dist/middleware/dm-policy.js.map +1 -1
- package/dist/middleware/owner-commands.d.ts +1 -1
- package/dist/middleware/owner-commands.d.ts.map +1 -1
- package/dist/middleware/owner-commands.js +8 -7
- package/dist/middleware/owner-commands.js.map +1 -1
- package/dist/middleware/pairing-codes.d.ts +12 -0
- package/dist/middleware/pairing-codes.d.ts.map +1 -0
- package/dist/middleware/pairing-codes.js +26 -0
- package/dist/middleware/pairing-codes.js.map +1 -0
- package/dist/middleware/pairing.d.ts +3 -3
- package/dist/middleware/pairing.d.ts.map +1 -1
- package/dist/middleware/pairing.js +18 -25
- package/dist/middleware/pairing.js.map +1 -1
- package/dist/middleware/rate-limit.d.ts +3 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +21 -6
- package/dist/middleware/rate-limit.js.map +1 -1
- package/dist/runtime-builder.d.ts +11 -0
- package/dist/runtime-builder.d.ts.map +1 -1
- package/dist/runtime-builder.js +212 -62
- package/dist/runtime-builder.js.map +1 -1
- package/dist/simple-timer-store.d.ts +6 -0
- package/dist/simple-timer-store.d.ts.map +1 -1
- package/dist/simple-timer-store.js +8 -1
- package/dist/simple-timer-store.js.map +1 -1
- package/dist/stream-buffer.d.ts +14 -7
- package/dist/stream-buffer.d.ts.map +1 -1
- package/dist/stream-buffer.js +129 -84
- package/dist/stream-buffer.js.map +1 -1
- package/dist/tools/path-guard.d.ts +4 -0
- package/dist/tools/path-guard.d.ts.map +1 -0
- package/dist/tools/path-guard.js +53 -0
- package/dist/tools/path-guard.js.map +1 -0
- package/dist/tools/self-config.d.ts +12 -2
- package/dist/tools/self-config.d.ts.map +1 -1
- package/dist/tools/self-config.js +53 -16
- package/dist/tools/self-config.js.map +1 -1
- package/package.json +27 -21
- package/dist/media/index.d.ts +0 -7
- package/dist/media/index.d.ts.map +0 -1
- package/dist/media/index.js +0 -5
- package/dist/media/index.js.map +0 -1
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.
|
|
42
|
-
? new
|
|
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.
|
|
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:
|
|
72
|
-
totalSessions:
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}, 4000);
|
|
231
|
+
await this.sessionManager.incrementMessageCount?.(session.id);
|
|
232
|
+
}
|
|
233
|
+
if (this.config.memory && this.config.session?.compaction) {
|
|
152
234
|
try {
|
|
153
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
257
|
-
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
}
|