@alejandroroman/agent-kit 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_memory/dist/config.d.ts +14 -0
- package/dist/_memory/dist/config.js +16 -0
- package/dist/_memory/dist/db/client.d.ts +2 -0
- package/dist/_memory/dist/db/client.js +15 -0
- package/dist/_memory/dist/db/schema.d.ts +14 -0
- package/dist/_memory/dist/db/schema.js +51 -0
- package/dist/_memory/dist/embeddings/ollama.d.ts +12 -0
- package/dist/_memory/dist/embeddings/ollama.js +22 -0
- package/dist/_memory/dist/embeddings/provider.d.ts +4 -0
- package/dist/_memory/dist/embeddings/provider.js +1 -0
- package/dist/_memory/dist/index.d.ts +10 -0
- package/dist/_memory/dist/index.js +6 -0
- package/dist/_memory/dist/search.d.ts +30 -0
- package/dist/_memory/dist/search.js +121 -0
- package/dist/_memory/dist/server.d.ts +8 -0
- package/dist/_memory/dist/server.js +126 -0
- package/dist/_memory/dist/store.d.ts +51 -0
- package/dist/_memory/dist/store.js +115 -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 +6 -5
- package/dist/bootstrap.js +26 -7
- package/dist/cli/chat.js +18 -63
- 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/create.js +1 -4
- package/dist/cli/env.d.ts +14 -0
- package/dist/cli/env.js +68 -0
- package/dist/cli/init.js +14 -7
- package/dist/cli/list.js +1 -2
- package/dist/cli/paths.d.ts +3 -0
- package/dist/cli/paths.js +4 -0
- package/dist/cli/repl.d.ts +23 -0
- package/dist/cli/repl.js +73 -0
- package/dist/cli/slack-setup.d.ts +6 -0
- package/dist/cli/slack-setup.js +234 -0
- package/dist/cli/start.js +96 -96
- package/dist/cli/ui.d.ts +2 -2
- package/dist/cli/ui.js +5 -5
- package/dist/cli/validate.js +1 -4
- 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/index.js +4 -209
- 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/dist/tools/registry.js +8 -1
- package/package.json +26 -20
package/dist/heartbeat/runner.js
CHANGED
|
@@ -5,9 +5,11 @@ import { setupAgentSession } from "../agent/setup.js";
|
|
|
5
5
|
import { resolveAgent, resolveModelAlias, resolveWebSearch } from "../config/resolve.js";
|
|
6
6
|
import { createBuiltinRegistry } from "../tools/builtin/index.js";
|
|
7
7
|
import { registerSpawnWrappers } from "../tools/builtin/spawn.js";
|
|
8
|
-
import { createActivateSkillTool } from "../skills/index.js";
|
|
8
|
+
import { createActivateSkillTool, preActivateSkills } from "../skills/index.js";
|
|
9
9
|
import { createLogger } from "../logger.js";
|
|
10
10
|
import { dateContext } from "../text.js";
|
|
11
|
+
import { context, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
12
|
+
import { getTracer, ATTR } from "../telemetry/index.js";
|
|
11
13
|
const log = createLogger("heartbeat");
|
|
12
14
|
const HEARTBEAT_OK = "HEARTBEAT_OK";
|
|
13
15
|
export function isHeartbeatSuppressed(text) {
|
|
@@ -64,93 +66,99 @@ export class HeartbeatRunner {
|
|
|
64
66
|
if (!agentDef?.heartbeat)
|
|
65
67
|
return undefined;
|
|
66
68
|
const hb = agentDef.heartbeat;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
log.warn({ agent: agentName }, "no HEARTBEAT.md found, skipping");
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
// Isolated tool registry per tick (same pattern as CronScheduler)
|
|
77
|
-
const sandbox = agentDef.sandbox ?? this.config.defaults.sandbox;
|
|
78
|
-
const memoryConfig = this.config.defaults.memory;
|
|
79
|
-
const tickRegistry = createBuiltinRegistry({
|
|
80
|
-
allowedCommands: sandbox?.allowedCommands,
|
|
81
|
-
allowedPaths: sandbox?.allowedPaths,
|
|
82
|
-
memoryConfig,
|
|
83
|
-
webSearch: resolveWebSearch(agentName, this.config),
|
|
69
|
+
const tracer = getTracer("heartbeat");
|
|
70
|
+
const span = tracer.startSpan("source.heartbeat", {
|
|
71
|
+
attributes: {
|
|
72
|
+
[ATTR.AGENT]: agentName,
|
|
73
|
+
[ATTR.SOURCE]: "heartbeat",
|
|
74
|
+
},
|
|
84
75
|
});
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
manifests: resolved.skills,
|
|
92
|
-
skillsDir: this.skillsDir,
|
|
93
|
-
toolRegistry: tickRegistry,
|
|
94
|
-
promptFragments,
|
|
95
|
-
activatedSkills: new Set(),
|
|
96
|
-
};
|
|
97
|
-
const activateTool = createActivateSkillTool(ctx);
|
|
98
|
-
tickRegistry.register(activateTool);
|
|
99
|
-
// Auto-activate all skills upfront
|
|
100
|
-
for (const manifest of resolved.skills) {
|
|
101
|
-
try {
|
|
102
|
-
const result = await activateTool.execute({ skill_name: manifest.name });
|
|
103
|
-
if (typeof result === "string" && result.startsWith("Error")) {
|
|
104
|
-
log.error({ agent: agentName, skill: manifest.name, result }, "failed to auto-activate skill");
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
log.error({ err, agent: agentName, skill: manifest.name }, "skill auto-activation threw");
|
|
109
|
-
}
|
|
76
|
+
const spanCtx = trace.setSpan(context.active(), span);
|
|
77
|
+
try {
|
|
78
|
+
if (!isWithinActiveHours(hb.activeHours)) {
|
|
79
|
+
log.debug({ agent: agentName }, "outside active hours, skipping");
|
|
80
|
+
span.setAttribute(ATTR.SUPPRESSED, "outside_active_hours");
|
|
81
|
+
return undefined;
|
|
110
82
|
}
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
83
|
+
const instructions = this.loadHeartbeatInstructions(agentName);
|
|
84
|
+
if (!instructions) {
|
|
85
|
+
log.warn({ agent: agentName }, "no HEARTBEAT.md found, skipping");
|
|
86
|
+
span.setAttribute(ATTR.SUPPRESSED, "no_instructions");
|
|
87
|
+
return undefined;
|
|
116
88
|
}
|
|
89
|
+
return await context.with(spanCtx, async () => {
|
|
90
|
+
// Isolated tool registry per tick (same pattern as CronScheduler)
|
|
91
|
+
const sandbox = agentDef.sandbox ?? this.config.defaults.sandbox;
|
|
92
|
+
const memoryConfig = this.config.defaults.memory;
|
|
93
|
+
const tickRegistry = createBuiltinRegistry({
|
|
94
|
+
allowedCommands: sandbox?.allowedCommands,
|
|
95
|
+
allowedPaths: sandbox?.allowedPaths,
|
|
96
|
+
memoryConfig,
|
|
97
|
+
webSearch: resolveWebSearch(agentName, this.config),
|
|
98
|
+
});
|
|
99
|
+
const resolved = resolveAgent(agentName, this.config, tickRegistry, this.skillsDir);
|
|
100
|
+
// Skills setup — auto-activate all skills upfront so Haiku doesn't need to call activate_skill
|
|
101
|
+
const promptFragments = [];
|
|
102
|
+
let skillsIndex = "";
|
|
103
|
+
if (resolved.skills.length > 0) {
|
|
104
|
+
const ctx = {
|
|
105
|
+
manifests: resolved.skills,
|
|
106
|
+
skillsDir: this.skillsDir,
|
|
107
|
+
toolRegistry: tickRegistry,
|
|
108
|
+
promptFragments,
|
|
109
|
+
activatedSkills: new Set(),
|
|
110
|
+
};
|
|
111
|
+
const activateTool = createActivateSkillTool(ctx);
|
|
112
|
+
tickRegistry.register(activateTool);
|
|
113
|
+
// Heartbeat always auto-activates (Haiku can't reliably call activate_skill)
|
|
114
|
+
skillsIndex = await preActivateSkills(ctx, activateTool, log);
|
|
115
|
+
}
|
|
116
|
+
// Spawn wrappers
|
|
117
|
+
if (resolved.canSpawn.length > 0) {
|
|
118
|
+
registerSpawnWrappers(resolved.canSpawn, this.config, this.agentRegistry, tickRegistry, this.usageStore);
|
|
119
|
+
}
|
|
120
|
+
// Ephemeral session: no history accumulation, append-only audit log
|
|
121
|
+
const sessionId = `heartbeat-${agentName}`;
|
|
122
|
+
const { soul, session } = setupAgentSession(this.dataDir, agentName, sessionId);
|
|
123
|
+
// Build tick prompt with fresh instructions + timestamp
|
|
124
|
+
const tickPrompt = `[Heartbeat tick — ${new Date().toISOString()}]\n\n${instructions}`;
|
|
125
|
+
const userMsg = { role: "user", content: tickPrompt };
|
|
126
|
+
session.append(userMsg); // audit only
|
|
127
|
+
const messages = [userMsg]; // ephemeral — no history
|
|
128
|
+
// Model: heartbeat.model overrides agent model
|
|
129
|
+
const model = hb.model
|
|
130
|
+
? resolveModelAlias(hb.model, this.config.models)
|
|
131
|
+
: resolved.model;
|
|
132
|
+
const systemPrompt = [soul, dateContext(), skillsIndex, ...promptFragments]
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join("\n\n") || undefined;
|
|
135
|
+
const result = await runAgentLoop(messages, {
|
|
136
|
+
model,
|
|
137
|
+
fallbacks: resolved.fallbacks,
|
|
138
|
+
systemPrompt,
|
|
139
|
+
toolRegistry: tickRegistry,
|
|
140
|
+
maxIterations: hb.maxIterations ?? resolved.maxIterations,
|
|
141
|
+
compactionThreshold: resolved.compactionThreshold,
|
|
142
|
+
maxToolResultSize: resolved.maxToolResultSize,
|
|
143
|
+
agentName,
|
|
144
|
+
usageStore: this.usageStore,
|
|
145
|
+
source: "heartbeat",
|
|
146
|
+
});
|
|
147
|
+
// Audit: append all new messages from the agent loop
|
|
148
|
+
for (const msg of result.messages.slice(1)) {
|
|
149
|
+
session.append(msg);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
});
|
|
117
153
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
catch (err) {
|
|
155
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
|
|
156
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
157
|
+
throw err;
|
|
121
158
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const { soul, session } = setupAgentSession(this.dataDir, agentName, sessionId);
|
|
125
|
-
// Build tick prompt with fresh instructions + timestamp
|
|
126
|
-
const tickPrompt = `[Heartbeat tick — ${new Date().toISOString()}]\n\n${instructions}`;
|
|
127
|
-
const userMsg = { role: "user", content: tickPrompt };
|
|
128
|
-
session.append(userMsg); // audit only
|
|
129
|
-
const messages = [userMsg]; // ephemeral — no history
|
|
130
|
-
// Model: heartbeat.model overrides agent model
|
|
131
|
-
const model = hb.model
|
|
132
|
-
? resolveModelAlias(hb.model, this.config.models)
|
|
133
|
-
: resolved.model;
|
|
134
|
-
const systemPrompt = [soul, dateContext(), skillsIndex, ...promptFragments]
|
|
135
|
-
.filter(Boolean)
|
|
136
|
-
.join("\n\n") || undefined;
|
|
137
|
-
const result = await runAgentLoop(messages, {
|
|
138
|
-
model,
|
|
139
|
-
fallbacks: resolved.fallbacks,
|
|
140
|
-
systemPrompt,
|
|
141
|
-
toolRegistry: tickRegistry,
|
|
142
|
-
maxIterations: hb.maxIterations ?? resolved.maxIterations,
|
|
143
|
-
compactionThreshold: resolved.compactionThreshold,
|
|
144
|
-
maxToolResultSize: resolved.maxToolResultSize,
|
|
145
|
-
agentName,
|
|
146
|
-
usageStore: this.usageStore,
|
|
147
|
-
source: "heartbeat",
|
|
148
|
-
});
|
|
149
|
-
// Audit: append all new messages from the agent loop
|
|
150
|
-
for (const msg of result.messages.slice(1)) {
|
|
151
|
-
session.append(msg);
|
|
159
|
+
finally {
|
|
160
|
+
span.end();
|
|
152
161
|
}
|
|
153
|
-
return result;
|
|
154
162
|
}
|
|
155
163
|
start(callbacks) {
|
|
156
164
|
for (const [agentName, agentDef] of Object.entries(this.config.agents)) {
|
|
@@ -162,6 +170,15 @@ export class HeartbeatRunner {
|
|
|
162
170
|
const runTick = async () => {
|
|
163
171
|
if (this.running.has(agentName)) {
|
|
164
172
|
log.info({ agent: agentName }, "previous tick still running, skipping");
|
|
173
|
+
const tracer = getTracer("heartbeat");
|
|
174
|
+
const skipSpan = tracer.startSpan("source.heartbeat", {
|
|
175
|
+
attributes: {
|
|
176
|
+
[ATTR.AGENT]: agentName,
|
|
177
|
+
[ATTR.SOURCE]: "heartbeat",
|
|
178
|
+
[ATTR.SUPPRESSED]: "already_running",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
skipSpan.end();
|
|
165
182
|
return;
|
|
166
183
|
}
|
|
167
184
|
this.running.add(agentName);
|
package/dist/index.js
CHANGED
|
@@ -1,209 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { loadConfig } from "./config/index.js";
|
|
6
|
-
import { CronScheduler } from "./cron/scheduler.js";
|
|
7
|
-
import { HeartbeatRunner } from "./heartbeat/index.js";
|
|
8
|
-
import { createSlackGateway } from "./gateways/slack/index.js";
|
|
9
|
-
import { createLogger } from "./logger.js";
|
|
10
|
-
import { createApiServer } from "./api/router.js";
|
|
11
|
-
import { buildAgentRuntime, buildSystemPrompt, initUsageStore, createAgentExecutor, } from "./bootstrap.js";
|
|
12
|
-
const log = createLogger("startup");
|
|
13
|
-
const CONFIG_PATH = path.join(process.cwd(), "agent-kit.json");
|
|
14
|
-
const DATA_DIR = path.join(process.cwd(), "data");
|
|
15
|
-
const DEFAULT_AGENT = "default";
|
|
16
|
-
const SKILLS_DIR = path.join(process.cwd(), "skills");
|
|
17
|
-
async function main() {
|
|
18
|
-
const config = loadConfig(CONFIG_PATH);
|
|
19
|
-
const { usageStore, apiPort } = initUsageStore(config);
|
|
20
|
-
const apiServer = createApiServer({
|
|
21
|
-
config,
|
|
22
|
-
configPath: CONFIG_PATH,
|
|
23
|
-
usageStore,
|
|
24
|
-
dataDir: DATA_DIR,
|
|
25
|
-
}, apiPort);
|
|
26
|
-
const agentName = process.argv[2] ?? DEFAULT_AGENT;
|
|
27
|
-
// Validate all agent tool references at startup
|
|
28
|
-
const runtime = buildAgentRuntime(agentName, config, {
|
|
29
|
-
dataDir: DATA_DIR,
|
|
30
|
-
skillsDir: SKILLS_DIR,
|
|
31
|
-
usageStore,
|
|
32
|
-
});
|
|
33
|
-
const toolWarnings = runtime.toolRegistry.validateAgents(config.agents);
|
|
34
|
-
for (const w of toolWarnings)
|
|
35
|
-
console.warn(` ⚠ ${w}`);
|
|
36
|
-
const { resolved, toolRegistry, agentRegistry, promptFragments, skillsIndex, soul, session } = runtime;
|
|
37
|
-
const sandbox = config.agents[agentName]?.sandbox ?? config.defaults.sandbox;
|
|
38
|
-
console.log("Agent Kit");
|
|
39
|
-
console.log(` Model: ${resolved.model}`);
|
|
40
|
-
if (resolved.fallbacks.length > 0) {
|
|
41
|
-
console.log(` Fallbacks: ${resolved.fallbacks.join(" → ")}`);
|
|
42
|
-
}
|
|
43
|
-
console.log(` Agent: ${agentName}`);
|
|
44
|
-
console.log(` SOUL: ${soul ? "loaded" : "none (create data/agents/default/SOUL.md)"}`);
|
|
45
|
-
console.log(` Tools: ${resolved.tools.map((t) => t.name).join(", ")}`);
|
|
46
|
-
if (resolved.skills.length > 0) {
|
|
47
|
-
console.log(` Skills: ${resolved.skills.map((s) => s.name).join(", ")}`);
|
|
48
|
-
}
|
|
49
|
-
const sandboxSummary = (() => {
|
|
50
|
-
if (!sandbox)
|
|
51
|
-
return "off (unrestricted)";
|
|
52
|
-
const parts = [];
|
|
53
|
-
if (sandbox.allowedCommands?.length) {
|
|
54
|
-
parts.push(`${sandbox.allowedCommands.length} commands allowed`);
|
|
55
|
-
}
|
|
56
|
-
if (sandbox.allowedPaths?.length) {
|
|
57
|
-
parts.push(`${sandbox.allowedPaths.length} paths allowed`);
|
|
58
|
-
}
|
|
59
|
-
return parts.length > 0 ? parts.join(", ") : "configured (no restrictions)";
|
|
60
|
-
})();
|
|
61
|
-
console.log(` Sandbox: ${sandboxSummary}`);
|
|
62
|
-
if (config.defaults.usage) {
|
|
63
|
-
console.log(` Usage: tracking enabled (${config.defaults.usage.dbPath})`);
|
|
64
|
-
}
|
|
65
|
-
const scheduler = new CronScheduler(config, toolRegistry, agentRegistry, DATA_DIR, SKILLS_DIR, usageStore);
|
|
66
|
-
const enabledJobs = scheduler.getJobs().filter((j) => j.enabled);
|
|
67
|
-
// Agent executor for inbound Slack messages
|
|
68
|
-
const executeAgent = createAgentExecutor(config, {
|
|
69
|
-
dataDir: DATA_DIR,
|
|
70
|
-
skillsDir: SKILLS_DIR,
|
|
71
|
-
agentRegistry,
|
|
72
|
-
usageStore,
|
|
73
|
-
source: "slack",
|
|
74
|
-
});
|
|
75
|
-
// Slack gateway (optional — requires both tokens + agent slack bindings)
|
|
76
|
-
let gateway;
|
|
77
|
-
const hasSlackBindings = Object.values(config.agents).some((a) => a.slack);
|
|
78
|
-
if (hasSlackBindings && process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) {
|
|
79
|
-
try {
|
|
80
|
-
gateway = createSlackGateway(config, { onAgentRequest: executeAgent });
|
|
81
|
-
await gateway.start();
|
|
82
|
-
const bindingCount = Object.values(config.agents).filter((a) => a.slack).length;
|
|
83
|
-
log.info({ bindings: bindingCount }, "Slack connected");
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
log.warn({ err }, "Slack failed to connect");
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
const actualPort = await apiServer.start();
|
|
91
|
-
console.log(` API: http://localhost:${actualPort}/api`);
|
|
92
|
-
}
|
|
93
|
-
catch (err) {
|
|
94
|
-
log.warn({ err }, "failed to start API server — continuing without it");
|
|
95
|
-
}
|
|
96
|
-
if (enabledJobs.length > 0) {
|
|
97
|
-
const jobList = enabledJobs.map((j) => `${j.id} @ ${j.schedule}`).join(", ");
|
|
98
|
-
log.info({ jobs: enabledJobs.length, jobList }, "cron scheduled");
|
|
99
|
-
scheduler.start({
|
|
100
|
-
onResult: (jobId, agentName, result) => {
|
|
101
|
-
log.info({ jobId, tokens: result.usage.inputTokens + result.usage.outputTokens }, "cron job completed");
|
|
102
|
-
process.stdout.write("You: ");
|
|
103
|
-
const channelOverride = config.cron.find((j) => j.id === jobId)?.slack?.channelId;
|
|
104
|
-
gateway?.onJobResult(agentName, jobId, result, channelOverride).catch((err) => log.warn({ err, jobId }, "failed to post cron result to Slack"));
|
|
105
|
-
},
|
|
106
|
-
onError: (jobId, agentName, error) => {
|
|
107
|
-
log.error({ err: error, jobId }, "cron job failed");
|
|
108
|
-
process.stdout.write("You: ");
|
|
109
|
-
const channelOverride = config.cron.find((j) => j.id === jobId)?.slack?.channelId;
|
|
110
|
-
gateway?.onJobError(agentName, jobId, error, channelOverride).catch((err) => log.warn({ err, jobId }, "failed to post cron error to Slack"));
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
const heartbeat = new HeartbeatRunner(config, agentRegistry, DATA_DIR, SKILLS_DIR, usageStore);
|
|
115
|
-
const heartbeatAgents = heartbeat.getHeartbeatAgents();
|
|
116
|
-
if (heartbeatAgents.length > 0) {
|
|
117
|
-
log.info({ agents: heartbeatAgents }, "heartbeat started");
|
|
118
|
-
heartbeat.start({
|
|
119
|
-
onResult: (agentName, result) => {
|
|
120
|
-
log.info({ agent: agentName, tokens: result.usage.inputTokens + result.usage.outputTokens }, "heartbeat alert");
|
|
121
|
-
process.stdout.write("You: ");
|
|
122
|
-
const channelOverride = config.agents[agentName]?.heartbeat?.slack?.channelId;
|
|
123
|
-
gateway?.onJobResult(agentName, "heartbeat", result, channelOverride).catch((err) => log.warn({ err, agent: agentName }, "failed to post heartbeat result to Slack"));
|
|
124
|
-
},
|
|
125
|
-
onError: (agentName, error) => {
|
|
126
|
-
log.error({ err: error, agent: agentName }, "heartbeat failed");
|
|
127
|
-
process.stdout.write("You: ");
|
|
128
|
-
const channelOverride = config.agents[agentName]?.heartbeat?.slack?.channelId;
|
|
129
|
-
gateway?.onJobError(agentName, "heartbeat", error, channelOverride).catch((err) => log.warn({ err, agent: agentName }, "failed to post heartbeat error to Slack"));
|
|
130
|
-
},
|
|
131
|
-
onSuppressed: (agentName) => {
|
|
132
|
-
log.debug({ agent: agentName }, "heartbeat OK, suppressed");
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
if (heartbeatAgents.length > 0) {
|
|
137
|
-
console.log(` Heartbeat: ${heartbeatAgents.join(", ")}`);
|
|
138
|
-
}
|
|
139
|
-
console.log(` Commands: /new (reset), /quit`);
|
|
140
|
-
console.log();
|
|
141
|
-
let messages = session.getMessages();
|
|
142
|
-
const rl = readline.createInterface({
|
|
143
|
-
input: process.stdin,
|
|
144
|
-
output: process.stdout,
|
|
145
|
-
});
|
|
146
|
-
const ask = () => {
|
|
147
|
-
rl.question("You: ", async (input) => {
|
|
148
|
-
const trimmed = input.trim();
|
|
149
|
-
if (trimmed === "/quit") {
|
|
150
|
-
scheduler.stop();
|
|
151
|
-
heartbeat.stop();
|
|
152
|
-
await gateway?.stop();
|
|
153
|
-
await apiServer.stop();
|
|
154
|
-
try {
|
|
155
|
-
usageStore?.close();
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
log.warn({ err }, "failed to close usage store");
|
|
159
|
-
}
|
|
160
|
-
finally {
|
|
161
|
-
rl.close();
|
|
162
|
-
}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (trimmed === "/new") {
|
|
166
|
-
messages = [];
|
|
167
|
-
console.log(" Session reset.\n");
|
|
168
|
-
ask();
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (!trimmed) {
|
|
172
|
-
ask();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const userMsg = { role: "user", content: trimmed };
|
|
176
|
-
messages.push(userMsg);
|
|
177
|
-
session.append(userMsg);
|
|
178
|
-
messages = await compactMessages(messages, resolved.model, resolved.compactionThreshold);
|
|
179
|
-
try {
|
|
180
|
-
const systemPrompt = buildSystemPrompt(soul, skillsIndex, promptFragments);
|
|
181
|
-
const result = await runAgentLoop(messages, {
|
|
182
|
-
model: resolved.model,
|
|
183
|
-
fallbacks: resolved.fallbacks,
|
|
184
|
-
systemPrompt,
|
|
185
|
-
toolRegistry,
|
|
186
|
-
maxIterations: resolved.maxIterations,
|
|
187
|
-
compactionThreshold: resolved.compactionThreshold,
|
|
188
|
-
maxToolResultSize: resolved.maxToolResultSize,
|
|
189
|
-
agentName,
|
|
190
|
-
usageStore,
|
|
191
|
-
source: "cli",
|
|
192
|
-
});
|
|
193
|
-
const newMessages = result.messages.slice(messages.length - 1);
|
|
194
|
-
for (const msg of newMessages) {
|
|
195
|
-
session.append(msg);
|
|
196
|
-
}
|
|
197
|
-
messages = result.messages;
|
|
198
|
-
console.log(`\nAgent: ${result.text}`);
|
|
199
|
-
console.log(` (${result.usage.inputTokens + result.usage.outputTokens} tokens)\n`);
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
console.error(`\n Error: ${err instanceof Error ? err.message : err}\n`);
|
|
203
|
-
}
|
|
204
|
-
ask();
|
|
205
|
-
});
|
|
206
|
-
};
|
|
207
|
-
ask();
|
|
208
|
-
}
|
|
209
|
-
main();
|
|
1
|
+
// Legacy entry point — delegates to the CLI start command.
|
|
2
|
+
// Kept for backwards compatibility with `pnpm dev` / `pnpm dev:local`.
|
|
3
|
+
import { start } from "./cli/start.js";
|
|
4
|
+
start();
|
package/dist/llm/anthropic.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare class AnthropicProvider implements LLMProvider {
|
|
|
7
7
|
complete(model: string, messages: Message[], options?: CompleteOptions): Promise<LLMResponse>;
|
|
8
8
|
stream(model: string, messages: Message[], options?: CompleteOptions): AsyncIterable<StreamEvent>;
|
|
9
9
|
private toAnthropicMessages;
|
|
10
|
+
private mapUserContent;
|
|
10
11
|
private fromAnthropicResponse;
|
|
11
12
|
private mapStopReason;
|
|
12
13
|
}
|
package/dist/llm/anthropic.js
CHANGED
|
@@ -39,7 +39,7 @@ export class AnthropicProvider {
|
|
|
39
39
|
toAnthropicMessages(messages) {
|
|
40
40
|
return messages.map((msg) => {
|
|
41
41
|
if (msg.role === "user")
|
|
42
|
-
return { role: "user", content: msg.content };
|
|
42
|
+
return { role: "user", content: this.mapUserContent(msg.content) };
|
|
43
43
|
if (msg.role === "assistant") {
|
|
44
44
|
return {
|
|
45
45
|
role: "assistant",
|
|
@@ -51,11 +51,20 @@ export class AnthropicProvider {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
if (msg.role === "tool_result") {
|
|
54
|
-
return { role: "user", content: [{ type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content }] };
|
|
54
|
+
return { role: "user", content: [{ type: "tool_result", tool_use_id: msg.tool_call_id, content: this.mapUserContent(msg.content) }] };
|
|
55
55
|
}
|
|
56
56
|
return msg;
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
mapUserContent(content) {
|
|
60
|
+
if (typeof content === "string")
|
|
61
|
+
return content;
|
|
62
|
+
return content.map((block) => {
|
|
63
|
+
if (block.type === "text")
|
|
64
|
+
return { type: "text", text: block.text };
|
|
65
|
+
return { type: "image", source: block.source };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
59
68
|
fromAnthropicResponse(response) {
|
|
60
69
|
const content = response.content.map((block) => {
|
|
61
70
|
if (block.type === "text")
|
package/dist/llm/fallback.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "../logger.js";
|
|
2
|
+
import { getTracer, ATTR } from "../telemetry/index.js";
|
|
3
|
+
import { SpanStatusCode, context } from "@opentelemetry/api";
|
|
2
4
|
const log = createLogger("llm");
|
|
3
5
|
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503]);
|
|
4
6
|
export function isRetryableError(err) {
|
|
@@ -12,17 +14,47 @@ export function isRetryableError(err) {
|
|
|
12
14
|
return false;
|
|
13
15
|
}
|
|
14
16
|
export async function completeWithFallback(primary, fallbacks, messages, options, completeFn) {
|
|
17
|
+
const tracer = getTracer("llm");
|
|
15
18
|
const models = [primary, ...fallbacks];
|
|
16
19
|
let lastError;
|
|
17
|
-
for (
|
|
20
|
+
for (let idx = 0; idx < models.length; idx++) {
|
|
21
|
+
const model = models[idx];
|
|
22
|
+
const [provider = "unknown"] = model.split(":");
|
|
23
|
+
const attemptSpan = tracer.startSpan("llm.attempt", {}, context.active());
|
|
24
|
+
attemptSpan.setAttribute(ATTR.PROVIDER, provider);
|
|
25
|
+
attemptSpan.setAttribute(ATTR.MODEL, model);
|
|
18
26
|
try {
|
|
19
|
-
|
|
27
|
+
const response = await completeFn(model, messages, options);
|
|
28
|
+
// Set token attributes on successful attempt
|
|
29
|
+
attemptSpan.setAttribute(ATTR.INPUT_TOKENS, response.usage.inputTokens);
|
|
30
|
+
attemptSpan.setAttribute(ATTR.OUTPUT_TOKENS, response.usage.outputTokens);
|
|
31
|
+
attemptSpan.setAttribute(ATTR.CACHE_CREATION_TOKENS, response.usage.cacheCreationTokens);
|
|
32
|
+
attemptSpan.setAttribute(ATTR.CACHE_READ_TOKENS, response.usage.cacheReadTokens);
|
|
33
|
+
attemptSpan.setAttribute(ATTR.STOP_REASON, response.stopReason);
|
|
34
|
+
if (response.latencyMs != null) {
|
|
35
|
+
attemptSpan.setAttribute(ATTR.LATENCY_MS, response.latencyMs);
|
|
36
|
+
}
|
|
37
|
+
attemptSpan.end();
|
|
38
|
+
return response;
|
|
20
39
|
}
|
|
21
40
|
catch (err) {
|
|
22
41
|
lastError = err;
|
|
42
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
43
|
+
attemptSpan.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
44
|
+
if (err instanceof Error)
|
|
45
|
+
attemptSpan.recordException(err);
|
|
46
|
+
attemptSpan.end();
|
|
23
47
|
if (!isRetryableError(err) || model === models[models.length - 1]) {
|
|
24
48
|
throw err;
|
|
25
49
|
}
|
|
50
|
+
// Record fallback span for the transition to the next model
|
|
51
|
+
const nextModel = models[idx + 1];
|
|
52
|
+
const fallbackSpan = tracer.startSpan("llm.fallback", {}, context.active());
|
|
53
|
+
fallbackSpan.setAttribute(ATTR.FALLBACK_ORIGINAL, model);
|
|
54
|
+
fallbackSpan.setAttribute(ATTR.FALLBACK_MODEL, nextModel);
|
|
55
|
+
fallbackSpan.setAttribute(ATTR.FALLBACK_ERROR, errMsg);
|
|
56
|
+
fallbackSpan.setStatus({ code: SpanStatusCode.ERROR, message: errMsg });
|
|
57
|
+
fallbackSpan.end();
|
|
26
58
|
log.warn({ err, model }, "model failed, trying next");
|
|
27
59
|
}
|
|
28
60
|
}
|
package/dist/llm/openai.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare class OpenAIProvider implements LLMProvider {
|
|
|
7
7
|
complete(model: string, messages: Message[], options?: CompleteOptions): Promise<LLMResponse>;
|
|
8
8
|
stream(model: string, messages: Message[], options?: CompleteOptions): AsyncIterable<StreamEvent>;
|
|
9
9
|
private toOpenAIMessages;
|
|
10
|
+
private mapImageBlock;
|
|
11
|
+
private mapUserContentToOpenAI;
|
|
10
12
|
private fromOpenAIResponse;
|
|
11
13
|
private mapStopReason;
|
|
12
14
|
}
|
package/dist/llm/openai.js
CHANGED
|
@@ -30,7 +30,7 @@ export class OpenAIProvider {
|
|
|
30
30
|
result.push({ role: "system", content: systemPrompt });
|
|
31
31
|
for (const msg of messages) {
|
|
32
32
|
if (msg.role === "user") {
|
|
33
|
-
result.push({ role: "user", content: msg.content });
|
|
33
|
+
result.push({ role: "user", content: this.mapUserContentToOpenAI(msg.content) });
|
|
34
34
|
}
|
|
35
35
|
else if (msg.role === "assistant") {
|
|
36
36
|
const textParts = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
@@ -45,11 +45,42 @@ export class OpenAIProvider {
|
|
|
45
45
|
result.push(openaiMsg);
|
|
46
46
|
}
|
|
47
47
|
else if (msg.role === "tool_result") {
|
|
48
|
-
|
|
48
|
+
if (typeof msg.content === "string") {
|
|
49
|
+
result.push({ role: "tool", tool_call_id: msg.tool_call_id, content: msg.content });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const textParts = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
53
|
+
const imageBlocks = msg.content.filter((b) => b.type === "image");
|
|
54
|
+
result.push({ role: "tool", tool_call_id: msg.tool_call_id, content: textParts });
|
|
55
|
+
if (imageBlocks.length) {
|
|
56
|
+
result.push({
|
|
57
|
+
role: "user",
|
|
58
|
+
content: [
|
|
59
|
+
{ type: "text", text: "Image(s) from tool result:" },
|
|
60
|
+
...imageBlocks.map((b) => this.mapImageBlock(b)),
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
return result;
|
|
52
68
|
}
|
|
69
|
+
mapImageBlock(block) {
|
|
70
|
+
if (block.source.type === "base64") {
|
|
71
|
+
return { type: "image_url", image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` } };
|
|
72
|
+
}
|
|
73
|
+
return { type: "image_url", image_url: { url: block.source.url } };
|
|
74
|
+
}
|
|
75
|
+
mapUserContentToOpenAI(content) {
|
|
76
|
+
if (typeof content === "string")
|
|
77
|
+
return content;
|
|
78
|
+
return content.map((block) => {
|
|
79
|
+
if (block.type === "text")
|
|
80
|
+
return { type: "text", text: block.text };
|
|
81
|
+
return this.mapImageBlock(block);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
53
84
|
fromOpenAIResponse(response) {
|
|
54
85
|
const choice = response.choices[0];
|
|
55
86
|
const content = [];
|
package/dist/llm/types.d.ts
CHANGED
|
@@ -8,10 +8,22 @@ export interface ToolCallBlock {
|
|
|
8
8
|
name: string;
|
|
9
9
|
arguments: Record<string, unknown>;
|
|
10
10
|
}
|
|
11
|
+
export interface ImageBlock {
|
|
12
|
+
type: "image";
|
|
13
|
+
source: {
|
|
14
|
+
type: "base64";
|
|
15
|
+
media_type: string;
|
|
16
|
+
data: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "url";
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export type UserContentBlock = TextBlock | ImageBlock;
|
|
11
23
|
export type ContentBlock = TextBlock | ToolCallBlock;
|
|
12
24
|
export interface UserMessage {
|
|
13
25
|
role: "user";
|
|
14
|
-
content: string;
|
|
26
|
+
content: string | UserContentBlock[];
|
|
15
27
|
}
|
|
16
28
|
export interface AssistantMessage {
|
|
17
29
|
role: "assistant";
|
|
@@ -20,7 +32,7 @@ export interface AssistantMessage {
|
|
|
20
32
|
export interface ToolResultMessage {
|
|
21
33
|
role: "tool_result";
|
|
22
34
|
tool_call_id: string;
|
|
23
|
-
content: string;
|
|
35
|
+
content: string | UserContentBlock[];
|
|
24
36
|
}
|
|
25
37
|
export type Message = UserMessage | AssistantMessage | ToolResultMessage;
|
|
26
38
|
export interface ToolDefinition {
|
|
@@ -71,3 +83,5 @@ export interface ErrorEvent {
|
|
|
71
83
|
}
|
|
72
84
|
export type StreamEvent = TextDeltaEvent | ToolCallStartEvent | ToolCallDeltaEvent | DoneEvent | ErrorEvent;
|
|
73
85
|
export declare function extractText(content: ContentBlock[]): string;
|
|
86
|
+
/** Extract text from user content (string or UserContentBlock[]) */
|
|
87
|
+
export declare function extractUserText(content: string | UserContentBlock[]): string;
|
package/dist/llm/types.js
CHANGED
|
@@ -4,3 +4,12 @@ export function extractText(content) {
|
|
|
4
4
|
.map((b) => b.text)
|
|
5
5
|
.join("");
|
|
6
6
|
}
|
|
7
|
+
/** Extract text from user content (string or UserContentBlock[]) */
|
|
8
|
+
export function extractUserText(content) {
|
|
9
|
+
if (typeof content === "string")
|
|
10
|
+
return content;
|
|
11
|
+
return content
|
|
12
|
+
.filter((b) => b.type === "text")
|
|
13
|
+
.map((b) => b.text)
|
|
14
|
+
.join("");
|
|
15
|
+
}
|
package/dist/logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pino from "pino";
|
|
2
|
+
import { trace } from "@opentelemetry/api";
|
|
2
3
|
const root = pino({
|
|
3
4
|
level: process.env.LOG_LEVEL ?? "info",
|
|
4
5
|
transport: {
|
|
@@ -9,6 +10,13 @@ const root = pino({
|
|
|
9
10
|
colorize: true,
|
|
10
11
|
},
|
|
11
12
|
},
|
|
13
|
+
mixin() {
|
|
14
|
+
const span = trace.getActiveSpan();
|
|
15
|
+
if (!span)
|
|
16
|
+
return {};
|
|
17
|
+
const ctx = span.spanContext();
|
|
18
|
+
return { trace_id: ctx.traceId, span_id: ctx.spanId };
|
|
19
|
+
},
|
|
12
20
|
});
|
|
13
21
|
export function createLogger(name) {
|
|
14
22
|
return root.child({ component: name });
|