@alejandroroman/agent-kit 0.1.4 → 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.
- package/dist/_memory/dist/server.js +0 -0
- package/dist/_memory/server.js +0 -0
- package/dist/agent/loop.js +210 -111
- package/dist/api/errors.d.ts +3 -0
- package/dist/api/errors.js +37 -0
- package/dist/api/events.d.ts +5 -0
- package/dist/api/events.js +28 -0
- package/dist/api/router.js +10 -0
- package/dist/api/traces.d.ts +3 -0
- package/dist/api/traces.js +35 -0
- package/dist/api/types.d.ts +2 -0
- package/dist/bootstrap.d.ts +3 -1
- package/dist/bootstrap.js +26 -7
- package/dist/cli/chat.js +3 -1
- package/dist/cli/claude-md-template.d.ts +5 -0
- package/dist/cli/claude-md-template.js +220 -0
- package/dist/cli/config-writer.js +3 -0
- package/dist/cli/env.d.ts +14 -0
- package/dist/cli/env.js +68 -0
- package/dist/cli/init.js +10 -0
- package/dist/cli/slack-setup.d.ts +6 -0
- package/dist/cli/slack-setup.js +234 -0
- package/dist/cli/start.js +65 -16
- package/dist/cli/ui.d.ts +2 -0
- package/dist/cli/ui.js +4 -1
- package/dist/cli/whats-new.d.ts +1 -0
- package/dist/cli/whats-new.js +69 -0
- package/dist/cli.js +14 -0
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.js +1 -0
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.js +1 -0
- package/dist/config/writer.d.ts +18 -0
- package/dist/config/writer.js +85 -0
- package/dist/cron/scheduler.d.ts +4 -1
- package/dist/cron/scheduler.js +99 -52
- package/dist/gateways/slack/client.d.ts +1 -0
- package/dist/gateways/slack/client.js +9 -0
- package/dist/gateways/slack/handler.js +2 -1
- package/dist/gateways/slack/index.js +75 -29
- package/dist/gateways/slack/listener.d.ts +8 -1
- package/dist/gateways/slack/listener.js +36 -10
- package/dist/heartbeat/runner.js +99 -82
- package/dist/llm/anthropic.d.ts +1 -0
- package/dist/llm/anthropic.js +11 -2
- package/dist/llm/fallback.js +34 -2
- package/dist/llm/openai.d.ts +2 -0
- package/dist/llm/openai.js +33 -2
- package/dist/llm/types.d.ts +16 -2
- package/dist/llm/types.js +9 -0
- package/dist/logger.js +8 -0
- package/dist/media/sanitize.d.ts +5 -0
- package/dist/media/sanitize.js +53 -0
- package/dist/multi/spawn.js +29 -10
- package/dist/session/compaction.js +3 -1
- package/dist/session/prune-images.d.ts +9 -0
- package/dist/session/prune-images.js +42 -0
- package/dist/skills/activate.d.ts +6 -0
- package/dist/skills/activate.js +72 -27
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/telemetry/db.d.ts +63 -0
- package/dist/telemetry/db.js +193 -0
- package/dist/telemetry/index.d.ts +17 -0
- package/dist/telemetry/index.js +82 -0
- package/dist/telemetry/sanitize.d.ts +6 -0
- package/dist/telemetry/sanitize.js +48 -0
- package/dist/telemetry/sqlite-processor.d.ts +11 -0
- package/dist/telemetry/sqlite-processor.js +108 -0
- package/dist/telemetry/types.d.ts +30 -0
- package/dist/telemetry/types.js +31 -0
- package/dist/tools/builtin/index.d.ts +2 -0
- package/dist/tools/builtin/index.js +2 -0
- package/dist/tools/builtin/self-config.d.ts +4 -0
- package/dist/tools/builtin/self-config.js +182 -0
- package/package.json +25 -18
|
File without changes
|
package/dist/_memory/server.js
CHANGED
|
File without changes
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
-
//
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
109
|
-
|
|
147
|
+
config.usageStore.recordCall({
|
|
148
|
+
runId,
|
|
110
149
|
agentName,
|
|
111
|
-
|
|
112
|
-
model:
|
|
113
|
-
usage:
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
158
|
+
log.error({ err, runId }, "failed to record LLM call");
|
|
121
159
|
}
|
|
122
160
|
}
|
|
123
|
-
log.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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,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
|
+
}
|
package/dist/api/router.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/api/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/bootstrap.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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, {
|