@alejandroroman/agent-kit 0.1.3 → 0.2.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 (102) hide show
  1. package/dist/_memory/dist/config.d.ts +14 -0
  2. package/dist/_memory/dist/config.js +16 -0
  3. package/dist/_memory/dist/db/client.d.ts +2 -0
  4. package/dist/_memory/dist/db/client.js +15 -0
  5. package/dist/_memory/dist/db/schema.d.ts +14 -0
  6. package/dist/_memory/dist/db/schema.js +51 -0
  7. package/dist/_memory/dist/embeddings/ollama.d.ts +12 -0
  8. package/dist/_memory/dist/embeddings/ollama.js +22 -0
  9. package/dist/_memory/dist/embeddings/provider.d.ts +4 -0
  10. package/dist/_memory/dist/embeddings/provider.js +1 -0
  11. package/dist/_memory/dist/index.d.ts +10 -0
  12. package/dist/_memory/dist/index.js +6 -0
  13. package/dist/_memory/dist/search.d.ts +30 -0
  14. package/dist/_memory/dist/search.js +121 -0
  15. package/dist/_memory/dist/server.d.ts +8 -0
  16. package/dist/_memory/dist/server.js +126 -0
  17. package/dist/_memory/dist/store.d.ts +51 -0
  18. package/dist/_memory/dist/store.js +115 -0
  19. package/dist/_memory/server.js +0 -0
  20. package/dist/agent/loop.js +210 -111
  21. package/dist/api/errors.d.ts +3 -0
  22. package/dist/api/errors.js +37 -0
  23. package/dist/api/events.d.ts +5 -0
  24. package/dist/api/events.js +28 -0
  25. package/dist/api/router.js +10 -0
  26. package/dist/api/traces.d.ts +3 -0
  27. package/dist/api/traces.js +35 -0
  28. package/dist/api/types.d.ts +2 -0
  29. package/dist/bootstrap.d.ts +6 -5
  30. package/dist/bootstrap.js +26 -7
  31. package/dist/cli/chat.js +18 -63
  32. package/dist/cli/claude-md-template.d.ts +5 -0
  33. package/dist/cli/claude-md-template.js +220 -0
  34. package/dist/cli/config-writer.js +3 -0
  35. package/dist/cli/create.js +1 -4
  36. package/dist/cli/env.d.ts +14 -0
  37. package/dist/cli/env.js +68 -0
  38. package/dist/cli/init.js +14 -7
  39. package/dist/cli/list.js +1 -2
  40. package/dist/cli/paths.d.ts +3 -0
  41. package/dist/cli/paths.js +4 -0
  42. package/dist/cli/repl.d.ts +23 -0
  43. package/dist/cli/repl.js +73 -0
  44. package/dist/cli/slack-setup.d.ts +6 -0
  45. package/dist/cli/slack-setup.js +234 -0
  46. package/dist/cli/start.js +96 -96
  47. package/dist/cli/ui.d.ts +2 -2
  48. package/dist/cli/ui.js +5 -5
  49. package/dist/cli/validate.js +1 -4
  50. package/dist/cli/whats-new.d.ts +1 -0
  51. package/dist/cli/whats-new.js +69 -0
  52. package/dist/cli.js +14 -0
  53. package/dist/config/resolve.d.ts +1 -0
  54. package/dist/config/resolve.js +1 -0
  55. package/dist/config/schema.d.ts +2 -0
  56. package/dist/config/schema.js +1 -0
  57. package/dist/config/writer.d.ts +18 -0
  58. package/dist/config/writer.js +85 -0
  59. package/dist/cron/scheduler.d.ts +4 -1
  60. package/dist/cron/scheduler.js +99 -52
  61. package/dist/gateways/slack/client.d.ts +1 -0
  62. package/dist/gateways/slack/client.js +9 -0
  63. package/dist/gateways/slack/handler.js +2 -1
  64. package/dist/gateways/slack/index.js +75 -29
  65. package/dist/gateways/slack/listener.d.ts +8 -1
  66. package/dist/gateways/slack/listener.js +36 -10
  67. package/dist/heartbeat/runner.js +99 -82
  68. package/dist/index.js +4 -209
  69. package/dist/llm/anthropic.d.ts +1 -0
  70. package/dist/llm/anthropic.js +11 -2
  71. package/dist/llm/fallback.js +34 -2
  72. package/dist/llm/openai.d.ts +2 -0
  73. package/dist/llm/openai.js +33 -2
  74. package/dist/llm/types.d.ts +16 -2
  75. package/dist/llm/types.js +9 -0
  76. package/dist/logger.js +8 -0
  77. package/dist/media/sanitize.d.ts +5 -0
  78. package/dist/media/sanitize.js +53 -0
  79. package/dist/multi/spawn.js +29 -10
  80. package/dist/session/compaction.js +3 -1
  81. package/dist/session/prune-images.d.ts +9 -0
  82. package/dist/session/prune-images.js +42 -0
  83. package/dist/skills/activate.d.ts +6 -0
  84. package/dist/skills/activate.js +72 -27
  85. package/dist/skills/index.d.ts +1 -1
  86. package/dist/skills/index.js +1 -1
  87. package/dist/telemetry/db.d.ts +63 -0
  88. package/dist/telemetry/db.js +193 -0
  89. package/dist/telemetry/index.d.ts +17 -0
  90. package/dist/telemetry/index.js +82 -0
  91. package/dist/telemetry/sanitize.d.ts +6 -0
  92. package/dist/telemetry/sanitize.js +48 -0
  93. package/dist/telemetry/sqlite-processor.d.ts +11 -0
  94. package/dist/telemetry/sqlite-processor.js +108 -0
  95. package/dist/telemetry/types.d.ts +30 -0
  96. package/dist/telemetry/types.js +31 -0
  97. package/dist/tools/builtin/index.d.ts +2 -0
  98. package/dist/tools/builtin/index.js +2 -0
  99. package/dist/tools/builtin/self-config.d.ts +4 -0
  100. package/dist/tools/builtin/self-config.js +182 -0
  101. package/dist/tools/registry.js +8 -1
  102. package/package.json +26 -20
@@ -4,10 +4,38 @@ import { extractText } from "../llm/types.js";
4
4
  import { nanoid } from "nanoid";
5
5
  import { toolToDefinition } from "../tools/types.js";
6
6
  import { compactMessages, estimateTotalTokens } from "../session/compaction.js";
7
+ import { pruneImages } from "../session/prune-images.js";
7
8
  import { createLogger } from "../logger.js";
8
9
  import { truncate } from "../text.js";
10
+ import { getTracer, ATTR } from "../telemetry/index.js";
11
+ import { context, trace, SpanStatusCode } from "@opentelemetry/api";
9
12
  const log = createLogger("agent");
10
13
  export async function runAgentLoop(initialMessages, config) {
14
+ const tracer = getTracer("agent");
15
+ const runSpan = tracer.startSpan("agent.run");
16
+ const agentName = config.agentName ?? config.label ?? "main";
17
+ runSpan.setAttribute(ATTR.AGENT, agentName);
18
+ runSpan.setAttribute(ATTR.SOURCE, config.source ?? "unknown");
19
+ runSpan.setAttribute(ATTR.MODEL, config.model);
20
+ const runCtx = trace.setSpan(context.active(), runSpan);
21
+ try {
22
+ const result = await _runAgentLoop(initialMessages, config, runSpan, tracer, runCtx);
23
+ return result;
24
+ }
25
+ catch (err) {
26
+ runSpan.setStatus({
27
+ code: SpanStatusCode.ERROR,
28
+ message: err instanceof Error ? err.message : String(err),
29
+ });
30
+ if (err instanceof Error)
31
+ runSpan.recordException(err);
32
+ throw err;
33
+ }
34
+ finally {
35
+ runSpan.end();
36
+ }
37
+ }
38
+ async function _runAgentLoop(initialMessages, config, runSpan, tracer, runCtx) {
11
39
  const messages = [...initialMessages];
12
40
  const registry = config.toolRegistry;
13
41
  const staticTools = config.tools ?? [];
@@ -20,6 +48,7 @@ export async function runAgentLoop(initialMessages, config) {
20
48
  const toolNames = currentTools.map((t) => t.name);
21
49
  log.info({ label, model: config.model, tools: toolNames.length, toolNames, maxIterations, messages: initialMessages.length }, "loop start");
22
50
  const runId = nanoid();
51
+ runSpan.setAttribute(ATTR.RUN_ID, runId);
23
52
  const agentName = config.agentName ?? config.label ?? "main";
24
53
  const totalUsage = {
25
54
  inputTokens: 0,
@@ -32,140 +61,209 @@ export async function runAgentLoop(initialMessages, config) {
32
61
  let didCompact = false;
33
62
  for (let i = 0; i < maxIterations; i++) {
34
63
  const iteration = i + 1;
35
- // Rebuild tool definitions each iteration (skills may register new tools mid-loop)
36
- const iterTools = registry
37
- ? registry.list().map((name) => registry.resolve([name])[0])
38
- : staticTools;
39
- const toolMap = new Map(iterTools.map((t) => [t.name, t]));
40
- const toolDefs = iterTools.map(toolToDefinition);
41
- // Compact conversation if it exceeds the token threshold (one-shot: only once per loop)
42
- if (!didCompact && config.compactionThreshold) {
43
- const estimated = estimateTotalTokens(messages);
44
- if (estimated > config.compactionThreshold) {
45
- log.info({ label, iteration, estimated, threshold: config.compactionThreshold }, "compacting messages");
46
- try {
47
- const compacted = await compactMessages(messages, config.model, config.compactionThreshold);
48
- messages.length = 0;
49
- messages.push(...compacted);
50
- didCompact = true;
51
- }
52
- catch (err) {
53
- const isBug = err instanceof TypeError || err instanceof ReferenceError || err instanceof SyntaxError || err instanceof RangeError;
54
- if (isBug) {
55
- log.fatal({ err, label, iteration }, "compaction bug — programming error");
56
- throw err;
64
+ // Create iteration span as child of the run span
65
+ const iterSpan = tracer.startSpan("agent.iteration", undefined, runCtx);
66
+ iterSpan.setAttribute(ATTR.ITERATION, iteration);
67
+ const iterCtx = trace.setSpan(runCtx, iterSpan);
68
+ try {
69
+ // Rebuild tool definitions each iteration (skills may register new tools mid-loop)
70
+ const iterTools = registry
71
+ ? registry.list().map((name) => registry.resolve([name])[0])
72
+ : staticTools;
73
+ const toolMap = new Map(iterTools.map((t) => [t.name, t]));
74
+ const toolDefs = iterTools.map(toolToDefinition);
75
+ // Compact conversation if it exceeds the token threshold (one-shot: only once per loop)
76
+ if (!didCompact && config.compactionThreshold) {
77
+ const estimated = estimateTotalTokens(messages);
78
+ if (estimated > config.compactionThreshold) {
79
+ log.info({ label, iteration, estimated, threshold: config.compactionThreshold }, "compacting messages");
80
+ const compactionSpan = tracer.startSpan("session.compaction", undefined, iterCtx);
81
+ compactionSpan.setAttribute(ATTR.TOKENS_BEFORE, estimated);
82
+ try {
83
+ const compacted = await compactMessages(messages, config.model, config.compactionThreshold);
84
+ messages.length = 0;
85
+ messages.push(...compacted);
86
+ didCompact = true;
87
+ const tokensAfter = estimateTotalTokens(messages);
88
+ compactionSpan.setAttribute(ATTR.TOKENS_AFTER, tokensAfter);
89
+ }
90
+ catch (err) {
91
+ const isBug = err instanceof TypeError || err instanceof ReferenceError || err instanceof SyntaxError || err instanceof RangeError;
92
+ compactionSpan.setStatus({
93
+ code: SpanStatusCode.ERROR,
94
+ message: err instanceof Error ? err.message : String(err),
95
+ });
96
+ if (err instanceof Error)
97
+ compactionSpan.recordException(err);
98
+ if (isBug) {
99
+ log.fatal({ err, label, iteration }, "compaction bug — programming error");
100
+ throw err;
101
+ }
102
+ log.warn({ err, label, iteration }, "compaction failed, proceeding with full context");
103
+ }
104
+ finally {
105
+ compactionSpan.end();
57
106
  }
58
- log.warn({ err, label, iteration }, "compaction failed, proceeding with full context");
59
107
  }
60
108
  }
61
- }
62
- const response = await completeWithFallback(config.model, config.fallbacks ?? [], messages, {
63
- systemPrompt: config.systemPrompt,
64
- tools: toolDefs.length > 0 ? toolDefs : undefined,
65
- }, complete);
66
- totalUsage.inputTokens += response.usage.inputTokens;
67
- totalUsage.outputTokens += response.usage.outputTokens;
68
- totalUsage.cacheCreationTokens += response.usage.cacheCreationTokens;
69
- totalUsage.cacheReadTokens += response.usage.cacheReadTokens;
70
- totalUsage.reasoningTokens += response.usage.reasoningTokens;
71
- if (config.usageStore) {
109
+ // Prune images from old messages to save context tokens
110
+ const prunedMessages = pruneImages(messages, 4);
111
+ // LLM call span
112
+ const llmSpan = tracer.startSpan("llm.call", undefined, iterCtx);
113
+ let response;
72
114
  try {
73
- config.usageStore.recordCall({
74
- runId,
75
- agentName,
76
- provider: response.model.split(":")[0],
77
- model: response.model,
78
- usage: response.usage,
79
- stopReason: response.stopReason,
80
- latencyMs: response.latencyMs,
81
- });
115
+ response = await completeWithFallback(config.model, config.fallbacks ?? [], prunedMessages, {
116
+ systemPrompt: config.systemPrompt,
117
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
118
+ }, complete);
119
+ llmSpan.setAttribute(ATTR.INPUT_TOKENS, response.usage.inputTokens);
120
+ llmSpan.setAttribute(ATTR.OUTPUT_TOKENS, response.usage.outputTokens);
121
+ llmSpan.setAttribute(ATTR.CACHE_READ_TOKENS, response.usage.cacheReadTokens);
122
+ llmSpan.setAttribute(ATTR.CACHE_CREATION_TOKENS, response.usage.cacheCreationTokens);
123
+ llmSpan.setAttribute(ATTR.STOP_REASON, response.stopReason);
124
+ llmSpan.setAttribute(ATTR.MODEL, response.model);
125
+ if (response.latencyMs != null)
126
+ llmSpan.setAttribute(ATTR.LATENCY_MS, response.latencyMs);
82
127
  }
83
128
  catch (err) {
84
- log.error({ err, runId }, "failed to record LLM call");
129
+ llmSpan.setStatus({
130
+ code: SpanStatusCode.ERROR,
131
+ message: err instanceof Error ? err.message : String(err),
132
+ });
133
+ if (err instanceof Error)
134
+ llmSpan.recordException(err);
135
+ throw err;
85
136
  }
86
- }
87
- log.debug({ label, iteration, model: config.model, stopReason: response.stopReason, inputTokens: response.usage.inputTokens, outputTokens: response.usage.outputTokens }, "iteration complete");
88
- messages.push({ role: "assistant", content: response.content });
89
- if (response.stopReason !== "tool_use") {
90
- let text = extractText(response.content);
91
- // If the final turn has no text (e.g. LLM said everything alongside a tool call
92
- // in a prior turn), scan backward for the last assistant text.
93
- if (!text) {
94
- for (let j = messages.length - 2; j >= 0; j--) {
95
- const msg = messages[j];
96
- if (msg.role === "assistant") {
97
- const found = extractText(msg.content);
98
- if (found) {
99
- text = found;
100
- break;
101
- }
102
- }
103
- }
137
+ finally {
138
+ llmSpan.end();
104
139
  }
105
- const elapsed = Date.now() - loopStart;
140
+ totalUsage.inputTokens += response.usage.inputTokens;
141
+ totalUsage.outputTokens += response.usage.outputTokens;
142
+ totalUsage.cacheCreationTokens += response.usage.cacheCreationTokens;
143
+ totalUsage.cacheReadTokens += response.usage.cacheReadTokens;
144
+ totalUsage.reasoningTokens += response.usage.reasoningTokens;
106
145
  if (config.usageStore) {
107
146
  try {
108
- config.usageStore.recordRun({
109
- id: runId,
147
+ config.usageStore.recordCall({
148
+ runId,
110
149
  agentName,
111
- source: config.source ?? "unknown",
112
- model: config.model,
113
- usage: totalUsage,
114
- iterations: iteration,
115
- toolCallsCount,
116
- durationMs: elapsed,
150
+ provider: response.model.split(":")[0],
151
+ model: response.model,
152
+ usage: response.usage,
153
+ stopReason: response.stopReason,
154
+ latencyMs: response.latencyMs,
117
155
  });
118
156
  }
119
157
  catch (err) {
120
- log.error({ err, runId }, "failed to record agent run");
158
+ log.error({ err, runId }, "failed to record LLM call");
121
159
  }
122
160
  }
123
- log.info({ label, stopReason: response.stopReason, iterations: iteration, inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens, elapsed }, "loop end");
124
- return {
125
- text,
126
- messages,
127
- usage: totalUsage,
128
- };
129
- }
130
- const toolCalls = response.content.filter((b) => b.type === "tool_call");
131
- toolCallsCount += toolCalls.length;
132
- for (const call of toolCalls) {
133
- log.debug({ label, iteration, tool: call.name, args: truncate(JSON.stringify(call.arguments), 200) }, "tool call");
134
- const tool = toolMap.get(call.name);
135
- let result;
136
- if (!tool) {
137
- log.warn({ label, iteration, tool: call.name }, "LLM called unknown tool");
138
- result = `Error: unknown tool "${call.name}"`;
161
+ log.debug({ label, iteration, model: config.model, stopReason: response.stopReason, inputTokens: response.usage.inputTokens, outputTokens: response.usage.outputTokens }, "iteration complete");
162
+ messages.push({ role: "assistant", content: response.content });
163
+ if (response.stopReason !== "tool_use") {
164
+ let text = extractText(response.content);
165
+ // If the final turn has no text (e.g. LLM said everything alongside a tool call
166
+ // in a prior turn), scan backward for the last assistant text.
167
+ if (!text) {
168
+ for (let j = messages.length - 2; j >= 0; j--) {
169
+ const msg = messages[j];
170
+ if (msg.role === "assistant") {
171
+ const found = extractText(msg.content);
172
+ if (found) {
173
+ text = found;
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ const elapsed = Date.now() - loopStart;
180
+ if (config.usageStore) {
181
+ try {
182
+ config.usageStore.recordRun({
183
+ id: runId,
184
+ agentName,
185
+ source: config.source ?? "unknown",
186
+ model: config.model,
187
+ usage: totalUsage,
188
+ iterations: iteration,
189
+ toolCallsCount,
190
+ durationMs: elapsed,
191
+ });
192
+ }
193
+ catch (err) {
194
+ log.error({ err, runId }, "failed to record agent run");
195
+ }
196
+ }
197
+ log.info({ label, stopReason: response.stopReason, iterations: iteration, inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens, elapsed }, "loop end");
198
+ runSpan.setAttribute(ATTR.ITERATION, iteration);
199
+ return {
200
+ text,
201
+ messages,
202
+ usage: totalUsage,
203
+ };
139
204
  }
140
- else {
141
- const toolStart = Date.now();
205
+ const toolCalls = response.content.filter((b) => b.type === "tool_call");
206
+ toolCallsCount += toolCalls.length;
207
+ for (const call of toolCalls) {
208
+ log.debug({ label, iteration, tool: call.name, args: truncate(JSON.stringify(call.arguments), 200) }, "tool call");
209
+ const tool = toolMap.get(call.name);
210
+ let result;
211
+ let toolDidThrow = false;
212
+ const toolSpan = tracer.startSpan("tool.execute", undefined, iterCtx);
213
+ toolSpan.setAttribute(ATTR.TOOL_NAME, call.name);
142
214
  try {
143
- result = await tool.execute(call.arguments);
144
- const toolElapsed = Date.now() - toolStart;
145
- log.debug({ label, iteration, tool: call.name, resultLength: result.length, snippet: truncate(result, 150), elapsed: toolElapsed }, "tool result");
146
- }
147
- catch (err) {
148
- const isBug = err instanceof TypeError || err instanceof ReferenceError || err instanceof SyntaxError || err instanceof RangeError;
149
- if (isBug) {
150
- log.fatal({ err, tool: call.name, label, iteration }, "tool bug — programming error");
215
+ if (!tool) {
216
+ log.warn({ label, iteration, tool: call.name }, "LLM called unknown tool");
217
+ result = `Error: unknown tool "${call.name}"`;
151
218
  }
152
219
  else {
153
- log.error({ err, tool: call.name, label, iteration }, "tool execution failed");
220
+ const toolStart = Date.now();
221
+ try {
222
+ result = await tool.execute(call.arguments);
223
+ const toolElapsed = Date.now() - toolStart;
224
+ log.debug({ label, iteration, tool: call.name, resultLength: result.length, snippet: truncate(result, 150), elapsed: toolElapsed }, "tool result");
225
+ }
226
+ catch (err) {
227
+ toolDidThrow = true;
228
+ const isBug = err instanceof TypeError || err instanceof ReferenceError || err instanceof SyntaxError || err instanceof RangeError;
229
+ if (isBug) {
230
+ log.fatal({ err, tool: call.name, label, iteration }, "tool bug — programming error");
231
+ }
232
+ else {
233
+ log.error({ err, tool: call.name, label, iteration }, "tool execution failed");
234
+ }
235
+ result = `Error: ${err instanceof Error ? err.message : String(err)}`;
236
+ toolSpan.setStatus({
237
+ code: SpanStatusCode.ERROR,
238
+ message: err instanceof Error ? err.message : String(err),
239
+ });
240
+ if (err instanceof Error)
241
+ toolSpan.recordException(err);
242
+ }
243
+ }
244
+ // Detect error-as-string results (only if the tool didn't already throw)
245
+ if (!toolDidThrow && typeof result === "string" && result.startsWith("Error:")) {
246
+ toolSpan.setStatus({ code: SpanStatusCode.ERROR, message: result });
154
247
  }
155
- result = `Error: ${err instanceof Error ? err.message : String(err)}`;
156
248
  }
249
+ finally {
250
+ toolSpan.end();
251
+ }
252
+ const maxResultSize = config.maxToolResultSize ?? 8192;
253
+ if (result.length > maxResultSize) {
254
+ const originalLength = result.length;
255
+ result = result.slice(0, maxResultSize) + `\n\n[Output truncated from ${originalLength} to ${maxResultSize} characters]`;
256
+ log.debug({ label, iteration, tool: call.name, originalLength, maxResultSize }, "tool result truncated");
257
+ }
258
+ messages.push({
259
+ role: "tool_result",
260
+ tool_call_id: call.id,
261
+ content: result,
262
+ });
157
263
  }
158
- const maxResultSize = config.maxToolResultSize ?? 8192;
159
- if (result.length > maxResultSize) {
160
- const originalLength = result.length;
161
- result = result.slice(0, maxResultSize) + `\n\n[Output truncated from ${originalLength} to ${maxResultSize} characters]`;
162
- log.debug({ label, iteration, tool: call.name, originalLength, maxResultSize }, "tool result truncated");
163
- }
164
- messages.push({
165
- role: "tool_result",
166
- tool_call_id: call.id,
167
- content: result,
168
- });
264
+ }
265
+ finally {
266
+ iterSpan.end();
169
267
  }
170
268
  }
171
269
  const elapsed = Date.now() - loopStart;
@@ -187,6 +285,7 @@ export async function runAgentLoop(initialMessages, config) {
187
285
  }
188
286
  }
189
287
  log.warn({ label, maxIterations, inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens, elapsed }, "max iterations reached");
288
+ runSpan.setAttribute(ATTR.ITERATION, maxIterations);
190
289
  return {
191
290
  text: "[Agent reached maximum iterations]",
192
291
  messages,
@@ -0,0 +1,3 @@
1
+ import type { TelemetryDb } from "../telemetry/db.js";
2
+ import { type RouteHandler } from "./types.js";
3
+ export declare function errorRoutes(db: TelemetryDb): Record<string, RouteHandler>;
@@ -0,0 +1,37 @@
1
+ import { sendJson, sendError } from "./types.js";
2
+ const DEFAULT_LIMIT = 100;
3
+ const MAX_LIMIT = 1000;
4
+ const VALID_GROUP_BY = new Set(["fingerprint", "agent"]);
5
+ function clampLimit(val) {
6
+ if (val === null)
7
+ return DEFAULT_LIMIT;
8
+ const n = parseInt(val, 10);
9
+ if (isNaN(n) || n < 1)
10
+ return DEFAULT_LIMIT;
11
+ return Math.min(n, MAX_LIMIT);
12
+ }
13
+ export function errorRoutes(db) {
14
+ return {
15
+ "GET /api/errors": (url, _req, res) => {
16
+ const data = db.getRecentErrors({
17
+ agent: url.searchParams.get("agent") ?? undefined,
18
+ source: url.searchParams.get("source") ?? undefined,
19
+ since: url.searchParams.get("since") ?? undefined,
20
+ limit: clampLimit(url.searchParams.get("limit")),
21
+ });
22
+ sendJson(res, data);
23
+ },
24
+ "GET /api/errors/stats": (url, _req, res) => {
25
+ const groupByParam = url.searchParams.get("group_by");
26
+ if (groupByParam && !VALID_GROUP_BY.has(groupByParam)) {
27
+ sendError(res, 400, "Invalid group_by value. Must be one of: fingerprint, agent");
28
+ return;
29
+ }
30
+ const data = db.getErrorStats({
31
+ since: url.searchParams.get("since") ?? undefined,
32
+ groupBy: groupByParam ?? undefined,
33
+ });
34
+ sendJson(res, data);
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,5 @@
1
+ import type { RouteHandler } from "./types.js";
2
+ export declare function addEventListener(res: import("http").ServerResponse): void;
3
+ export declare function removeEventListener(res: import("http").ServerResponse): void;
4
+ export declare function broadcastEvent(data: string): void;
5
+ export declare function eventRoutes(): Record<string, RouteHandler>;
@@ -0,0 +1,28 @@
1
+ const clients = new Set();
2
+ export function addEventListener(res) {
3
+ clients.add(res);
4
+ }
5
+ export function removeEventListener(res) {
6
+ clients.delete(res);
7
+ }
8
+ export function broadcastEvent(data) {
9
+ for (const client of clients) {
10
+ client.write(`data: ${data}\n\n`);
11
+ }
12
+ }
13
+ export function eventRoutes() {
14
+ return {
15
+ "GET /api/events/stream": (_url, req, res) => {
16
+ res.writeHead(200, {
17
+ "Content-Type": "text/event-stream",
18
+ "Cache-Control": "no-cache",
19
+ Connection: "keep-alive",
20
+ });
21
+ res.write(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}\n\n`);
22
+ addEventListener(res);
23
+ req.on("close", () => {
24
+ removeEventListener(res);
25
+ });
26
+ },
27
+ };
28
+ }
@@ -7,6 +7,9 @@ import { configRoutes } from "./config.js";
7
7
  import { cronRoutes } from "./cron.js";
8
8
  import { logRoutes } from "./logs.js";
9
9
  import { usageRoutes } from "./usage.js";
10
+ import { traceRoutes } from "./traces.js";
11
+ import { errorRoutes } from "./errors.js";
12
+ import { eventRoutes } from "./events.js";
10
13
  import { createLogger } from "../logger.js";
11
14
  const log = createLogger("api");
12
15
  function parseRouteKey(key) {
@@ -29,6 +32,13 @@ export function createApiServer(ctx, port) {
29
32
  if (ctx.usageStore) {
30
33
  Object.assign(routeHandlers, usageRoutes(ctx.usageStore));
31
34
  }
35
+ // If telemetry db exists, add telemetry routes
36
+ if (ctx.telemetryDb) {
37
+ Object.assign(routeHandlers, traceRoutes(ctx.telemetryDb));
38
+ Object.assign(routeHandlers, errorRoutes(ctx.telemetryDb));
39
+ }
40
+ // SSE events stream (always available)
41
+ Object.assign(routeHandlers, eventRoutes());
32
42
  const routes = Object.entries(routeHandlers).map(([key, handler]) => {
33
43
  const { method, pattern } = parseRouteKey(key);
34
44
  return { method, pattern, handler };
@@ -0,0 +1,3 @@
1
+ import type { TelemetryDb } from "../telemetry/db.js";
2
+ import { type RouteHandler } from "./types.js";
3
+ export declare function traceRoutes(db: TelemetryDb): Record<string, RouteHandler>;
@@ -0,0 +1,35 @@
1
+ import { sendJson, sendError } from "./types.js";
2
+ const MAX_LIMIT = 1000;
3
+ const DEFAULT_LIMIT = 100;
4
+ function clampLimit(val) {
5
+ if (val === null)
6
+ return DEFAULT_LIMIT;
7
+ const n = parseInt(val, 10);
8
+ if (isNaN(n) || n < 1)
9
+ return DEFAULT_LIMIT;
10
+ return Math.min(n, MAX_LIMIT);
11
+ }
12
+ export function traceRoutes(db) {
13
+ return {
14
+ "GET /api/traces": (url, _req, res) => {
15
+ const data = db.listTraces({
16
+ agent: url.searchParams.get("agent") ?? undefined,
17
+ source: url.searchParams.get("source") ?? undefined,
18
+ status: url.searchParams.get("status") ?? undefined,
19
+ since: url.searchParams.get("since") ?? undefined,
20
+ limit: clampLimit(url.searchParams.get("limit")),
21
+ });
22
+ sendJson(res, data);
23
+ },
24
+ "GET /api/traces/:traceId": (url, _req, res) => {
25
+ const match = url.pathname.match(/^\/api\/traces\/([^/]+)$/);
26
+ if (!match) {
27
+ sendError(res, 400, "missing traceId");
28
+ return;
29
+ }
30
+ const traceId = decodeURIComponent(match[1]);
31
+ const spans = db.getTraceSpans(traceId);
32
+ sendJson(res, spans);
33
+ },
34
+ };
35
+ }
@@ -1,11 +1,13 @@
1
1
  import type { Config } from "../config/schema.js";
2
2
  import type { UsageStore } from "../usage/store.js";
3
+ import type { TelemetryDb } from "../telemetry/db.js";
3
4
  export interface ApiContext {
4
5
  config: Config;
5
6
  configPath: string;
6
7
  usageStore?: UsageStore;
7
8
  dataDir: string;
8
9
  memoryDbPath?: string;
10
+ telemetryDb?: TelemetryDb;
9
11
  }
10
12
  export type RouteHandler = (url: URL, req: import("http").IncomingMessage, res: import("http").ServerResponse) => void;
11
13
  export declare function sendJson(res: import("http").ServerResponse, data: unknown, status?: number): void;
@@ -6,7 +6,8 @@ import type { Message } from "./llm/types.js";
6
6
  import type { AgentResult } from "./agent/types.js";
7
7
  import type { ResolvedAgent } from "./config/resolve.js";
8
8
  import type { SessionManager } from "./session/manager.js";
9
- export interface AgentRuntime {
9
+ type AgentExecutorFn = (agentName: string, messages: Message[]) => Promise<AgentResult>;
10
+ interface AgentRuntime {
10
11
  resolved: ResolvedAgent;
11
12
  toolRegistry: ToolRegistry;
12
13
  agentRegistry: AgentRegistry;
@@ -15,9 +16,6 @@ export interface AgentRuntime {
15
16
  soul: string | undefined;
16
17
  session: SessionManager;
17
18
  }
18
- export interface AgentExecutorFn {
19
- (agentName: string, messages: Message[]): Promise<AgentResult>;
20
- }
21
19
  /**
22
20
  * Build a fully-initialized agent runtime: tool registry, skills, spawn wrappers, session.
23
21
  */
@@ -26,7 +24,8 @@ export declare function buildAgentRuntime(agentName: string, config: Config, opt
26
24
  skillsDir: string;
27
25
  sessionId?: string;
28
26
  usageStore?: UsageStore;
29
- }): AgentRuntime;
27
+ configPath?: string;
28
+ }): Promise<AgentRuntime>;
30
29
  /**
31
30
  * Assemble a system prompt from soul, date context, skills index, and prompt fragments.
32
31
  */
@@ -48,4 +47,6 @@ export declare function createAgentExecutor(config: Config, opts: {
48
47
  agentRegistry: AgentRegistry;
49
48
  usageStore?: UsageStore;
50
49
  source: string;
50
+ configPath?: string;
51
51
  }): AgentExecutorFn;
52
+ export {};
package/dist/bootstrap.js CHANGED
@@ -2,9 +2,11 @@ import * as path from "path";
2
2
  import * as fs from "fs";
3
3
  import { resolveAgent, resolveWebSearch } from "./config/resolve.js";
4
4
  import { createBuiltinRegistry } from "./tools/builtin/index.js";
5
- import { createActivateSkillTool } from "./skills/index.js";
5
+ import { createActivateSkillTool, preActivateSkills } from "./skills/index.js";
6
6
  import { AgentRegistry } from "./multi/registry.js";
7
7
  import { registerSpawnWrappers } from "./tools/builtin/spawn.js";
8
+ import { ConfigWriter } from "./config/writer.js";
9
+ import { createUpdateAgentConfigTool, createManageCronTool } from "./tools/builtin/index.js";
8
10
  import { setupAgentSession } from "./agent/setup.js";
9
11
  import { runAgentLoop } from "./agent/loop.js";
10
12
  import { dateContext } from "./text.js";
@@ -14,7 +16,7 @@ const log = createLogger("bootstrap");
14
16
  /**
15
17
  * Build a fully-initialized agent runtime: tool registry, skills, spawn wrappers, session.
16
18
  */
17
- export function buildAgentRuntime(agentName, config, opts) {
19
+ export async function buildAgentRuntime(agentName, config, opts) {
18
20
  const agentDef = config.agents[agentName];
19
21
  if (!agentDef)
20
22
  throw new Error(`Agent "${agentName}" not found in config`);
@@ -38,16 +40,32 @@ export function buildAgentRuntime(agentName, config, opts) {
38
40
  promptFragments,
39
41
  activatedSkills: new Set(),
40
42
  };
41
- toolRegistry.register(createActivateSkillTool(ctx));
42
- skillsIndex = "\n\nYou have the following skills available:\n\n"
43
- + resolved.skills.map((s) => `- **${s.name}**: ${s.description}`).join("\n")
44
- + "\n\nTo use a skill, call the `activate_skill` tool with the skill name.";
43
+ const activateTool = createActivateSkillTool(ctx);
44
+ toolRegistry.register(activateTool);
45
+ if (resolved.autoActivateSkills) {
46
+ skillsIndex = await preActivateSkills(ctx, activateTool, log);
47
+ }
48
+ else {
49
+ skillsIndex = "\n\nYou have the following skills available:\n\n"
50
+ + resolved.skills.map((s) => `- **${s.name}**: ${s.description}`).join("\n")
51
+ + "\n\nTo use a skill, call the `activate_skill` tool with the skill name.";
52
+ }
45
53
  }
46
54
  // Spawn wrapper registration
47
55
  if (resolved.canSpawn.length > 0) {
48
56
  registerSpawnWrappers(resolved.canSpawn, config, agentRegistry, toolRegistry, opts.usageStore);
49
57
  log.info({ targets: resolved.canSpawn.map((t) => `${t.tool} -> ${t.agent}`) }, "spawn wrappers registered");
50
58
  }
59
+ // Self-config tool registration
60
+ if (opts.configPath) {
61
+ const writer = new ConfigWriter(opts.configPath);
62
+ if (agentDef.tools.includes("update_agent_config")) {
63
+ toolRegistry.register(createUpdateAgentConfigTool(agentName, writer));
64
+ }
65
+ if (agentDef.tools.includes("manage_cron")) {
66
+ toolRegistry.register(createManageCronTool(agentName, writer));
67
+ }
68
+ }
51
69
  const sid = opts.sessionId ?? `session-${Date.now()}`;
52
70
  const { soul, session } = setupAgentSession(opts.dataDir, agentName, sid);
53
71
  return { resolved, toolRegistry, agentRegistry, promptFragments, skillsIndex, soul, session };
@@ -87,11 +105,12 @@ export function initUsageStore(config) {
87
105
  */
88
106
  export function createAgentExecutor(config, opts) {
89
107
  return async (agentName, messages) => {
90
- const runtime = buildAgentRuntime(agentName, config, {
108
+ const runtime = await buildAgentRuntime(agentName, config, {
91
109
  dataDir: opts.dataDir,
92
110
  skillsDir: opts.skillsDir,
93
111
  sessionId: `${opts.source}-${Date.now()}`,
94
112
  usageStore: opts.usageStore,
113
+ configPath: opts.configPath,
95
114
  });
96
115
  const systemPrompt = buildSystemPrompt(runtime.soul, runtime.skillsIndex, runtime.promptFragments);
97
116
  return runAgentLoop(messages, {