@alejandroroman/agent-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_memory/config.d.ts +14 -0
- package/dist/_memory/config.js +16 -0
- package/dist/_memory/db/client.d.ts +2 -0
- package/dist/_memory/db/client.js +15 -0
- package/dist/_memory/db/schema.d.ts +14 -0
- package/dist/_memory/db/schema.js +51 -0
- package/dist/_memory/embeddings/ollama.d.ts +12 -0
- package/dist/_memory/embeddings/ollama.js +22 -0
- package/dist/_memory/embeddings/provider.d.ts +4 -0
- package/dist/_memory/embeddings/provider.js +1 -0
- package/dist/_memory/index.d.ts +10 -0
- package/dist/_memory/index.js +6 -0
- package/dist/_memory/search.d.ts +30 -0
- package/dist/_memory/search.js +121 -0
- package/dist/_memory/server.d.ts +8 -0
- package/dist/_memory/server.js +126 -0
- package/dist/_memory/store.d.ts +51 -0
- package/dist/_memory/store.js +115 -0
- package/dist/agent/loop.d.ts +3 -0
- package/dist/agent/loop.js +195 -0
- package/dist/agent/setup.d.ts +6 -0
- package/dist/agent/setup.js +11 -0
- package/dist/agent/soul.d.ts +1 -0
- package/dist/agent/soul.js +8 -0
- package/dist/agent/types.d.ts +23 -0
- package/dist/agent/types.js +1 -0
- package/dist/api/agents.d.ts +2 -0
- package/dist/api/agents.js +43 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +20 -0
- package/dist/api/cron.d.ts +2 -0
- package/dist/api/cron.js +15 -0
- package/dist/api/health.d.ts +2 -0
- package/dist/api/health.js +8 -0
- package/dist/api/logs.d.ts +5 -0
- package/dist/api/logs.js +28 -0
- package/dist/api/router.d.ts +6 -0
- package/dist/api/router.js +80 -0
- package/dist/api/sessions.d.ts +2 -0
- package/dist/api/sessions.js +67 -0
- package/dist/api/types.d.ts +12 -0
- package/dist/api/types.js +13 -0
- package/dist/api/usage.d.ts +3 -0
- package/dist/api/usage.js +50 -0
- package/dist/bootstrap.d.ts +51 -0
- package/dist/bootstrap.js +110 -0
- package/dist/cli/chat.d.ts +1 -0
- package/dist/cli/chat.js +102 -0
- package/dist/cli/config-writer.d.ts +40 -0
- package/dist/cli/config-writer.js +108 -0
- package/dist/cli/create.d.ts +1 -0
- package/dist/cli/create.js +37 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +85 -0
- package/dist/cli/list.d.ts +1 -0
- package/dist/cli/list.js +36 -0
- package/dist/cli/ollama.d.ts +6 -0
- package/dist/cli/ollama.js +44 -0
- package/dist/cli/setup-agent/index.d.ts +9 -0
- package/dist/cli/setup-agent/index.js +100 -0
- package/dist/cli/setup-agent/soul.d.ts +2 -0
- package/dist/cli/setup-agent/soul.js +79 -0
- package/dist/cli/setup-agent/tools.d.ts +9 -0
- package/dist/cli/setup-agent/tools.js +362 -0
- package/dist/cli/start.d.ts +1 -0
- package/dist/cli/start.js +235 -0
- package/dist/cli/ui.d.ts +17 -0
- package/dist/cli/ui.js +79 -0
- package/dist/cli/validate.d.ts +1 -0
- package/dist/cli/validate.js +47 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +59 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +3 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +10 -0
- package/dist/config/resolve.d.ts +22 -0
- package/dist/config/resolve.js +45 -0
- package/dist/config/schema.d.ts +217 -0
- package/dist/config/schema.js +159 -0
- package/dist/cron/scheduler.d.ts +22 -0
- package/dist/cron/scheduler.js +115 -0
- package/dist/gateways/slack/client.d.ts +13 -0
- package/dist/gateways/slack/client.js +44 -0
- package/dist/gateways/slack/format.d.ts +30 -0
- package/dist/gateways/slack/format.js +170 -0
- package/dist/gateways/slack/handler.d.ts +9 -0
- package/dist/gateways/slack/handler.js +95 -0
- package/dist/gateways/slack/index.d.ts +16 -0
- package/dist/gateways/slack/index.js +102 -0
- package/dist/gateways/slack/listener.d.ts +10 -0
- package/dist/gateways/slack/listener.js +35 -0
- package/dist/gateways/slack/sessions.d.ts +11 -0
- package/dist/gateways/slack/sessions.js +32 -0
- package/dist/gateways/slack/types.d.ts +13 -0
- package/dist/gateways/slack/types.js +7 -0
- package/dist/heartbeat/index.d.ts +2 -0
- package/dist/heartbeat/index.js +1 -0
- package/dist/heartbeat/runner.d.ts +31 -0
- package/dist/heartbeat/runner.js +215 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +207 -0
- package/dist/llm/anthropic.d.ts +12 -0
- package/dist/llm/anthropic.js +89 -0
- package/dist/llm/fallback.d.ts +6 -0
- package/dist/llm/fallback.js +30 -0
- package/dist/llm/index.d.ts +9 -0
- package/dist/llm/index.js +40 -0
- package/dist/llm/openai.d.ts +12 -0
- package/dist/llm/openai.js +85 -0
- package/dist/llm/provider.d.ts +12 -0
- package/dist/llm/provider.js +9 -0
- package/dist/llm/types.d.ts +73 -0
- package/dist/llm/types.js +6 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +15 -0
- package/dist/multi/registry.d.ts +15 -0
- package/dist/multi/registry.js +28 -0
- package/dist/multi/spawn.d.ts +14 -0
- package/dist/multi/spawn.js +14 -0
- package/dist/scripts/validate-agent-cli.d.ts +1 -0
- package/dist/scripts/validate-agent-cli.js +47 -0
- package/dist/scripts/validate-agent.d.ts +17 -0
- package/dist/scripts/validate-agent.js +242 -0
- package/dist/session/compaction.d.ts +4 -0
- package/dist/session/compaction.js +30 -0
- package/dist/session/manager.d.ts +9 -0
- package/dist/session/manager.js +41 -0
- package/dist/skills/activate.d.ts +11 -0
- package/dist/skills/activate.js +62 -0
- package/dist/skills/index.d.ts +3 -0
- package/dist/skills/index.js +3 -0
- package/dist/skills/loader.d.ts +3 -0
- package/dist/skills/loader.js +20 -0
- package/dist/skills/schema.d.ts +8 -0
- package/dist/skills/schema.js +7 -0
- package/dist/text.d.ts +8 -0
- package/dist/text.js +24 -0
- package/dist/tools/builtin/index.d.ts +21 -0
- package/dist/tools/builtin/index.js +21 -0
- package/dist/tools/builtin/memory.d.ts +8 -0
- package/dist/tools/builtin/memory.js +164 -0
- package/dist/tools/builtin/read-file.d.ts +3 -0
- package/dist/tools/builtin/read-file.js +30 -0
- package/dist/tools/builtin/run-command.d.ts +3 -0
- package/dist/tools/builtin/run-command.js +37 -0
- package/dist/tools/builtin/spawn.d.ts +15 -0
- package/dist/tools/builtin/spawn.js +59 -0
- package/dist/tools/builtin/web-search.d.ts +8 -0
- package/dist/tools/builtin/web-search.js +99 -0
- package/dist/tools/builtin/write-file.d.ts +3 -0
- package/dist/tools/builtin/write-file.js +34 -0
- package/dist/tools/registry.d.ts +10 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tools/sandbox.d.ts +9 -0
- package/dist/tools/sandbox.js +74 -0
- package/dist/tools/types.d.ts +8 -0
- package/dist/tools/types.js +7 -0
- package/dist/usage/index.d.ts +4 -0
- package/dist/usage/index.js +3 -0
- package/dist/usage/pricing.d.ts +10 -0
- package/dist/usage/pricing.js +35 -0
- package/dist/usage/schema.d.ts +1 -0
- package/dist/usage/schema.js +45 -0
- package/dist/usage/store.d.ts +10 -0
- package/dist/usage/store.js +227 -0
- package/dist/usage/types.d.ts +61 -0
- package/dist/usage/types.js +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { complete } from "../llm/index.js";
|
|
2
|
+
import { completeWithFallback } from "../llm/fallback.js";
|
|
3
|
+
import { extractText } from "../llm/types.js";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
import { toolToDefinition } from "../tools/types.js";
|
|
6
|
+
import { compactMessages, estimateTotalTokens } from "../session/compaction.js";
|
|
7
|
+
import { createLogger } from "../logger.js";
|
|
8
|
+
import { truncate } from "../text.js";
|
|
9
|
+
const log = createLogger("agent");
|
|
10
|
+
export async function runAgentLoop(initialMessages, config) {
|
|
11
|
+
const messages = [...initialMessages];
|
|
12
|
+
const registry = config.toolRegistry;
|
|
13
|
+
const staticTools = config.tools ?? [];
|
|
14
|
+
const maxIterations = config.maxIterations ?? 20;
|
|
15
|
+
const label = config.label ?? "main";
|
|
16
|
+
const loopStart = Date.now();
|
|
17
|
+
const currentTools = registry
|
|
18
|
+
? registry.list().map((name) => registry.resolve([name])[0])
|
|
19
|
+
: staticTools;
|
|
20
|
+
const toolNames = currentTools.map((t) => t.name);
|
|
21
|
+
log.info({ label, model: config.model, tools: toolNames.length, toolNames, maxIterations, messages: initialMessages.length }, "loop start");
|
|
22
|
+
const runId = nanoid();
|
|
23
|
+
const agentName = config.agentName ?? config.label ?? "main";
|
|
24
|
+
const totalUsage = {
|
|
25
|
+
inputTokens: 0,
|
|
26
|
+
outputTokens: 0,
|
|
27
|
+
cacheCreationTokens: 0,
|
|
28
|
+
cacheReadTokens: 0,
|
|
29
|
+
reasoningTokens: 0,
|
|
30
|
+
};
|
|
31
|
+
let toolCallsCount = 0;
|
|
32
|
+
let didCompact = false;
|
|
33
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
34
|
+
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;
|
|
57
|
+
}
|
|
58
|
+
log.warn({ err, label, iteration }, "compaction failed, proceeding with full context");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
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) {
|
|
72
|
+
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
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
log.error({ err, runId }, "failed to record LLM call");
|
|
85
|
+
}
|
|
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
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const elapsed = Date.now() - loopStart;
|
|
106
|
+
if (config.usageStore) {
|
|
107
|
+
try {
|
|
108
|
+
config.usageStore.recordRun({
|
|
109
|
+
id: runId,
|
|
110
|
+
agentName,
|
|
111
|
+
source: config.source ?? "unknown",
|
|
112
|
+
model: config.model,
|
|
113
|
+
usage: totalUsage,
|
|
114
|
+
iterations: iteration,
|
|
115
|
+
toolCallsCount,
|
|
116
|
+
durationMs: elapsed,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
log.error({ err, runId }, "failed to record agent run");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
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}"`;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const toolStart = Date.now();
|
|
142
|
+
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");
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
log.error({ err, tool: call.name, label, iteration }, "tool execution failed");
|
|
154
|
+
}
|
|
155
|
+
result = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
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
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const elapsed = Date.now() - loopStart;
|
|
172
|
+
if (config.usageStore) {
|
|
173
|
+
try {
|
|
174
|
+
config.usageStore.recordRun({
|
|
175
|
+
id: runId,
|
|
176
|
+
agentName,
|
|
177
|
+
source: config.source ?? "unknown",
|
|
178
|
+
model: config.model,
|
|
179
|
+
usage: totalUsage,
|
|
180
|
+
iterations: maxIterations,
|
|
181
|
+
toolCallsCount,
|
|
182
|
+
durationMs: elapsed,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
log.error({ err, runId }, "failed to record agent run");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
log.warn({ label, maxIterations, inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens, elapsed }, "max iterations reached");
|
|
190
|
+
return {
|
|
191
|
+
text: "[Agent reached maximum iterations]",
|
|
192
|
+
messages,
|
|
193
|
+
usage: totalUsage,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { loadSoul } from "./soul.js";
|
|
3
|
+
import { SessionManager } from "../session/manager.js";
|
|
4
|
+
export function setupAgentSession(dataDir, agentName, sessionId) {
|
|
5
|
+
const agentDir = path.join(dataDir, "agents", agentName);
|
|
6
|
+
const sessionDir = path.join(agentDir, "sessions");
|
|
7
|
+
return {
|
|
8
|
+
soul: loadSoul(agentDir),
|
|
9
|
+
session: new SessionManager(sessionDir, sessionId),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadSoul(agentDir: string): string | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Tool } from "../tools/types.js";
|
|
2
|
+
import type { ToolRegistry } from "../tools/registry.js";
|
|
3
|
+
import type { TokenUsage } from "../llm/types.js";
|
|
4
|
+
import type { UsageStore } from "../usage/store.js";
|
|
5
|
+
export interface AgentConfig {
|
|
6
|
+
model: string;
|
|
7
|
+
fallbacks?: string[];
|
|
8
|
+
systemPrompt?: string;
|
|
9
|
+
tools?: Tool[];
|
|
10
|
+
toolRegistry?: ToolRegistry;
|
|
11
|
+
maxIterations?: number;
|
|
12
|
+
label?: string;
|
|
13
|
+
agentName?: string;
|
|
14
|
+
usageStore?: UsageStore;
|
|
15
|
+
source?: string;
|
|
16
|
+
maxToolResultSize?: number;
|
|
17
|
+
compactionThreshold?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface AgentResult {
|
|
20
|
+
text: string;
|
|
21
|
+
messages: import("../llm/types.js").Message[];
|
|
22
|
+
usage: TokenUsage;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { sendJson, sendError } from "./types.js";
|
|
2
|
+
export function agentRoutes(ctx) {
|
|
3
|
+
return {
|
|
4
|
+
"GET /api/agents": (_url, _req, res) => {
|
|
5
|
+
const agents = Object.entries(ctx.config.agents).map(([name, def]) => ({
|
|
6
|
+
name,
|
|
7
|
+
displayName: def.displayName ?? null,
|
|
8
|
+
emoji: def.emoji ?? null,
|
|
9
|
+
avatar: def.avatar ?? null,
|
|
10
|
+
model: def.model ?? ctx.config.defaults.model,
|
|
11
|
+
tools: def.tools,
|
|
12
|
+
skills: def.skills,
|
|
13
|
+
spawnOnly: def.spawn_only,
|
|
14
|
+
canSpawn: def.can_spawn,
|
|
15
|
+
maxIterations: def.maxIterations ?? ctx.config.defaults.maxIterations,
|
|
16
|
+
slack: def.slack ?? null,
|
|
17
|
+
}));
|
|
18
|
+
sendJson(res, agents);
|
|
19
|
+
},
|
|
20
|
+
"GET /api/agents/:name": (url, _req, res) => {
|
|
21
|
+
const name = url.pathname.split("/")[3];
|
|
22
|
+
const def = ctx.config.agents[name];
|
|
23
|
+
if (!def) {
|
|
24
|
+
sendError(res, 404, "agent not found");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
sendJson(res, {
|
|
28
|
+
name,
|
|
29
|
+
displayName: def.displayName ?? null,
|
|
30
|
+
emoji: def.emoji ?? null,
|
|
31
|
+
avatar: def.avatar ?? null,
|
|
32
|
+
model: def.model ?? ctx.config.defaults.model,
|
|
33
|
+
tools: def.tools,
|
|
34
|
+
skills: def.skills,
|
|
35
|
+
spawnOnly: def.spawn_only,
|
|
36
|
+
canSpawn: def.can_spawn,
|
|
37
|
+
maxIterations: def.maxIterations ?? ctx.config.defaults.maxIterations,
|
|
38
|
+
slack: def.slack ?? null,
|
|
39
|
+
sandbox: def.sandbox ?? ctx.config.defaults.sandbox ?? null,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { sendJson } from "./types.js";
|
|
3
|
+
export function configRoutes(ctx) {
|
|
4
|
+
return {
|
|
5
|
+
"GET /api/config": (_url, _req, res) => {
|
|
6
|
+
const raw = fs.readFileSync(ctx.configPath, "utf-8");
|
|
7
|
+
const config = JSON.parse(raw);
|
|
8
|
+
sendJson(res, config);
|
|
9
|
+
},
|
|
10
|
+
"GET /api/config/models": (_url, _req, res) => {
|
|
11
|
+
const models = (ctx.config.models ?? []).map((m) => ({
|
|
12
|
+
alias: m.alias,
|
|
13
|
+
provider: m.model.split(":")[0],
|
|
14
|
+
model: m.model,
|
|
15
|
+
isDefault: m.alias === ctx.config.defaults.model,
|
|
16
|
+
}));
|
|
17
|
+
sendJson(res, models);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/api/cron.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sendJson } from "./types.js";
|
|
2
|
+
export function cronRoutes(ctx) {
|
|
3
|
+
return {
|
|
4
|
+
"GET /api/cron": (_url, _req, res) => {
|
|
5
|
+
const jobs = ctx.config.cron.map((job) => ({
|
|
6
|
+
id: job.id,
|
|
7
|
+
agent: job.agent,
|
|
8
|
+
schedule: job.schedule,
|
|
9
|
+
prompt: job.prompt,
|
|
10
|
+
enabled: job.enabled,
|
|
11
|
+
}));
|
|
12
|
+
sendJson(res, jobs);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RouteHandler } from "./types.js";
|
|
2
|
+
export declare function addLogListener(res: import("http").ServerResponse): void;
|
|
3
|
+
export declare function removeLogListener(res: import("http").ServerResponse): void;
|
|
4
|
+
export declare function broadcastLog(data: string): void;
|
|
5
|
+
export declare function logRoutes(): Record<string, RouteHandler>;
|
package/dist/api/logs.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const clients = new Set();
|
|
2
|
+
export function addLogListener(res) {
|
|
3
|
+
clients.add(res);
|
|
4
|
+
}
|
|
5
|
+
export function removeLogListener(res) {
|
|
6
|
+
clients.delete(res);
|
|
7
|
+
}
|
|
8
|
+
export function broadcastLog(data) {
|
|
9
|
+
for (const client of clients) {
|
|
10
|
+
client.write(`data: ${data}\n\n`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function logRoutes() {
|
|
14
|
+
return {
|
|
15
|
+
"GET /api/logs/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
|
+
addLogListener(res);
|
|
23
|
+
req.on("close", () => {
|
|
24
|
+
removeLogListener(res);
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import { sendError } from "./types.js";
|
|
3
|
+
import { healthRoutes } from "./health.js";
|
|
4
|
+
import { sessionRoutes } from "./sessions.js";
|
|
5
|
+
import { agentRoutes } from "./agents.js";
|
|
6
|
+
import { configRoutes } from "./config.js";
|
|
7
|
+
import { cronRoutes } from "./cron.js";
|
|
8
|
+
import { logRoutes } from "./logs.js";
|
|
9
|
+
import { usageRoutes } from "./usage.js";
|
|
10
|
+
import { createLogger } from "../logger.js";
|
|
11
|
+
const log = createLogger("api");
|
|
12
|
+
function parseRouteKey(key) {
|
|
13
|
+
const [method, path] = key.split(" ");
|
|
14
|
+
// Convert :param segments to regex groups
|
|
15
|
+
const regexStr = "^" + path.replace(/:([^/]+)/g, "([^/]+)") + "$";
|
|
16
|
+
return { method, pattern: new RegExp(regexStr) };
|
|
17
|
+
}
|
|
18
|
+
export function createApiServer(ctx, port) {
|
|
19
|
+
// Collect routes from all modules
|
|
20
|
+
const routeHandlers = {
|
|
21
|
+
...healthRoutes(ctx),
|
|
22
|
+
...sessionRoutes(ctx),
|
|
23
|
+
...agentRoutes(ctx),
|
|
24
|
+
...configRoutes(ctx),
|
|
25
|
+
...cronRoutes(ctx),
|
|
26
|
+
...logRoutes(),
|
|
27
|
+
};
|
|
28
|
+
// If usage store exists, add usage routes
|
|
29
|
+
if (ctx.usageStore) {
|
|
30
|
+
Object.assign(routeHandlers, usageRoutes(ctx.usageStore));
|
|
31
|
+
}
|
|
32
|
+
const routes = Object.entries(routeHandlers).map(([key, handler]) => {
|
|
33
|
+
const { method, pattern } = parseRouteKey(key);
|
|
34
|
+
return { method, pattern, handler };
|
|
35
|
+
});
|
|
36
|
+
const server = http.createServer((req, res) => {
|
|
37
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
38
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
39
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
40
|
+
if (req.method === "OPTIONS") {
|
|
41
|
+
res.writeHead(204);
|
|
42
|
+
res.end();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
res.setHeader("Content-Type", "application/json");
|
|
46
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
47
|
+
const method = req.method ?? "GET";
|
|
48
|
+
const matched = routes.find((r) => r.method === method && r.pattern.test(url.pathname));
|
|
49
|
+
if (matched) {
|
|
50
|
+
try {
|
|
51
|
+
matched.handler(url, req, res);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
log.error({ err, path: url.pathname }, "API error");
|
|
55
|
+
sendError(res, 500, "internal server error");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
sendError(res, 404, "not found");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
start() {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
server.once("error", reject);
|
|
66
|
+
server.listen(port, () => {
|
|
67
|
+
const addr = server.address();
|
|
68
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
69
|
+
log.info({ port: actualPort }, "API server started");
|
|
70
|
+
resolve(actualPort);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
stop() {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { sendJson, sendError } from "./types.js";
|
|
4
|
+
export function sessionRoutes(ctx) {
|
|
5
|
+
return {
|
|
6
|
+
"GET /api/sessions": (url, _req, res) => {
|
|
7
|
+
const agentsDir = path.join(ctx.dataDir, "agents");
|
|
8
|
+
if (!fs.existsSync(agentsDir)) {
|
|
9
|
+
sendJson(res, { total: 0, sessions: [] });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const agentFilter = url.searchParams.get("agent") ?? undefined;
|
|
13
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
14
|
+
const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
|
|
15
|
+
const sessions = [];
|
|
16
|
+
const agentDirs = fs.readdirSync(agentsDir);
|
|
17
|
+
for (const agent of agentDirs) {
|
|
18
|
+
if (agentFilter && agent !== agentFilter)
|
|
19
|
+
continue;
|
|
20
|
+
const agentDir = path.join(agentsDir, agent);
|
|
21
|
+
if (!fs.statSync(agentDir).isDirectory())
|
|
22
|
+
continue;
|
|
23
|
+
const files = fs.readdirSync(agentDir).filter((f) => f.endsWith(".jsonl"));
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const filePath = path.join(agentDir, file);
|
|
26
|
+
const stat = fs.statSync(filePath);
|
|
27
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
28
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
29
|
+
sessions.push({
|
|
30
|
+
agent,
|
|
31
|
+
sessionId: file.replace(".jsonl", ""),
|
|
32
|
+
messageCount: lines.length,
|
|
33
|
+
sizeBytes: stat.size,
|
|
34
|
+
lastModified: stat.mtime.toISOString(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
sessions.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
|
|
39
|
+
const page = sessions.slice(offset, offset + limit);
|
|
40
|
+
sendJson(res, { total: sessions.length, sessions: page });
|
|
41
|
+
},
|
|
42
|
+
"GET /api/sessions/:agent/:id": (url, _req, res) => {
|
|
43
|
+
const parts = url.pathname.split("/");
|
|
44
|
+
const agent = parts[3];
|
|
45
|
+
const id = parts[4];
|
|
46
|
+
const filePath = path.join(ctx.dataDir, "agents", agent, `${id}.jsonl`);
|
|
47
|
+
if (!fs.existsSync(filePath)) {
|
|
48
|
+
sendError(res, 404, "session not found");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
52
|
+
const messages = content
|
|
53
|
+
.split("\n")
|
|
54
|
+
.filter((l) => l.trim())
|
|
55
|
+
.map((line) => {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(line);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
sendJson(res, { agent, sessionId: id, messages });
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Config } from "../config/schema.js";
|
|
2
|
+
import type { UsageStore } from "../usage/store.js";
|
|
3
|
+
export interface ApiContext {
|
|
4
|
+
config: Config;
|
|
5
|
+
configPath: string;
|
|
6
|
+
usageStore?: UsageStore;
|
|
7
|
+
dataDir: string;
|
|
8
|
+
memoryDbPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export type RouteHandler = (url: URL, req: import("http").IncomingMessage, res: import("http").ServerResponse) => void;
|
|
11
|
+
export declare function sendJson(res: import("http").ServerResponse, data: unknown, status?: number): void;
|
|
12
|
+
export declare function sendError(res: import("http").ServerResponse, status: number, message: string): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function sendJson(res, data, status = 200) {
|
|
2
|
+
if (res.headersSent)
|
|
3
|
+
return;
|
|
4
|
+
const body = JSON.stringify(data);
|
|
5
|
+
res.writeHead(status);
|
|
6
|
+
res.end(body);
|
|
7
|
+
}
|
|
8
|
+
export function sendError(res, status, message) {
|
|
9
|
+
if (res.headersSent)
|
|
10
|
+
return;
|
|
11
|
+
res.writeHead(status);
|
|
12
|
+
res.end(JSON.stringify({ error: message }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { sendJson, sendError } from "./types.js";
|
|
2
|
+
import { GROUP_BY_VALUES } from "../usage/types.js";
|
|
3
|
+
const VALID_GROUP_BY = new Set(GROUP_BY_VALUES);
|
|
4
|
+
function parseIntParam(val) {
|
|
5
|
+
if (val === null)
|
|
6
|
+
return undefined;
|
|
7
|
+
const n = parseInt(val, 10);
|
|
8
|
+
return isNaN(n) || n < 0 ? undefined : n;
|
|
9
|
+
}
|
|
10
|
+
export function usageRoutes(store) {
|
|
11
|
+
return {
|
|
12
|
+
"GET /api/usage/calls": (url, _req, res) => {
|
|
13
|
+
const data = store.getCalls({
|
|
14
|
+
model: url.searchParams.get("model") ?? undefined,
|
|
15
|
+
agentName: url.searchParams.get("agent") ?? undefined,
|
|
16
|
+
runId: url.searchParams.get("run_id") ?? undefined,
|
|
17
|
+
from: url.searchParams.get("from") ?? undefined,
|
|
18
|
+
to: url.searchParams.get("to") ?? undefined,
|
|
19
|
+
limit: parseIntParam(url.searchParams.get("limit")),
|
|
20
|
+
offset: parseIntParam(url.searchParams.get("offset")),
|
|
21
|
+
});
|
|
22
|
+
sendJson(res, data);
|
|
23
|
+
},
|
|
24
|
+
"GET /api/usage/runs": (url, _req, res) => {
|
|
25
|
+
const data = store.getRuns({
|
|
26
|
+
agentName: url.searchParams.get("agent") ?? undefined,
|
|
27
|
+
source: url.searchParams.get("source") ?? undefined,
|
|
28
|
+
model: url.searchParams.get("model") ?? undefined,
|
|
29
|
+
from: url.searchParams.get("from") ?? undefined,
|
|
30
|
+
to: url.searchParams.get("to") ?? undefined,
|
|
31
|
+
limit: parseIntParam(url.searchParams.get("limit")),
|
|
32
|
+
offset: parseIntParam(url.searchParams.get("offset")),
|
|
33
|
+
});
|
|
34
|
+
sendJson(res, data);
|
|
35
|
+
},
|
|
36
|
+
"GET /api/usage/summary": (url, _req, res) => {
|
|
37
|
+
const groupByParam = url.searchParams.get("group_by");
|
|
38
|
+
if (groupByParam && !VALID_GROUP_BY.has(groupByParam)) {
|
|
39
|
+
sendError(res, 400, "Invalid group_by value. Must be one of: model, agent, source, day");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const data = store.getSummary({
|
|
43
|
+
from: url.searchParams.get("from") ?? undefined,
|
|
44
|
+
to: url.searchParams.get("to") ?? undefined,
|
|
45
|
+
groupBy: groupByParam ?? undefined,
|
|
46
|
+
});
|
|
47
|
+
sendJson(res, data);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|