botinabox 1.8.3 → 1.8.5
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.
|
@@ -72,6 +72,8 @@ export declare class ChatPipeline {
|
|
|
72
72
|
private readonly tasks;
|
|
73
73
|
private readonly wakeups;
|
|
74
74
|
private readonly threadChannelMap;
|
|
75
|
+
/** Last dispatch promise — exposed for testing. */
|
|
76
|
+
lastDispatch: Promise<void>;
|
|
75
77
|
constructor(db: DataStore, hooks: HookBus, config: ChatPipelineConfig);
|
|
76
78
|
/**
|
|
77
79
|
* Resolve the Slack channel ID for a thread (for response delivery).
|
|
@@ -87,8 +89,15 @@ export declare class ChatPipeline {
|
|
|
87
89
|
private isDuplicate;
|
|
88
90
|
/**
|
|
89
91
|
* Async interpretation + task dispatch (Layers 3-5).
|
|
92
|
+
*
|
|
93
|
+
* ALWAYS creates a task programmatically — task creation does not depend
|
|
94
|
+
* on LLM classification. Interpretation enriches but never gates dispatch.
|
|
90
95
|
*/
|
|
91
96
|
private interpretAndDispatch;
|
|
97
|
+
/**
|
|
98
|
+
* Programmatic task creation — guaranteed, no LLM dependency.
|
|
99
|
+
*/
|
|
100
|
+
private guaranteedTaskDispatch;
|
|
92
101
|
/**
|
|
93
102
|
* Route and dispatch extracted tasks.
|
|
94
103
|
*/
|
|
@@ -36,6 +36,8 @@ export class ChatPipeline {
|
|
|
36
36
|
// In-memory thread → channel mapping for response routing
|
|
37
37
|
// (before thread_task_map exists)
|
|
38
38
|
threadChannelMap = new Map();
|
|
39
|
+
/** Last dispatch promise — exposed for testing. */
|
|
40
|
+
lastDispatch = Promise.resolve();
|
|
39
41
|
constructor(db, hooks, config) {
|
|
40
42
|
this.db = db;
|
|
41
43
|
this.hooks = hooks;
|
|
@@ -119,8 +121,12 @@ export class ChatPipeline {
|
|
|
119
121
|
skipFilter: true,
|
|
120
122
|
skipRedundancyCheck: true,
|
|
121
123
|
});
|
|
122
|
-
// ── Layer 3-5:
|
|
123
|
-
|
|
124
|
+
// ── Layer 3-5: Interpretation + guaranteed task dispatch ────
|
|
125
|
+
// ALWAYS create a task programmatically. Interpretation enriches
|
|
126
|
+
// it with classification, but task creation is not LLM-dependent.
|
|
127
|
+
const dispatchPromise = this.interpretAndDispatch(messageId, msg, threadTs, channelId);
|
|
128
|
+
this.lastDispatch = dispatchPromise;
|
|
129
|
+
void dispatchPromise;
|
|
124
130
|
});
|
|
125
131
|
// Layer 6: Task execution response
|
|
126
132
|
this.hooks.register('run.completed', async (ctx) => {
|
|
@@ -144,6 +150,7 @@ export class ChatPipeline {
|
|
|
144
150
|
agentId: ctx.agentId,
|
|
145
151
|
taskId,
|
|
146
152
|
source: 'agent',
|
|
153
|
+
skipRedundancyCheck: true, // Task results are always delivered
|
|
147
154
|
});
|
|
148
155
|
}, { priority: 90 });
|
|
149
156
|
}
|
|
@@ -169,32 +176,93 @@ export class ChatPipeline {
|
|
|
169
176
|
}
|
|
170
177
|
/**
|
|
171
178
|
* Async interpretation + task dispatch (Layers 3-5).
|
|
179
|
+
*
|
|
180
|
+
* ALWAYS creates a task programmatically — task creation does not depend
|
|
181
|
+
* on LLM classification. Interpretation enriches but never gates dispatch.
|
|
172
182
|
*/
|
|
173
183
|
async interpretAndDispatch(messageId, msg, threadTs, channelId) {
|
|
184
|
+
// Layer 5: ALWAYS create a task — this is programmatic, not LLM-dependent
|
|
185
|
+
await this.guaranteedTaskDispatch(msg, threadTs, channelId);
|
|
186
|
+
// Layer 3-4: Interpretation is best-effort enrichment (memories, user context)
|
|
174
187
|
try {
|
|
175
188
|
const result = await this.interpreter.interpret(messageId);
|
|
176
|
-
//
|
|
177
|
-
if (result.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
// Store any extracted memories (enrichment only — task already created above)
|
|
190
|
+
if (result.memories.length > 0 || result.userContext.length > 0) {
|
|
191
|
+
try {
|
|
192
|
+
const parts = [];
|
|
193
|
+
if (result.memories.length > 0) {
|
|
194
|
+
parts.push(`Noted ${result.memories.length} thing${result.memories.length > 1 ? 's' : ''} to remember.`);
|
|
195
|
+
}
|
|
196
|
+
if (parts.length > 0) {
|
|
197
|
+
await this.responder.sendResponse({
|
|
198
|
+
text: parts.join(' '),
|
|
199
|
+
channel: this.channel,
|
|
200
|
+
threadId: threadTs,
|
|
201
|
+
source: 'interpretation',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Non-fatal
|
|
207
|
+
}
|
|
189
208
|
}
|
|
190
209
|
}
|
|
191
210
|
catch (err) {
|
|
192
|
-
// Interpretation failure is non-fatal —
|
|
211
|
+
// Interpretation failure is non-fatal — task was already created above
|
|
212
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
193
213
|
await this.hooks.emit('interpretation.error', {
|
|
194
214
|
messageId,
|
|
195
|
-
error:
|
|
215
|
+
error: errMsg,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Programmatic task creation — guaranteed, no LLM dependency.
|
|
221
|
+
*/
|
|
222
|
+
async guaranteedTaskDispatch(msg, threadTs, channelId) {
|
|
223
|
+
// Route to best agent
|
|
224
|
+
const { agentSlug: targetSlug } = await this.router.route(msg);
|
|
225
|
+
if (!targetSlug)
|
|
226
|
+
return;
|
|
227
|
+
const agents = await this.db.query('agents', { where: { slug: targetSlug } });
|
|
228
|
+
const targetAgent = agents[0];
|
|
229
|
+
if (!targetAgent)
|
|
230
|
+
return;
|
|
231
|
+
const handlerAgentId = targetAgent.id;
|
|
232
|
+
// Follow-up in existing thread
|
|
233
|
+
if (threadTs) {
|
|
234
|
+
const existing = await this.db.query('thread_task_map', {
|
|
235
|
+
where: { thread_ts: threadTs, channel_id: channelId },
|
|
236
|
+
});
|
|
237
|
+
if (existing.length > 0) {
|
|
238
|
+
const taskId = existing[0].task_id;
|
|
239
|
+
const task = await this.tasks.get(taskId);
|
|
240
|
+
if (task && task.status !== 'done' && task.status !== 'cancelled') {
|
|
241
|
+
const updatedDesc = `${task.description ?? ''}\n\n---\n**Follow-up (${new Date().toISOString()}):**\n${msg.body}`;
|
|
242
|
+
await this.tasks.update(taskId, { description: updatedDesc });
|
|
243
|
+
await this.wakeups.enqueue(handlerAgentId, 'chat_followup', { taskId });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// New task — programmatic, guaranteed
|
|
249
|
+
const description = `## Chat Message\n\n**Channel:** ${channelId}\n**Thread:** ${threadTs}\n**From:** ${msg.from}\n**Time:** ${msg.receivedAt}\n\n---\n\n${msg.body}`;
|
|
250
|
+
const taskId = randomUUID();
|
|
251
|
+
if (threadTs) {
|
|
252
|
+
await this.db.insert('thread_task_map', {
|
|
253
|
+
thread_ts: threadTs,
|
|
254
|
+
channel_id: channelId,
|
|
255
|
+
task_id: taskId,
|
|
196
256
|
});
|
|
197
257
|
}
|
|
258
|
+
await this.tasks.create({
|
|
259
|
+
id: taskId,
|
|
260
|
+
title: msg.body.slice(0, 120),
|
|
261
|
+
description,
|
|
262
|
+
assignee_id: handlerAgentId,
|
|
263
|
+
priority: 5,
|
|
264
|
+
});
|
|
265
|
+
await this.wakeups.enqueue(handlerAgentId, 'chat_dispatch', { taskId });
|
|
198
266
|
}
|
|
199
267
|
/**
|
|
200
268
|
* Route and dispatch extracted tasks.
|
|
@@ -131,7 +131,9 @@ export class MessageInterpreter {
|
|
|
131
131
|
system: INTERPRET_SYSTEM,
|
|
132
132
|
maxTokens: 1000,
|
|
133
133
|
});
|
|
134
|
-
|
|
134
|
+
// Strip markdown fences if present (LLMs often wrap JSON in ```json...```)
|
|
135
|
+
const raw = result.content.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
|
|
136
|
+
const parsed = JSON.parse(raw);
|
|
135
137
|
return {
|
|
136
138
|
tasks: (parsed.tasks ?? []).map(t => ({
|
|
137
139
|
title: t.title,
|
package/dist/index.d.ts
CHANGED
|
@@ -1360,8 +1360,15 @@ declare class ChatPipeline {
|
|
|
1360
1360
|
private isDuplicate;
|
|
1361
1361
|
/**
|
|
1362
1362
|
* Async interpretation + task dispatch (Layers 3-5).
|
|
1363
|
+
*
|
|
1364
|
+
* ALWAYS creates a task programmatically — task creation does not depend
|
|
1365
|
+
* on LLM classification. Interpretation enriches but never gates dispatch.
|
|
1363
1366
|
*/
|
|
1364
1367
|
private interpretAndDispatch;
|
|
1368
|
+
/**
|
|
1369
|
+
* Programmatic task creation — guaranteed, no LLM dependency.
|
|
1370
|
+
*/
|
|
1371
|
+
private guaranteedTaskDispatch;
|
|
1365
1372
|
/**
|
|
1366
1373
|
* Route and dispatch extracted tasks.
|
|
1367
1374
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1576,7 +1576,8 @@ var MessageInterpreter = class {
|
|
|
1576
1576
|
system: INTERPRET_SYSTEM,
|
|
1577
1577
|
maxTokens: 1e3
|
|
1578
1578
|
});
|
|
1579
|
-
const
|
|
1579
|
+
const raw = result.content.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
1580
|
+
const parsed = JSON.parse(raw);
|
|
1580
1581
|
return {
|
|
1581
1582
|
tasks: (parsed.tasks ?? []).map((t) => ({
|
|
1582
1583
|
title: t.title,
|
|
@@ -1671,12 +1672,13 @@ var ChatPipeline = class {
|
|
|
1671
1672
|
if (this.messageFilter && !this.messageFilter(msg)) return;
|
|
1672
1673
|
if (await this.isDuplicate(msg)) return;
|
|
1673
1674
|
const rawTs = msg.raw?.ts;
|
|
1674
|
-
const threadTs = msg.threadId ?? rawTs ?? msg.id;
|
|
1675
1675
|
const channelId = msg.account ?? "";
|
|
1676
|
+
const threadTs = msg.threadId ?? channelId ?? rawTs ?? msg.id;
|
|
1676
1677
|
if (threadTs && channelId) {
|
|
1677
1678
|
this.threadChannelMap.set(threadTs, channelId);
|
|
1678
1679
|
}
|
|
1679
|
-
const {
|
|
1680
|
+
const msgWithThread = { ...msg, threadId: threadTs };
|
|
1681
|
+
const { messageId } = await this.messageStore.storeInbound(msgWithThread);
|
|
1680
1682
|
const userHistory = await this.messageStore.getUserHistory(
|
|
1681
1683
|
msg.from,
|
|
1682
1684
|
this.channel,
|
|
@@ -1725,7 +1727,9 @@ ${historyContext}` : void 0
|
|
|
1725
1727
|
threadId: mapping.thread_ts,
|
|
1726
1728
|
agentId: ctx.agentId,
|
|
1727
1729
|
taskId,
|
|
1728
|
-
source: "agent"
|
|
1730
|
+
source: "agent",
|
|
1731
|
+
skipRedundancyCheck: true
|
|
1732
|
+
// Task results are always delivered
|
|
1729
1733
|
});
|
|
1730
1734
|
}, { priority: 90 });
|
|
1731
1735
|
}
|
|
@@ -1749,25 +1753,31 @@ ${historyContext}` : void 0
|
|
|
1749
1753
|
}
|
|
1750
1754
|
/**
|
|
1751
1755
|
* Async interpretation + task dispatch (Layers 3-5).
|
|
1756
|
+
*
|
|
1757
|
+
* ALWAYS creates a task programmatically — task creation does not depend
|
|
1758
|
+
* on LLM classification. Interpretation enriches but never gates dispatch.
|
|
1752
1759
|
*/
|
|
1753
1760
|
async interpretAndDispatch(messageId, msg, threadTs, channelId) {
|
|
1761
|
+
await this.guaranteedTaskDispatch(msg, threadTs, channelId);
|
|
1754
1762
|
try {
|
|
1755
1763
|
const result = await this.interpreter.interpret(messageId);
|
|
1756
|
-
if (result.
|
|
1764
|
+
if (result.memories.length > 0 || result.userContext.length > 0) {
|
|
1757
1765
|
try {
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1766
|
+
const parts = [];
|
|
1767
|
+
if (result.memories.length > 0) {
|
|
1768
|
+
parts.push(`Noted ${result.memories.length} thing${result.memories.length > 1 ? "s" : ""} to remember.`);
|
|
1769
|
+
}
|
|
1770
|
+
if (parts.length > 0) {
|
|
1771
|
+
await this.responder.sendResponse({
|
|
1772
|
+
text: parts.join(" "),
|
|
1773
|
+
channel: this.channel,
|
|
1774
|
+
threadId: threadTs,
|
|
1775
|
+
source: "interpretation"
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1765
1778
|
} catch {
|
|
1766
1779
|
}
|
|
1767
1780
|
}
|
|
1768
|
-
if (result.tasks.length > 0) {
|
|
1769
|
-
await this.dispatchTasks(result, msg, threadTs, channelId);
|
|
1770
|
-
}
|
|
1771
1781
|
} catch (err) {
|
|
1772
1782
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1773
1783
|
await this.hooks.emit("interpretation.error", {
|
|
@@ -1776,6 +1786,62 @@ ${historyContext}` : void 0
|
|
|
1776
1786
|
});
|
|
1777
1787
|
}
|
|
1778
1788
|
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Programmatic task creation — guaranteed, no LLM dependency.
|
|
1791
|
+
*/
|
|
1792
|
+
async guaranteedTaskDispatch(msg, threadTs, channelId) {
|
|
1793
|
+
const { agentSlug: targetSlug } = await this.router.route(msg);
|
|
1794
|
+
if (!targetSlug) return;
|
|
1795
|
+
const agents = await this.db.query("agents", { where: { slug: targetSlug } });
|
|
1796
|
+
const targetAgent = agents[0];
|
|
1797
|
+
if (!targetAgent) return;
|
|
1798
|
+
const handlerAgentId = targetAgent.id;
|
|
1799
|
+
if (threadTs) {
|
|
1800
|
+
const existing = await this.db.query("thread_task_map", {
|
|
1801
|
+
where: { thread_ts: threadTs, channel_id: channelId }
|
|
1802
|
+
});
|
|
1803
|
+
if (existing.length > 0) {
|
|
1804
|
+
const taskId2 = existing[0].task_id;
|
|
1805
|
+
const task = await this.tasks.get(taskId2);
|
|
1806
|
+
if (task && task.status !== "done" && task.status !== "cancelled") {
|
|
1807
|
+
const updatedDesc = `${task.description ?? ""}
|
|
1808
|
+
|
|
1809
|
+
---
|
|
1810
|
+
**Follow-up (${(/* @__PURE__ */ new Date()).toISOString()}):**
|
|
1811
|
+
${msg.body}`;
|
|
1812
|
+
await this.tasks.update(taskId2, { description: updatedDesc });
|
|
1813
|
+
await this.wakeups.enqueue(handlerAgentId, "chat_followup", { taskId: taskId2 });
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const description = `## Chat Message
|
|
1819
|
+
|
|
1820
|
+
**Channel:** ${channelId}
|
|
1821
|
+
**Thread:** ${threadTs}
|
|
1822
|
+
**From:** ${msg.from}
|
|
1823
|
+
**Time:** ${msg.receivedAt}
|
|
1824
|
+
|
|
1825
|
+
---
|
|
1826
|
+
|
|
1827
|
+
${msg.body}`;
|
|
1828
|
+
const taskId = randomUUID();
|
|
1829
|
+
if (threadTs) {
|
|
1830
|
+
await this.db.insert("thread_task_map", {
|
|
1831
|
+
thread_ts: threadTs,
|
|
1832
|
+
channel_id: channelId,
|
|
1833
|
+
task_id: taskId
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
await this.tasks.create({
|
|
1837
|
+
id: taskId,
|
|
1838
|
+
title: msg.body.slice(0, 120),
|
|
1839
|
+
description,
|
|
1840
|
+
assignee_id: handlerAgentId,
|
|
1841
|
+
priority: 5
|
|
1842
|
+
});
|
|
1843
|
+
await this.wakeups.enqueue(handlerAgentId, "chat_dispatch", { taskId });
|
|
1844
|
+
}
|
|
1779
1845
|
/**
|
|
1780
1846
|
* Route and dispatch extracted tasks.
|
|
1781
1847
|
*/
|