@alejandroroman/agent-kit 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/loop.js +213 -111
- package/dist/agent/types.d.ts +2 -0
- 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/setup-agent/index.js +61 -18
- 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.d.ts +1 -0
- package/dist/logger.js +11 -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 +10 -2
package/dist/skills/activate.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { loadSkillManifest } from "./loader.js";
|
|
4
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
5
|
+
import { getTracer, ATTR } from "../telemetry/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Auto-activate all skills upfront so the LLM doesn't need to call activate_skill.
|
|
8
|
+
* Returns the skillsIndex string to append to the system prompt.
|
|
9
|
+
*/
|
|
10
|
+
export async function preActivateSkills(ctx, activateTool, logger) {
|
|
11
|
+
for (const manifest of ctx.manifests) {
|
|
12
|
+
try {
|
|
13
|
+
const result = await activateTool.execute({ skill_name: manifest.name });
|
|
14
|
+
if (typeof result === "string" && result.startsWith("Error")) {
|
|
15
|
+
logger?.error({ skill: manifest.name, result }, "failed to auto-activate skill");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
logger?.error({ err, skill: manifest.name }, "skill auto-activation threw");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const activatedNames = [...ctx.activatedSkills];
|
|
23
|
+
if (activatedNames.length > 0) {
|
|
24
|
+
return "\n\nThe following skills are pre-activated: "
|
|
25
|
+
+ activatedNames.join(", ")
|
|
26
|
+
+ ".\nTheir tools are already available — you do not need to call activate_skill.";
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
4
30
|
export function createActivateSkillTool(ctx) {
|
|
5
31
|
return {
|
|
6
32
|
name: "activate_skill",
|
|
@@ -24,39 +50,58 @@ export function createActivateSkillTool(ctx) {
|
|
|
24
50
|
if (!manifest) {
|
|
25
51
|
return `Skill "${skillName}" is not available. Available skills: ${ctx.manifests.map((m) => m.name).join(", ")}`;
|
|
26
52
|
}
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
const tracer = getTracer("skills");
|
|
54
|
+
const span = tracer.startSpan("skill.activate", {
|
|
55
|
+
attributes: {
|
|
56
|
+
[ATTR.SKILL_NAME]: skillName,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
const skillDir = path.join(ctx.skillsDir, skillName);
|
|
61
|
+
const fullManifest = loadSkillManifest(skillDir);
|
|
62
|
+
const loadedTools = [];
|
|
63
|
+
// Load prompt fragment
|
|
64
|
+
if (fullManifest.prompt) {
|
|
65
|
+
const promptPath = path.join(skillDir, fullManifest.prompt);
|
|
66
|
+
if (fs.existsSync(promptPath)) {
|
|
67
|
+
ctx.promptFragments.push(fs.readFileSync(promptPath, "utf-8"));
|
|
68
|
+
}
|
|
35
69
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
70
|
+
// Load tools via dynamic import
|
|
71
|
+
if (fullManifest.tools) {
|
|
72
|
+
for (const toolName of fullManifest.tools) {
|
|
73
|
+
const toolPath = path.join(skillDir, "tools", `${toolName}.ts`);
|
|
74
|
+
if (fs.existsSync(toolPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const mod = await import(toolPath);
|
|
77
|
+
const tool = mod.default;
|
|
78
|
+
ctx.toolRegistry.register(tool);
|
|
79
|
+
loadedTools.push(tool.name);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
|
|
83
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
84
|
+
return `Error loading tool "${toolName}" from skill "${skillName}": ${err instanceof Error ? err.message : String(err)}`;
|
|
85
|
+
}
|
|
50
86
|
}
|
|
51
87
|
}
|
|
52
88
|
}
|
|
89
|
+
ctx.activatedSkills.add(skillName);
|
|
90
|
+
span.setAttribute(ATTR.SKILL_TOOLS_LOADED, loadedTools.length);
|
|
91
|
+
const parts = [`Skill "${skillName}" activated.`];
|
|
92
|
+
if (loadedTools.length > 0) {
|
|
93
|
+
parts.push(`Tools now available: ${loadedTools.join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
return parts.join(" ");
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
|
|
99
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
100
|
+
throw err;
|
|
53
101
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (loadedTools.length > 0) {
|
|
57
|
-
parts.push(`Tools now available: ${loadedTools.join(", ")}`);
|
|
102
|
+
finally {
|
|
103
|
+
span.end();
|
|
58
104
|
}
|
|
59
|
-
return parts.join(" ");
|
|
60
105
|
},
|
|
61
106
|
};
|
|
62
107
|
}
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { SkillManifestSchema, type SkillManifest } from "./schema.js";
|
|
2
2
|
export { loadSkillManifest, loadAllSkillManifests } from "./loader.js";
|
|
3
|
-
export { createActivateSkillTool, type SkillActivationContext } from "./activate.js";
|
|
3
|
+
export { createActivateSkillTool, preActivateSkills, type SkillActivationContext } from "./activate.js";
|
package/dist/skills/index.js
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface SpanRow {
|
|
2
|
+
trace_id: string;
|
|
3
|
+
span_id: string;
|
|
4
|
+
parent_span_id?: string | null;
|
|
5
|
+
name: string;
|
|
6
|
+
kind: string;
|
|
7
|
+
status: string;
|
|
8
|
+
agent?: string | null;
|
|
9
|
+
source?: string | null;
|
|
10
|
+
start_time: number;
|
|
11
|
+
end_time: number;
|
|
12
|
+
duration_ms: number;
|
|
13
|
+
attributes: string;
|
|
14
|
+
events: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ErrorRow {
|
|
17
|
+
trace_id: string;
|
|
18
|
+
span_id: string;
|
|
19
|
+
fingerprint: string;
|
|
20
|
+
error_type?: string | null;
|
|
21
|
+
message: string;
|
|
22
|
+
stack?: string | null;
|
|
23
|
+
agent?: string | null;
|
|
24
|
+
source?: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface EventRow {
|
|
27
|
+
trace_id?: string | null;
|
|
28
|
+
span_id?: string | null;
|
|
29
|
+
name: string;
|
|
30
|
+
agent?: string | null;
|
|
31
|
+
body?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ListTracesOptions {
|
|
34
|
+
agent?: string;
|
|
35
|
+
source?: string;
|
|
36
|
+
status?: string;
|
|
37
|
+
since?: string;
|
|
38
|
+
limit?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface RecentErrorsOptions {
|
|
41
|
+
agent?: string;
|
|
42
|
+
source?: string;
|
|
43
|
+
since?: string;
|
|
44
|
+
limit?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ErrorStatsOptions {
|
|
47
|
+
since?: string;
|
|
48
|
+
groupBy?: "fingerprint" | "agent";
|
|
49
|
+
}
|
|
50
|
+
export declare class TelemetryDb {
|
|
51
|
+
private db;
|
|
52
|
+
constructor(dbPath: string);
|
|
53
|
+
insertSpan(span: SpanRow): void;
|
|
54
|
+
insertError(error: ErrorRow): void;
|
|
55
|
+
insertEvent(event: EventRow): void;
|
|
56
|
+
getTraceSpans(traceId: string): any[];
|
|
57
|
+
listTraces(options: ListTracesOptions): any[];
|
|
58
|
+
getRecentErrors(options: RecentErrorsOptions): any[];
|
|
59
|
+
private static readonly ALLOWED_GROUP_BY;
|
|
60
|
+
getErrorStats(options: ErrorStatsOptions): any[];
|
|
61
|
+
cleanup(retentionDays: number, vacuum?: boolean): void;
|
|
62
|
+
close(): void;
|
|
63
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
const SCHEMA_SQL = `
|
|
3
|
+
CREATE TABLE IF NOT EXISTS spans (
|
|
4
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5
|
+
trace_id TEXT NOT NULL,
|
|
6
|
+
span_id TEXT NOT NULL,
|
|
7
|
+
parent_span_id TEXT,
|
|
8
|
+
name TEXT NOT NULL,
|
|
9
|
+
kind TEXT NOT NULL DEFAULT 'internal',
|
|
10
|
+
status TEXT NOT NULL DEFAULT 'unset',
|
|
11
|
+
agent TEXT,
|
|
12
|
+
source TEXT,
|
|
13
|
+
start_time INTEGER NOT NULL,
|
|
14
|
+
end_time INTEGER NOT NULL,
|
|
15
|
+
duration_ms REAL NOT NULL,
|
|
16
|
+
attributes TEXT NOT NULL DEFAULT '{}',
|
|
17
|
+
events TEXT NOT NULL DEFAULT '[]',
|
|
18
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
19
|
+
UNIQUE(trace_id, span_id)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS errors (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
trace_id TEXT NOT NULL,
|
|
25
|
+
span_id TEXT NOT NULL,
|
|
26
|
+
fingerprint TEXT NOT NULL,
|
|
27
|
+
error_type TEXT,
|
|
28
|
+
message TEXT NOT NULL,
|
|
29
|
+
stack TEXT,
|
|
30
|
+
agent TEXT,
|
|
31
|
+
source TEXT,
|
|
32
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
33
|
+
FOREIGN KEY (trace_id, span_id) REFERENCES spans(trace_id, span_id) ON DELETE CASCADE
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
trace_id TEXT,
|
|
39
|
+
span_id TEXT,
|
|
40
|
+
name TEXT NOT NULL,
|
|
41
|
+
agent TEXT,
|
|
42
|
+
body TEXT NOT NULL DEFAULT '{}',
|
|
43
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_spans_trace ON spans(trace_id);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_spans_parent ON spans(parent_span_id);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_spans_name_time ON spans(name, start_time);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_spans_agent_time ON spans(agent, created_at);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_spans_source_time ON spans(source, created_at);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_spans_status ON spans(status, created_at);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_errors_agent_time ON errors(agent, created_at);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_errors_fingerprint ON errors(fingerprint, created_at);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_errors_trace ON errors(trace_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_events_time ON events(timestamp);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_events_agent ON events(agent, timestamp);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_events_span ON events(span_id);
|
|
58
|
+
`;
|
|
59
|
+
export class TelemetryDb {
|
|
60
|
+
db;
|
|
61
|
+
constructor(dbPath) {
|
|
62
|
+
this.db = new Database(dbPath);
|
|
63
|
+
this.db.pragma("journal_mode = WAL");
|
|
64
|
+
this.db.pragma("foreign_keys = ON");
|
|
65
|
+
this.db.exec(SCHEMA_SQL);
|
|
66
|
+
}
|
|
67
|
+
insertSpan(span) {
|
|
68
|
+
this.db
|
|
69
|
+
.prepare(`INSERT INTO spans (trace_id, span_id, parent_span_id, name, kind, status, agent, source, start_time, end_time, duration_ms, attributes, events)
|
|
70
|
+
VALUES (@trace_id, @span_id, @parent_span_id, @name, @kind, @status, @agent, @source, @start_time, @end_time, @duration_ms, @attributes, @events)`)
|
|
71
|
+
.run({
|
|
72
|
+
trace_id: span.trace_id,
|
|
73
|
+
span_id: span.span_id,
|
|
74
|
+
parent_span_id: span.parent_span_id ?? null,
|
|
75
|
+
name: span.name,
|
|
76
|
+
kind: span.kind,
|
|
77
|
+
status: span.status,
|
|
78
|
+
agent: span.agent ?? null,
|
|
79
|
+
source: span.source ?? null,
|
|
80
|
+
start_time: span.start_time,
|
|
81
|
+
end_time: span.end_time,
|
|
82
|
+
duration_ms: span.duration_ms,
|
|
83
|
+
attributes: span.attributes,
|
|
84
|
+
events: span.events,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
insertError(error) {
|
|
88
|
+
this.db
|
|
89
|
+
.prepare(`INSERT INTO errors (trace_id, span_id, fingerprint, error_type, message, stack, agent, source)
|
|
90
|
+
VALUES (@trace_id, @span_id, @fingerprint, @error_type, @message, @stack, @agent, @source)`)
|
|
91
|
+
.run({
|
|
92
|
+
trace_id: error.trace_id,
|
|
93
|
+
span_id: error.span_id,
|
|
94
|
+
fingerprint: error.fingerprint,
|
|
95
|
+
error_type: error.error_type ?? null,
|
|
96
|
+
message: error.message,
|
|
97
|
+
stack: error.stack ?? null,
|
|
98
|
+
agent: error.agent ?? null,
|
|
99
|
+
source: error.source ?? null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
insertEvent(event) {
|
|
103
|
+
this.db
|
|
104
|
+
.prepare(`INSERT INTO events (trace_id, span_id, name, agent, body)
|
|
105
|
+
VALUES (@trace_id, @span_id, @name, @agent, @body)`)
|
|
106
|
+
.run({
|
|
107
|
+
trace_id: event.trace_id ?? null,
|
|
108
|
+
span_id: event.span_id ?? null,
|
|
109
|
+
name: event.name,
|
|
110
|
+
agent: event.agent ?? null,
|
|
111
|
+
body: event.body ?? "{}",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
getTraceSpans(traceId) {
|
|
115
|
+
return this.db
|
|
116
|
+
.prepare("SELECT * FROM spans WHERE trace_id = ? ORDER BY start_time")
|
|
117
|
+
.all(traceId);
|
|
118
|
+
}
|
|
119
|
+
listTraces(options) {
|
|
120
|
+
const conditions = ["parent_span_id IS NULL"];
|
|
121
|
+
const params = [];
|
|
122
|
+
if (options.agent) {
|
|
123
|
+
conditions.push("agent = ?");
|
|
124
|
+
params.push(options.agent);
|
|
125
|
+
}
|
|
126
|
+
if (options.source) {
|
|
127
|
+
conditions.push("source = ?");
|
|
128
|
+
params.push(options.source);
|
|
129
|
+
}
|
|
130
|
+
if (options.status) {
|
|
131
|
+
conditions.push("status = ?");
|
|
132
|
+
params.push(options.status);
|
|
133
|
+
}
|
|
134
|
+
if (options.since) {
|
|
135
|
+
conditions.push("created_at >= ?");
|
|
136
|
+
params.push(options.since);
|
|
137
|
+
}
|
|
138
|
+
const limit = options.limit ?? 100;
|
|
139
|
+
const sql = `SELECT * FROM spans WHERE ${conditions.join(" AND ")} ORDER BY start_time DESC LIMIT ?`;
|
|
140
|
+
params.push(limit);
|
|
141
|
+
return this.db.prepare(sql).all(...params);
|
|
142
|
+
}
|
|
143
|
+
getRecentErrors(options) {
|
|
144
|
+
const conditions = [];
|
|
145
|
+
const params = [];
|
|
146
|
+
if (options.agent) {
|
|
147
|
+
conditions.push("agent = ?");
|
|
148
|
+
params.push(options.agent);
|
|
149
|
+
}
|
|
150
|
+
if (options.source) {
|
|
151
|
+
conditions.push("source = ?");
|
|
152
|
+
params.push(options.source);
|
|
153
|
+
}
|
|
154
|
+
if (options.since) {
|
|
155
|
+
conditions.push("created_at >= ?");
|
|
156
|
+
params.push(options.since);
|
|
157
|
+
}
|
|
158
|
+
const limit = options.limit ?? 100;
|
|
159
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
160
|
+
const sql = `SELECT * FROM errors ${where} ORDER BY created_at DESC LIMIT ?`;
|
|
161
|
+
params.push(limit);
|
|
162
|
+
return this.db.prepare(sql).all(...params);
|
|
163
|
+
}
|
|
164
|
+
static ALLOWED_GROUP_BY = new Set(["fingerprint", "agent"]);
|
|
165
|
+
getErrorStats(options) {
|
|
166
|
+
const conditions = [];
|
|
167
|
+
const params = [];
|
|
168
|
+
if (options.since) {
|
|
169
|
+
conditions.push("created_at >= ?");
|
|
170
|
+
params.push(options.since);
|
|
171
|
+
}
|
|
172
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
173
|
+
const groupBy = options.groupBy ?? "fingerprint";
|
|
174
|
+
if (!TelemetryDb.ALLOWED_GROUP_BY.has(groupBy)) {
|
|
175
|
+
throw new Error(`Invalid groupBy: ${groupBy}`);
|
|
176
|
+
}
|
|
177
|
+
const sql = `SELECT ${groupBy}, COUNT(*) as count FROM errors ${where} GROUP BY ${groupBy} ORDER BY count DESC`;
|
|
178
|
+
return this.db.prepare(sql).all(...params);
|
|
179
|
+
}
|
|
180
|
+
cleanup(retentionDays, vacuum = false) {
|
|
181
|
+
const cutoff = `-${retentionDays} days`;
|
|
182
|
+
this.db.transaction(() => {
|
|
183
|
+
this.db.prepare("DELETE FROM events WHERE timestamp < datetime('now', ?)").run(cutoff);
|
|
184
|
+
this.db.prepare("DELETE FROM spans WHERE created_at < datetime('now', ?)").run(cutoff);
|
|
185
|
+
})();
|
|
186
|
+
if (vacuum) {
|
|
187
|
+
this.db.exec("VACUUM");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
close() {
|
|
191
|
+
this.db.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Tracer } from "@opentelemetry/api";
|
|
2
|
+
import { TelemetryDb } from "./db.js";
|
|
3
|
+
export interface TelemetryConfig {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
serviceName?: string;
|
|
6
|
+
otlpEndpoint?: string;
|
|
7
|
+
sentryDsn?: string;
|
|
8
|
+
dbPath?: string;
|
|
9
|
+
sampleRate?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Get the TelemetryDb instance (available after initTelemetry with a dbPath) */
|
|
12
|
+
export declare function getTelemetryDb(): TelemetryDb | undefined;
|
|
13
|
+
export declare function initTelemetry(config?: TelemetryConfig): () => Promise<void>;
|
|
14
|
+
export declare function getTracer(name: string): Tracer;
|
|
15
|
+
export { ATTR } from "./types.js";
|
|
16
|
+
export { TelemetryDb } from "./db.js";
|
|
17
|
+
export { SQLiteSpanProcessor } from "./sqlite-processor.js";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import { trace } from "@opentelemetry/api";
|
|
4
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
5
|
+
import { SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
6
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
7
|
+
import * as Sentry from "@sentry/node";
|
|
8
|
+
import { createLogger } from "../logger.js";
|
|
9
|
+
import { TelemetryDb } from "./db.js";
|
|
10
|
+
import { SQLiteSpanProcessor } from "./sqlite-processor.js";
|
|
11
|
+
const log = createLogger("telemetry");
|
|
12
|
+
const DEFAULT_DB_PATH = path.join(os.homedir(), ".agent-kit", "telemetry.db");
|
|
13
|
+
let sdk;
|
|
14
|
+
let telemetryDb;
|
|
15
|
+
/** Get the TelemetryDb instance (available after initTelemetry with a dbPath) */
|
|
16
|
+
export function getTelemetryDb() {
|
|
17
|
+
return telemetryDb;
|
|
18
|
+
}
|
|
19
|
+
export function initTelemetry(config = {}) {
|
|
20
|
+
const { enabled = process.env.OTEL_ENABLED !== "false", serviceName = process.env.OTEL_SERVICE_NAME ?? "agent-kit", otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT, sentryDsn, dbPath = DEFAULT_DB_PATH, sampleRate, } = config;
|
|
21
|
+
if (!enabled) {
|
|
22
|
+
log.info("telemetry disabled");
|
|
23
|
+
return async () => { };
|
|
24
|
+
}
|
|
25
|
+
// Guard against double initialization
|
|
26
|
+
if (sdk) {
|
|
27
|
+
log.warn("telemetry already initialized, shutting down previous instance");
|
|
28
|
+
sdk.shutdown().catch(() => { });
|
|
29
|
+
sdk = undefined;
|
|
30
|
+
telemetryDb = undefined;
|
|
31
|
+
}
|
|
32
|
+
const spanProcessors = [];
|
|
33
|
+
// SQLite span processor (local persistence)
|
|
34
|
+
if (dbPath) {
|
|
35
|
+
try {
|
|
36
|
+
telemetryDb = new TelemetryDb(dbPath);
|
|
37
|
+
spanProcessors.push(new SQLiteSpanProcessor(telemetryDb));
|
|
38
|
+
log.info({ dbPath }, "SQLite span processor enabled");
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
log.error({ err, dbPath }, "failed to initialize SQLite span processor");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// OTLP exporter (optional — for Grafana/Jaeger)
|
|
45
|
+
if (otlpEndpoint) {
|
|
46
|
+
spanProcessors.push(new SimpleSpanProcessor(new OTLPTraceExporter({ url: otlpEndpoint })));
|
|
47
|
+
log.info({ endpoint: otlpEndpoint }, "OTLP exporter enabled");
|
|
48
|
+
}
|
|
49
|
+
// Console exporter for dev (when no OTLP endpoint)
|
|
50
|
+
if (!otlpEndpoint && process.env.OTEL_DEBUG === "true") {
|
|
51
|
+
spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
|
|
52
|
+
}
|
|
53
|
+
// Sentry error tracking (optional)
|
|
54
|
+
if (sentryDsn) {
|
|
55
|
+
Sentry.init({
|
|
56
|
+
dsn: sentryDsn,
|
|
57
|
+
tracesSampleRate: sampleRate ?? 1.0,
|
|
58
|
+
release: process.env.npm_package_version,
|
|
59
|
+
environment: process.env.NODE_ENV ?? "development",
|
|
60
|
+
});
|
|
61
|
+
log.info("Sentry initialized");
|
|
62
|
+
}
|
|
63
|
+
sdk = new NodeSDK({
|
|
64
|
+
serviceName,
|
|
65
|
+
spanProcessors,
|
|
66
|
+
});
|
|
67
|
+
sdk.start();
|
|
68
|
+
log.info({ serviceName }, "telemetry initialized");
|
|
69
|
+
return async () => {
|
|
70
|
+
if (sdk) {
|
|
71
|
+
await sdk.shutdown();
|
|
72
|
+
log.info("telemetry shut down");
|
|
73
|
+
}
|
|
74
|
+
await Sentry.close(2000);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function getTracer(name) {
|
|
78
|
+
return trace.getTracer(name);
|
|
79
|
+
}
|
|
80
|
+
export { ATTR } from "./types.js";
|
|
81
|
+
export { TelemetryDb } from "./db.js";
|
|
82
|
+
export { SQLiteSpanProcessor } from "./sqlite-processor.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize span attributes before persisting:
|
|
3
|
+
* 1. Redact values matching known secret patterns
|
|
4
|
+
* 2. Truncate long string values (unless key is in the safe list)
|
|
5
|
+
*/
|
|
6
|
+
export declare function sanitizeAttributes(attrs: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ATTR } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Patterns that match known secret/credential formats.
|
|
4
|
+
* If any pattern matches within a string value, the entire value is redacted.
|
|
5
|
+
*/
|
|
6
|
+
const SECRET_PATTERNS = [
|
|
7
|
+
/sk-[a-zA-Z0-9\-]{20,}/, // OpenAI/Anthropic API keys
|
|
8
|
+
/xoxb-[a-zA-Z0-9\-]+/, // Slack bot tokens
|
|
9
|
+
/xoxp-[a-zA-Z0-9\-]+/, // Slack user tokens
|
|
10
|
+
/xapp-[a-zA-Z0-9\-]+/, // Slack app tokens
|
|
11
|
+
/Bearer\s+[a-zA-Z0-9\-_.]+/, // Bearer tokens
|
|
12
|
+
/https?:\/\/[^@\s]+@[^\s]*sentry\.io[^\s]*/, // Sentry DSNs
|
|
13
|
+
/ghp_[a-zA-Z0-9]{36,}/, // GitHub personal access tokens
|
|
14
|
+
/gho_[a-zA-Z0-9]{36,}/, // GitHub OAuth tokens
|
|
15
|
+
];
|
|
16
|
+
const MAX_STRING_LENGTH = 500;
|
|
17
|
+
/**
|
|
18
|
+
* ATTR keys that are controlled by our code and should never be truncated.
|
|
19
|
+
* They are still checked for secret patterns (in case a user accidentally
|
|
20
|
+
* passes a credential as e.g. an agent name).
|
|
21
|
+
*/
|
|
22
|
+
const SAFE_KEYS = new Set(Object.values(ATTR));
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize span attributes before persisting:
|
|
25
|
+
* 1. Redact values matching known secret patterns
|
|
26
|
+
* 2. Truncate long string values (unless key is in the safe list)
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeAttributes(attrs) {
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
31
|
+
result[key] = sanitizeValue(key, value);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function sanitizeValue(key, value) {
|
|
36
|
+
if (typeof value !== "string")
|
|
37
|
+
return value;
|
|
38
|
+
// Check for secret patterns — always, even for safe keys
|
|
39
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
40
|
+
if (pattern.test(value))
|
|
41
|
+
return "[REDACTED]";
|
|
42
|
+
}
|
|
43
|
+
// Truncate long strings, but skip safe (ATTR) keys
|
|
44
|
+
if (!SAFE_KEYS.has(key) && value.length > MAX_STRING_LENGTH) {
|
|
45
|
+
return value.slice(0, MAX_STRING_LENGTH) + "...[truncated]";
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from "@opentelemetry/api";
|
|
2
|
+
import type { SpanProcessor, Span, ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
import type { TelemetryDb } from "./db.js";
|
|
4
|
+
export declare class SQLiteSpanProcessor implements SpanProcessor {
|
|
5
|
+
private db;
|
|
6
|
+
constructor(db: TelemetryDb);
|
|
7
|
+
onStart(_span: Span, _parentContext: Context): void;
|
|
8
|
+
onEnd(span: ReadableSpan): void;
|
|
9
|
+
shutdown(): Promise<void>;
|
|
10
|
+
forceFlush(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
import { sanitizeAttributes } from "./sanitize.js";
|
|
3
|
+
import { ATTR } from "./types.js";
|
|
4
|
+
const SPAN_KIND_MAP = {
|
|
5
|
+
0: "internal",
|
|
6
|
+
1: "server",
|
|
7
|
+
2: "client",
|
|
8
|
+
3: "producer",
|
|
9
|
+
4: "consumer",
|
|
10
|
+
};
|
|
11
|
+
const STATUS_MAP = {
|
|
12
|
+
[SpanStatusCode.UNSET]: "unset",
|
|
13
|
+
[SpanStatusCode.OK]: "ok",
|
|
14
|
+
[SpanStatusCode.ERROR]: "error",
|
|
15
|
+
};
|
|
16
|
+
/** Convert OTel HrTime [seconds, nanoseconds] to milliseconds */
|
|
17
|
+
function hrToMs(hr) {
|
|
18
|
+
return hr[0] * 1000 + hr[1] / 1e6;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate an error fingerprint from exception info.
|
|
22
|
+
* Prefers: error_type + first file + line from stack trace.
|
|
23
|
+
* Fallback: error_type + message.
|
|
24
|
+
*/
|
|
25
|
+
function generateFingerprint(errorType, message, stack) {
|
|
26
|
+
const type = errorType ?? "Error";
|
|
27
|
+
if (stack) {
|
|
28
|
+
// Try to extract file:line from first stack frame
|
|
29
|
+
// Match patterns like: at Object.<anonymous> (src/agent/loop.ts:42:10)
|
|
30
|
+
// or: at src/agent/loop.ts:42:10
|
|
31
|
+
const frameMatch = stack.match(/(?:at\s+(?:.*?\s+)?(?:\()?)((?:[a-zA-Z]:)?[^:(\s]+):(\d+)/);
|
|
32
|
+
if (frameMatch) {
|
|
33
|
+
const file = frameMatch[1];
|
|
34
|
+
const line = frameMatch[2];
|
|
35
|
+
return `${type}:${file}:${line}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return `${type}:${message}`;
|
|
39
|
+
}
|
|
40
|
+
export class SQLiteSpanProcessor {
|
|
41
|
+
db;
|
|
42
|
+
constructor(db) {
|
|
43
|
+
this.db = db;
|
|
44
|
+
}
|
|
45
|
+
onStart(_span, _parentContext) {
|
|
46
|
+
// no-op
|
|
47
|
+
}
|
|
48
|
+
onEnd(span) {
|
|
49
|
+
const ctx = span.spanContext();
|
|
50
|
+
const traceId = ctx.traceId;
|
|
51
|
+
const spanId = ctx.spanId;
|
|
52
|
+
const parentSpanId = span.parentSpanContext?.spanId ?? null;
|
|
53
|
+
const agent = span.attributes[ATTR.AGENT];
|
|
54
|
+
const source = span.attributes[ATTR.SOURCE];
|
|
55
|
+
const startTime = hrToMs(span.startTime);
|
|
56
|
+
const endTime = hrToMs(span.endTime);
|
|
57
|
+
const durationMs = hrToMs(span.duration);
|
|
58
|
+
const kind = SPAN_KIND_MAP[span.kind] ?? "internal";
|
|
59
|
+
const status = STATUS_MAP[span.status.code] ?? "unset";
|
|
60
|
+
// Serialize all span events for storage
|
|
61
|
+
const serializedEvents = span.events.map((e) => ({
|
|
62
|
+
name: e.name,
|
|
63
|
+
time: hrToMs(e.time),
|
|
64
|
+
attributes: e.attributes ?? {},
|
|
65
|
+
}));
|
|
66
|
+
// Insert span
|
|
67
|
+
this.db.insertSpan({
|
|
68
|
+
trace_id: traceId,
|
|
69
|
+
span_id: spanId,
|
|
70
|
+
parent_span_id: parentSpanId,
|
|
71
|
+
name: span.name,
|
|
72
|
+
kind,
|
|
73
|
+
status,
|
|
74
|
+
agent: agent ?? null,
|
|
75
|
+
source: source ?? null,
|
|
76
|
+
start_time: startTime,
|
|
77
|
+
end_time: endTime,
|
|
78
|
+
duration_ms: durationMs,
|
|
79
|
+
attributes: JSON.stringify(sanitizeAttributes(span.attributes)),
|
|
80
|
+
events: JSON.stringify(serializedEvents),
|
|
81
|
+
});
|
|
82
|
+
// Extract exception events and write to errors table
|
|
83
|
+
for (const event of span.events) {
|
|
84
|
+
if (event.name === "exception") {
|
|
85
|
+
const errorType = event.attributes?.["exception.type"];
|
|
86
|
+
const message = event.attributes?.["exception.message"] ?? "Unknown error";
|
|
87
|
+
const stack = event.attributes?.["exception.stacktrace"];
|
|
88
|
+
const fingerprint = generateFingerprint(errorType, message, stack);
|
|
89
|
+
this.db.insertError({
|
|
90
|
+
trace_id: traceId,
|
|
91
|
+
span_id: spanId,
|
|
92
|
+
fingerprint,
|
|
93
|
+
error_type: errorType ?? null,
|
|
94
|
+
message,
|
|
95
|
+
stack: stack ?? null,
|
|
96
|
+
agent: agent ?? null,
|
|
97
|
+
source: source ?? null,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async shutdown() {
|
|
103
|
+
this.db.close();
|
|
104
|
+
}
|
|
105
|
+
async forceFlush() {
|
|
106
|
+
// Writes are synchronous with better-sqlite3, nothing to flush
|
|
107
|
+
}
|
|
108
|
+
}
|