@alejandroroman/agent-kit 0.1.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/_memory/config.d.ts +14 -0
- package/dist/_memory/config.js +16 -0
- package/dist/_memory/db/client.d.ts +2 -0
- package/dist/_memory/db/client.js +15 -0
- package/dist/_memory/db/schema.d.ts +14 -0
- package/dist/_memory/db/schema.js +51 -0
- package/dist/_memory/embeddings/ollama.d.ts +12 -0
- package/dist/_memory/embeddings/ollama.js +22 -0
- package/dist/_memory/embeddings/provider.d.ts +4 -0
- package/dist/_memory/embeddings/provider.js +1 -0
- package/dist/_memory/index.d.ts +10 -0
- package/dist/_memory/index.js +6 -0
- package/dist/_memory/search.d.ts +30 -0
- package/dist/_memory/search.js +121 -0
- package/dist/_memory/server.d.ts +8 -0
- package/dist/_memory/server.js +126 -0
- package/dist/_memory/store.d.ts +51 -0
- package/dist/_memory/store.js +115 -0
- package/dist/agent/loop.d.ts +3 -0
- package/dist/agent/loop.js +195 -0
- package/dist/agent/setup.d.ts +6 -0
- package/dist/agent/setup.js +11 -0
- package/dist/agent/soul.d.ts +1 -0
- package/dist/agent/soul.js +8 -0
- package/dist/agent/types.d.ts +23 -0
- package/dist/agent/types.js +1 -0
- package/dist/api/agents.d.ts +2 -0
- package/dist/api/agents.js +43 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +20 -0
- package/dist/api/cron.d.ts +2 -0
- package/dist/api/cron.js +15 -0
- package/dist/api/health.d.ts +2 -0
- package/dist/api/health.js +8 -0
- package/dist/api/logs.d.ts +5 -0
- package/dist/api/logs.js +28 -0
- package/dist/api/router.d.ts +6 -0
- package/dist/api/router.js +80 -0
- package/dist/api/sessions.d.ts +2 -0
- package/dist/api/sessions.js +67 -0
- package/dist/api/types.d.ts +12 -0
- package/dist/api/types.js +13 -0
- package/dist/api/usage.d.ts +3 -0
- package/dist/api/usage.js +50 -0
- package/dist/bootstrap.d.ts +51 -0
- package/dist/bootstrap.js +110 -0
- package/dist/cli/chat.d.ts +1 -0
- package/dist/cli/chat.js +102 -0
- package/dist/cli/config-writer.d.ts +40 -0
- package/dist/cli/config-writer.js +108 -0
- package/dist/cli/create.d.ts +1 -0
- package/dist/cli/create.js +37 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +85 -0
- package/dist/cli/list.d.ts +1 -0
- package/dist/cli/list.js +36 -0
- package/dist/cli/ollama.d.ts +6 -0
- package/dist/cli/ollama.js +44 -0
- package/dist/cli/setup-agent/index.d.ts +9 -0
- package/dist/cli/setup-agent/index.js +100 -0
- package/dist/cli/setup-agent/soul.d.ts +2 -0
- package/dist/cli/setup-agent/soul.js +79 -0
- package/dist/cli/setup-agent/tools.d.ts +9 -0
- package/dist/cli/setup-agent/tools.js +362 -0
- package/dist/cli/start.d.ts +1 -0
- package/dist/cli/start.js +235 -0
- package/dist/cli/ui.d.ts +17 -0
- package/dist/cli/ui.js +79 -0
- package/dist/cli/validate.d.ts +1 -0
- package/dist/cli/validate.js +47 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +59 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +3 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +10 -0
- package/dist/config/resolve.d.ts +22 -0
- package/dist/config/resolve.js +45 -0
- package/dist/config/schema.d.ts +217 -0
- package/dist/config/schema.js +159 -0
- package/dist/cron/scheduler.d.ts +22 -0
- package/dist/cron/scheduler.js +115 -0
- package/dist/gateways/slack/client.d.ts +13 -0
- package/dist/gateways/slack/client.js +44 -0
- package/dist/gateways/slack/format.d.ts +30 -0
- package/dist/gateways/slack/format.js +170 -0
- package/dist/gateways/slack/handler.d.ts +9 -0
- package/dist/gateways/slack/handler.js +95 -0
- package/dist/gateways/slack/index.d.ts +16 -0
- package/dist/gateways/slack/index.js +102 -0
- package/dist/gateways/slack/listener.d.ts +10 -0
- package/dist/gateways/slack/listener.js +35 -0
- package/dist/gateways/slack/sessions.d.ts +11 -0
- package/dist/gateways/slack/sessions.js +32 -0
- package/dist/gateways/slack/types.d.ts +13 -0
- package/dist/gateways/slack/types.js +7 -0
- package/dist/heartbeat/index.d.ts +2 -0
- package/dist/heartbeat/index.js +1 -0
- package/dist/heartbeat/runner.d.ts +31 -0
- package/dist/heartbeat/runner.js +215 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +207 -0
- package/dist/llm/anthropic.d.ts +12 -0
- package/dist/llm/anthropic.js +89 -0
- package/dist/llm/fallback.d.ts +6 -0
- package/dist/llm/fallback.js +30 -0
- package/dist/llm/index.d.ts +9 -0
- package/dist/llm/index.js +40 -0
- package/dist/llm/openai.d.ts +12 -0
- package/dist/llm/openai.js +85 -0
- package/dist/llm/provider.d.ts +12 -0
- package/dist/llm/provider.js +9 -0
- package/dist/llm/types.d.ts +73 -0
- package/dist/llm/types.js +6 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +15 -0
- package/dist/multi/registry.d.ts +15 -0
- package/dist/multi/registry.js +28 -0
- package/dist/multi/spawn.d.ts +14 -0
- package/dist/multi/spawn.js +14 -0
- package/dist/scripts/validate-agent-cli.d.ts +1 -0
- package/dist/scripts/validate-agent-cli.js +47 -0
- package/dist/scripts/validate-agent.d.ts +17 -0
- package/dist/scripts/validate-agent.js +242 -0
- package/dist/session/compaction.d.ts +4 -0
- package/dist/session/compaction.js +30 -0
- package/dist/session/manager.d.ts +9 -0
- package/dist/session/manager.js +41 -0
- package/dist/skills/activate.d.ts +11 -0
- package/dist/skills/activate.js +62 -0
- package/dist/skills/index.d.ts +3 -0
- package/dist/skills/index.js +3 -0
- package/dist/skills/loader.d.ts +3 -0
- package/dist/skills/loader.js +20 -0
- package/dist/skills/schema.d.ts +8 -0
- package/dist/skills/schema.js +7 -0
- package/dist/text.d.ts +8 -0
- package/dist/text.js +24 -0
- package/dist/tools/builtin/index.d.ts +21 -0
- package/dist/tools/builtin/index.js +21 -0
- package/dist/tools/builtin/memory.d.ts +8 -0
- package/dist/tools/builtin/memory.js +164 -0
- package/dist/tools/builtin/read-file.d.ts +3 -0
- package/dist/tools/builtin/read-file.js +30 -0
- package/dist/tools/builtin/run-command.d.ts +3 -0
- package/dist/tools/builtin/run-command.js +37 -0
- package/dist/tools/builtin/spawn.d.ts +15 -0
- package/dist/tools/builtin/spawn.js +59 -0
- package/dist/tools/builtin/web-search.d.ts +8 -0
- package/dist/tools/builtin/web-search.js +99 -0
- package/dist/tools/builtin/write-file.d.ts +3 -0
- package/dist/tools/builtin/write-file.js +34 -0
- package/dist/tools/registry.d.ts +10 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tools/sandbox.d.ts +9 -0
- package/dist/tools/sandbox.js +74 -0
- package/dist/tools/types.d.ts +8 -0
- package/dist/tools/types.js +7 -0
- package/dist/usage/index.d.ts +4 -0
- package/dist/usage/index.js +3 -0
- package/dist/usage/pricing.d.ts +10 -0
- package/dist/usage/pricing.js +35 -0
- package/dist/usage/schema.d.ts +1 -0
- package/dist/usage/schema.js +45 -0
- package/dist/usage/store.d.ts +10 -0
- package/dist/usage/store.js +227 -0
- package/dist/usage/types.d.ts +61 -0
- package/dist/usage/types.js +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert markdown (as produced by LLMs) to Slack Block Kit blocks.
|
|
3
|
+
*
|
|
4
|
+
* Produces an array of Block Kit objects for rich rendering:
|
|
5
|
+
* - Headings → header blocks
|
|
6
|
+
* - Horizontal rules → divider blocks
|
|
7
|
+
* - Tables → section blocks with monospace code for alignment
|
|
8
|
+
* - Text paragraphs → section blocks with mrkdwn
|
|
9
|
+
* - Code blocks → section blocks with triple-backtick mrkdwn
|
|
10
|
+
*/
|
|
11
|
+
/** Convert markdown to Slack mrkdwn inline formatting */
|
|
12
|
+
export function convertInlineFormatting(text) {
|
|
13
|
+
let result = text.replace(/\*\*(.+?)\*\*/g, "*$1*");
|
|
14
|
+
result = result.replace(/__(.+?)__/g, "*$1*");
|
|
15
|
+
result = result.replace(/~~(.+?)~~/g, "~$1~");
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
/** Convert full markdown string to Slack Block Kit blocks */
|
|
19
|
+
export function markdownToBlocks(md) {
|
|
20
|
+
const lines = md.split("\n");
|
|
21
|
+
const blocks = [];
|
|
22
|
+
let i = 0;
|
|
23
|
+
let textBuffer = [];
|
|
24
|
+
function flushText() {
|
|
25
|
+
const text = textBuffer.join("\n").trim();
|
|
26
|
+
if (text) {
|
|
27
|
+
blocks.push({
|
|
28
|
+
type: "section",
|
|
29
|
+
text: { type: "mrkdwn", text: convertInlineFormatting(text) },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
textBuffer = [];
|
|
33
|
+
}
|
|
34
|
+
while (i < lines.length) {
|
|
35
|
+
const line = lines[i];
|
|
36
|
+
// Fenced code blocks → section with triple backticks
|
|
37
|
+
if (line.trimStart().startsWith("```")) {
|
|
38
|
+
flushText();
|
|
39
|
+
const codeLines = [];
|
|
40
|
+
const lang = line.trim().slice(3);
|
|
41
|
+
i++;
|
|
42
|
+
while (i < lines.length && !lines[i].trimStart().startsWith("```")) {
|
|
43
|
+
codeLines.push(lines[i]);
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
if (i < lines.length)
|
|
47
|
+
i++; // skip closing ```
|
|
48
|
+
blocks.push({
|
|
49
|
+
type: "section",
|
|
50
|
+
text: { type: "mrkdwn", text: "```" + (lang ? lang + "\n" : "\n") + codeLines.join("\n") + "\n```" },
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Horizontal rules → divider
|
|
55
|
+
if (/^-{3,}$/.test(line.trim()) || /^\*{3,}$/.test(line.trim())) {
|
|
56
|
+
flushText();
|
|
57
|
+
blocks.push({ type: "divider" });
|
|
58
|
+
i++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Markdown table → monospace code block for perfect alignment
|
|
62
|
+
if (isTableRow(line)) {
|
|
63
|
+
flushText();
|
|
64
|
+
const tableLines = [];
|
|
65
|
+
while (i < lines.length && isTableRow(lines[i])) {
|
|
66
|
+
if (!isTableSeparator(lines[i])) {
|
|
67
|
+
tableLines.push(lines[i]);
|
|
68
|
+
}
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
const formatted = formatTableMonospace(tableLines);
|
|
72
|
+
blocks.push({
|
|
73
|
+
type: "section",
|
|
74
|
+
text: { type: "mrkdwn", text: "```\n" + formatted + "\n```" },
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Headings → header block (plain_text, max 150 chars)
|
|
79
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
80
|
+
if (headingMatch) {
|
|
81
|
+
flushText();
|
|
82
|
+
// Strip markdown formatting and emojis for header blocks (plain_text only)
|
|
83
|
+
let headerText = headingMatch[2]
|
|
84
|
+
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
85
|
+
.replace(/__(.+?)__/g, "$1");
|
|
86
|
+
if (headerText.length > 150)
|
|
87
|
+
headerText = headerText.slice(0, 147) + "...";
|
|
88
|
+
blocks.push({
|
|
89
|
+
type: "header",
|
|
90
|
+
text: { type: "plain_text", text: headerText, emoji: true },
|
|
91
|
+
});
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// Empty line — could be paragraph break
|
|
96
|
+
if (line.trim() === "") {
|
|
97
|
+
// If we have buffered text, flush as a section
|
|
98
|
+
if (textBuffer.length > 0 && textBuffer.some((l) => l.trim() !== "")) {
|
|
99
|
+
flushText();
|
|
100
|
+
}
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Regular text — buffer it
|
|
105
|
+
textBuffer.push(line);
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
flushText();
|
|
109
|
+
// Slack limits: max 50 blocks per message
|
|
110
|
+
if (blocks.length > 50) {
|
|
111
|
+
const truncated = blocks.slice(0, 49);
|
|
112
|
+
truncated.push({
|
|
113
|
+
type: "section",
|
|
114
|
+
text: { type: "mrkdwn", text: "_... (truncated)_" },
|
|
115
|
+
});
|
|
116
|
+
return truncated;
|
|
117
|
+
}
|
|
118
|
+
return blocks;
|
|
119
|
+
}
|
|
120
|
+
/** Plain-text fallback for notifications/accessibility */
|
|
121
|
+
export function markdownToPlainText(md) {
|
|
122
|
+
return md
|
|
123
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
124
|
+
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
125
|
+
.replace(/__(.+?)__/g, "$1")
|
|
126
|
+
.replace(/~~(.+?)~~/g, "$1")
|
|
127
|
+
.replace(/^-{3,}$/gm, "")
|
|
128
|
+
.replace(/^\*{3,}$/gm, "")
|
|
129
|
+
.trim();
|
|
130
|
+
}
|
|
131
|
+
function isTableRow(line) {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
return trimmed.startsWith("|") && trimmed.endsWith("|");
|
|
134
|
+
}
|
|
135
|
+
function isTableSeparator(line) {
|
|
136
|
+
return /^\|[\s\-:|]+\|$/.test(line.trim());
|
|
137
|
+
}
|
|
138
|
+
/** Format table rows as monospace-aligned text for code blocks */
|
|
139
|
+
function formatTableMonospace(rows) {
|
|
140
|
+
if (rows.length === 0)
|
|
141
|
+
return "";
|
|
142
|
+
const parsed = rows.map((row) => row
|
|
143
|
+
.split("|")
|
|
144
|
+
.slice(1, -1)
|
|
145
|
+
.map((cell) => cell.trim()));
|
|
146
|
+
const colCount = parsed[0].length;
|
|
147
|
+
const widths = Array(colCount).fill(0);
|
|
148
|
+
for (const row of parsed) {
|
|
149
|
+
for (let c = 0; c < Math.min(row.length, colCount); c++) {
|
|
150
|
+
// Strip emoji for width calc (they render as ~2 chars in monospace)
|
|
151
|
+
const stripped = row[c].replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, " ");
|
|
152
|
+
widths[c] = Math.max(widths[c], stripped.length);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const cells of parsed) {
|
|
157
|
+
const padded = cells.map((cell, c) => {
|
|
158
|
+
const stripped = cell.replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, " ");
|
|
159
|
+
const padding = (widths[c] ?? 0) - stripped.length;
|
|
160
|
+
return cell + " ".repeat(Math.max(0, padding));
|
|
161
|
+
});
|
|
162
|
+
out.push(padded.join(" "));
|
|
163
|
+
}
|
|
164
|
+
// Add a separator line after the header
|
|
165
|
+
if (out.length > 1) {
|
|
166
|
+
const sep = widths.map((w) => "-".repeat(w)).join(" ");
|
|
167
|
+
out.splice(1, 0, sep);
|
|
168
|
+
}
|
|
169
|
+
return out.join("\n");
|
|
170
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AgentResult } from "../../agent/types.js";
|
|
2
|
+
import type { SlackClient } from "./client.js";
|
|
3
|
+
import type { SlackGatewayConfig } from "./types.js";
|
|
4
|
+
export interface SlackHandler {
|
|
5
|
+
onJobResult(agentName: string, jobId: string, result: AgentResult, channelOverride?: string): Promise<void>;
|
|
6
|
+
onJobError(agentName: string, jobId: string, error: Error, channelOverride?: string): Promise<void>;
|
|
7
|
+
checkBudgetAlerts(agentName: string, budgetJson: string, channelOverride?: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function createSlackHandler(client: SlackClient, config: SlackGatewayConfig): SlackHandler;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { markdownToBlocks, markdownToPlainText } from "./format.js";
|
|
3
|
+
import { BudgetStatusSchema } from "./types.js";
|
|
4
|
+
import { createLogger } from "../../logger.js";
|
|
5
|
+
function findBudgetToolResults(messages) {
|
|
6
|
+
const toolCallNames = new Map();
|
|
7
|
+
for (const msg of messages) {
|
|
8
|
+
if (msg.role === "assistant") {
|
|
9
|
+
for (const block of msg.content) {
|
|
10
|
+
if (block.type === "tool_call") {
|
|
11
|
+
toolCallNames.set(block.id, block.name);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const results = [];
|
|
17
|
+
for (const msg of messages) {
|
|
18
|
+
if (msg.role === "tool_result" && toolCallNames.get(msg.tool_call_id) === "get_budget_status") {
|
|
19
|
+
results.push(msg.content);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
const log = createLogger("slack:handler");
|
|
25
|
+
export function createSlackHandler(client, config) {
|
|
26
|
+
function getChannel(agentName) {
|
|
27
|
+
return config.channels[agentName]?.channelId;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
async onJobResult(agentName, jobId, result, channelOverride) {
|
|
31
|
+
const channelId = channelOverride ?? getChannel(agentName);
|
|
32
|
+
if (!channelId) {
|
|
33
|
+
log.warn({ agent: agentName }, "no channel binding");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const blocks = [
|
|
38
|
+
{ type: "section", text: { type: "mrkdwn", text: `*[${jobId}]*` } },
|
|
39
|
+
...markdownToBlocks(result.text),
|
|
40
|
+
];
|
|
41
|
+
const fallback = `[${jobId}] ${markdownToPlainText(result.text)}`;
|
|
42
|
+
await client.postMessage(channelId, fallback, blocks);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
log.error({ err, jobId, channelId }, "failed to post result");
|
|
46
|
+
}
|
|
47
|
+
// Auto-check budget alerts from raw tool results
|
|
48
|
+
for (const budgetJson of findBudgetToolResults(result.messages)) {
|
|
49
|
+
await this.checkBudgetAlerts(agentName, budgetJson, channelOverride);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async onJobError(agentName, jobId, error, channelOverride) {
|
|
53
|
+
const channelId = channelOverride ?? getChannel(agentName);
|
|
54
|
+
if (!channelId) {
|
|
55
|
+
log.warn({ agent: agentName }, "no channel binding");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await client.postMessage(channelId, `*[${jobId}] Error:* ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
log.error({ err, jobId, channelId }, "failed to post error");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async checkBudgetAlerts(agentName, budgetJson, channelOverride) {
|
|
66
|
+
const channelId = channelOverride ?? getChannel(agentName);
|
|
67
|
+
if (!channelId) {
|
|
68
|
+
log.warn({ agent: agentName }, "no channel binding");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(budgetJson);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
log.warn({ agent: agentName }, "could not parse budget status");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const result = z.array(BudgetStatusSchema).safeParse(parsed);
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
log.warn({ agent: agentName, error: result.error.message }, "invalid budget status shape");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const overBudget = result.data.filter((s) => s.percent > 100);
|
|
85
|
+
for (const item of overBudget) {
|
|
86
|
+
try {
|
|
87
|
+
await client.postMessage(channelId, `*Budget Alert:* ${item.category} at $${item.spent.toLocaleString("en-US")} / $${item.limit.toLocaleString("en-US")} (${item.percent}%)`);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
log.error({ err, category: item.category }, "failed to post budget alert");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Config } from "../../config/schema.js";
|
|
2
|
+
import type { AgentResult } from "../../agent/types.js";
|
|
3
|
+
import type { Message } from "../../llm/types.js";
|
|
4
|
+
export interface SlackGateway {
|
|
5
|
+
start(): Promise<void>;
|
|
6
|
+
stop(): Promise<void>;
|
|
7
|
+
onJobResult(agentName: string, jobId: string, result: AgentResult, channelOverride?: string): Promise<void>;
|
|
8
|
+
onJobError(agentName: string, jobId: string, error: Error, channelOverride?: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export type AgentExecutor = (agentName: string, messages: Message[]) => Promise<AgentResult>;
|
|
11
|
+
export interface SlackGatewayOptions {
|
|
12
|
+
botToken?: string;
|
|
13
|
+
appToken?: string;
|
|
14
|
+
onAgentRequest?: AgentExecutor;
|
|
15
|
+
}
|
|
16
|
+
export declare function createSlackGateway(config: Config, options?: SlackGatewayOptions): SlackGateway;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createSlackClient } from "./client.js";
|
|
2
|
+
import { createSlackHandler } from "./handler.js";
|
|
3
|
+
import { createSlackListener } from "./listener.js";
|
|
4
|
+
import { createThreadSessionManager } from "./sessions.js";
|
|
5
|
+
import { markdownToBlocks, markdownToPlainText } from "./format.js";
|
|
6
|
+
import { createLogger } from "../../logger.js";
|
|
7
|
+
const log = createLogger("slack");
|
|
8
|
+
export function createSlackGateway(config, options) {
|
|
9
|
+
const botToken = options?.botToken ?? process.env.SLACK_BOT_TOKEN;
|
|
10
|
+
const appToken = options?.appToken ?? process.env.SLACK_APP_TOKEN;
|
|
11
|
+
if (!botToken || !appToken) {
|
|
12
|
+
throw new Error("SLACK_BOT_TOKEN and SLACK_APP_TOKEN are required for Slack gateway");
|
|
13
|
+
}
|
|
14
|
+
const gatewayConfig = { channels: {} };
|
|
15
|
+
for (const [name, agent] of Object.entries(config.agents)) {
|
|
16
|
+
if (agent.slack) {
|
|
17
|
+
gatewayConfig.channels[name] = agent.slack;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const client = createSlackClient({ botToken, appToken });
|
|
21
|
+
const handler = createSlackHandler(client, gatewayConfig);
|
|
22
|
+
const sessions = createThreadSessionManager();
|
|
23
|
+
const executor = options?.onAgentRequest;
|
|
24
|
+
// Per-thread lock to serialize concurrent messages in the same thread
|
|
25
|
+
const threadLocks = new Map();
|
|
26
|
+
async function handleInbound(msg) {
|
|
27
|
+
if (!executor)
|
|
28
|
+
return;
|
|
29
|
+
log.info({ agent: msg.agentName, thread: msg.threadTs, text: msg.userText.slice(0, 80) }, "received message");
|
|
30
|
+
// Serialize per thread
|
|
31
|
+
const prev = threadLocks.get(msg.threadTs) ?? Promise.resolve();
|
|
32
|
+
const current = prev.then(() => processMessage(msg));
|
|
33
|
+
// Prevent rejected promise from blocking the next message in this thread.
|
|
34
|
+
// The actual error is propagated via `await current` below.
|
|
35
|
+
threadLocks.set(msg.threadTs, current.catch((err) => {
|
|
36
|
+
log.warn({ err, thread: msg.threadTs }, "thread lock caught error");
|
|
37
|
+
}));
|
|
38
|
+
await current;
|
|
39
|
+
}
|
|
40
|
+
async function processMessage(msg) {
|
|
41
|
+
if (!executor)
|
|
42
|
+
return;
|
|
43
|
+
const userMsg = { role: "user", content: msg.userText };
|
|
44
|
+
sessions.append(msg.threadTs, userMsg);
|
|
45
|
+
const messages = sessions.get(msg.threadTs);
|
|
46
|
+
try {
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
log.info({ agent: msg.agentName, thread: msg.threadTs, history: messages.length }, "executing agent");
|
|
49
|
+
const result = await executor(msg.agentName, messages);
|
|
50
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
51
|
+
log.info({ agent: msg.agentName, thread: msg.threadTs, elapsed, tokens: result.usage.inputTokens + result.usage.outputTokens }, "agent completed");
|
|
52
|
+
// Replace session with full message history from agent
|
|
53
|
+
sessions.set(msg.threadTs, result.messages);
|
|
54
|
+
const text = result.text || "(completed with no text response)";
|
|
55
|
+
const blocks = markdownToBlocks(text);
|
|
56
|
+
const fallback = markdownToPlainText(text);
|
|
57
|
+
await client.postMessage(msg.channelId, fallback, blocks, msg.threadTs);
|
|
58
|
+
log.info({ agent: msg.agentName, thread: msg.threadTs }, "reply posted");
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
log.error({ err, agent: msg.agentName, thread: msg.threadTs }, "error handling inbound message");
|
|
62
|
+
// Roll back user message on failure to avoid malformed conversation history
|
|
63
|
+
const current = sessions.get(msg.threadTs);
|
|
64
|
+
if (current.length > 0 && current[current.length - 1].role === "user") {
|
|
65
|
+
sessions.set(msg.threadTs, current.slice(0, -1));
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await client.postMessage(msg.channelId, "Sorry, I encountered an error processing your message. Please try again in a moment.", undefined, msg.threadTs);
|
|
69
|
+
}
|
|
70
|
+
catch (postErr) {
|
|
71
|
+
log.error({ err: postErr, thread: msg.threadTs }, "failed to send error reply");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Eviction timer handle
|
|
76
|
+
let evictionTimer;
|
|
77
|
+
return {
|
|
78
|
+
async start() {
|
|
79
|
+
await client.start();
|
|
80
|
+
if (executor) {
|
|
81
|
+
createSlackListener(client.app, gatewayConfig, handleInbound);
|
|
82
|
+
// Evict stale threads every hour
|
|
83
|
+
evictionTimer = setInterval(() => {
|
|
84
|
+
sessions.evict();
|
|
85
|
+
for (const threadTs of threadLocks.keys()) {
|
|
86
|
+
if (sessions.get(threadTs).length === 0) {
|
|
87
|
+
threadLocks.delete(threadTs);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, 60 * 60 * 1000);
|
|
91
|
+
evictionTimer.unref();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async stop() {
|
|
95
|
+
if (evictionTimer)
|
|
96
|
+
clearInterval(evictionTimer);
|
|
97
|
+
await client.stop();
|
|
98
|
+
},
|
|
99
|
+
onJobResult: (agentName, jobId, result, channelOverride) => handler.onJobResult(agentName, jobId, result, channelOverride),
|
|
100
|
+
onJobError: (agentName, jobId, error, channelOverride) => handler.onJobError(agentName, jobId, error, channelOverride),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { App } from "@slack/bolt";
|
|
2
|
+
import type { SlackGatewayConfig } from "./types.js";
|
|
3
|
+
export interface InboundMessage {
|
|
4
|
+
agentName: string;
|
|
5
|
+
userText: string;
|
|
6
|
+
threadTs: string;
|
|
7
|
+
channelId: string;
|
|
8
|
+
}
|
|
9
|
+
export type OnMessageCallback = (message: InboundMessage) => Promise<void>;
|
|
10
|
+
export declare function createSlackListener(app: App, config: SlackGatewayConfig, onMessage: OnMessageCallback): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createLogger } from "../../logger.js";
|
|
2
|
+
const log = createLogger("slack:listener");
|
|
3
|
+
export function createSlackListener(app, config, onMessage) {
|
|
4
|
+
// Build reverse map: channelId → agentName
|
|
5
|
+
const channelToAgent = new Map();
|
|
6
|
+
for (const [agentName, binding] of Object.entries(config.channels)) {
|
|
7
|
+
channelToAgent.set(binding.channelId, agentName);
|
|
8
|
+
}
|
|
9
|
+
app.event("message", async (payload) => {
|
|
10
|
+
// Handle both Bolt's { event } wrapper and direct event (tests)
|
|
11
|
+
const msg = payload.event ?? payload;
|
|
12
|
+
// Filter out bot messages and subtypes (edits, deletes, etc.)
|
|
13
|
+
if (msg.bot_id || msg.subtype)
|
|
14
|
+
return;
|
|
15
|
+
if (!msg.text || typeof msg.text !== "string")
|
|
16
|
+
return;
|
|
17
|
+
const channelId = msg.channel;
|
|
18
|
+
const agentName = channelToAgent.get(channelId);
|
|
19
|
+
if (!agentName)
|
|
20
|
+
return;
|
|
21
|
+
// Use thread_ts if in a thread, otherwise use ts as the thread parent
|
|
22
|
+
const threadTs = msg.thread_ts ?? msg.ts;
|
|
23
|
+
// Fire-and-forget: don't await the handler so the Bolt event pipeline
|
|
24
|
+
// returns immediately. This prevents long-running agent loops from
|
|
25
|
+
// stalling WebSocket ping/pong and causing socket timeouts.
|
|
26
|
+
onMessage({
|
|
27
|
+
agentName,
|
|
28
|
+
userText: msg.text,
|
|
29
|
+
threadTs,
|
|
30
|
+
channelId,
|
|
31
|
+
}).catch((err) => {
|
|
32
|
+
log.error({ err, channelId }, "unhandled error processing message");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Message } from "../../llm/types.js";
|
|
2
|
+
export interface ThreadSessionManager {
|
|
3
|
+
get(threadTs: string): Message[];
|
|
4
|
+
set(threadTs: string, messages: Message[]): void;
|
|
5
|
+
append(threadTs: string, message: Message): void;
|
|
6
|
+
evict(): void;
|
|
7
|
+
}
|
|
8
|
+
export interface ThreadSessionManagerOptions {
|
|
9
|
+
maxAgeMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function createThreadSessionManager(options?: ThreadSessionManagerOptions): ThreadSessionManager;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
2
|
+
export function createThreadSessionManager(options) {
|
|
3
|
+
const maxAgeMs = options?.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
4
|
+
const threads = new Map();
|
|
5
|
+
return {
|
|
6
|
+
get(threadTs) {
|
|
7
|
+
const entry = threads.get(threadTs);
|
|
8
|
+
return entry ? [...entry.messages] : [];
|
|
9
|
+
},
|
|
10
|
+
set(threadTs, messages) {
|
|
11
|
+
threads.set(threadTs, { messages: [...messages], lastActivity: Date.now() });
|
|
12
|
+
},
|
|
13
|
+
append(threadTs, message) {
|
|
14
|
+
const entry = threads.get(threadTs);
|
|
15
|
+
if (entry) {
|
|
16
|
+
entry.messages.push(message);
|
|
17
|
+
entry.lastActivity = Date.now();
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
threads.set(threadTs, { messages: [message], lastActivity: Date.now() });
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
evict() {
|
|
24
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
25
|
+
for (const [key, entry] of threads) {
|
|
26
|
+
if (entry.lastActivity < cutoff) {
|
|
27
|
+
threads.delete(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { SlackBinding } from "../../config/schema.js";
|
|
3
|
+
export declare const BudgetStatusSchema: z.ZodObject<{
|
|
4
|
+
category: z.ZodString;
|
|
5
|
+
spent: z.ZodNumber;
|
|
6
|
+
limit: z.ZodNumber;
|
|
7
|
+
percent: z.ZodNumber;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type BudgetStatus = z.infer<typeof BudgetStatusSchema>;
|
|
10
|
+
export interface SlackGatewayConfig {
|
|
11
|
+
/** Map of agent name to its slack binding (only agents with slack config) */
|
|
12
|
+
channels: Record<string, SlackBinding>;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HeartbeatRunner, isHeartbeatSuppressed, isWithinActiveHours } from "./runner.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Config } from "../config/schema.js";
|
|
2
|
+
import type { AgentRegistry } from "../multi/registry.js";
|
|
3
|
+
import type { AgentResult } from "../agent/types.js";
|
|
4
|
+
import type { UsageStore } from "../usage/store.js";
|
|
5
|
+
export declare function isHeartbeatSuppressed(text: string): boolean;
|
|
6
|
+
export declare function isWithinActiveHours(activeHours: {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
timezone: string;
|
|
10
|
+
} | undefined): boolean;
|
|
11
|
+
export interface HeartbeatCallbacks {
|
|
12
|
+
onResult?: (agentName: string, result: AgentResult) => void;
|
|
13
|
+
onError?: (agentName: string, error: Error) => void;
|
|
14
|
+
onSuppressed?: (agentName: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare class HeartbeatRunner {
|
|
17
|
+
private timers;
|
|
18
|
+
private running;
|
|
19
|
+
private config;
|
|
20
|
+
private agentRegistry;
|
|
21
|
+
private dataDir;
|
|
22
|
+
private skillsDir;
|
|
23
|
+
private usageStore?;
|
|
24
|
+
constructor(config: Config, agentRegistry: AgentRegistry, dataDir: string, skillsDir?: string, usageStore?: UsageStore);
|
|
25
|
+
getHeartbeatAgents(): string[];
|
|
26
|
+
private loadHeartbeatInstructions;
|
|
27
|
+
tick(agentName: string): Promise<AgentResult | undefined>;
|
|
28
|
+
start(callbacks?: HeartbeatCallbacks): void;
|
|
29
|
+
stop(): void;
|
|
30
|
+
isRunning(agentName: string): boolean;
|
|
31
|
+
}
|