@alejandroroman/agent-kit 0.1.4 → 0.2.1

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 (77) hide show
  1. package/dist/agent/loop.js +213 -111
  2. package/dist/agent/types.d.ts +2 -0
  3. package/dist/api/errors.d.ts +3 -0
  4. package/dist/api/errors.js +37 -0
  5. package/dist/api/events.d.ts +5 -0
  6. package/dist/api/events.js +28 -0
  7. package/dist/api/router.js +10 -0
  8. package/dist/api/traces.d.ts +3 -0
  9. package/dist/api/traces.js +35 -0
  10. package/dist/api/types.d.ts +2 -0
  11. package/dist/bootstrap.d.ts +3 -1
  12. package/dist/bootstrap.js +26 -7
  13. package/dist/cli/chat.js +3 -1
  14. package/dist/cli/claude-md-template.d.ts +5 -0
  15. package/dist/cli/claude-md-template.js +220 -0
  16. package/dist/cli/config-writer.js +3 -0
  17. package/dist/cli/env.d.ts +14 -0
  18. package/dist/cli/env.js +68 -0
  19. package/dist/cli/init.js +10 -0
  20. package/dist/cli/setup-agent/index.js +61 -18
  21. package/dist/cli/slack-setup.d.ts +6 -0
  22. package/dist/cli/slack-setup.js +234 -0
  23. package/dist/cli/start.js +65 -16
  24. package/dist/cli/ui.d.ts +2 -0
  25. package/dist/cli/ui.js +4 -1
  26. package/dist/cli/whats-new.d.ts +1 -0
  27. package/dist/cli/whats-new.js +69 -0
  28. package/dist/cli.js +14 -0
  29. package/dist/config/resolve.d.ts +1 -0
  30. package/dist/config/resolve.js +1 -0
  31. package/dist/config/schema.d.ts +2 -0
  32. package/dist/config/schema.js +1 -0
  33. package/dist/config/writer.d.ts +18 -0
  34. package/dist/config/writer.js +85 -0
  35. package/dist/cron/scheduler.d.ts +4 -1
  36. package/dist/cron/scheduler.js +99 -52
  37. package/dist/gateways/slack/client.d.ts +1 -0
  38. package/dist/gateways/slack/client.js +9 -0
  39. package/dist/gateways/slack/handler.js +2 -1
  40. package/dist/gateways/slack/index.js +75 -29
  41. package/dist/gateways/slack/listener.d.ts +8 -1
  42. package/dist/gateways/slack/listener.js +36 -10
  43. package/dist/heartbeat/runner.js +99 -82
  44. package/dist/llm/anthropic.d.ts +1 -0
  45. package/dist/llm/anthropic.js +11 -2
  46. package/dist/llm/fallback.js +34 -2
  47. package/dist/llm/openai.d.ts +2 -0
  48. package/dist/llm/openai.js +33 -2
  49. package/dist/llm/types.d.ts +16 -2
  50. package/dist/llm/types.js +9 -0
  51. package/dist/logger.d.ts +1 -0
  52. package/dist/logger.js +11 -0
  53. package/dist/media/sanitize.d.ts +5 -0
  54. package/dist/media/sanitize.js +53 -0
  55. package/dist/multi/spawn.js +29 -10
  56. package/dist/session/compaction.js +3 -1
  57. package/dist/session/prune-images.d.ts +9 -0
  58. package/dist/session/prune-images.js +42 -0
  59. package/dist/skills/activate.d.ts +6 -0
  60. package/dist/skills/activate.js +72 -27
  61. package/dist/skills/index.d.ts +1 -1
  62. package/dist/skills/index.js +1 -1
  63. package/dist/telemetry/db.d.ts +63 -0
  64. package/dist/telemetry/db.js +193 -0
  65. package/dist/telemetry/index.d.ts +17 -0
  66. package/dist/telemetry/index.js +82 -0
  67. package/dist/telemetry/sanitize.d.ts +6 -0
  68. package/dist/telemetry/sanitize.js +48 -0
  69. package/dist/telemetry/sqlite-processor.d.ts +11 -0
  70. package/dist/telemetry/sqlite-processor.js +108 -0
  71. package/dist/telemetry/types.d.ts +30 -0
  72. package/dist/telemetry/types.js +31 -0
  73. package/dist/tools/builtin/index.d.ts +2 -0
  74. package/dist/tools/builtin/index.js +2 -0
  75. package/dist/tools/builtin/self-config.d.ts +4 -0
  76. package/dist/tools/builtin/self-config.js +182 -0
  77. package/package.json +10 -2
@@ -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,212 @@ 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
+ config.onProgress?.("thinking");
113
+ const llmSpan = tracer.startSpan("llm.call", undefined, iterCtx);
114
+ let response;
72
115
  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
- });
116
+ response = await completeWithFallback(config.model, config.fallbacks ?? [], prunedMessages, {
117
+ systemPrompt: config.systemPrompt,
118
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
119
+ }, complete);
120
+ llmSpan.setAttribute(ATTR.INPUT_TOKENS, response.usage.inputTokens);
121
+ llmSpan.setAttribute(ATTR.OUTPUT_TOKENS, response.usage.outputTokens);
122
+ llmSpan.setAttribute(ATTR.CACHE_READ_TOKENS, response.usage.cacheReadTokens);
123
+ llmSpan.setAttribute(ATTR.CACHE_CREATION_TOKENS, response.usage.cacheCreationTokens);
124
+ llmSpan.setAttribute(ATTR.STOP_REASON, response.stopReason);
125
+ llmSpan.setAttribute(ATTR.MODEL, response.model);
126
+ if (response.latencyMs != null)
127
+ llmSpan.setAttribute(ATTR.LATENCY_MS, response.latencyMs);
82
128
  }
83
129
  catch (err) {
84
- log.error({ err, runId }, "failed to record LLM call");
130
+ llmSpan.setStatus({
131
+ code: SpanStatusCode.ERROR,
132
+ message: err instanceof Error ? err.message : String(err),
133
+ });
134
+ if (err instanceof Error)
135
+ llmSpan.recordException(err);
136
+ throw err;
85
137
  }
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
- }
138
+ finally {
139
+ llmSpan.end();
104
140
  }
105
- const elapsed = Date.now() - loopStart;
141
+ totalUsage.inputTokens += response.usage.inputTokens;
142
+ totalUsage.outputTokens += response.usage.outputTokens;
143
+ totalUsage.cacheCreationTokens += response.usage.cacheCreationTokens;
144
+ totalUsage.cacheReadTokens += response.usage.cacheReadTokens;
145
+ totalUsage.reasoningTokens += response.usage.reasoningTokens;
106
146
  if (config.usageStore) {
107
147
  try {
108
- config.usageStore.recordRun({
109
- id: runId,
148
+ config.usageStore.recordCall({
149
+ runId,
110
150
  agentName,
111
- source: config.source ?? "unknown",
112
- model: config.model,
113
- usage: totalUsage,
114
- iterations: iteration,
115
- toolCallsCount,
116
- durationMs: elapsed,
151
+ provider: response.model.split(":")[0],
152
+ model: response.model,
153
+ usage: response.usage,
154
+ stopReason: response.stopReason,
155
+ latencyMs: response.latencyMs,
117
156
  });
118
157
  }
119
158
  catch (err) {
120
- log.error({ err, runId }, "failed to record agent run");
159
+ log.error({ err, runId }, "failed to record LLM call");
121
160
  }
122
161
  }
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}"`;
162
+ log.debug({ label, iteration, model: config.model, stopReason: response.stopReason, inputTokens: response.usage.inputTokens, outputTokens: response.usage.outputTokens }, "iteration complete");
163
+ messages.push({ role: "assistant", content: response.content });
164
+ if (response.stopReason !== "tool_use") {
165
+ let text = extractText(response.content);
166
+ // If the final turn has no text (e.g. LLM said everything alongside a tool call
167
+ // in a prior turn), scan backward for the last assistant text.
168
+ if (!text) {
169
+ for (let j = messages.length - 2; j >= 0; j--) {
170
+ const msg = messages[j];
171
+ if (msg.role === "assistant") {
172
+ const found = extractText(msg.content);
173
+ if (found) {
174
+ text = found;
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ }
180
+ const elapsed = Date.now() - loopStart;
181
+ if (config.usageStore) {
182
+ try {
183
+ config.usageStore.recordRun({
184
+ id: runId,
185
+ agentName,
186
+ source: config.source ?? "unknown",
187
+ model: config.model,
188
+ usage: totalUsage,
189
+ iterations: iteration,
190
+ toolCallsCount,
191
+ durationMs: elapsed,
192
+ });
193
+ }
194
+ catch (err) {
195
+ log.error({ err, runId }, "failed to record agent run");
196
+ }
197
+ }
198
+ config.onProgress?.("complete");
199
+ log.info({ label, stopReason: response.stopReason, iterations: iteration, inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens, elapsed }, "loop end");
200
+ runSpan.setAttribute(ATTR.ITERATION, iteration);
201
+ return {
202
+ text,
203
+ messages,
204
+ usage: totalUsage,
205
+ };
139
206
  }
140
- else {
141
- const toolStart = Date.now();
207
+ const toolCalls = response.content.filter((b) => b.type === "tool_call");
208
+ toolCallsCount += toolCalls.length;
209
+ for (const call of toolCalls) {
210
+ log.debug({ label, iteration, tool: call.name, args: truncate(JSON.stringify(call.arguments), 200) }, "tool call");
211
+ const tool = toolMap.get(call.name);
212
+ let result;
213
+ let toolDidThrow = false;
214
+ const toolSpan = tracer.startSpan("tool.execute", undefined, iterCtx);
215
+ toolSpan.setAttribute(ATTR.TOOL_NAME, call.name);
142
216
  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");
217
+ if (!tool) {
218
+ log.warn({ label, iteration, tool: call.name }, "LLM called unknown tool");
219
+ result = `Error: unknown tool "${call.name}"`;
151
220
  }
152
221
  else {
153
- log.error({ err, tool: call.name, label, iteration }, "tool execution failed");
222
+ const toolStart = Date.now();
223
+ try {
224
+ config.onProgress?.("tool_use", call.name);
225
+ result = await tool.execute(call.arguments);
226
+ const toolElapsed = Date.now() - toolStart;
227
+ log.debug({ label, iteration, tool: call.name, resultLength: result.length, snippet: truncate(result, 150), elapsed: toolElapsed }, "tool result");
228
+ }
229
+ catch (err) {
230
+ toolDidThrow = true;
231
+ const isBug = err instanceof TypeError || err instanceof ReferenceError || err instanceof SyntaxError || err instanceof RangeError;
232
+ if (isBug) {
233
+ log.fatal({ err, tool: call.name, label, iteration }, "tool bug — programming error");
234
+ }
235
+ else {
236
+ log.error({ err, tool: call.name, label, iteration }, "tool execution failed");
237
+ }
238
+ result = `Error: ${err instanceof Error ? err.message : String(err)}`;
239
+ toolSpan.setStatus({
240
+ code: SpanStatusCode.ERROR,
241
+ message: err instanceof Error ? err.message : String(err),
242
+ });
243
+ if (err instanceof Error)
244
+ toolSpan.recordException(err);
245
+ }
246
+ }
247
+ // Detect error-as-string results (only if the tool didn't already throw)
248
+ if (!toolDidThrow && typeof result === "string" && result.startsWith("Error:")) {
249
+ toolSpan.setStatus({ code: SpanStatusCode.ERROR, message: result });
154
250
  }
155
- result = `Error: ${err instanceof Error ? err.message : String(err)}`;
156
251
  }
252
+ finally {
253
+ toolSpan.end();
254
+ }
255
+ const maxResultSize = config.maxToolResultSize ?? 8192;
256
+ if (result.length > maxResultSize) {
257
+ const originalLength = result.length;
258
+ result = result.slice(0, maxResultSize) + `\n\n[Output truncated from ${originalLength} to ${maxResultSize} characters]`;
259
+ log.debug({ label, iteration, tool: call.name, originalLength, maxResultSize }, "tool result truncated");
260
+ }
261
+ messages.push({
262
+ role: "tool_result",
263
+ tool_call_id: call.id,
264
+ content: result,
265
+ });
157
266
  }
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
- });
267
+ }
268
+ finally {
269
+ iterSpan.end();
169
270
  }
170
271
  }
171
272
  const elapsed = Date.now() - loopStart;
@@ -187,6 +288,7 @@ export async function runAgentLoop(initialMessages, config) {
187
288
  }
188
289
  }
189
290
  log.warn({ label, maxIterations, inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens, elapsed }, "max iterations reached");
291
+ runSpan.setAttribute(ATTR.ITERATION, maxIterations);
190
292
  return {
191
293
  text: "[Agent reached maximum iterations]",
192
294
  messages,
@@ -2,6 +2,7 @@ import type { Tool } from "../tools/types.js";
2
2
  import type { ToolRegistry } from "../tools/registry.js";
3
3
  import type { TokenUsage } from "../llm/types.js";
4
4
  import type { UsageStore } from "../usage/store.js";
5
+ export type ProgressPhase = "thinking" | "tool_use" | "complete";
5
6
  export interface AgentConfig {
6
7
  model: string;
7
8
  fallbacks?: string[];
@@ -15,6 +16,7 @@ export interface AgentConfig {
15
16
  source?: string;
16
17
  maxToolResultSize?: number;
17
18
  compactionThreshold?: number;
19
+ onProgress?: (phase: ProgressPhase, detail?: string) => void;
18
20
  }
19
21
  export interface AgentResult {
20
22
  text: string;
@@ -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;
@@ -24,7 +24,8 @@ export declare function buildAgentRuntime(agentName: string, config: Config, opt
24
24
  skillsDir: string;
25
25
  sessionId?: string;
26
26
  usageStore?: UsageStore;
27
- }): AgentRuntime;
27
+ configPath?: string;
28
+ }): Promise<AgentRuntime>;
28
29
  /**
29
30
  * Assemble a system prompt from soul, date context, skills index, and prompt fragments.
30
31
  */
@@ -46,5 +47,6 @@ export declare function createAgentExecutor(config: Config, opts: {
46
47
  agentRegistry: AgentRegistry;
47
48
  usageStore?: UsageStore;
48
49
  source: string;
50
+ configPath?: string;
49
51
  }): AgentExecutorFn;
50
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, {