@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,215 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { runAgentLoop } from "../agent/loop.js";
4
+ import { setupAgentSession } from "../agent/setup.js";
5
+ import { resolveAgent, resolveModelAlias, resolveWebSearch } from "../config/resolve.js";
6
+ import { createBuiltinRegistry } from "../tools/builtin/index.js";
7
+ import { registerSpawnWrappers } from "../tools/builtin/spawn.js";
8
+ import { createActivateSkillTool } from "../skills/index.js";
9
+ import { createLogger } from "../logger.js";
10
+ import { dateContext } from "../text.js";
11
+ const log = createLogger("heartbeat");
12
+ const HEARTBEAT_OK = "HEARTBEAT_OK";
13
+ export function isHeartbeatSuppressed(text) {
14
+ return text.trim() === HEARTBEAT_OK;
15
+ }
16
+ export function isWithinActiveHours(activeHours) {
17
+ if (!activeHours)
18
+ return true;
19
+ const formatter = new Intl.DateTimeFormat("en-US", {
20
+ hour: "numeric",
21
+ hour12: false,
22
+ timeZone: activeHours.timezone,
23
+ });
24
+ const hour = parseInt(formatter.format(new Date()), 10);
25
+ const { start, end } = activeHours;
26
+ if (start < end)
27
+ return hour >= start && hour < end;
28
+ return hour >= start || hour < end; // midnight wraparound
29
+ }
30
+ export class HeartbeatRunner {
31
+ timers = new Map();
32
+ running = new Set();
33
+ config;
34
+ agentRegistry;
35
+ dataDir;
36
+ skillsDir;
37
+ usageStore;
38
+ constructor(config, agentRegistry, dataDir, skillsDir, usageStore) {
39
+ this.config = config;
40
+ this.agentRegistry = agentRegistry;
41
+ this.dataDir = dataDir;
42
+ this.skillsDir = skillsDir ?? path.join(process.cwd(), "skills");
43
+ this.usageStore = usageStore;
44
+ }
45
+ getHeartbeatAgents() {
46
+ return Object.entries(this.config.agents)
47
+ .filter(([_, def]) => def.heartbeat?.enabled)
48
+ .map(([name]) => name);
49
+ }
50
+ loadHeartbeatInstructions(agentName) {
51
+ const filePath = path.join(this.dataDir, "agents", agentName, "HEARTBEAT.md");
52
+ try {
53
+ return fs.readFileSync(filePath, "utf-8");
54
+ }
55
+ catch (err) {
56
+ if (err.code === "ENOENT")
57
+ return undefined;
58
+ log.error({ err, agent: agentName, filePath }, "failed to read HEARTBEAT.md");
59
+ return undefined;
60
+ }
61
+ }
62
+ async tick(agentName) {
63
+ const agentDef = this.config.agents[agentName];
64
+ if (!agentDef?.heartbeat)
65
+ return undefined;
66
+ const hb = agentDef.heartbeat;
67
+ if (!isWithinActiveHours(hb.activeHours)) {
68
+ log.debug({ agent: agentName }, "outside active hours, skipping");
69
+ return undefined;
70
+ }
71
+ const instructions = this.loadHeartbeatInstructions(agentName);
72
+ if (!instructions) {
73
+ log.warn({ agent: agentName }, "no HEARTBEAT.md found, skipping");
74
+ return undefined;
75
+ }
76
+ // Isolated tool registry per tick (same pattern as CronScheduler)
77
+ const sandbox = agentDef.sandbox ?? this.config.defaults.sandbox;
78
+ const memoryConfig = this.config.defaults.memory;
79
+ const tickRegistry = createBuiltinRegistry({
80
+ allowedCommands: sandbox?.allowedCommands,
81
+ allowedPaths: sandbox?.allowedPaths,
82
+ memoryConfig,
83
+ webSearch: resolveWebSearch(agentName, this.config),
84
+ });
85
+ const resolved = resolveAgent(agentName, this.config, tickRegistry, this.skillsDir);
86
+ // Skills setup — auto-activate all skills upfront so Haiku doesn't need to call activate_skill
87
+ const promptFragments = [];
88
+ let skillsIndex = "";
89
+ if (resolved.skills.length > 0) {
90
+ const ctx = {
91
+ manifests: resolved.skills,
92
+ skillsDir: this.skillsDir,
93
+ toolRegistry: tickRegistry,
94
+ promptFragments,
95
+ activatedSkills: new Set(),
96
+ };
97
+ const activateTool = createActivateSkillTool(ctx);
98
+ tickRegistry.register(activateTool);
99
+ // Auto-activate all skills upfront
100
+ for (const manifest of resolved.skills) {
101
+ try {
102
+ const result = await activateTool.execute({ skill_name: manifest.name });
103
+ if (typeof result === "string" && result.startsWith("Error")) {
104
+ log.error({ agent: agentName, skill: manifest.name, result }, "failed to auto-activate skill");
105
+ }
106
+ }
107
+ catch (err) {
108
+ log.error({ err, agent: agentName, skill: manifest.name }, "skill auto-activation threw");
109
+ }
110
+ }
111
+ const activatedNames = [...ctx.activatedSkills];
112
+ if (activatedNames.length > 0) {
113
+ skillsIndex = "\n\nThe following skills are pre-activated: "
114
+ + activatedNames.join(", ")
115
+ + ".\nTheir tools are already available — you do not need to call activate_skill.";
116
+ }
117
+ }
118
+ // Spawn wrappers
119
+ if (resolved.canSpawn.length > 0) {
120
+ registerSpawnWrappers(resolved.canSpawn, this.config, this.agentRegistry, tickRegistry, this.usageStore);
121
+ }
122
+ // Ephemeral session: no history accumulation, append-only audit log
123
+ const sessionId = `heartbeat-${agentName}`;
124
+ const { soul, session } = setupAgentSession(this.dataDir, agentName, sessionId);
125
+ // Build tick prompt with fresh instructions + timestamp
126
+ const tickPrompt = `[Heartbeat tick — ${new Date().toISOString()}]\n\n${instructions}`;
127
+ const userMsg = { role: "user", content: tickPrompt };
128
+ session.append(userMsg); // audit only
129
+ const messages = [userMsg]; // ephemeral — no history
130
+ // Model: heartbeat.model overrides agent model
131
+ const model = hb.model
132
+ ? resolveModelAlias(hb.model, this.config.models)
133
+ : resolved.model;
134
+ const systemPrompt = [soul, dateContext(), skillsIndex, ...promptFragments]
135
+ .filter(Boolean)
136
+ .join("\n\n") || undefined;
137
+ const result = await runAgentLoop(messages, {
138
+ model,
139
+ fallbacks: resolved.fallbacks,
140
+ systemPrompt,
141
+ toolRegistry: tickRegistry,
142
+ maxIterations: hb.maxIterations ?? resolved.maxIterations,
143
+ compactionThreshold: resolved.compactionThreshold,
144
+ maxToolResultSize: resolved.maxToolResultSize,
145
+ agentName,
146
+ usageStore: this.usageStore,
147
+ source: "heartbeat",
148
+ });
149
+ // Audit: append all new messages from the agent loop
150
+ for (const msg of result.messages.slice(1)) {
151
+ session.append(msg);
152
+ }
153
+ return result;
154
+ }
155
+ start(callbacks) {
156
+ for (const [agentName, agentDef] of Object.entries(this.config.agents)) {
157
+ if (!agentDef.heartbeat?.enabled)
158
+ continue;
159
+ if (this.timers.has(agentName))
160
+ continue; // already started
161
+ const intervalMs = agentDef.heartbeat.intervalMinutes * 60_000;
162
+ const runTick = async () => {
163
+ if (this.running.has(agentName)) {
164
+ log.info({ agent: agentName }, "previous tick still running, skipping");
165
+ return;
166
+ }
167
+ this.running.add(agentName);
168
+ log.info({ agent: agentName }, "heartbeat tick");
169
+ let result;
170
+ try {
171
+ result = await this.tick(agentName);
172
+ }
173
+ catch (err) {
174
+ const error = err instanceof Error ? err : new Error(String(err));
175
+ log.error({ err: error, agent: agentName }, "heartbeat tick failed");
176
+ try {
177
+ callbacks?.onError?.(agentName, error);
178
+ }
179
+ catch { /* callback error */ }
180
+ return;
181
+ }
182
+ finally {
183
+ this.running.delete(agentName);
184
+ }
185
+ if (!result)
186
+ return;
187
+ try {
188
+ if (isHeartbeatSuppressed(result.text)) {
189
+ log.info({ agent: agentName }, "heartbeat suppressed (HEARTBEAT_OK)");
190
+ callbacks?.onSuppressed?.(agentName);
191
+ }
192
+ else {
193
+ callbacks?.onResult?.(agentName, result);
194
+ }
195
+ }
196
+ catch (err) {
197
+ log.error({ err, agent: agentName }, "heartbeat callback threw");
198
+ }
199
+ };
200
+ // Fire immediately, then on interval
201
+ runTick();
202
+ const timer = setInterval(runTick, intervalMs);
203
+ timer.unref();
204
+ this.timers.set(agentName, timer);
205
+ }
206
+ }
207
+ stop() {
208
+ for (const timer of this.timers.values())
209
+ clearInterval(timer);
210
+ this.timers.clear();
211
+ }
212
+ isRunning(agentName) {
213
+ return this.running.has(agentName);
214
+ }
215
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,207 @@
1
+ import * as readline from "readline";
2
+ import * as path from "path";
3
+ import { runAgentLoop } from "./agent/loop.js";
4
+ import { compactMessages } from "./session/compaction.js";
5
+ import { loadConfig } from "./config/index.js";
6
+ import { CronScheduler } from "./cron/scheduler.js";
7
+ import { HeartbeatRunner } from "./heartbeat/index.js";
8
+ import { createSlackGateway } from "./gateways/slack/index.js";
9
+ import { createLogger } from "./logger.js";
10
+ import { createApiServer } from "./api/router.js";
11
+ import { buildAgentRuntime, buildSystemPrompt, initUsageStore, createAgentExecutor, } from "./bootstrap.js";
12
+ const log = createLogger("startup");
13
+ const CONFIG_PATH = path.join(process.cwd(), "agent-kit.json");
14
+ const DATA_DIR = path.join(process.cwd(), "data");
15
+ const DEFAULT_AGENT = "default";
16
+ const SKILLS_DIR = path.join(process.cwd(), "skills");
17
+ async function main() {
18
+ const config = loadConfig(CONFIG_PATH);
19
+ const { usageStore, apiPort } = initUsageStore(config);
20
+ const apiServer = createApiServer({
21
+ config,
22
+ configPath: CONFIG_PATH,
23
+ usageStore,
24
+ dataDir: DATA_DIR,
25
+ }, apiPort);
26
+ const agentName = process.argv[2] ?? DEFAULT_AGENT;
27
+ // Validate all agent tool references at startup
28
+ const runtime = buildAgentRuntime(agentName, config, {
29
+ dataDir: DATA_DIR,
30
+ skillsDir: SKILLS_DIR,
31
+ usageStore,
32
+ });
33
+ runtime.toolRegistry.validateAgents(config.agents);
34
+ const { resolved, toolRegistry, agentRegistry, promptFragments, skillsIndex, soul, session } = runtime;
35
+ const sandbox = config.agents[agentName]?.sandbox ?? config.defaults.sandbox;
36
+ console.log("Agent Kit");
37
+ console.log(` Model: ${resolved.model}`);
38
+ if (resolved.fallbacks.length > 0) {
39
+ console.log(` Fallbacks: ${resolved.fallbacks.join(" → ")}`);
40
+ }
41
+ console.log(` Agent: ${agentName}`);
42
+ console.log(` SOUL: ${soul ? "loaded" : "none (create data/agents/default/SOUL.md)"}`);
43
+ console.log(` Tools: ${resolved.tools.map((t) => t.name).join(", ")}`);
44
+ if (resolved.skills.length > 0) {
45
+ console.log(` Skills: ${resolved.skills.map((s) => s.name).join(", ")}`);
46
+ }
47
+ const sandboxSummary = (() => {
48
+ if (!sandbox)
49
+ return "off (unrestricted)";
50
+ const parts = [];
51
+ if (sandbox.allowedCommands?.length) {
52
+ parts.push(`${sandbox.allowedCommands.length} commands allowed`);
53
+ }
54
+ if (sandbox.allowedPaths?.length) {
55
+ parts.push(`${sandbox.allowedPaths.length} paths allowed`);
56
+ }
57
+ return parts.length > 0 ? parts.join(", ") : "configured (no restrictions)";
58
+ })();
59
+ console.log(` Sandbox: ${sandboxSummary}`);
60
+ if (config.defaults.usage) {
61
+ console.log(` Usage: tracking enabled (${config.defaults.usage.dbPath})`);
62
+ }
63
+ const scheduler = new CronScheduler(config, toolRegistry, agentRegistry, DATA_DIR, SKILLS_DIR, usageStore);
64
+ const enabledJobs = scheduler.getJobs().filter((j) => j.enabled);
65
+ // Agent executor for inbound Slack messages
66
+ const executeAgent = createAgentExecutor(config, {
67
+ dataDir: DATA_DIR,
68
+ skillsDir: SKILLS_DIR,
69
+ agentRegistry,
70
+ usageStore,
71
+ source: "slack",
72
+ });
73
+ // Slack gateway (optional — requires both tokens + agent slack bindings)
74
+ let gateway;
75
+ const hasSlackBindings = Object.values(config.agents).some((a) => a.slack);
76
+ if (hasSlackBindings && process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) {
77
+ try {
78
+ gateway = createSlackGateway(config, { onAgentRequest: executeAgent });
79
+ await gateway.start();
80
+ const bindingCount = Object.values(config.agents).filter((a) => a.slack).length;
81
+ log.info({ bindings: bindingCount }, "Slack connected");
82
+ }
83
+ catch (err) {
84
+ log.warn({ err }, "Slack failed to connect");
85
+ }
86
+ }
87
+ try {
88
+ const actualPort = await apiServer.start();
89
+ console.log(` API: http://localhost:${actualPort}/api`);
90
+ }
91
+ catch (err) {
92
+ log.warn({ err }, "failed to start API server — continuing without it");
93
+ }
94
+ if (enabledJobs.length > 0) {
95
+ const jobList = enabledJobs.map((j) => `${j.id} @ ${j.schedule}`).join(", ");
96
+ log.info({ jobs: enabledJobs.length, jobList }, "cron scheduled");
97
+ scheduler.start({
98
+ onResult: (jobId, agentName, result) => {
99
+ log.info({ jobId, tokens: result.usage.inputTokens + result.usage.outputTokens }, "cron job completed");
100
+ process.stdout.write("You: ");
101
+ const channelOverride = config.cron.find((j) => j.id === jobId)?.slack?.channelId;
102
+ gateway?.onJobResult(agentName, jobId, result, channelOverride).catch((err) => log.warn({ err, jobId }, "failed to post cron result to Slack"));
103
+ },
104
+ onError: (jobId, agentName, error) => {
105
+ log.error({ err: error, jobId }, "cron job failed");
106
+ process.stdout.write("You: ");
107
+ const channelOverride = config.cron.find((j) => j.id === jobId)?.slack?.channelId;
108
+ gateway?.onJobError(agentName, jobId, error, channelOverride).catch((err) => log.warn({ err, jobId }, "failed to post cron error to Slack"));
109
+ },
110
+ });
111
+ }
112
+ const heartbeat = new HeartbeatRunner(config, agentRegistry, DATA_DIR, SKILLS_DIR, usageStore);
113
+ const heartbeatAgents = heartbeat.getHeartbeatAgents();
114
+ if (heartbeatAgents.length > 0) {
115
+ log.info({ agents: heartbeatAgents }, "heartbeat started");
116
+ heartbeat.start({
117
+ onResult: (agentName, result) => {
118
+ log.info({ agent: agentName, tokens: result.usage.inputTokens + result.usage.outputTokens }, "heartbeat alert");
119
+ process.stdout.write("You: ");
120
+ const channelOverride = config.agents[agentName]?.heartbeat?.slack?.channelId;
121
+ gateway?.onJobResult(agentName, "heartbeat", result, channelOverride).catch((err) => log.warn({ err, agent: agentName }, "failed to post heartbeat result to Slack"));
122
+ },
123
+ onError: (agentName, error) => {
124
+ log.error({ err: error, agent: agentName }, "heartbeat failed");
125
+ process.stdout.write("You: ");
126
+ const channelOverride = config.agents[agentName]?.heartbeat?.slack?.channelId;
127
+ gateway?.onJobError(agentName, "heartbeat", error, channelOverride).catch((err) => log.warn({ err, agent: agentName }, "failed to post heartbeat error to Slack"));
128
+ },
129
+ onSuppressed: (agentName) => {
130
+ log.debug({ agent: agentName }, "heartbeat OK, suppressed");
131
+ },
132
+ });
133
+ }
134
+ if (heartbeatAgents.length > 0) {
135
+ console.log(` Heartbeat: ${heartbeatAgents.join(", ")}`);
136
+ }
137
+ console.log(` Commands: /new (reset), /quit`);
138
+ console.log();
139
+ let messages = session.getMessages();
140
+ const rl = readline.createInterface({
141
+ input: process.stdin,
142
+ output: process.stdout,
143
+ });
144
+ const ask = () => {
145
+ rl.question("You: ", async (input) => {
146
+ const trimmed = input.trim();
147
+ if (trimmed === "/quit") {
148
+ scheduler.stop();
149
+ heartbeat.stop();
150
+ await gateway?.stop();
151
+ await apiServer.stop();
152
+ try {
153
+ usageStore?.close();
154
+ }
155
+ catch (err) {
156
+ log.warn({ err }, "failed to close usage store");
157
+ }
158
+ finally {
159
+ rl.close();
160
+ }
161
+ return;
162
+ }
163
+ if (trimmed === "/new") {
164
+ messages = [];
165
+ console.log(" Session reset.\n");
166
+ ask();
167
+ return;
168
+ }
169
+ if (!trimmed) {
170
+ ask();
171
+ return;
172
+ }
173
+ const userMsg = { role: "user", content: trimmed };
174
+ messages.push(userMsg);
175
+ session.append(userMsg);
176
+ messages = await compactMessages(messages, resolved.model, resolved.compactionThreshold);
177
+ try {
178
+ const systemPrompt = buildSystemPrompt(soul, skillsIndex, promptFragments);
179
+ const result = await runAgentLoop(messages, {
180
+ model: resolved.model,
181
+ fallbacks: resolved.fallbacks,
182
+ systemPrompt,
183
+ toolRegistry,
184
+ maxIterations: resolved.maxIterations,
185
+ compactionThreshold: resolved.compactionThreshold,
186
+ maxToolResultSize: resolved.maxToolResultSize,
187
+ agentName,
188
+ usageStore,
189
+ source: "cli",
190
+ });
191
+ const newMessages = result.messages.slice(messages.length - 1);
192
+ for (const msg of newMessages) {
193
+ session.append(msg);
194
+ }
195
+ messages = result.messages;
196
+ console.log(`\nAgent: ${result.text}`);
197
+ console.log(` (${result.usage.inputTokens + result.usage.outputTokens} tokens)\n`);
198
+ }
199
+ catch (err) {
200
+ console.error(`\n Error: ${err instanceof Error ? err.message : err}\n`);
201
+ }
202
+ ask();
203
+ });
204
+ };
205
+ ask();
206
+ }
207
+ main();
@@ -0,0 +1,12 @@
1
+ import type { LLMProvider, CompleteOptions } from "./provider.js";
2
+ import type { Message, LLMResponse, StreamEvent } from "./types.js";
3
+ export declare class AnthropicProvider implements LLMProvider {
4
+ name: "anthropic";
5
+ private client;
6
+ constructor(apiKey?: string);
7
+ complete(model: string, messages: Message[], options?: CompleteOptions): Promise<LLMResponse>;
8
+ stream(model: string, messages: Message[], options?: CompleteOptions): AsyncIterable<StreamEvent>;
9
+ private toAnthropicMessages;
10
+ private fromAnthropicResponse;
11
+ private mapStopReason;
12
+ }
@@ -0,0 +1,89 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { defaultStream } from "./provider.js";
3
+ export class AnthropicProvider {
4
+ name = "anthropic";
5
+ client;
6
+ constructor(apiKey) {
7
+ this.client = new Anthropic({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });
8
+ }
9
+ async complete(model, messages, options) {
10
+ const params = {
11
+ model,
12
+ max_tokens: options?.maxTokens ?? 4096,
13
+ messages: this.toAnthropicMessages(messages),
14
+ };
15
+ if (options?.systemPrompt) {
16
+ params.system = [
17
+ { type: "text", text: options.systemPrompt, cache_control: { type: "ephemeral" } },
18
+ ];
19
+ }
20
+ if (options?.tools?.length) {
21
+ params.tools = options.tools.map((t, i) => {
22
+ const def = {
23
+ name: t.name,
24
+ description: t.description,
25
+ input_schema: t.parameters,
26
+ };
27
+ if (i === options.tools.length - 1) {
28
+ def.cache_control = { type: "ephemeral" };
29
+ }
30
+ return def;
31
+ });
32
+ }
33
+ const response = await this.client.messages.create(params);
34
+ return this.fromAnthropicResponse(response);
35
+ }
36
+ async *stream(model, messages, options) {
37
+ yield* defaultStream(this, model, messages, options);
38
+ }
39
+ toAnthropicMessages(messages) {
40
+ return messages.map((msg) => {
41
+ if (msg.role === "user")
42
+ return { role: "user", content: msg.content };
43
+ if (msg.role === "assistant") {
44
+ return {
45
+ role: "assistant",
46
+ content: msg.content.map((block) => {
47
+ if (block.type === "text")
48
+ return { type: "text", text: block.text };
49
+ return { type: "tool_use", id: block.id, name: block.name, input: block.arguments };
50
+ }),
51
+ };
52
+ }
53
+ if (msg.role === "tool_result") {
54
+ return { role: "user", content: [{ type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content }] };
55
+ }
56
+ return msg;
57
+ });
58
+ }
59
+ fromAnthropicResponse(response) {
60
+ const content = response.content.map((block) => {
61
+ if (block.type === "text")
62
+ return { type: "text", text: block.text };
63
+ if (block.type === "tool_use")
64
+ return { type: "tool_call", id: block.id, name: block.name, arguments: block.input };
65
+ return { type: "text", text: "" };
66
+ });
67
+ return {
68
+ content,
69
+ stopReason: this.mapStopReason(response.stop_reason),
70
+ usage: {
71
+ inputTokens: response.usage.input_tokens,
72
+ outputTokens: response.usage.output_tokens,
73
+ cacheCreationTokens: response.usage.cache_creation_input_tokens ?? 0,
74
+ cacheReadTokens: response.usage.cache_read_input_tokens ?? 0,
75
+ reasoningTokens: 0,
76
+ },
77
+ model: "",
78
+ latencyMs: 0,
79
+ };
80
+ }
81
+ mapStopReason(reason) {
82
+ switch (reason) {
83
+ case "end_turn": return "end_turn";
84
+ case "tool_use": return "tool_use";
85
+ case "max_tokens": return "max_tokens";
86
+ default: return "end_turn";
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,6 @@
1
+ import type { Message, LLMResponse } from "./types.js";
2
+ import type { CompleteOptions } from "./provider.js";
3
+ export declare function isRetryableError(err: unknown): boolean;
4
+ type CompleteFn = (model: string, messages: Message[], options?: CompleteOptions) => Promise<LLMResponse>;
5
+ export declare function completeWithFallback(primary: string, fallbacks: string[], messages: Message[], options: CompleteOptions | undefined, completeFn: CompleteFn): Promise<LLMResponse>;
6
+ export {};
@@ -0,0 +1,30 @@
1
+ import { createLogger } from "../logger.js";
2
+ const log = createLogger("llm");
3
+ const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503]);
4
+ export function isRetryableError(err) {
5
+ if (err instanceof Error) {
6
+ if (err.name === "TimeoutError")
7
+ return true;
8
+ if ("status" in err && typeof err.status === "number") {
9
+ return RETRYABLE_STATUS_CODES.has(err.status);
10
+ }
11
+ }
12
+ return false;
13
+ }
14
+ export async function completeWithFallback(primary, fallbacks, messages, options, completeFn) {
15
+ const models = [primary, ...fallbacks];
16
+ let lastError;
17
+ for (const model of models) {
18
+ try {
19
+ return await completeFn(model, messages, options);
20
+ }
21
+ catch (err) {
22
+ lastError = err;
23
+ if (!isRetryableError(err) || model === models[models.length - 1]) {
24
+ throw err;
25
+ }
26
+ log.warn({ err, model }, "model failed, trying next");
27
+ }
28
+ }
29
+ throw lastError ?? new Error("completeWithFallback: no models were attempted");
30
+ }
@@ -0,0 +1,9 @@
1
+ import type { Message, LLMResponse, StreamEvent } from "./types.js";
2
+ import type { LLMProvider, CompleteOptions } from "./provider.js";
3
+ export type { Message, LLMResponse, StreamEvent, LLMProvider, CompleteOptions };
4
+ export type { ContentBlock, TextBlock, ToolCallBlock, ToolDefinition, StopReason, TokenUsage } from "./types.js";
5
+ export type ProviderName = "anthropic" | "openai";
6
+ export declare function createProvider(name: ProviderName, apiKey?: string): LLMProvider;
7
+ export declare function complete(modelString: string, messages: Message[], options?: CompleteOptions): Promise<LLMResponse>;
8
+ export declare function stream(modelString: string, messages: Message[], options?: CompleteOptions): AsyncIterable<StreamEvent>;
9
+ export { completeWithFallback, isRetryableError } from "./fallback.js";
@@ -0,0 +1,40 @@
1
+ import { AnthropicProvider } from "./anthropic.js";
2
+ import { OpenAIProvider } from "./openai.js";
3
+ const providers = new Map();
4
+ export function createProvider(name, apiKey) {
5
+ switch (name) {
6
+ case "anthropic": return new AnthropicProvider(apiKey);
7
+ case "openai": return new OpenAIProvider(apiKey);
8
+ default: throw new Error(`Unknown provider: ${name}`);
9
+ }
10
+ }
11
+ function getOrCreateProvider(name) {
12
+ if (!providers.has(name))
13
+ providers.set(name, createProvider(name));
14
+ return providers.get(name);
15
+ }
16
+ function parseModelString(modelString) {
17
+ const [provider, ...rest] = modelString.split(":");
18
+ if (!rest.length)
19
+ throw new Error(`Model string must be "provider:model", got "${modelString}"`);
20
+ return { provider: provider, model: rest.join(":") };
21
+ }
22
+ export async function complete(modelString, messages, options) {
23
+ const { provider, model } = parseModelString(modelString);
24
+ const start = performance.now();
25
+ const response = await getOrCreateProvider(provider).complete(model, messages, options);
26
+ const latencyMs = Math.round(performance.now() - start);
27
+ return { ...response, model: modelString, latencyMs };
28
+ }
29
+ export async function* stream(modelString, messages, options) {
30
+ const { provider, model } = parseModelString(modelString);
31
+ for await (const event of getOrCreateProvider(provider).stream(model, messages, options)) {
32
+ if (event.type === "done") {
33
+ yield { ...event, response: { ...event.response, model: modelString, latencyMs: 0 } };
34
+ }
35
+ else {
36
+ yield event;
37
+ }
38
+ }
39
+ }
40
+ export { completeWithFallback, isRetryableError } from "./fallback.js";
@@ -0,0 +1,12 @@
1
+ import type { LLMProvider, CompleteOptions } from "./provider.js";
2
+ import type { Message, LLMResponse, StreamEvent } from "./types.js";
3
+ export declare class OpenAIProvider implements LLMProvider {
4
+ name: "openai";
5
+ private client;
6
+ constructor(apiKey?: string);
7
+ complete(model: string, messages: Message[], options?: CompleteOptions): Promise<LLMResponse>;
8
+ stream(model: string, messages: Message[], options?: CompleteOptions): AsyncIterable<StreamEvent>;
9
+ private toOpenAIMessages;
10
+ private fromOpenAIResponse;
11
+ private mapStopReason;
12
+ }