botinabox 2.8.1 → 2.9.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/channel-CVm1AWUF.d.ts +82 -0
- package/dist/channels/discord/index.d.ts +2 -1
- package/dist/channels/slack/index.d.ts +43 -52
- package/dist/channels/slack/index.js +68 -102
- package/dist/channels/webhook/index.d.ts +2 -1
- package/dist/chat-pipeline-aBSj7a4E.d.ts +655 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -3
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { c as ContentBlock } from './provider-BHkqkSdq.js';
|
|
2
|
+
|
|
3
|
+
/** Channel adapter types — Story 1.5 / 4.1 */
|
|
4
|
+
|
|
5
|
+
type ChatType = "direct" | "group" | "channel";
|
|
6
|
+
type FormattingMode = "markdown" | "mrkdwn" | "html" | "plain";
|
|
7
|
+
interface ChannelCapabilities {
|
|
8
|
+
chatTypes: ChatType[];
|
|
9
|
+
threads: boolean;
|
|
10
|
+
reactions: boolean;
|
|
11
|
+
editing: boolean;
|
|
12
|
+
media: boolean;
|
|
13
|
+
polls: boolean;
|
|
14
|
+
maxTextLength: number;
|
|
15
|
+
formattingMode: FormattingMode;
|
|
16
|
+
}
|
|
17
|
+
interface ChannelMeta {
|
|
18
|
+
displayName: string;
|
|
19
|
+
icon?: string;
|
|
20
|
+
homepage?: string;
|
|
21
|
+
}
|
|
22
|
+
interface InboundMessage {
|
|
23
|
+
id: string;
|
|
24
|
+
channel: string;
|
|
25
|
+
account?: string;
|
|
26
|
+
from: string;
|
|
27
|
+
userId?: string;
|
|
28
|
+
body: string;
|
|
29
|
+
threadId?: string;
|
|
30
|
+
replyToId?: string;
|
|
31
|
+
attachments?: Attachment[];
|
|
32
|
+
/**
|
|
33
|
+
* Multimodal content blocks produced by attachment enrichers.
|
|
34
|
+
* When set, ChatPipeline builds a multimodal user message that passes
|
|
35
|
+
* these blocks through to the LLM provider alongside `body`.
|
|
36
|
+
*/
|
|
37
|
+
attachmentBlocks?: ContentBlock[];
|
|
38
|
+
receivedAt: string;
|
|
39
|
+
raw?: unknown;
|
|
40
|
+
}
|
|
41
|
+
type AttachmentMediaType = "image" | "video" | "audio" | "pdf" | "doc" | "excel" | "presentation" | "html" | "link" | "misc";
|
|
42
|
+
interface Attachment {
|
|
43
|
+
type: AttachmentMediaType;
|
|
44
|
+
url?: string;
|
|
45
|
+
mimeType?: string;
|
|
46
|
+
filename?: string;
|
|
47
|
+
size?: number;
|
|
48
|
+
}
|
|
49
|
+
interface OutboundPayload {
|
|
50
|
+
text: string;
|
|
51
|
+
threadId?: string;
|
|
52
|
+
replyToId?: string;
|
|
53
|
+
attachments?: Attachment[];
|
|
54
|
+
}
|
|
55
|
+
interface SendResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
messageId?: string;
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
interface HealthStatus {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
latencyMs?: number;
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
type ChannelConfig = Record<string, unknown>;
|
|
66
|
+
interface ChannelAdapter {
|
|
67
|
+
/** Unique identifier for this adapter instance */
|
|
68
|
+
id: string;
|
|
69
|
+
meta: ChannelMeta;
|
|
70
|
+
capabilities: ChannelCapabilities;
|
|
71
|
+
connect(config: ChannelConfig): Promise<void>;
|
|
72
|
+
disconnect(): Promise<void>;
|
|
73
|
+
healthCheck(): Promise<HealthStatus>;
|
|
74
|
+
send(target: {
|
|
75
|
+
peerId: string;
|
|
76
|
+
threadId?: string;
|
|
77
|
+
}, payload: OutboundPayload): Promise<SendResult>;
|
|
78
|
+
/** Called when a message arrives — set by the framework */
|
|
79
|
+
onMessage?: (message: InboundMessage) => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type { Attachment as A, ChannelAdapter as C, FormattingMode as F, HealthStatus as H, InboundMessage as I, OutboundPayload as O, SendResult as S, AttachmentMediaType as a, ChannelCapabilities as b, ChannelConfig as c, ChannelMeta as d, ChatType as e };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-
|
|
1
|
+
import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-CVm1AWUF.js';
|
|
2
|
+
import '../../provider-BHkqkSdq.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* DiscordAdapter — ChannelAdapter implementation for Discord.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult, A as Attachment, a as AttachmentMediaType } from '../../channel-
|
|
2
|
-
import { H as HookBus, c as ChatPipeline } from '../../chat-pipeline-
|
|
1
|
+
import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult, A as Attachment, a as AttachmentMediaType } from '../../channel-CVm1AWUF.js';
|
|
2
|
+
import { H as HookBus, c as ChatPipeline } from '../../chat-pipeline-aBSj7a4E.js';
|
|
3
|
+
import { c as ContentBlock } from '../../provider-BHkqkSdq.js';
|
|
3
4
|
import 'better-sqlite3';
|
|
4
|
-
import '../../provider-BHkqkSdq.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* SlackAdapter — ChannelAdapter implementation for Slack.
|
|
@@ -163,20 +163,30 @@ interface SlackConfig {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* surfaces the filename/URL as a fallback when the enricher returns null.
|
|
170
|
-
*
|
|
171
|
-
* @param attachment - The attachment metadata (type, url, filename, etc.)
|
|
172
|
-
* @param botToken - Slack bot token, required for authenticated downloads from url_private
|
|
166
|
+
* Transport-specific context passed to every enricher. Extensible — add a new
|
|
167
|
+
* optional sub-context when wiring a new source (gmail, drive, dropbox, etc.)
|
|
168
|
+
* and enrichers that need it can type-guard.
|
|
173
169
|
*/
|
|
174
|
-
|
|
170
|
+
interface EnrichmentContext {
|
|
171
|
+
slack?: {
|
|
172
|
+
botToken: string;
|
|
173
|
+
};
|
|
174
|
+
drive?: {
|
|
175
|
+
client: unknown;
|
|
176
|
+
};
|
|
177
|
+
gmail?: {
|
|
178
|
+
client: unknown;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
175
181
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
182
|
+
* An AttachmentEnricher downloads an attachment and returns Claude content
|
|
183
|
+
* blocks representing it. Text blocks become part of the message body; image
|
|
184
|
+
* and document blocks flow through to the Anthropic provider unchanged.
|
|
185
|
+
*
|
|
186
|
+
* Enrichers throw on failure. The framework catches and falls back to a
|
|
187
|
+
* plain `[Attached: <filename>]` breadcrumb in the message body.
|
|
179
188
|
*/
|
|
189
|
+
type AttachmentEnricher = (attachment: Attachment, ctx: EnrichmentContext) => Promise<ContentBlock[]>;
|
|
180
190
|
type AttachmentEnricherMap = Partial<Record<AttachmentMediaType, AttachmentEnricher>>;
|
|
181
191
|
/** Internal: the result of running enrichAttachments on a message. */
|
|
182
192
|
interface EnrichedMessage extends InboundMessage {
|
|
@@ -214,51 +224,32 @@ declare class SlackBoltAdapter {
|
|
|
214
224
|
}
|
|
215
225
|
|
|
216
226
|
/**
|
|
217
|
-
* Run enrichers over each attachment on an InboundMessage
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* Enrichers are run sequentially. If an enricher throws or returns null, the
|
|
221
|
-
* attachment is surfaced as `[Attached: <filename>]` with no content — the
|
|
222
|
-
* LLM still sees that a file was present.
|
|
223
|
-
*
|
|
224
|
-
* Extracted text is appended to the body in this format:
|
|
227
|
+
* Run enrichers over each attachment on an InboundMessage. Text blocks get
|
|
228
|
+
* appended to `body`; image/document blocks get stored on `attachmentBlocks`
|
|
229
|
+
* so ChatPipeline can assemble a multimodal user message.
|
|
225
230
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* <extracted content>
|
|
231
|
+
* Enrichers that throw or return empty arrays fall back to a plain
|
|
232
|
+
* `[Attached: <filename>]` breadcrumb — the LLM still sees the attachment
|
|
233
|
+
* was there, just without content.
|
|
230
234
|
*/
|
|
231
|
-
declare function enrichAttachments(msg: InboundMessage,
|
|
235
|
+
declare function enrichAttachments(msg: InboundMessage, ctx: EnrichmentContext, enrichers: AttachmentEnricherMap): Promise<InboundMessage>;
|
|
232
236
|
|
|
233
|
-
interface ImageEnricherConfig {
|
|
234
|
-
/** Anthropic API key for vision calls. */
|
|
235
|
-
apiKey: string;
|
|
236
|
-
/** Model to use. Defaults to claude-sonnet-4-6. */
|
|
237
|
-
model?: string;
|
|
238
|
-
/** Max tokens for the description. Defaults to 1024. */
|
|
239
|
-
maxTokens?: number;
|
|
240
|
-
/** Prompt used for image description. */
|
|
241
|
-
prompt?: string;
|
|
242
|
-
}
|
|
243
237
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
238
|
+
* Slack image enricher. Downloads the image via the Slack bot token and
|
|
239
|
+
* returns a single `image` ContentBlock containing the base64 data. No
|
|
240
|
+
* intermediate vision API call — the downstream Anthropic provider sees
|
|
241
|
+
* the raw image and processes it natively.
|
|
246
242
|
*
|
|
247
|
-
*
|
|
248
|
-
* dependency of botinabox — the consumer must have it installed.
|
|
243
|
+
* Consumers wire this as `attachmentEnrichers.image = createSlackImageEnricher()`.
|
|
249
244
|
*/
|
|
250
|
-
declare function
|
|
245
|
+
declare function createSlackImageEnricher(): AttachmentEnricher;
|
|
251
246
|
|
|
252
|
-
interface PdfEnricherConfig {
|
|
253
|
-
apiKey: string;
|
|
254
|
-
model?: string;
|
|
255
|
-
maxTokens?: number;
|
|
256
|
-
prompt?: string;
|
|
257
|
-
}
|
|
258
247
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
248
|
+
* Slack PDF enricher. Downloads the PDF via the Slack bot token and returns
|
|
249
|
+
* a single `document` ContentBlock containing the base64 data. No intermediate
|
|
250
|
+
* document-extraction API call — the downstream Anthropic provider ingests
|
|
251
|
+
* the PDF natively.
|
|
261
252
|
*/
|
|
262
|
-
declare function
|
|
253
|
+
declare function createSlackPdfEnricher(): AttachmentEnricher;
|
|
263
254
|
|
|
264
|
-
export { type AttachmentEnricher, type AttachmentEnricherMap, type BoltClient, type EnrichedMessage, type
|
|
255
|
+
export { type AttachmentEnricher, type AttachmentEnricherMap, type BoltClient, type EnrichedMessage, type EnrichmentContext, SlackAdapter, SlackBoltAdapter, type SlackBoltAdapterConfig, type SlackConfig, type SlackEvent, type SlackFile, type TranscribeOptions, type TranscribeResult, createSlackImageEnricher, createSlackPdfEnricher, createSlackAdapter as default, downloadAudio, enrichAttachments, enrichVoiceMessage, extractVoiceTranscript, formatForSlack, parseSlackEvent, transcribeAudio };
|
|
@@ -86,30 +86,43 @@ function createSlackAdapter(client) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// src/channels/slack/enrichers/enrich.ts
|
|
89
|
-
async function enrichAttachments(msg,
|
|
89
|
+
async function enrichAttachments(msg, ctx, enrichers) {
|
|
90
90
|
if (!msg.attachments?.length) return msg;
|
|
91
|
-
const
|
|
91
|
+
const textParts = msg.body ? [msg.body] : [];
|
|
92
|
+
const mediaBlocks = [];
|
|
92
93
|
for (const att of msg.attachments) {
|
|
93
94
|
const enricher = enrichers[att.type];
|
|
94
95
|
const label = att.filename ?? att.url ?? att.type;
|
|
95
96
|
if (!enricher) {
|
|
96
|
-
|
|
97
|
+
textParts.push(`[Attached: ${label}]`);
|
|
97
98
|
continue;
|
|
98
99
|
}
|
|
99
|
-
let
|
|
100
|
+
let blocks;
|
|
100
101
|
try {
|
|
101
|
-
|
|
102
|
+
blocks = await enricher(att, ctx);
|
|
102
103
|
} catch {
|
|
103
|
-
|
|
104
|
+
textParts.push(`[Attached: ${label}]`);
|
|
105
|
+
continue;
|
|
104
106
|
}
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
107
|
+
if (blocks.length === 0) {
|
|
108
|
+
textParts.push(`[Attached: ${label}]`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const block of blocks) {
|
|
112
|
+
if (block.type === "text") {
|
|
113
|
+
textParts.push(`[Attached: ${label}]
|
|
114
|
+
${block.text}`);
|
|
115
|
+
} else if (block.type === "image" || block.type === "document") {
|
|
116
|
+
mediaBlocks.push(block);
|
|
117
|
+
textParts.push(`[Attached: ${label}]`);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
}
|
|
112
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
...msg,
|
|
123
|
+
body: textParts.join("\n\n"),
|
|
124
|
+
attachmentBlocks: mediaBlocks.length > 0 ? mediaBlocks : void 0
|
|
125
|
+
};
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
// src/channels/slack/bolt-adapter.ts
|
|
@@ -141,7 +154,11 @@ var SlackBoltAdapter = class {
|
|
|
141
154
|
inbound = await enrichVoiceMessage2(inbound, botToken);
|
|
142
155
|
}
|
|
143
156
|
if (inbound.attachments?.length && this.config.attachmentEnrichers) {
|
|
144
|
-
inbound = await enrichAttachments(
|
|
157
|
+
inbound = await enrichAttachments(
|
|
158
|
+
inbound,
|
|
159
|
+
{ slack: { botToken } },
|
|
160
|
+
this.config.attachmentEnrichers
|
|
161
|
+
);
|
|
145
162
|
}
|
|
146
163
|
await hooks.emit("message.inbound", inbound);
|
|
147
164
|
});
|
|
@@ -196,108 +213,57 @@ var SlackBoltAdapter = class {
|
|
|
196
213
|
};
|
|
197
214
|
|
|
198
215
|
// src/channels/slack/enrichers/image-enricher.ts
|
|
199
|
-
|
|
200
|
-
async
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
216
|
+
function createSlackImageEnricher() {
|
|
217
|
+
return async (att, ctx) => {
|
|
218
|
+
if (!att.url) throw new Error("image enricher: attachment has no url");
|
|
219
|
+
if (!ctx.slack?.botToken) throw new Error("image enricher: ctx.slack.botToken required");
|
|
220
|
+
const response = await fetch(att.url, {
|
|
221
|
+
headers: { Authorization: `Bearer ${ctx.slack.botToken}` },
|
|
204
222
|
signal: AbortSignal.timeout(3e4)
|
|
205
223
|
});
|
|
206
|
-
if (!response.ok)
|
|
207
|
-
const
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const base64 = await downloadAsBase64(att.url, botToken);
|
|
219
|
-
if (!base64) return null;
|
|
220
|
-
const anthropicModule = "@anthropic-ai/sdk";
|
|
221
|
-
const sdk = await import(anthropicModule);
|
|
222
|
-
const client = new sdk.default({ apiKey });
|
|
223
|
-
try {
|
|
224
|
-
const message = await client.messages.create({
|
|
225
|
-
model,
|
|
226
|
-
max_tokens: maxTokens,
|
|
227
|
-
messages: [
|
|
228
|
-
{
|
|
229
|
-
role: "user",
|
|
230
|
-
content: [
|
|
231
|
-
{
|
|
232
|
-
type: "image",
|
|
233
|
-
source: { type: "base64", media_type: mediaType, data: base64 }
|
|
234
|
-
},
|
|
235
|
-
{ type: "text", text: prompt }
|
|
236
|
-
]
|
|
237
|
-
}
|
|
238
|
-
]
|
|
239
|
-
});
|
|
240
|
-
const textBlock = message.content.find((c) => c.type === "text");
|
|
241
|
-
return textBlock?.text ?? null;
|
|
242
|
-
} catch {
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
224
|
+
if (!response.ok) throw new Error(`slack download failed: ${response.status}`);
|
|
225
|
+
const base64 = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
226
|
+
return [
|
|
227
|
+
{
|
|
228
|
+
type: "image",
|
|
229
|
+
source: {
|
|
230
|
+
type: "base64",
|
|
231
|
+
media_type: att.mimeType ?? "image/jpeg",
|
|
232
|
+
data: base64
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
];
|
|
245
236
|
};
|
|
246
237
|
}
|
|
247
238
|
|
|
248
239
|
// src/channels/slack/enrichers/pdf-enricher.ts
|
|
249
|
-
|
|
250
|
-
async
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
240
|
+
function createSlackPdfEnricher() {
|
|
241
|
+
return async (att, ctx) => {
|
|
242
|
+
if (!att.url) throw new Error("pdf enricher: attachment has no url");
|
|
243
|
+
if (!ctx.slack?.botToken) throw new Error("pdf enricher: ctx.slack.botToken required");
|
|
244
|
+
const response = await fetch(att.url, {
|
|
245
|
+
headers: { Authorization: `Bearer ${ctx.slack.botToken}` },
|
|
254
246
|
signal: AbortSignal.timeout(6e4)
|
|
255
247
|
});
|
|
256
|
-
if (!response.ok)
|
|
257
|
-
const
|
|
258
|
-
return
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (!base64) return null;
|
|
269
|
-
const anthropicModule = "@anthropic-ai/sdk";
|
|
270
|
-
const sdk = await import(anthropicModule);
|
|
271
|
-
const client = new sdk.default({ apiKey });
|
|
272
|
-
try {
|
|
273
|
-
const message = await client.messages.create({
|
|
274
|
-
model,
|
|
275
|
-
max_tokens: maxTokens,
|
|
276
|
-
messages: [
|
|
277
|
-
{
|
|
278
|
-
role: "user",
|
|
279
|
-
content: [
|
|
280
|
-
{
|
|
281
|
-
type: "document",
|
|
282
|
-
source: { type: "base64", media_type: "application/pdf", data: base64 }
|
|
283
|
-
},
|
|
284
|
-
{ type: "text", text: prompt }
|
|
285
|
-
]
|
|
286
|
-
}
|
|
287
|
-
]
|
|
288
|
-
});
|
|
289
|
-
const textBlock = message.content.find((c) => c.type === "text");
|
|
290
|
-
return textBlock?.text ?? null;
|
|
291
|
-
} catch {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
248
|
+
if (!response.ok) throw new Error(`slack download failed: ${response.status}`);
|
|
249
|
+
const base64 = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
250
|
+
return [
|
|
251
|
+
{
|
|
252
|
+
type: "document",
|
|
253
|
+
source: {
|
|
254
|
+
type: "base64",
|
|
255
|
+
media_type: "application/pdf",
|
|
256
|
+
data: base64
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
];
|
|
294
260
|
};
|
|
295
261
|
}
|
|
296
262
|
export {
|
|
297
263
|
SlackAdapter,
|
|
298
264
|
SlackBoltAdapter,
|
|
299
|
-
|
|
300
|
-
|
|
265
|
+
createSlackImageEnricher,
|
|
266
|
+
createSlackPdfEnricher,
|
|
301
267
|
createSlackAdapter as default,
|
|
302
268
|
downloadAudio,
|
|
303
269
|
enrichAttachments,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-
|
|
1
|
+
import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-CVm1AWUF.js';
|
|
2
|
+
import '../../provider-BHkqkSdq.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* WebhookAdapter — ChannelAdapter implementation for webhook-based channels.
|
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import * as better_sqlite3 from 'better-sqlite3';
|
|
2
|
+
import { I as InboundMessage } from './channel-CVm1AWUF.js';
|
|
3
|
+
import { C as ChatMessage } from './provider-BHkqkSdq.js';
|
|
4
|
+
|
|
5
|
+
type HookHandler = (context: Record<string, unknown>) => Promise<void> | void;
|
|
6
|
+
type Unsubscribe = () => void;
|
|
7
|
+
interface HookOptions {
|
|
8
|
+
/** 0–100, default 50. Lower = runs first. */
|
|
9
|
+
priority?: number;
|
|
10
|
+
/** Auto-unsubscribe after first invocation. */
|
|
11
|
+
once?: boolean;
|
|
12
|
+
/** Only fire if context matches all filter key/value pairs. */
|
|
13
|
+
filter?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
interface HookRegistration {
|
|
16
|
+
event: string;
|
|
17
|
+
handler: HookHandler;
|
|
18
|
+
priority: number;
|
|
19
|
+
once: boolean;
|
|
20
|
+
filter?: Record<string, unknown>;
|
|
21
|
+
/** Internal auto-increment for stable sort within same priority */
|
|
22
|
+
id: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Priority-ordered event bus for decoupled inter-layer communication.
|
|
27
|
+
* Story 1.1 — handlers run in priority order, errors are isolated,
|
|
28
|
+
* and registrations are unsubscribable.
|
|
29
|
+
*/
|
|
30
|
+
declare class HookBus {
|
|
31
|
+
private readonly registrations;
|
|
32
|
+
private nextId;
|
|
33
|
+
register(event: string, handler: HookHandler, opts?: HookOptions): Unsubscribe;
|
|
34
|
+
emit(event: string, context: Record<string, unknown>): Promise<void>;
|
|
35
|
+
/** Emit synchronously (use only when async is not needed) */
|
|
36
|
+
emitSync(event: string, context: Record<string, unknown>): void;
|
|
37
|
+
hasListeners(event: string): boolean;
|
|
38
|
+
listRegistered(): string[];
|
|
39
|
+
/** Remove all handlers for an event, or all handlers if no event given */
|
|
40
|
+
clear(event?: string): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface TableDefinition {
|
|
44
|
+
columns: Record<string, string>;
|
|
45
|
+
primaryKey?: string | string[];
|
|
46
|
+
tableConstraints?: string[];
|
|
47
|
+
relations?: Record<string, RelationDef>;
|
|
48
|
+
render?: string | ((rows: Row[]) => string);
|
|
49
|
+
outputFile?: string;
|
|
50
|
+
filter?: (rows: Row[]) => Row[];
|
|
51
|
+
}
|
|
52
|
+
interface RelationDef {
|
|
53
|
+
type: 'belongsTo' | 'hasMany';
|
|
54
|
+
table: string;
|
|
55
|
+
foreignKey: string;
|
|
56
|
+
references?: string;
|
|
57
|
+
}
|
|
58
|
+
interface EntityContextDef {
|
|
59
|
+
table: string;
|
|
60
|
+
directory: string;
|
|
61
|
+
slugColumn: string;
|
|
62
|
+
files: Record<string, EntityFileSpec>;
|
|
63
|
+
indexFile?: string;
|
|
64
|
+
/** Custom index render function. If omitted, a default listing is generated. */
|
|
65
|
+
indexRender?: (rows: Row[]) => string;
|
|
66
|
+
protectedFiles?: string[];
|
|
67
|
+
/** When true, this entity's data is never rendered into other entities' context files. */
|
|
68
|
+
protected?: boolean;
|
|
69
|
+
/** Enable at-rest encryption. Requires encryptionKey in Lattice options. */
|
|
70
|
+
encrypted?: boolean | {
|
|
71
|
+
columns: string[];
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
interface EntityFileSpec {
|
|
75
|
+
source: EntitySource;
|
|
76
|
+
render: string | ((rows: Row[]) => string);
|
|
77
|
+
junctionColumns?: string[];
|
|
78
|
+
omitIfEmpty?: boolean;
|
|
79
|
+
}
|
|
80
|
+
type EntitySource = {
|
|
81
|
+
type: 'self';
|
|
82
|
+
} | {
|
|
83
|
+
type: 'hasMany';
|
|
84
|
+
table: string;
|
|
85
|
+
foreignKey: string;
|
|
86
|
+
filters?: Filter[];
|
|
87
|
+
softDelete?: boolean;
|
|
88
|
+
orderBy?: string;
|
|
89
|
+
limit?: number;
|
|
90
|
+
} | {
|
|
91
|
+
type: 'manyToMany';
|
|
92
|
+
junctionTable: string;
|
|
93
|
+
localKey: string;
|
|
94
|
+
remoteKey: string;
|
|
95
|
+
remoteTable: string;
|
|
96
|
+
filters?: Filter[];
|
|
97
|
+
softDelete?: boolean;
|
|
98
|
+
orderBy?: string;
|
|
99
|
+
limit?: number;
|
|
100
|
+
} | {
|
|
101
|
+
type: 'belongsTo';
|
|
102
|
+
table: string;
|
|
103
|
+
foreignKey: string;
|
|
104
|
+
} | {
|
|
105
|
+
type: 'enriched';
|
|
106
|
+
include: Record<string, EntitySource>;
|
|
107
|
+
} | {
|
|
108
|
+
type: 'custom';
|
|
109
|
+
resolve: (row: Row, adapter: SqliteAdapter) => Row[];
|
|
110
|
+
};
|
|
111
|
+
interface Filter {
|
|
112
|
+
col: string;
|
|
113
|
+
op: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'in' | 'isNull' | 'isNotNull';
|
|
114
|
+
val?: unknown;
|
|
115
|
+
}
|
|
116
|
+
interface QueryOptions {
|
|
117
|
+
where?: Record<string, unknown>;
|
|
118
|
+
filters?: Filter[];
|
|
119
|
+
orderBy?: string | Array<{
|
|
120
|
+
col: string;
|
|
121
|
+
dir: 'asc' | 'desc';
|
|
122
|
+
}>;
|
|
123
|
+
orderDir?: 'asc' | 'desc';
|
|
124
|
+
limit?: number;
|
|
125
|
+
offset?: number;
|
|
126
|
+
}
|
|
127
|
+
type Row = Record<string, unknown>;
|
|
128
|
+
type PkLookup = string | Record<string, unknown>;
|
|
129
|
+
interface SqliteAdapter {
|
|
130
|
+
run(sql: string, params?: unknown[]): better_sqlite3.RunResult;
|
|
131
|
+
get<T = Row>(sql: string, params?: unknown[]): T | undefined;
|
|
132
|
+
all<T = Row>(sql: string, params?: unknown[]): T[];
|
|
133
|
+
tableInfo(table: string): TableInfoRow[];
|
|
134
|
+
invalidateTableCache(table: string): void;
|
|
135
|
+
}
|
|
136
|
+
interface TableInfoRow {
|
|
137
|
+
cid: number;
|
|
138
|
+
name: string;
|
|
139
|
+
type: string;
|
|
140
|
+
notnull: number;
|
|
141
|
+
dflt_value: unknown;
|
|
142
|
+
pk: number;
|
|
143
|
+
}
|
|
144
|
+
interface SeedItem {
|
|
145
|
+
table: string;
|
|
146
|
+
rows: Row[];
|
|
147
|
+
naturalKey?: string | string[];
|
|
148
|
+
junctions?: Array<{
|
|
149
|
+
table: string;
|
|
150
|
+
items: Array<Row>;
|
|
151
|
+
}>;
|
|
152
|
+
softDeleteMissing?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
declare class DataStoreError extends Error {
|
|
156
|
+
constructor(message: string);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Thin wrapper around Lattice that provides the botinabox DataStore API.
|
|
160
|
+
*
|
|
161
|
+
* Delegates all data operations to the latticesql package. Application-level
|
|
162
|
+
* events (task.created, run.completed, etc.) remain on the HookBus — they are
|
|
163
|
+
* emitted by orchestrator modules, not the data layer.
|
|
164
|
+
*/
|
|
165
|
+
declare class DataStore {
|
|
166
|
+
private lattice;
|
|
167
|
+
private readonly hooks;
|
|
168
|
+
private readonly outputDir;
|
|
169
|
+
private _initialized;
|
|
170
|
+
private readonly deferredStatements;
|
|
171
|
+
constructor(opts: {
|
|
172
|
+
dbPath: string;
|
|
173
|
+
outputDir?: string;
|
|
174
|
+
wal?: boolean;
|
|
175
|
+
hooks?: HookBus;
|
|
176
|
+
});
|
|
177
|
+
/**
|
|
178
|
+
* Register a table definition. Must be called before init().
|
|
179
|
+
*
|
|
180
|
+
* tableConstraints may contain both inline constraints (FOREIGN KEY, UNIQUE)
|
|
181
|
+
* and standalone SQL statements (CREATE INDEX). Standalone statements are
|
|
182
|
+
* deferred and executed after init() creates the tables.
|
|
183
|
+
*/
|
|
184
|
+
define(name: string, def: TableDefinition): void;
|
|
185
|
+
/**
|
|
186
|
+
* Register an entity context definition for per-entity file rendering.
|
|
187
|
+
*/
|
|
188
|
+
defineEntityContext(name: string, def: EntityContextDef): void;
|
|
189
|
+
init(opts?: {
|
|
190
|
+
migrations?: Array<{
|
|
191
|
+
version: string;
|
|
192
|
+
sql: string;
|
|
193
|
+
}>;
|
|
194
|
+
}): Promise<void>;
|
|
195
|
+
private assertInitialized;
|
|
196
|
+
insert(table: string, row: Row): Promise<Row>;
|
|
197
|
+
upsert(table: string, row: Row): Promise<Row>;
|
|
198
|
+
update(table: string, pk: PkLookup, changes: Row): Promise<Row>;
|
|
199
|
+
delete(table: string, pk: PkLookup): Promise<void>;
|
|
200
|
+
/**
|
|
201
|
+
* Get a single row by primary key.
|
|
202
|
+
* Returns undefined if not found (Lattice returns null).
|
|
203
|
+
*/
|
|
204
|
+
get(table: string, pk: PkLookup): Promise<Row | undefined>;
|
|
205
|
+
query(table: string, opts?: QueryOptions): Promise<Row[]>;
|
|
206
|
+
count(table: string, opts?: QueryOptions): Promise<number>;
|
|
207
|
+
link(junctionTable: string, row: Row): Promise<void>;
|
|
208
|
+
unlink(junctionTable: string, row: Row): Promise<void>;
|
|
209
|
+
migrate(migrations: Array<{
|
|
210
|
+
version: string;
|
|
211
|
+
sql: string;
|
|
212
|
+
}>): Promise<void>;
|
|
213
|
+
seed(items: SeedItem[]): Promise<void>;
|
|
214
|
+
render(): Promise<void>;
|
|
215
|
+
reconcile(): Promise<void>;
|
|
216
|
+
tableInfo(table: string): TableInfoRow[];
|
|
217
|
+
close(): void;
|
|
218
|
+
on(event: string, handler: (context: Record<string, unknown>) => void): void;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* MessageStore — store-before-respond guarantee for all chat interactions.
|
|
223
|
+
* Story 7.1
|
|
224
|
+
*
|
|
225
|
+
* Every inbound message (with attachments) is stored BEFORE the bot responds.
|
|
226
|
+
* Every outbound message is stored BEFORE it is sent to the user.
|
|
227
|
+
* Storage confirmation is required before any response flows.
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
interface StoredAttachment {
|
|
231
|
+
fileType: string;
|
|
232
|
+
filename?: string;
|
|
233
|
+
mimeType?: string;
|
|
234
|
+
sizeBytes?: number;
|
|
235
|
+
contents?: string;
|
|
236
|
+
summary?: string;
|
|
237
|
+
url?: string;
|
|
238
|
+
}
|
|
239
|
+
interface StoreResult {
|
|
240
|
+
messageId: string;
|
|
241
|
+
attachmentIds: string[];
|
|
242
|
+
}
|
|
243
|
+
declare class MessageStore {
|
|
244
|
+
private db;
|
|
245
|
+
private hooks;
|
|
246
|
+
constructor(db: DataStore, hooks: HookBus);
|
|
247
|
+
/**
|
|
248
|
+
* Store an inbound message and its attachments.
|
|
249
|
+
* Must complete successfully before any bot response is generated.
|
|
250
|
+
*/
|
|
251
|
+
storeInbound(msg: InboundMessage): Promise<StoreResult>;
|
|
252
|
+
/**
|
|
253
|
+
* Store an outbound message BEFORE sending it.
|
|
254
|
+
* Returns the message ID for confirmation tracking.
|
|
255
|
+
*/
|
|
256
|
+
storeOutbound(opts: {
|
|
257
|
+
channel: string;
|
|
258
|
+
text: string;
|
|
259
|
+
threadId?: string;
|
|
260
|
+
agentId?: string;
|
|
261
|
+
agentSlug?: string;
|
|
262
|
+
taskId?: string;
|
|
263
|
+
}): Promise<string>;
|
|
264
|
+
/**
|
|
265
|
+
* Store an attachment linked to a message.
|
|
266
|
+
*/
|
|
267
|
+
storeAttachment(messageId: string, att: StoredAttachment): Promise<string>;
|
|
268
|
+
/**
|
|
269
|
+
* Get recent messages in a thread for context building.
|
|
270
|
+
*/
|
|
271
|
+
getThreadHistory(threadId: string, limit?: number): Promise<Array<Record<string, unknown>>>;
|
|
272
|
+
/**
|
|
273
|
+
* Get recent outbound messages in a thread for redundancy checking.
|
|
274
|
+
*/
|
|
275
|
+
getRecentOutbound(threadId: string, limit?: number): Promise<Array<Record<string, unknown>>>;
|
|
276
|
+
/**
|
|
277
|
+
* Get recent messages in a channel (all threads combined).
|
|
278
|
+
* More reliable than getThreadHistory for DMs where thread_ids are inconsistent.
|
|
279
|
+
*/
|
|
280
|
+
getChannelHistory(channel: string, limit?: number): Promise<Array<Record<string, unknown>>>;
|
|
281
|
+
/**
|
|
282
|
+
* Get recent messages from a specific user across all threads.
|
|
283
|
+
*/
|
|
284
|
+
getUserHistory(userId: string, channel: string, limit?: number): Promise<Array<Record<string, unknown>>>;
|
|
285
|
+
/**
|
|
286
|
+
* Get attachments for a message.
|
|
287
|
+
*/
|
|
288
|
+
getAttachments(messageId: string): Promise<Array<Record<string, unknown>>>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* ChatResponder — fast conversational layer with LLM-filtered responses.
|
|
293
|
+
* Story 7.2
|
|
294
|
+
*
|
|
295
|
+
* Provides rapid (<2s) conversational responses using a cheap LLM (Haiku).
|
|
296
|
+
* The responder has awareness of tools and capabilities but does NOT execute
|
|
297
|
+
* anything — it keeps the conversation going while work happens async.
|
|
298
|
+
*
|
|
299
|
+
* All outbound messages (direct, post-interpretation, task execution) are
|
|
300
|
+
* filtered through this layer for human readability and redundancy checking.
|
|
301
|
+
*/
|
|
302
|
+
|
|
303
|
+
interface ChatResponderConfig {
|
|
304
|
+
/** System prompt for the conversational responder */
|
|
305
|
+
systemPrompt?: string;
|
|
306
|
+
/** Max tokens for context window. Default: 4000 */
|
|
307
|
+
contextWindowTokens?: number;
|
|
308
|
+
/** Max recent outbound messages to check for redundancy. Default: 10 */
|
|
309
|
+
redundancyWindow?: number;
|
|
310
|
+
/** Model to use for responses. Default: 'fast' (resolved via ModelRouter) */
|
|
311
|
+
model?: string;
|
|
312
|
+
/** Caller-provided LLM call function */
|
|
313
|
+
llmCall: (params: {
|
|
314
|
+
model: string;
|
|
315
|
+
messages: ChatMessage[];
|
|
316
|
+
system?: string;
|
|
317
|
+
maxTokens?: number;
|
|
318
|
+
}) => Promise<{
|
|
319
|
+
content: string;
|
|
320
|
+
}>;
|
|
321
|
+
}
|
|
322
|
+
declare class ChatResponder {
|
|
323
|
+
private db;
|
|
324
|
+
private hooks;
|
|
325
|
+
private messageStore;
|
|
326
|
+
private readonly systemPrompt;
|
|
327
|
+
private readonly contextWindowTokens;
|
|
328
|
+
private readonly redundancyWindow;
|
|
329
|
+
private readonly model;
|
|
330
|
+
private readonly llmCall;
|
|
331
|
+
constructor(db: DataStore, hooks: HookBus, messageStore: MessageStore, config: ChatResponderConfig);
|
|
332
|
+
/**
|
|
333
|
+
* Generate a fast conversational response to an inbound message.
|
|
334
|
+
* Uses rolling context window from thread history.
|
|
335
|
+
*/
|
|
336
|
+
respond(opts: {
|
|
337
|
+
messageBody: string;
|
|
338
|
+
threadId: string;
|
|
339
|
+
channel: string;
|
|
340
|
+
userName?: string;
|
|
341
|
+
capabilities?: string;
|
|
342
|
+
additionalContext?: string;
|
|
343
|
+
}): Promise<string>;
|
|
344
|
+
/**
|
|
345
|
+
* Filter any outbound message through the LLM for human readability.
|
|
346
|
+
* This is the single funnel ALL responses pass through.
|
|
347
|
+
*/
|
|
348
|
+
filterResponse(text: string, context?: {
|
|
349
|
+
channel?: string;
|
|
350
|
+
threadId?: string;
|
|
351
|
+
source?: string;
|
|
352
|
+
}): Promise<string>;
|
|
353
|
+
/**
|
|
354
|
+
* Check if a candidate outbound message is redundant with recent messages.
|
|
355
|
+
* Returns true if the message should be suppressed.
|
|
356
|
+
*/
|
|
357
|
+
isRedundant(text: string, threadId: string): Promise<boolean>;
|
|
358
|
+
/**
|
|
359
|
+
* Full send pipeline: check redundancy → filter → store → deliver.
|
|
360
|
+
* Returns the message ID, or undefined if suppressed as redundant.
|
|
361
|
+
*/
|
|
362
|
+
sendResponse(opts: {
|
|
363
|
+
text: string;
|
|
364
|
+
channel: string;
|
|
365
|
+
threadId: string;
|
|
366
|
+
agentId?: string;
|
|
367
|
+
agentSlug?: string;
|
|
368
|
+
taskId?: string;
|
|
369
|
+
source?: string;
|
|
370
|
+
skipRedundancyCheck?: boolean;
|
|
371
|
+
skipFilter?: boolean;
|
|
372
|
+
}): Promise<string | undefined>;
|
|
373
|
+
/**
|
|
374
|
+
* Build a context window from thread history, trimmed to token limit.
|
|
375
|
+
*
|
|
376
|
+
* Only includes inbound (user) messages. Outbound (bot) messages are
|
|
377
|
+
* excluded to prevent the ack layer from mimicking prior verbose responses,
|
|
378
|
+
* which caused hallucinated system state and walls of text.
|
|
379
|
+
*/
|
|
380
|
+
private buildContextWindow;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* TriageRouter — content-based routing with deterministic-first resolution.
|
|
385
|
+
* Story 6.3
|
|
386
|
+
*
|
|
387
|
+
* Replaces the simple channel→agent binding with intelligent routing:
|
|
388
|
+
* 1. Keyword/regex rules evaluated first (deterministic, ~4ms)
|
|
389
|
+
* 2. LLM classification only for ambiguous messages (async, ~2-4s)
|
|
390
|
+
* 3. Ownership chain logged for every routing decision
|
|
391
|
+
*
|
|
392
|
+
* Key constraint: specialists return to triage, never to another specialist.
|
|
393
|
+
*/
|
|
394
|
+
|
|
395
|
+
interface RoutingRule {
|
|
396
|
+
/** Target agent slug */
|
|
397
|
+
agentSlug: string;
|
|
398
|
+
/** Keywords that trigger this rule (case-insensitive) */
|
|
399
|
+
keywords?: string[];
|
|
400
|
+
/** Regex patterns that trigger this rule */
|
|
401
|
+
patterns?: string[];
|
|
402
|
+
/** Priority — lower number wins ties. Default: 50 */
|
|
403
|
+
priority?: number;
|
|
404
|
+
}
|
|
405
|
+
interface RoutingDecision {
|
|
406
|
+
timestamp: string;
|
|
407
|
+
source: string;
|
|
408
|
+
target: string;
|
|
409
|
+
reason: string;
|
|
410
|
+
method: 'deterministic' | 'llm';
|
|
411
|
+
messageId?: string;
|
|
412
|
+
channel?: string;
|
|
413
|
+
}
|
|
414
|
+
interface TriageRouterConfig {
|
|
415
|
+
/** Static routing rules evaluated deterministically */
|
|
416
|
+
rules: RoutingRule[];
|
|
417
|
+
/** Fallback agent if no rule matches and LLM is unavailable */
|
|
418
|
+
fallbackAgent?: string;
|
|
419
|
+
/** Whether to use LLM for ambiguous messages. Default: true */
|
|
420
|
+
llmFallback?: boolean;
|
|
421
|
+
/** Log decisions to the database. Default: true */
|
|
422
|
+
persist?: boolean;
|
|
423
|
+
}
|
|
424
|
+
declare class TriageRouter {
|
|
425
|
+
private db;
|
|
426
|
+
private hooks;
|
|
427
|
+
private readonly rules;
|
|
428
|
+
private readonly fallbackAgent?;
|
|
429
|
+
private readonly llmFallback;
|
|
430
|
+
private readonly persist;
|
|
431
|
+
private readonly compiledRules;
|
|
432
|
+
constructor(db: DataStore, hooks: HookBus, config: TriageRouterConfig);
|
|
433
|
+
/**
|
|
434
|
+
* Route an inbound message to the best agent.
|
|
435
|
+
* Returns the agent slug and the routing decision.
|
|
436
|
+
*/
|
|
437
|
+
route(msg: InboundMessage): Promise<{
|
|
438
|
+
agentSlug: string | undefined;
|
|
439
|
+
decision: RoutingDecision;
|
|
440
|
+
}>;
|
|
441
|
+
/**
|
|
442
|
+
* Query the ownership chain for a given message or channel.
|
|
443
|
+
*/
|
|
444
|
+
getDecisionHistory(filter?: {
|
|
445
|
+
channel?: string;
|
|
446
|
+
limit?: number;
|
|
447
|
+
}): Promise<RoutingDecision[]>;
|
|
448
|
+
/**
|
|
449
|
+
* LLM classification — emits a hook for external LLM integration.
|
|
450
|
+
* Returns agent slug + reason, or undefined if LLM is unavailable.
|
|
451
|
+
*/
|
|
452
|
+
private classifyWithLLM;
|
|
453
|
+
private buildDecision;
|
|
454
|
+
private logDecision;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* MessageInterpreter — async structured extraction from messages.
|
|
459
|
+
* Story 7.3
|
|
460
|
+
*
|
|
461
|
+
* After every message is stored, the interpreter runs async to extract
|
|
462
|
+
* structured data types: tasks, memories, files, user context, and custom types.
|
|
463
|
+
*
|
|
464
|
+
* Uses a cheap LLM (Haiku) for classification and extraction.
|
|
465
|
+
* Pluggable extractors allow apps to add custom data types.
|
|
466
|
+
*/
|
|
467
|
+
|
|
468
|
+
interface ExtractedTask {
|
|
469
|
+
title: string;
|
|
470
|
+
description?: string;
|
|
471
|
+
dueDate?: string;
|
|
472
|
+
scheduled?: boolean;
|
|
473
|
+
priority?: number;
|
|
474
|
+
}
|
|
475
|
+
interface ExtractedMemory {
|
|
476
|
+
summary: string;
|
|
477
|
+
contents: string;
|
|
478
|
+
tags?: string[];
|
|
479
|
+
category?: string;
|
|
480
|
+
}
|
|
481
|
+
interface ExtractedFile {
|
|
482
|
+
filename: string;
|
|
483
|
+
fileType: string;
|
|
484
|
+
contents: string;
|
|
485
|
+
summary: string;
|
|
486
|
+
}
|
|
487
|
+
interface ExtractedUserContext {
|
|
488
|
+
trait: string;
|
|
489
|
+
value: string;
|
|
490
|
+
}
|
|
491
|
+
interface InterpretationResult {
|
|
492
|
+
messageId: string;
|
|
493
|
+
tasks: ExtractedTask[];
|
|
494
|
+
memories: ExtractedMemory[];
|
|
495
|
+
files: ExtractedFile[];
|
|
496
|
+
userContext: ExtractedUserContext[];
|
|
497
|
+
custom: Record<string, unknown[]>;
|
|
498
|
+
isTaskRequest: boolean;
|
|
499
|
+
}
|
|
500
|
+
type LLMCallFn = (params: {
|
|
501
|
+
model: string;
|
|
502
|
+
messages: ChatMessage[];
|
|
503
|
+
system?: string;
|
|
504
|
+
maxTokens?: number;
|
|
505
|
+
}) => Promise<{
|
|
506
|
+
content: string;
|
|
507
|
+
}>;
|
|
508
|
+
/**
|
|
509
|
+
* Pluggable extractor interface for custom data types.
|
|
510
|
+
*/
|
|
511
|
+
interface Extractor {
|
|
512
|
+
readonly type: string;
|
|
513
|
+
extract(message: {
|
|
514
|
+
body: string;
|
|
515
|
+
attachments?: Array<Record<string, unknown>>;
|
|
516
|
+
}, llmCall: LLMCallFn): Promise<unknown[]>;
|
|
517
|
+
}
|
|
518
|
+
interface MessageInterpreterConfig {
|
|
519
|
+
/** Additional custom extractors beyond built-in ones */
|
|
520
|
+
extractors?: Extractor[];
|
|
521
|
+
/** Model for interpretation LLM calls. Default: 'fast' */
|
|
522
|
+
model?: string;
|
|
523
|
+
/** LLM call function */
|
|
524
|
+
llmCall: LLMCallFn;
|
|
525
|
+
/** Auto-create tasks from extracted tasks. Default: false */
|
|
526
|
+
autoCreateTasks?: boolean;
|
|
527
|
+
}
|
|
528
|
+
declare class MessageInterpreter {
|
|
529
|
+
private db;
|
|
530
|
+
private hooks;
|
|
531
|
+
private readonly extractors;
|
|
532
|
+
private readonly model;
|
|
533
|
+
private readonly llmCall;
|
|
534
|
+
private readonly autoCreateTasks;
|
|
535
|
+
constructor(db: DataStore, hooks: HookBus, config: MessageInterpreterConfig);
|
|
536
|
+
/**
|
|
537
|
+
* Interpret a stored message asynchronously.
|
|
538
|
+
* Extracts tasks, memories, files, user context, and custom types.
|
|
539
|
+
*/
|
|
540
|
+
interpret(messageId: string): Promise<InterpretationResult>;
|
|
541
|
+
/**
|
|
542
|
+
* Extract structured data from message text using LLM.
|
|
543
|
+
*/
|
|
544
|
+
private extractWithLLM;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* ChatPipeline — configurable 6-layer chat orchestration.
|
|
549
|
+
* Story 7.4
|
|
550
|
+
*
|
|
551
|
+
* Replaces duplicated handler code across apps with a single configurable
|
|
552
|
+
* pipeline. Apps provide: system prompt, routing rules, LLM call function,
|
|
553
|
+
* and optional message filter. Everything else is framework-level.
|
|
554
|
+
*
|
|
555
|
+
* Layers:
|
|
556
|
+
* 1. Dedup + Storage (MessageStore)
|
|
557
|
+
* 2. Fast Response (ChatResponder)
|
|
558
|
+
* 3. Interpretation (MessageInterpreter)
|
|
559
|
+
* 4. Post-Interpretation Response
|
|
560
|
+
* 5. Task Dispatch (TriageRouter)
|
|
561
|
+
* 6. Task Execution Response
|
|
562
|
+
*/
|
|
563
|
+
|
|
564
|
+
interface ChatPipelineConfig {
|
|
565
|
+
/** LLM call function for chat responses and interpretation */
|
|
566
|
+
llmCall: ChatResponderConfig['llmCall'];
|
|
567
|
+
/** System prompt for the conversational responder */
|
|
568
|
+
systemPrompt: string;
|
|
569
|
+
/** Agent routing rules for task dispatch */
|
|
570
|
+
routingRules: RoutingRule[];
|
|
571
|
+
/** Default agent when no rule matches */
|
|
572
|
+
fallbackAgent: string;
|
|
573
|
+
/** Optional message filter — return false to ignore a message */
|
|
574
|
+
messageFilter?: (msg: InboundMessage) => boolean;
|
|
575
|
+
/** Optional capabilities description for the responder */
|
|
576
|
+
capabilities?: string;
|
|
577
|
+
/** Channel this pipeline handles (default: 'slack') */
|
|
578
|
+
channel?: string;
|
|
579
|
+
/** Custom extractors for MessageInterpreter */
|
|
580
|
+
extractors?: Extractor[];
|
|
581
|
+
/** Dedup window in ms (default: 300_000 = 5 min) */
|
|
582
|
+
dedupWindowMs?: number;
|
|
583
|
+
/** Model for fast responses (default: 'fast') */
|
|
584
|
+
model?: string;
|
|
585
|
+
/** Enable LLM fallback routing (default: false) */
|
|
586
|
+
llmRouting?: boolean;
|
|
587
|
+
/** Skip the ack layer — no fast response before task dispatch (default: false) */
|
|
588
|
+
skipAck?: boolean;
|
|
589
|
+
/** TaskQueue instance — required for task dispatch */
|
|
590
|
+
tasks: {
|
|
591
|
+
create(task: Record<string, unknown>): Promise<string>;
|
|
592
|
+
update(id: string, changes: Record<string, unknown>): Promise<void>;
|
|
593
|
+
get(id: string): Promise<Record<string, unknown> | undefined>;
|
|
594
|
+
};
|
|
595
|
+
/** WakeupQueue instance — required for agent waking */
|
|
596
|
+
wakeups: {
|
|
597
|
+
enqueue(agentId: string, source: string, context?: Record<string, unknown>): Promise<string>;
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
declare class ChatPipeline {
|
|
601
|
+
private db;
|
|
602
|
+
private hooks;
|
|
603
|
+
readonly messageStore: MessageStore;
|
|
604
|
+
readonly responder: ChatResponder;
|
|
605
|
+
readonly interpreter: MessageInterpreter;
|
|
606
|
+
readonly router: TriageRouter;
|
|
607
|
+
private readonly channel;
|
|
608
|
+
private readonly messageFilter?;
|
|
609
|
+
private readonly capabilities?;
|
|
610
|
+
private readonly dedupWindowMs;
|
|
611
|
+
private readonly tasks;
|
|
612
|
+
private readonly wakeups;
|
|
613
|
+
private readonly skipAck;
|
|
614
|
+
private readonly threadChannelMap;
|
|
615
|
+
/** Last dispatch promise — exposed for testing. */
|
|
616
|
+
lastDispatch: Promise<void>;
|
|
617
|
+
constructor(db: DataStore, hooks: HookBus, config: ChatPipelineConfig);
|
|
618
|
+
/**
|
|
619
|
+
* Resolve the Slack channel ID for a thread (for response delivery).
|
|
620
|
+
*/
|
|
621
|
+
resolveChannel(threadId: string, taskId?: string): Promise<string | undefined>;
|
|
622
|
+
/**
|
|
623
|
+
* Register the 6-layer pipeline on the HookBus.
|
|
624
|
+
*/
|
|
625
|
+
private registerHandlers;
|
|
626
|
+
/**
|
|
627
|
+
* Check and record message dedup (SHA256 hash, configurable window).
|
|
628
|
+
*/
|
|
629
|
+
private isDuplicate;
|
|
630
|
+
/**
|
|
631
|
+
* Async interpretation + task dispatch (Layers 3-5).
|
|
632
|
+
*
|
|
633
|
+
* ALWAYS creates a task programmatically — task creation does not depend
|
|
634
|
+
* on LLM classification. Interpretation enriches but never gates dispatch.
|
|
635
|
+
*/
|
|
636
|
+
private interpretAndDispatch;
|
|
637
|
+
/**
|
|
638
|
+
* Programmatic task creation — guaranteed, no LLM dependency.
|
|
639
|
+
*/
|
|
640
|
+
private guaranteedTaskDispatch;
|
|
641
|
+
/**
|
|
642
|
+
* Route and dispatch extracted tasks.
|
|
643
|
+
*/
|
|
644
|
+
private dispatchTasks;
|
|
645
|
+
/**
|
|
646
|
+
* Resolve Slack channel ID from thread_task_map or in-memory fallback.
|
|
647
|
+
*/
|
|
648
|
+
private resolveChannelId;
|
|
649
|
+
/**
|
|
650
|
+
* Build human-readable interpretation summary.
|
|
651
|
+
*/
|
|
652
|
+
private buildSummary;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export { type ChatResponderConfig as C, DataStore as D, type Extractor as E, type Filter as F, HookBus as H, type InterpretationResult as I, type LLMCallFn as L, MessageStore as M, type PkLookup as P, type QueryOptions as Q, type RelationDef as R, type SeedItem as S, type TableDefinition as T, type Unsubscribe as U, ChatResponder as a, MessageInterpreter as b, ChatPipeline as c, type ChatPipelineConfig as d, DataStoreError as e, type EntityContextDef as f, type EntityFileSpec as g, type EntitySource as h, type ExtractedFile as i, type ExtractedMemory as j, type ExtractedTask as k, type ExtractedUserContext as l, type HookHandler as m, type HookOptions as n, type HookRegistration as o, type MessageInterpreterConfig as p, type RoutingDecision as q, type RoutingRule as r, type Row as s, type SqliteAdapter as t, type StoreResult as u, type StoredAttachment as v, type TableInfoRow as w, TriageRouter as x, type TriageRouterConfig as y };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { C as ChannelAdapter, H as HealthStatus, I as InboundMessage } from './channel-
|
|
2
|
-
export { A as Attachment, a as AttachmentMediaType, b as ChannelCapabilities, c as ChannelConfig, d as ChannelMeta, e as ChatType, F as FormattingMode, O as OutboundPayload, S as SendResult } from './channel-
|
|
1
|
+
import { C as ChannelAdapter, H as HealthStatus, I as InboundMessage } from './channel-CVm1AWUF.js';
|
|
2
|
+
export { A as Attachment, a as AttachmentMediaType, b as ChannelCapabilities, c as ChannelConfig, d as ChannelMeta, e as ChatType, F as FormattingMode, O as OutboundPayload, S as SendResult } from './channel-CVm1AWUF.js';
|
|
3
3
|
import { T as TokenUsage, L as LLMProvider, M as ModelInfo, R as ResolvedModel, C as ChatMessage } from './provider-BHkqkSdq.js';
|
|
4
4
|
export { a as ChatParams, b as ChatResult, c as ContentBlock, d as ToolUse } from './provider-BHkqkSdq.js';
|
|
5
5
|
import { C as ConnectorConfig } from './connector-B4Mj0P1b.js';
|
|
6
6
|
export { A as AuthResult, a as Connector, b as ConnectorMeta, P as PushResult, S as SyncOptions, c as SyncResult } from './connector-B4Mj0P1b.js';
|
|
7
|
-
import { C as ChatResponderConfig, D as DataStore, H as HookBus, M as MessageStore, a as ChatResponder, b as MessageInterpreter, E as Extractor } from './chat-pipeline-
|
|
8
|
-
export { c as ChatPipeline, d as ChatPipelineConfig, e as DataStoreError, f as EntityContextDef, g as EntityFileSpec, h as EntitySource, i as ExtractedFile, j as ExtractedMemory, k as ExtractedTask, l as ExtractedUserContext, F as Filter, m as HookHandler, n as HookOptions, o as HookRegistration, I as InterpretationResult, L as LLMCallFn, p as MessageInterpreterConfig, P as PkLookup, Q as QueryOptions, R as RelationDef, q as RoutingDecision, r as RoutingRule, s as Row, S as SeedItem, t as SqliteAdapter, u as StoreResult, v as StoredAttachment, T as TableDefinition, w as TableInfoRow, x as TriageRouter, y as TriageRouterConfig, U as Unsubscribe } from './chat-pipeline-
|
|
7
|
+
import { C as ChatResponderConfig, D as DataStore, H as HookBus, M as MessageStore, a as ChatResponder, b as MessageInterpreter, E as Extractor } from './chat-pipeline-aBSj7a4E.js';
|
|
8
|
+
export { c as ChatPipeline, d as ChatPipelineConfig, e as DataStoreError, f as EntityContextDef, g as EntityFileSpec, h as EntitySource, i as ExtractedFile, j as ExtractedMemory, k as ExtractedTask, l as ExtractedUserContext, F as Filter, m as HookHandler, n as HookOptions, o as HookRegistration, I as InterpretationResult, L as LLMCallFn, p as MessageInterpreterConfig, P as PkLookup, Q as QueryOptions, R as RelationDef, q as RoutingDecision, r as RoutingRule, s as Row, S as SeedItem, t as SqliteAdapter, u as StoreResult, v as StoredAttachment, T as TableDefinition, w as TableInfoRow, x as TriageRouter, y as TriageRouterConfig, U as Unsubscribe } from './chat-pipeline-aBSj7a4E.js';
|
|
9
9
|
import 'better-sqlite3';
|
|
10
10
|
|
|
11
11
|
/** Execution adapter types — Story 1.5 / 3.4 / 3.5 */
|
|
@@ -1065,6 +1065,7 @@ type ContentBlock = {
|
|
|
1065
1065
|
id?: string;
|
|
1066
1066
|
name?: string;
|
|
1067
1067
|
input?: unknown;
|
|
1068
|
+
source?: unknown;
|
|
1068
1069
|
};
|
|
1069
1070
|
type MessageParam = {
|
|
1070
1071
|
role: string;
|
package/dist/index.js
CHANGED
|
@@ -2161,7 +2161,8 @@ ${ctx2}`;
|
|
|
2161
2161
|
history,
|
|
2162
2162
|
msg.body,
|
|
2163
2163
|
threadTs,
|
|
2164
|
-
channelId
|
|
2164
|
+
channelId,
|
|
2165
|
+
msg.attachmentBlocks
|
|
2165
2166
|
);
|
|
2166
2167
|
await this.hooks.emit("typing.stop", { channel: this.channel, threadId: threadTs });
|
|
2167
2168
|
if (text) {
|
|
@@ -2209,14 +2210,15 @@ ${ctx2}`;
|
|
|
2209
2210
|
/**
|
|
2210
2211
|
* Primary agent tool loop — adapted from ExecutionEngine pattern.
|
|
2211
2212
|
*/
|
|
2212
|
-
async think(systemPrompt, history, currentMessage, threadTs, channelId) {
|
|
2213
|
+
async think(systemPrompt, history, currentMessage, threadTs, channelId, attachmentBlocks) {
|
|
2213
2214
|
const model = this.config.model ?? "claude-sonnet-4-6";
|
|
2214
2215
|
const maxIterations = this.config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
2215
2216
|
const maxTokens = this.config.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
2216
2217
|
const tasksDispatched = [];
|
|
2218
|
+
const userContent = attachmentBlocks?.length ? [...attachmentBlocks, { type: "text", text: currentMessage }] : currentMessage;
|
|
2217
2219
|
const messages = [
|
|
2218
2220
|
...history,
|
|
2219
|
-
{ role: "user", content:
|
|
2221
|
+
{ role: "user", content: userContent }
|
|
2220
2222
|
];
|
|
2221
2223
|
let finalText = "";
|
|
2222
2224
|
for (let i = 0; i < maxIterations; i++) {
|