@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.
Files changed (169) hide show
  1. package/dist/_memory/config.d.ts +14 -0
  2. package/dist/_memory/config.js +16 -0
  3. package/dist/_memory/db/client.d.ts +2 -0
  4. package/dist/_memory/db/client.js +15 -0
  5. package/dist/_memory/db/schema.d.ts +14 -0
  6. package/dist/_memory/db/schema.js +51 -0
  7. package/dist/_memory/embeddings/ollama.d.ts +12 -0
  8. package/dist/_memory/embeddings/ollama.js +22 -0
  9. package/dist/_memory/embeddings/provider.d.ts +4 -0
  10. package/dist/_memory/embeddings/provider.js +1 -0
  11. package/dist/_memory/index.d.ts +10 -0
  12. package/dist/_memory/index.js +6 -0
  13. package/dist/_memory/search.d.ts +30 -0
  14. package/dist/_memory/search.js +121 -0
  15. package/dist/_memory/server.d.ts +8 -0
  16. package/dist/_memory/server.js +126 -0
  17. package/dist/_memory/store.d.ts +51 -0
  18. package/dist/_memory/store.js +115 -0
  19. package/dist/agent/loop.d.ts +3 -0
  20. package/dist/agent/loop.js +195 -0
  21. package/dist/agent/setup.d.ts +6 -0
  22. package/dist/agent/setup.js +11 -0
  23. package/dist/agent/soul.d.ts +1 -0
  24. package/dist/agent/soul.js +8 -0
  25. package/dist/agent/types.d.ts +23 -0
  26. package/dist/agent/types.js +1 -0
  27. package/dist/api/agents.d.ts +2 -0
  28. package/dist/api/agents.js +43 -0
  29. package/dist/api/config.d.ts +2 -0
  30. package/dist/api/config.js +20 -0
  31. package/dist/api/cron.d.ts +2 -0
  32. package/dist/api/cron.js +15 -0
  33. package/dist/api/health.d.ts +2 -0
  34. package/dist/api/health.js +8 -0
  35. package/dist/api/logs.d.ts +5 -0
  36. package/dist/api/logs.js +28 -0
  37. package/dist/api/router.d.ts +6 -0
  38. package/dist/api/router.js +80 -0
  39. package/dist/api/sessions.d.ts +2 -0
  40. package/dist/api/sessions.js +67 -0
  41. package/dist/api/types.d.ts +12 -0
  42. package/dist/api/types.js +13 -0
  43. package/dist/api/usage.d.ts +3 -0
  44. package/dist/api/usage.js +50 -0
  45. package/dist/bootstrap.d.ts +51 -0
  46. package/dist/bootstrap.js +110 -0
  47. package/dist/cli/chat.d.ts +1 -0
  48. package/dist/cli/chat.js +102 -0
  49. package/dist/cli/config-writer.d.ts +40 -0
  50. package/dist/cli/config-writer.js +108 -0
  51. package/dist/cli/create.d.ts +1 -0
  52. package/dist/cli/create.js +37 -0
  53. package/dist/cli/init.d.ts +1 -0
  54. package/dist/cli/init.js +85 -0
  55. package/dist/cli/list.d.ts +1 -0
  56. package/dist/cli/list.js +36 -0
  57. package/dist/cli/ollama.d.ts +6 -0
  58. package/dist/cli/ollama.js +44 -0
  59. package/dist/cli/setup-agent/index.d.ts +9 -0
  60. package/dist/cli/setup-agent/index.js +100 -0
  61. package/dist/cli/setup-agent/soul.d.ts +2 -0
  62. package/dist/cli/setup-agent/soul.js +79 -0
  63. package/dist/cli/setup-agent/tools.d.ts +9 -0
  64. package/dist/cli/setup-agent/tools.js +362 -0
  65. package/dist/cli/start.d.ts +1 -0
  66. package/dist/cli/start.js +235 -0
  67. package/dist/cli/ui.d.ts +17 -0
  68. package/dist/cli/ui.js +79 -0
  69. package/dist/cli/validate.d.ts +1 -0
  70. package/dist/cli/validate.js +47 -0
  71. package/dist/cli.d.ts +4 -0
  72. package/dist/cli.js +59 -0
  73. package/dist/config/index.d.ts +4 -0
  74. package/dist/config/index.js +3 -0
  75. package/dist/config/loader.d.ts +2 -0
  76. package/dist/config/loader.js +10 -0
  77. package/dist/config/resolve.d.ts +22 -0
  78. package/dist/config/resolve.js +45 -0
  79. package/dist/config/schema.d.ts +217 -0
  80. package/dist/config/schema.js +159 -0
  81. package/dist/cron/scheduler.d.ts +22 -0
  82. package/dist/cron/scheduler.js +115 -0
  83. package/dist/gateways/slack/client.d.ts +13 -0
  84. package/dist/gateways/slack/client.js +44 -0
  85. package/dist/gateways/slack/format.d.ts +30 -0
  86. package/dist/gateways/slack/format.js +170 -0
  87. package/dist/gateways/slack/handler.d.ts +9 -0
  88. package/dist/gateways/slack/handler.js +95 -0
  89. package/dist/gateways/slack/index.d.ts +16 -0
  90. package/dist/gateways/slack/index.js +102 -0
  91. package/dist/gateways/slack/listener.d.ts +10 -0
  92. package/dist/gateways/slack/listener.js +35 -0
  93. package/dist/gateways/slack/sessions.d.ts +11 -0
  94. package/dist/gateways/slack/sessions.js +32 -0
  95. package/dist/gateways/slack/types.d.ts +13 -0
  96. package/dist/gateways/slack/types.js +7 -0
  97. package/dist/heartbeat/index.d.ts +2 -0
  98. package/dist/heartbeat/index.js +1 -0
  99. package/dist/heartbeat/runner.d.ts +31 -0
  100. package/dist/heartbeat/runner.js +215 -0
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +207 -0
  103. package/dist/llm/anthropic.d.ts +12 -0
  104. package/dist/llm/anthropic.js +89 -0
  105. package/dist/llm/fallback.d.ts +6 -0
  106. package/dist/llm/fallback.js +30 -0
  107. package/dist/llm/index.d.ts +9 -0
  108. package/dist/llm/index.js +40 -0
  109. package/dist/llm/openai.d.ts +12 -0
  110. package/dist/llm/openai.js +85 -0
  111. package/dist/llm/provider.d.ts +12 -0
  112. package/dist/llm/provider.js +9 -0
  113. package/dist/llm/types.d.ts +73 -0
  114. package/dist/llm/types.js +6 -0
  115. package/dist/logger.d.ts +2 -0
  116. package/dist/logger.js +15 -0
  117. package/dist/multi/registry.d.ts +15 -0
  118. package/dist/multi/registry.js +28 -0
  119. package/dist/multi/spawn.d.ts +14 -0
  120. package/dist/multi/spawn.js +14 -0
  121. package/dist/scripts/validate-agent-cli.d.ts +1 -0
  122. package/dist/scripts/validate-agent-cli.js +47 -0
  123. package/dist/scripts/validate-agent.d.ts +17 -0
  124. package/dist/scripts/validate-agent.js +242 -0
  125. package/dist/session/compaction.d.ts +4 -0
  126. package/dist/session/compaction.js +30 -0
  127. package/dist/session/manager.d.ts +9 -0
  128. package/dist/session/manager.js +41 -0
  129. package/dist/skills/activate.d.ts +11 -0
  130. package/dist/skills/activate.js +62 -0
  131. package/dist/skills/index.d.ts +3 -0
  132. package/dist/skills/index.js +3 -0
  133. package/dist/skills/loader.d.ts +3 -0
  134. package/dist/skills/loader.js +20 -0
  135. package/dist/skills/schema.d.ts +8 -0
  136. package/dist/skills/schema.js +7 -0
  137. package/dist/text.d.ts +8 -0
  138. package/dist/text.js +24 -0
  139. package/dist/tools/builtin/index.d.ts +21 -0
  140. package/dist/tools/builtin/index.js +21 -0
  141. package/dist/tools/builtin/memory.d.ts +8 -0
  142. package/dist/tools/builtin/memory.js +164 -0
  143. package/dist/tools/builtin/read-file.d.ts +3 -0
  144. package/dist/tools/builtin/read-file.js +30 -0
  145. package/dist/tools/builtin/run-command.d.ts +3 -0
  146. package/dist/tools/builtin/run-command.js +37 -0
  147. package/dist/tools/builtin/spawn.d.ts +15 -0
  148. package/dist/tools/builtin/spawn.js +59 -0
  149. package/dist/tools/builtin/web-search.d.ts +8 -0
  150. package/dist/tools/builtin/web-search.js +99 -0
  151. package/dist/tools/builtin/write-file.d.ts +3 -0
  152. package/dist/tools/builtin/write-file.js +34 -0
  153. package/dist/tools/registry.d.ts +10 -0
  154. package/dist/tools/registry.js +26 -0
  155. package/dist/tools/sandbox.d.ts +9 -0
  156. package/dist/tools/sandbox.js +74 -0
  157. package/dist/tools/types.d.ts +8 -0
  158. package/dist/tools/types.js +7 -0
  159. package/dist/usage/index.d.ts +4 -0
  160. package/dist/usage/index.js +3 -0
  161. package/dist/usage/pricing.d.ts +10 -0
  162. package/dist/usage/pricing.js +35 -0
  163. package/dist/usage/schema.d.ts +1 -0
  164. package/dist/usage/schema.js +45 -0
  165. package/dist/usage/store.d.ts +10 -0
  166. package/dist/usage/store.js +227 -0
  167. package/dist/usage/types.d.ts +61 -0
  168. package/dist/usage/types.js +1 -0
  169. 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,7 @@
1
+ import { z } from "zod";
2
+ export const BudgetStatusSchema = z.object({
3
+ category: z.string(),
4
+ spent: z.number(),
5
+ limit: z.number(),
6
+ percent: z.number(),
7
+ });
@@ -0,0 +1,2 @@
1
+ export { HeartbeatRunner, isHeartbeatSuppressed, isWithinActiveHours } from "./runner.js";
2
+ export type { HeartbeatCallbacks } from "./runner.js";
@@ -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
+ }