botinabox 2.3.2 → 2.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/dist/index.js CHANGED
@@ -1991,6 +1991,372 @@ ${extractedTask.description ?? msg.body}`;
1991
1991
  }
1992
1992
  };
1993
1993
 
1994
+ // src/core/chat/chat-pipeline-v2.ts
1995
+ import { createHash as createHash2 } from "crypto";
1996
+
1997
+ // src/core/data/context-builder.ts
1998
+ async function buildSystemContext(db, options) {
1999
+ const opts = {
2000
+ users: true,
2001
+ agents: true,
2002
+ projects: true,
2003
+ clients: true,
2004
+ files: true,
2005
+ org: true,
2006
+ ...options
2007
+ };
2008
+ const sections = [];
2009
+ if (opts.org) {
2010
+ const orgs = await db.query("org").catch(() => []);
2011
+ const org = orgs[0];
2012
+ if (org) {
2013
+ sections.push(`## Organization
2014
+ ${org.name} \u2014 ${org.description ?? ""}`);
2015
+ }
2016
+ }
2017
+ if (opts.users) {
2018
+ const users = await db.query("users").catch(() => []);
2019
+ if (users.length > 0) {
2020
+ const list = users.map((u) => `- ${u.name} (${u.role}${u.email ? `, ${u.email}` : ""})`).join("\n");
2021
+ sections.push(`## Users (${users.length})
2022
+ ${list}`);
2023
+ }
2024
+ }
2025
+ if (opts.clients) {
2026
+ const clients = await db.query("client").catch(() => []);
2027
+ if (clients.length > 0) {
2028
+ const list = clients.map((c) => `- ${c.name} (${c.status ?? "active"})`).join("\n");
2029
+ sections.push(`## Clients (${clients.length})
2030
+ ${list}`);
2031
+ }
2032
+ }
2033
+ if (opts.projects) {
2034
+ const projects = await db.query("project").catch(() => []);
2035
+ if (projects.length > 0) {
2036
+ const list = projects.map((p) => `- ${p.name} (${p.status ?? "unknown"})`).join("\n");
2037
+ sections.push(`## Projects (${projects.length})
2038
+ ${list}`);
2039
+ }
2040
+ }
2041
+ if (opts.files) {
2042
+ const files = await db.query("file").catch(() => []);
2043
+ if (files.length > 0) {
2044
+ const list = files.map(
2045
+ (f) => `- ${f.name}${f.file_path ? ` | path: ${f.file_path}` : ""}${f.access_level ? ` (${f.access_level})` : ""}`
2046
+ ).join("\n");
2047
+ sections.push(`## Files (${files.length})
2048
+ ${list}`);
2049
+ }
2050
+ }
2051
+ if (opts.agents) {
2052
+ const agents = await db.query("agents").catch(() => []);
2053
+ if (agents.length > 0) {
2054
+ const list = agents.map((a) => `- ${a.name} (${a.role}, ${a.status})`).join("\n");
2055
+ sections.push(`## Agents (${agents.length})
2056
+ ${list}`);
2057
+ }
2058
+ }
2059
+ return sections.join("\n\n");
2060
+ }
2061
+
2062
+ // src/core/chat/chat-pipeline-v2.ts
2063
+ var DEFAULT_DEDUP_WINDOW_MS2 = 5 * 60 * 1e3;
2064
+ var DEFAULT_MAX_ITERATIONS = 5;
2065
+ var DEFAULT_MAX_TOKENS = 4096;
2066
+ var DEFAULT_MAX_MESSAGES = 50;
2067
+ var DEFAULT_MAX_AGE_DAYS = 7;
2068
+ var ChatPipelineV2 = class {
2069
+ constructor(db, hooks, config) {
2070
+ this.db = db;
2071
+ this.hooks = hooks;
2072
+ this.config = config;
2073
+ this.channel = config.channel ?? "slack";
2074
+ this.messageFilter = config.messageFilter;
2075
+ this.dedupWindowMs = config.dedupWindowMs ?? DEFAULT_DEDUP_WINDOW_MS2;
2076
+ this.toolDefs = (config.tools ?? []).map((t) => t.definition);
2077
+ this.toolHandlers = new Map(
2078
+ (config.tools ?? []).map((t) => [t.definition.name, t.handler])
2079
+ );
2080
+ this.messageStore = new MessageStore(db, hooks);
2081
+ const simpleLlmCall = async (params) => {
2082
+ const result = await config.llmCall({
2083
+ model: params.model ?? config.model ?? "fast",
2084
+ messages: params.messages.map((m) => ({
2085
+ role: m.role,
2086
+ content: typeof m.content === "string" ? m.content : m.content.map((b) => "text" in b ? b.text : "").join("")
2087
+ })),
2088
+ system: params.system,
2089
+ maxTokens: params.maxTokens ?? 500
2090
+ });
2091
+ const text = result.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
2092
+ return { content: text };
2093
+ };
2094
+ this.responder = new ChatResponder(db, hooks, this.messageStore, {
2095
+ llmCall: simpleLlmCall,
2096
+ model: config.model ?? "fast",
2097
+ systemPrompt: config.systemPrompt
2098
+ });
2099
+ this.interpreter = new MessageInterpreter(db, hooks, {
2100
+ llmCall: simpleLlmCall,
2101
+ model: "fast",
2102
+ extractors: config.extractors
2103
+ });
2104
+ this.registerHandlers();
2105
+ }
2106
+ db;
2107
+ hooks;
2108
+ config;
2109
+ messageStore;
2110
+ responder;
2111
+ interpreter;
2112
+ channel;
2113
+ messageFilter;
2114
+ dedupWindowMs;
2115
+ threadChannelMap = /* @__PURE__ */ new Map();
2116
+ toolDefs;
2117
+ toolHandlers;
2118
+ /**
2119
+ * Resolve the channel ID for a thread (for response delivery).
2120
+ */
2121
+ async resolveChannel(threadId, taskId) {
2122
+ if (taskId) {
2123
+ const mappings = await this.db.query("thread_task_map", { where: { task_id: taskId } });
2124
+ if (mappings.length > 0) return mappings[0].channel_id;
2125
+ }
2126
+ if (threadId) {
2127
+ const mappings = await this.db.query("thread_task_map", { where: { thread_ts: threadId } });
2128
+ if (mappings.length > 0) return mappings[0].channel_id;
2129
+ }
2130
+ return this.threadChannelMap.get(threadId);
2131
+ }
2132
+ registerHandlers() {
2133
+ this.hooks.register("message.inbound", async (ctx) => {
2134
+ const msg = ctx;
2135
+ if (msg.channel !== this.channel) return;
2136
+ if (this.messageFilter && !this.messageFilter(msg)) return;
2137
+ if (await this.isDuplicate(msg)) return;
2138
+ const channelId = msg.account ?? "";
2139
+ const threadTs = channelId || msg.threadId || msg.id;
2140
+ if (threadTs && channelId) {
2141
+ this.threadChannelMap.set(threadTs, channelId);
2142
+ }
2143
+ const msgWithThread = { ...msg, threadId: threadTs };
2144
+ const { messageId } = await this.messageStore.storeInbound(msgWithThread);
2145
+ await this.hooks.emit("typing.start", { channel: this.channel, threadId: threadTs });
2146
+ try {
2147
+ const history = await this.buildHistory(channelId);
2148
+ let systemPrompt = this.config.systemPrompt;
2149
+ if (this.config.includeSystemContext !== false) {
2150
+ const ctx2 = await buildSystemContext(this.db, this.config.systemContextOptions);
2151
+ if (ctx2) systemPrompt += `
2152
+
2153
+ ${ctx2}`;
2154
+ }
2155
+ const { text, tasksDispatched } = await this.think(
2156
+ systemPrompt,
2157
+ history,
2158
+ msg.body,
2159
+ threadTs,
2160
+ channelId
2161
+ );
2162
+ await this.hooks.emit("typing.stop", { channel: this.channel, threadId: threadTs });
2163
+ if (text) {
2164
+ await this.responder.sendResponse({
2165
+ text,
2166
+ channel: this.channel,
2167
+ threadId: threadTs,
2168
+ source: "primary",
2169
+ skipFilter: true,
2170
+ skipRedundancyCheck: true
2171
+ });
2172
+ }
2173
+ void this.extractAsync(messageId);
2174
+ } catch (err) {
2175
+ await this.hooks.emit("typing.stop", { channel: this.channel, threadId: threadTs });
2176
+ await this.hooks.emit("pipeline.error", {
2177
+ messageId,
2178
+ error: err instanceof Error ? err.message : String(err)
2179
+ });
2180
+ }
2181
+ });
2182
+ this.hooks.register("run.completed", async (ctx) => {
2183
+ const taskId = ctx.taskId;
2184
+ if (!taskId) return;
2185
+ const task = await this.db.get("tasks", { id: taskId });
2186
+ const output = task?.result;
2187
+ if (!output) return;
2188
+ const mappings = await this.db.query("thread_task_map", { where: { task_id: taskId } });
2189
+ if (mappings.length === 0) return;
2190
+ const threadId = mappings[0].thread_ts;
2191
+ await this.responder.sendResponse({
2192
+ text: output,
2193
+ channel: this.channel,
2194
+ threadId,
2195
+ agentId: ctx.agentId,
2196
+ taskId,
2197
+ source: "agent",
2198
+ skipFilter: true,
2199
+ skipRedundancyCheck: true
2200
+ });
2201
+ }, { priority: 90 });
2202
+ }
2203
+ /**
2204
+ * Primary agent tool loop — adapted from ExecutionEngine pattern.
2205
+ */
2206
+ async think(systemPrompt, history, currentMessage, threadTs, channelId) {
2207
+ const model = this.config.model ?? "claude-sonnet-4-6";
2208
+ const maxIterations = this.config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
2209
+ const maxTokens = this.config.maxTokens ?? DEFAULT_MAX_TOKENS;
2210
+ const tasksDispatched = [];
2211
+ const messages = [
2212
+ ...history,
2213
+ { role: "user", content: currentMessage }
2214
+ ];
2215
+ let finalText = "";
2216
+ for (let i = 0; i < maxIterations; i++) {
2217
+ const params = {
2218
+ model,
2219
+ messages,
2220
+ system: systemPrompt,
2221
+ maxTokens
2222
+ };
2223
+ if (this.toolDefs.length > 0) {
2224
+ params.tools = this.toolDefs;
2225
+ params.tool_choice = { type: "auto" };
2226
+ }
2227
+ const response = await this.config.llmCall(params);
2228
+ const textBlocks = response.content.filter((b) => b.type === "text").map((b) => b.text ?? "");
2229
+ if (textBlocks.length > 0) finalText += textBlocks.join("");
2230
+ if (response.stop_reason !== "tool_use") break;
2231
+ const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
2232
+ const toolResults = [];
2233
+ for (const toolUse of toolUseBlocks) {
2234
+ const handler = this.toolHandlers.get(toolUse.name);
2235
+ if (handler) {
2236
+ try {
2237
+ const toolCtx = {
2238
+ taskId: "",
2239
+ agentId: "primary",
2240
+ hooks: this.hooks,
2241
+ db: this.db,
2242
+ resolveFilePath: this.config.resolveFilePath
2243
+ };
2244
+ const result = await handler(
2245
+ toolUse.input,
2246
+ toolCtx
2247
+ );
2248
+ if (toolUse.name === "dispatch_task" && result.includes("ID:")) {
2249
+ const idMatch = result.match(/ID:\s*([a-f0-9-]+)/);
2250
+ if (idMatch) {
2251
+ const taskId = idMatch[1];
2252
+ tasksDispatched.push(taskId);
2253
+ try {
2254
+ const existing = await this.db.query("thread_task_map", {
2255
+ where: { thread_ts: threadTs, channel_id: channelId }
2256
+ });
2257
+ if (existing.length > 0) {
2258
+ await this.db.update("thread_task_map", { id: existing[0].id }, { task_id: taskId });
2259
+ } else {
2260
+ await this.db.insert("thread_task_map", {
2261
+ thread_ts: threadTs,
2262
+ channel_id: channelId,
2263
+ task_id: taskId
2264
+ });
2265
+ }
2266
+ } catch {
2267
+ }
2268
+ }
2269
+ }
2270
+ toolResults.push({
2271
+ type: "tool_result",
2272
+ id: toolUse.id,
2273
+ text: result
2274
+ });
2275
+ } catch (err) {
2276
+ toolResults.push({
2277
+ type: "tool_result",
2278
+ id: toolUse.id,
2279
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
2280
+ });
2281
+ }
2282
+ }
2283
+ }
2284
+ messages.push({ role: "assistant", content: response.content });
2285
+ messages.push({ role: "user", content: toolResults });
2286
+ }
2287
+ return { text: finalText, tasksDispatched };
2288
+ }
2289
+ /**
2290
+ * Build conversation history from channel messages.
2291
+ * Includes BOTH user and assistant messages (unlike v1 which excluded bot messages).
2292
+ */
2293
+ async buildHistory(channelId) {
2294
+ const maxMessages = this.config.history?.maxMessages ?? DEFAULT_MAX_MESSAGES;
2295
+ const maxAgeDays = this.config.history?.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS;
2296
+ const includeAssistant = this.config.history?.includeAssistant !== false;
2297
+ const cutoff = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3).toISOString();
2298
+ let rows;
2299
+ try {
2300
+ rows = await this.db.query("messages", {
2301
+ where: { channel: this.channel },
2302
+ orderBy: "created_at",
2303
+ orderDir: "desc",
2304
+ limit: maxMessages
2305
+ });
2306
+ rows.reverse();
2307
+ rows = rows.filter((r) => {
2308
+ const ts = r.created_at;
2309
+ return !ts || ts >= cutoff;
2310
+ });
2311
+ } catch {
2312
+ return [];
2313
+ }
2314
+ const messages = [];
2315
+ const maxChars = 16e3;
2316
+ let charCount = 0;
2317
+ for (const row of rows) {
2318
+ const body = row.body ?? "";
2319
+ const direction = row.direction;
2320
+ if (!includeAssistant && direction !== "inbound") continue;
2321
+ if (charCount + body.length > maxChars) break;
2322
+ messages.push({
2323
+ role: direction === "inbound" ? "user" : "assistant",
2324
+ content: body
2325
+ });
2326
+ charCount += body.length;
2327
+ }
2328
+ return messages;
2329
+ }
2330
+ /**
2331
+ * Dedup check (same as v1).
2332
+ */
2333
+ async isDuplicate(msg) {
2334
+ const hash = createHash2("sha256").update(`${msg.from}:${msg.body}`).digest("hex");
2335
+ const cutoff = new Date(Date.now() - this.dedupWindowMs).toISOString();
2336
+ const recent = await this.db.query("message_dedup", { where: { content_hash: hash } });
2337
+ if (recent.some((r) => r.created_at > cutoff)) return true;
2338
+ await this.db.insert("message_dedup", {
2339
+ content_hash: hash,
2340
+ channel_id: msg.account ?? "",
2341
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2342
+ });
2343
+ return false;
2344
+ }
2345
+ /**
2346
+ * Async memory extraction (non-blocking, non-fatal).
2347
+ */
2348
+ async extractAsync(messageId) {
2349
+ try {
2350
+ await this.interpreter.interpret(messageId);
2351
+ } catch (err) {
2352
+ await this.hooks.emit("interpretation.error", {
2353
+ messageId,
2354
+ error: err instanceof Error ? err.message : String(err)
2355
+ });
2356
+ }
2357
+ }
2358
+ };
2359
+
1994
2360
  // src/core/chat/formatter.ts
1995
2361
  function formatText(text, mode) {
1996
2362
  switch (mode) {
@@ -3504,71 +3870,6 @@ ${c.instructions}` : null,
3504
3870
  }
3505
3871
  }
3506
3872
 
3507
- // src/core/data/context-builder.ts
3508
- async function buildSystemContext(db, options) {
3509
- const opts = {
3510
- users: true,
3511
- agents: true,
3512
- projects: true,
3513
- clients: true,
3514
- files: true,
3515
- org: true,
3516
- ...options
3517
- };
3518
- const sections = [];
3519
- if (opts.org) {
3520
- const orgs = await db.query("org").catch(() => []);
3521
- const org = orgs[0];
3522
- if (org) {
3523
- sections.push(`## Organization
3524
- ${org.name} \u2014 ${org.description ?? ""}`);
3525
- }
3526
- }
3527
- if (opts.users) {
3528
- const users = await db.query("users").catch(() => []);
3529
- if (users.length > 0) {
3530
- const list = users.map((u) => `- ${u.name} (${u.role}${u.email ? `, ${u.email}` : ""})`).join("\n");
3531
- sections.push(`## Users (${users.length})
3532
- ${list}`);
3533
- }
3534
- }
3535
- if (opts.clients) {
3536
- const clients = await db.query("client").catch(() => []);
3537
- if (clients.length > 0) {
3538
- const list = clients.map((c) => `- ${c.name} (${c.status ?? "active"})`).join("\n");
3539
- sections.push(`## Clients (${clients.length})
3540
- ${list}`);
3541
- }
3542
- }
3543
- if (opts.projects) {
3544
- const projects = await db.query("project").catch(() => []);
3545
- if (projects.length > 0) {
3546
- const list = projects.map((p) => `- ${p.name} (${p.status ?? "unknown"})`).join("\n");
3547
- sections.push(`## Projects (${projects.length})
3548
- ${list}`);
3549
- }
3550
- }
3551
- if (opts.files) {
3552
- const files = await db.query("file").catch(() => []);
3553
- if (files.length > 0) {
3554
- const list = files.map(
3555
- (f) => `- ${f.name}${f.file_path ? ` | path: ${f.file_path}` : ""}${f.access_level ? ` (${f.access_level})` : ""}`
3556
- ).join("\n");
3557
- sections.push(`## Files (${files.length})
3558
- ${list}`);
3559
- }
3560
- }
3561
- if (opts.agents) {
3562
- const agents = await db.query("agents").catch(() => []);
3563
- if (agents.length > 0) {
3564
- const list = agents.map((a) => `- ${a.name} (${a.role}, ${a.status})`).join("\n");
3565
- sections.push(`## Agents (${agents.length})
3566
- ${list}`);
3567
- }
3568
- }
3569
- return sections.join("\n\n");
3570
- }
3571
-
3572
3873
  // src/core/security/sanitizer.ts
3573
3874
  import { Buffer as Buffer2 } from "buffer";
3574
3875
  var DEFAULT_FIELD_LIMIT = 65535;
@@ -6751,6 +7052,30 @@ var nativeTools = [
6751
7052
  createAgentTool,
6752
7053
  createProjectTool
6753
7054
  ];
7055
+ var coordinatorTools = [
7056
+ // Task management
7057
+ dispatchTaskTool,
7058
+ cancelTaskTool,
7059
+ reassignTaskTool,
7060
+ // Progress monitoring
7061
+ getTaskStatusTool,
7062
+ getActiveTasksTool,
7063
+ // Agent management + awareness
7064
+ listAgentsTool,
7065
+ getAgentStatusTool,
7066
+ getAgentDetailTool,
7067
+ createAgentTool,
7068
+ // Communication (coordinator is the user-facing gateway)
7069
+ sendMessageTool,
7070
+ addTaskCommentTool,
7071
+ // Conversational context
7072
+ readConversationTool,
7073
+ searchConversationTool,
7074
+ // Awareness (for routing decisions, not for doing work)
7075
+ listFilesTool,
7076
+ listProjectsTool,
7077
+ sendFileTool
7078
+ ];
6754
7079
 
6755
7080
  // src/core/orchestrator/user-registry.ts
6756
7081
  import { v4 as uuidv4 } from "uuid";
@@ -7089,6 +7414,7 @@ export {
7089
7414
  ChannelRegistry,
7090
7415
  ChannelRegistryError,
7091
7416
  ChatPipeline,
7417
+ ChatPipelineV2,
7092
7418
  ChatResponder,
7093
7419
  ChatSessionManager,
7094
7420
  CircuitBreaker,
@@ -7147,6 +7473,7 @@ export {
7147
7473
  chunkText,
7148
7474
  classifyUpdate,
7149
7475
  compareVersions,
7476
+ coordinatorTools,
7150
7477
  createAgentTool,
7151
7478
  createConfigRevision,
7152
7479
  createDefaultLLMCall,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botinabox",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "Bot in a Box — framework for building multi-agent bots",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,8 +53,7 @@
53
53
  "build": "tsup",
54
54
  "test": "vitest run",
55
55
  "typecheck": "tsc --noEmit",
56
- "check-docs": "bash scripts/check-docs.sh",
57
- "prepublishOnly": "npm run build && npm run typecheck && npm test && npm run check-docs"
56
+ "prepublishOnly": "npm run build && npm run typecheck && npm test"
58
57
  },
59
58
  "dependencies": {
60
59
  "@types/uuid": "^10.0.0",