@timmeck/brain-core 2.36.37 → 2.36.39
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/command-center.html +22 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/index.d.ts +4 -0
- package/dist/llm/index.js +4 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/llm-service.d.ts +21 -0
- package/dist/llm/llm-service.js +45 -0
- package/dist/llm/llm-service.js.map +1 -1
- package/dist/llm/middleware.d.ts +122 -0
- package/dist/llm/middleware.js +185 -0
- package/dist/llm/middleware.js.map +1 -0
- package/dist/llm/structured-output.d.ts +76 -0
- package/dist/llm/structured-output.js +153 -0
- package/dist/llm/structured-output.js.map +1 -0
- package/dist/observability/index.d.ts +2 -0
- package/dist/observability/index.js +2 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/trace-collector.d.ts +111 -0
- package/dist/observability/trace-collector.js +303 -0
- package/dist/observability/trace-collector.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/llm/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured LLM Output — Typed ContentBlocks statt nur `{ text: string }`
|
|
3
|
+
*
|
|
4
|
+
* Inspiriert von LangChain's structured output parsing.
|
|
5
|
+
* Ermöglicht es, LLM-Antworten in typisierte Blöcke zu zerlegen:
|
|
6
|
+
* Text, Reasoning, Tool Calls, Citations, JSON.
|
|
7
|
+
*/
|
|
8
|
+
export interface TextBlock {
|
|
9
|
+
type: 'text';
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ReasoningBlock {
|
|
13
|
+
type: 'reasoning';
|
|
14
|
+
content: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ToolCallBlock {
|
|
17
|
+
type: 'tool_call';
|
|
18
|
+
toolName: string;
|
|
19
|
+
args: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface CitationBlock {
|
|
22
|
+
type: 'citation';
|
|
23
|
+
source: string;
|
|
24
|
+
quote: string;
|
|
25
|
+
}
|
|
26
|
+
export interface JsonBlock {
|
|
27
|
+
type: 'json';
|
|
28
|
+
data: unknown;
|
|
29
|
+
}
|
|
30
|
+
export type ContentBlock = TextBlock | ReasoningBlock | ToolCallBlock | CitationBlock | JsonBlock;
|
|
31
|
+
export interface StructuredLLMResponse {
|
|
32
|
+
/** Full raw text (for backwards-compat) */
|
|
33
|
+
text: string;
|
|
34
|
+
/** Parsed content blocks */
|
|
35
|
+
blocks: ContentBlock[];
|
|
36
|
+
/** Token usage */
|
|
37
|
+
tokensUsed: number;
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
cached: boolean;
|
|
41
|
+
model: string;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
provider: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse raw LLM output into structured ContentBlocks.
|
|
47
|
+
*
|
|
48
|
+
* Erkennt:
|
|
49
|
+
* - <thinking>...</thinking> → ReasoningBlock
|
|
50
|
+
* - <tool_call>{"name":"...","args":{...}}</tool_call> → ToolCallBlock
|
|
51
|
+
* - <citation source="...">...</citation> → CitationBlock
|
|
52
|
+
* - ```json ... ``` → JsonBlock
|
|
53
|
+
* - Alles andere → TextBlock
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseStructuredOutput(raw: string): ContentBlock[];
|
|
56
|
+
/**
|
|
57
|
+
* Extrahiert JSON aus einer LLM-Antwort.
|
|
58
|
+
* Versucht zuerst die gesamte Antwort als JSON zu parsen,
|
|
59
|
+
* dann sucht es nach ```json Code-Blöcken.
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractJson<T = unknown>(raw: string): T | null;
|
|
62
|
+
/**
|
|
63
|
+
* Validate parsed JSON against a simple schema (key presence check).
|
|
64
|
+
* Returns the data if valid, null otherwise.
|
|
65
|
+
*/
|
|
66
|
+
export declare function validateJsonSchema<T = unknown>(data: unknown, requiredKeys: string[]): T | null;
|
|
67
|
+
/** Get all blocks of a specific type */
|
|
68
|
+
export declare function getBlocks<T extends ContentBlock['type']>(blocks: ContentBlock[], type: T): Extract<ContentBlock, {
|
|
69
|
+
type: T;
|
|
70
|
+
}>[];
|
|
71
|
+
/** Get combined text from all TextBlocks */
|
|
72
|
+
export declare function getTextContent(blocks: ContentBlock[]): string;
|
|
73
|
+
/** Get all tool calls */
|
|
74
|
+
export declare function getToolCalls(blocks: ContentBlock[]): ToolCallBlock[];
|
|
75
|
+
/** Check if response contains reasoning */
|
|
76
|
+
export declare function hasReasoning(blocks: ContentBlock[]): boolean;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured LLM Output — Typed ContentBlocks statt nur `{ text: string }`
|
|
3
|
+
*
|
|
4
|
+
* Inspiriert von LangChain's structured output parsing.
|
|
5
|
+
* Ermöglicht es, LLM-Antworten in typisierte Blöcke zu zerlegen:
|
|
6
|
+
* Text, Reasoning, Tool Calls, Citations, JSON.
|
|
7
|
+
*/
|
|
8
|
+
// ── Parsing ─────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Parse raw LLM output into structured ContentBlocks.
|
|
11
|
+
*
|
|
12
|
+
* Erkennt:
|
|
13
|
+
* - <thinking>...</thinking> → ReasoningBlock
|
|
14
|
+
* - <tool_call>{"name":"...","args":{...}}</tool_call> → ToolCallBlock
|
|
15
|
+
* - <citation source="...">...</citation> → CitationBlock
|
|
16
|
+
* - ```json ... ``` → JsonBlock
|
|
17
|
+
* - Alles andere → TextBlock
|
|
18
|
+
*/
|
|
19
|
+
export function parseStructuredOutput(raw) {
|
|
20
|
+
if (!raw || raw.trim().length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
const blocks = [];
|
|
23
|
+
let remaining = raw;
|
|
24
|
+
// Regex patterns for structured sections
|
|
25
|
+
const patterns = [
|
|
26
|
+
{
|
|
27
|
+
// <thinking>...</thinking>
|
|
28
|
+
regex: /<thinking>([\s\S]*?)<\/thinking>/,
|
|
29
|
+
handler: (m) => ({ type: 'reasoning', content: m[1].trim() }),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
// <tool_call>{"name":"x","args":{...}}</tool_call>
|
|
33
|
+
regex: /<tool_call>([\s\S]*?)<\/tool_call>/,
|
|
34
|
+
handler: (m) => {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(m[1].trim());
|
|
37
|
+
return {
|
|
38
|
+
type: 'tool_call',
|
|
39
|
+
toolName: parsed.name ?? parsed.tool ?? 'unknown',
|
|
40
|
+
args: parsed.args ?? parsed.arguments ?? parsed.input ?? {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { type: 'text', content: m[0] };
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
// <citation source="...">...</citation>
|
|
50
|
+
regex: /<citation\s+source="([^"]*)">([\s\S]*?)<\/citation>/,
|
|
51
|
+
handler: (m) => ({ type: 'citation', source: m[1], quote: m[2].trim() }),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
// ```json ... ```
|
|
55
|
+
regex: /```json\s*\n([\s\S]*?)\n```/,
|
|
56
|
+
handler: (m) => {
|
|
57
|
+
try {
|
|
58
|
+
return { type: 'json', data: JSON.parse(m[1].trim()) };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { type: 'text', content: m[0] };
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
while (remaining.length > 0) {
|
|
67
|
+
// Find the earliest matching pattern
|
|
68
|
+
let earliest = null;
|
|
69
|
+
for (const p of patterns) {
|
|
70
|
+
const match = remaining.match(p.regex);
|
|
71
|
+
if (match && match.index !== undefined) {
|
|
72
|
+
if (!earliest || match.index < earliest.index) {
|
|
73
|
+
earliest = { index: match.index, match, handler: p.handler };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!earliest) {
|
|
78
|
+
// No more patterns found — rest is text
|
|
79
|
+
const trimmed = remaining.trim();
|
|
80
|
+
if (trimmed.length > 0) {
|
|
81
|
+
blocks.push({ type: 'text', content: trimmed });
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
// Text before the match
|
|
86
|
+
const before = remaining.slice(0, earliest.index).trim();
|
|
87
|
+
if (before.length > 0) {
|
|
88
|
+
blocks.push({ type: 'text', content: before });
|
|
89
|
+
}
|
|
90
|
+
// The matched block
|
|
91
|
+
blocks.push(earliest.handler(earliest.match));
|
|
92
|
+
// Continue after the match
|
|
93
|
+
remaining = remaining.slice(earliest.index + earliest.match[0].length);
|
|
94
|
+
}
|
|
95
|
+
return blocks;
|
|
96
|
+
}
|
|
97
|
+
// ── JSON Mode Helper ────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Extrahiert JSON aus einer LLM-Antwort.
|
|
100
|
+
* Versucht zuerst die gesamte Antwort als JSON zu parsen,
|
|
101
|
+
* dann sucht es nach ```json Code-Blöcken.
|
|
102
|
+
*/
|
|
103
|
+
export function extractJson(raw) {
|
|
104
|
+
// Try full string
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(raw.trim());
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Try code block
|
|
110
|
+
const match = raw.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
111
|
+
if (match) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(match[1].trim());
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Validate parsed JSON against a simple schema (key presence check).
|
|
124
|
+
* Returns the data if valid, null otherwise.
|
|
125
|
+
*/
|
|
126
|
+
export function validateJsonSchema(data, requiredKeys) {
|
|
127
|
+
if (!data || typeof data !== 'object')
|
|
128
|
+
return null;
|
|
129
|
+
const obj = data;
|
|
130
|
+
for (const key of requiredKeys) {
|
|
131
|
+
if (!(key in obj))
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
// ── Block Helpers ────────────────────────────────────────
|
|
137
|
+
/** Get all blocks of a specific type */
|
|
138
|
+
export function getBlocks(blocks, type) {
|
|
139
|
+
return blocks.filter((b) => b.type === type);
|
|
140
|
+
}
|
|
141
|
+
/** Get combined text from all TextBlocks */
|
|
142
|
+
export function getTextContent(blocks) {
|
|
143
|
+
return getBlocks(blocks, 'text').map(b => b.content).join('\n\n');
|
|
144
|
+
}
|
|
145
|
+
/** Get all tool calls */
|
|
146
|
+
export function getToolCalls(blocks) {
|
|
147
|
+
return getBlocks(blocks, 'tool_call');
|
|
148
|
+
}
|
|
149
|
+
/** Check if response contains reasoning */
|
|
150
|
+
export function hasReasoning(blocks) {
|
|
151
|
+
return blocks.some(b => b.type === 'reasoning');
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=structured-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-output.js","sourceRoot":"","sources":["../../src/llm/structured-output.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace Collector — Observability für das Brain Ecosystem
|
|
3
|
+
*
|
|
4
|
+
* Inspiriert von OpenTelemetry / LangSmith Tracing.
|
|
5
|
+
* Erfasst Spans (Arbeitseinheiten) in hierarchischen Traces,
|
|
6
|
+
* speichert sie in SQLite und berechnet Latenz-Statistiken.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const traceId = collector.startTrace('research-cycle');
|
|
11
|
+
* const spanId = collector.startSpan(traceId, 'llm-call', { template: 'explain' });
|
|
12
|
+
* // ... do work ...
|
|
13
|
+
* collector.endSpan(spanId, { tokens: 150, cost: 0.001 });
|
|
14
|
+
* collector.endTrace(traceId);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import type Database from 'better-sqlite3';
|
|
18
|
+
export interface Trace {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
status: 'running' | 'completed' | 'error';
|
|
22
|
+
startedAt: number;
|
|
23
|
+
endedAt: number | null;
|
|
24
|
+
durationMs: number | null;
|
|
25
|
+
spanCount: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
totalCost: number;
|
|
28
|
+
metadata: Record<string, unknown>;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface Span {
|
|
32
|
+
id: string;
|
|
33
|
+
traceId: string;
|
|
34
|
+
parentSpanId: string | null;
|
|
35
|
+
name: string;
|
|
36
|
+
status: 'running' | 'completed' | 'error';
|
|
37
|
+
startedAt: number;
|
|
38
|
+
endedAt: number | null;
|
|
39
|
+
durationMs: number | null;
|
|
40
|
+
tokens: number;
|
|
41
|
+
cost: number;
|
|
42
|
+
metadata: Record<string, unknown>;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface TraceTree {
|
|
46
|
+
trace: Trace;
|
|
47
|
+
spans: Span[];
|
|
48
|
+
}
|
|
49
|
+
export interface TraceStats {
|
|
50
|
+
totalTraces: number;
|
|
51
|
+
totalSpans: number;
|
|
52
|
+
totalTokens: number;
|
|
53
|
+
totalCost: number;
|
|
54
|
+
avgDurationMs: number;
|
|
55
|
+
p50DurationMs: number;
|
|
56
|
+
p99DurationMs: number;
|
|
57
|
+
tracesByName: Record<string, number>;
|
|
58
|
+
activeTraces: number;
|
|
59
|
+
}
|
|
60
|
+
export interface TraceListOptions {
|
|
61
|
+
limit?: number;
|
|
62
|
+
offset?: number;
|
|
63
|
+
name?: string;
|
|
64
|
+
status?: 'running' | 'completed' | 'error';
|
|
65
|
+
since?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface TraceCollectorStatus {
|
|
68
|
+
totalTraces: number;
|
|
69
|
+
activeTraces: number;
|
|
70
|
+
totalSpans: number;
|
|
71
|
+
totalTokens: number;
|
|
72
|
+
totalCost: number;
|
|
73
|
+
}
|
|
74
|
+
export declare function runTraceMigration(db: Database.Database): void;
|
|
75
|
+
export declare class TraceCollector {
|
|
76
|
+
private db;
|
|
77
|
+
private readonly log;
|
|
78
|
+
private stmtInsertTrace;
|
|
79
|
+
private stmtInsertSpan;
|
|
80
|
+
private stmtEndTrace;
|
|
81
|
+
private stmtEndSpan;
|
|
82
|
+
private stmtUpdateTraceStats;
|
|
83
|
+
constructor(db: Database.Database);
|
|
84
|
+
/** Start a new trace. Returns trace ID. */
|
|
85
|
+
startTrace(name: string, metadata?: Record<string, unknown>): string;
|
|
86
|
+
/** End a trace (complete or error). */
|
|
87
|
+
endTrace(traceId: string, error?: string): void;
|
|
88
|
+
/** Start a span within a trace. Returns span ID. */
|
|
89
|
+
startSpan(traceId: string, name: string, options?: {
|
|
90
|
+
parentSpanId?: string;
|
|
91
|
+
metadata?: Record<string, unknown>;
|
|
92
|
+
}): string;
|
|
93
|
+
/** End a span with optional results. */
|
|
94
|
+
endSpan(spanId: string, result?: {
|
|
95
|
+
tokens?: number;
|
|
96
|
+
cost?: number;
|
|
97
|
+
error?: string;
|
|
98
|
+
}): void;
|
|
99
|
+
/** Get a single trace with all its spans. */
|
|
100
|
+
getTrace(traceId: string): TraceTree | null;
|
|
101
|
+
/** List traces with optional filters. */
|
|
102
|
+
listTraces(options?: TraceListOptions): Trace[];
|
|
103
|
+
/** Get aggregate statistics. */
|
|
104
|
+
getStats(): TraceStats;
|
|
105
|
+
/** Get collector status (lightweight). */
|
|
106
|
+
getStatus(): TraceCollectorStatus;
|
|
107
|
+
/** Delete traces older than maxAgeDays. Returns number pruned. */
|
|
108
|
+
prune(maxAgeDays?: number): number;
|
|
109
|
+
private mapTrace;
|
|
110
|
+
private mapSpan;
|
|
111
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace Collector — Observability für das Brain Ecosystem
|
|
3
|
+
*
|
|
4
|
+
* Inspiriert von OpenTelemetry / LangSmith Tracing.
|
|
5
|
+
* Erfasst Spans (Arbeitseinheiten) in hierarchischen Traces,
|
|
6
|
+
* speichert sie in SQLite und berechnet Latenz-Statistiken.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const traceId = collector.startTrace('research-cycle');
|
|
11
|
+
* const spanId = collector.startSpan(traceId, 'llm-call', { template: 'explain' });
|
|
12
|
+
* // ... do work ...
|
|
13
|
+
* collector.endSpan(spanId, { tokens: 150, cost: 0.001 });
|
|
14
|
+
* collector.endTrace(traceId);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { getLogger } from '../utils/logger.js';
|
|
19
|
+
// ── Migration ───────────────────────────────────────────
|
|
20
|
+
export function runTraceMigration(db) {
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS workflow_traces (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
26
|
+
started_at INTEGER NOT NULL,
|
|
27
|
+
ended_at INTEGER,
|
|
28
|
+
duration_ms INTEGER,
|
|
29
|
+
span_count INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
total_cost REAL NOT NULL DEFAULT 0,
|
|
32
|
+
metadata TEXT DEFAULT '{}',
|
|
33
|
+
error TEXT,
|
|
34
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
35
|
+
);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_traces_name ON workflow_traces(name);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_traces_status ON workflow_traces(status);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_traces_started ON workflow_traces(started_at);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS trace_spans (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
trace_id TEXT NOT NULL,
|
|
43
|
+
parent_span_id TEXT,
|
|
44
|
+
name TEXT NOT NULL,
|
|
45
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
46
|
+
started_at INTEGER NOT NULL,
|
|
47
|
+
ended_at INTEGER,
|
|
48
|
+
duration_ms INTEGER,
|
|
49
|
+
tokens INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
cost REAL NOT NULL DEFAULT 0,
|
|
51
|
+
metadata TEXT DEFAULT '{}',
|
|
52
|
+
error TEXT,
|
|
53
|
+
FOREIGN KEY (trace_id) REFERENCES workflow_traces(id) ON DELETE CASCADE
|
|
54
|
+
);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_spans_trace ON trace_spans(trace_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_spans_parent ON trace_spans(parent_span_id);
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
// ── Collector ───────────────────────────────────────────
|
|
60
|
+
export class TraceCollector {
|
|
61
|
+
db;
|
|
62
|
+
log = getLogger();
|
|
63
|
+
stmtInsertTrace;
|
|
64
|
+
stmtInsertSpan;
|
|
65
|
+
stmtEndTrace;
|
|
66
|
+
stmtEndSpan;
|
|
67
|
+
stmtUpdateTraceStats;
|
|
68
|
+
constructor(db) {
|
|
69
|
+
this.db = db;
|
|
70
|
+
runTraceMigration(db);
|
|
71
|
+
this.stmtInsertTrace = db.prepare(`INSERT INTO workflow_traces (id, name, status, started_at, metadata)
|
|
72
|
+
VALUES (?, ?, 'running', ?, ?)`);
|
|
73
|
+
this.stmtInsertSpan = db.prepare(`INSERT INTO trace_spans (id, trace_id, parent_span_id, name, status, started_at, metadata)
|
|
74
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?)`);
|
|
75
|
+
this.stmtEndTrace = db.prepare(`UPDATE workflow_traces SET status = ?, ended_at = ?, duration_ms = ?, error = ?
|
|
76
|
+
WHERE id = ?`);
|
|
77
|
+
this.stmtEndSpan = db.prepare(`UPDATE trace_spans SET status = ?, ended_at = ?, duration_ms = ?, tokens = ?, cost = ?, error = ?
|
|
78
|
+
WHERE id = ?`);
|
|
79
|
+
this.stmtUpdateTraceStats = db.prepare(`UPDATE workflow_traces SET
|
|
80
|
+
span_count = (SELECT COUNT(*) FROM trace_spans WHERE trace_id = ?),
|
|
81
|
+
total_tokens = (SELECT COALESCE(SUM(tokens), 0) FROM trace_spans WHERE trace_id = ?),
|
|
82
|
+
total_cost = (SELECT COALESCE(SUM(cost), 0) FROM trace_spans WHERE trace_id = ?)
|
|
83
|
+
WHERE id = ?`);
|
|
84
|
+
this.log.debug('[TraceCollector] Initialized');
|
|
85
|
+
}
|
|
86
|
+
// ── Trace Lifecycle ─────────────────────────────────
|
|
87
|
+
/** Start a new trace. Returns trace ID. */
|
|
88
|
+
startTrace(name, metadata = {}) {
|
|
89
|
+
const id = randomUUID();
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
try {
|
|
92
|
+
this.stmtInsertTrace.run(id, name, now, JSON.stringify(metadata));
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
this.log.warn(`[TraceCollector] Failed to start trace: ${e.message}`);
|
|
96
|
+
}
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
/** End a trace (complete or error). */
|
|
100
|
+
endTrace(traceId, error) {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
try {
|
|
103
|
+
const trace = this.db.prepare('SELECT started_at FROM workflow_traces WHERE id = ?').get(traceId);
|
|
104
|
+
const duration = trace ? now - trace.started_at : 0;
|
|
105
|
+
const status = error ? 'error' : 'completed';
|
|
106
|
+
// Update aggregate stats from spans
|
|
107
|
+
this.stmtUpdateTraceStats.run(traceId, traceId, traceId, traceId);
|
|
108
|
+
this.stmtEndTrace.run(status, now, duration, error ?? null, traceId);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
this.log.warn(`[TraceCollector] Failed to end trace: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ── Span Lifecycle ──────────────────────────────────
|
|
115
|
+
/** Start a span within a trace. Returns span ID. */
|
|
116
|
+
startSpan(traceId, name, options) {
|
|
117
|
+
const id = randomUUID();
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
try {
|
|
120
|
+
this.stmtInsertSpan.run(id, traceId, options?.parentSpanId ?? null, name, now, JSON.stringify(options?.metadata ?? {}));
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
this.log.warn(`[TraceCollector] Failed to start span: ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
return id;
|
|
126
|
+
}
|
|
127
|
+
/** End a span with optional results. */
|
|
128
|
+
endSpan(spanId, result) {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
try {
|
|
131
|
+
const span = this.db.prepare('SELECT started_at FROM trace_spans WHERE id = ?').get(spanId);
|
|
132
|
+
const duration = span ? now - span.started_at : 0;
|
|
133
|
+
const status = result?.error ? 'error' : 'completed';
|
|
134
|
+
this.stmtEndSpan.run(status, now, duration, result?.tokens ?? 0, result?.cost ?? 0, result?.error ?? null, spanId);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
this.log.warn(`[TraceCollector] Failed to end span: ${e.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ── Queries ─────────────────────────────────────────
|
|
141
|
+
/** Get a single trace with all its spans. */
|
|
142
|
+
getTrace(traceId) {
|
|
143
|
+
try {
|
|
144
|
+
const row = this.db.prepare('SELECT * FROM workflow_traces WHERE id = ?').get(traceId);
|
|
145
|
+
if (!row)
|
|
146
|
+
return null;
|
|
147
|
+
const spans = this.db.prepare('SELECT * FROM trace_spans WHERE trace_id = ? ORDER BY started_at').all(traceId);
|
|
148
|
+
return {
|
|
149
|
+
trace: this.mapTrace(row),
|
|
150
|
+
spans: spans.map(s => this.mapSpan(s)),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** List traces with optional filters. */
|
|
158
|
+
listTraces(options = {}) {
|
|
159
|
+
const { limit = 50, offset = 0, name, status, since } = options;
|
|
160
|
+
try {
|
|
161
|
+
const conditions = [];
|
|
162
|
+
const params = [];
|
|
163
|
+
if (name) {
|
|
164
|
+
conditions.push('name = ?');
|
|
165
|
+
params.push(name);
|
|
166
|
+
}
|
|
167
|
+
if (status) {
|
|
168
|
+
conditions.push('status = ?');
|
|
169
|
+
params.push(status);
|
|
170
|
+
}
|
|
171
|
+
if (since) {
|
|
172
|
+
conditions.push('started_at >= ?');
|
|
173
|
+
params.push(since);
|
|
174
|
+
}
|
|
175
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
176
|
+
const sql = `SELECT * FROM workflow_traces ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?`;
|
|
177
|
+
params.push(limit, offset);
|
|
178
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
179
|
+
return rows.map(r => this.mapTrace(r));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Get aggregate statistics. */
|
|
186
|
+
getStats() {
|
|
187
|
+
try {
|
|
188
|
+
const totals = this.db.prepare(`
|
|
189
|
+
SELECT
|
|
190
|
+
COUNT(*) as total_traces,
|
|
191
|
+
COALESCE(SUM(span_count), 0) as total_spans,
|
|
192
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
193
|
+
COALESCE(SUM(total_cost), 0) as total_cost,
|
|
194
|
+
COALESCE(AVG(duration_ms), 0) as avg_duration
|
|
195
|
+
FROM workflow_traces
|
|
196
|
+
WHERE status != 'running'
|
|
197
|
+
`).get();
|
|
198
|
+
const active = this.db.prepare("SELECT COUNT(*) as count FROM workflow_traces WHERE status = 'running'").get();
|
|
199
|
+
// P50 / P99 from completed traces
|
|
200
|
+
const durations = this.db.prepare("SELECT duration_ms FROM workflow_traces WHERE status = 'completed' AND duration_ms IS NOT NULL ORDER BY duration_ms").all();
|
|
201
|
+
const p50 = durations.length > 0
|
|
202
|
+
? durations[Math.floor(durations.length * 0.5)]?.duration_ms ?? 0
|
|
203
|
+
: 0;
|
|
204
|
+
const p99 = durations.length > 0
|
|
205
|
+
? durations[Math.floor(durations.length * 0.99)]?.duration_ms ?? 0
|
|
206
|
+
: 0;
|
|
207
|
+
// Traces by name
|
|
208
|
+
const byName = this.db.prepare('SELECT name, COUNT(*) as count FROM workflow_traces GROUP BY name').all();
|
|
209
|
+
const tracesByName = {};
|
|
210
|
+
for (const row of byName) {
|
|
211
|
+
tracesByName[row.name] = row.count;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
totalTraces: totals.total_traces ?? 0,
|
|
215
|
+
totalSpans: totals.total_spans ?? 0,
|
|
216
|
+
totalTokens: totals.total_tokens ?? 0,
|
|
217
|
+
totalCost: totals.total_cost ?? 0,
|
|
218
|
+
avgDurationMs: Math.round(totals.avg_duration ?? 0),
|
|
219
|
+
p50DurationMs: p50,
|
|
220
|
+
p99DurationMs: p99,
|
|
221
|
+
tracesByName,
|
|
222
|
+
activeTraces: active.count,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return {
|
|
227
|
+
totalTraces: 0, totalSpans: 0, totalTokens: 0, totalCost: 0,
|
|
228
|
+
avgDurationMs: 0, p50DurationMs: 0, p99DurationMs: 0,
|
|
229
|
+
tracesByName: {}, activeTraces: 0,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Get collector status (lightweight). */
|
|
234
|
+
getStatus() {
|
|
235
|
+
try {
|
|
236
|
+
const row = this.db.prepare(`
|
|
237
|
+
SELECT
|
|
238
|
+
COUNT(*) as total,
|
|
239
|
+
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as active,
|
|
240
|
+
COALESCE(SUM(span_count), 0) as spans,
|
|
241
|
+
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
242
|
+
COALESCE(SUM(total_cost), 0) as cost
|
|
243
|
+
FROM workflow_traces
|
|
244
|
+
`).get();
|
|
245
|
+
return {
|
|
246
|
+
totalTraces: row.total ?? 0,
|
|
247
|
+
activeTraces: row.active ?? 0,
|
|
248
|
+
totalSpans: row.spans ?? 0,
|
|
249
|
+
totalTokens: row.tokens ?? 0,
|
|
250
|
+
totalCost: row.cost ?? 0,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return { totalTraces: 0, activeTraces: 0, totalSpans: 0, totalTokens: 0, totalCost: 0 };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Delete traces older than maxAgeDays. Returns number pruned. */
|
|
258
|
+
prune(maxAgeDays = 30) {
|
|
259
|
+
try {
|
|
260
|
+
const cutoff = Date.now() - maxAgeDays * 86_400_000;
|
|
261
|
+
// Delete spans first (FK constraint)
|
|
262
|
+
this.db.prepare('DELETE FROM trace_spans WHERE trace_id IN (SELECT id FROM workflow_traces WHERE started_at < ? AND status != ?)').run(cutoff, 'running');
|
|
263
|
+
const result = this.db.prepare('DELETE FROM workflow_traces WHERE started_at < ? AND status != ?').run(cutoff, 'running');
|
|
264
|
+
return result.changes;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ── Private ─────────────────────────────────────────
|
|
271
|
+
mapTrace(row) {
|
|
272
|
+
return {
|
|
273
|
+
id: row.id,
|
|
274
|
+
name: row.name,
|
|
275
|
+
status: row.status,
|
|
276
|
+
startedAt: row.started_at,
|
|
277
|
+
endedAt: row.ended_at,
|
|
278
|
+
durationMs: row.duration_ms,
|
|
279
|
+
spanCount: row.span_count,
|
|
280
|
+
totalTokens: row.total_tokens,
|
|
281
|
+
totalCost: row.total_cost,
|
|
282
|
+
metadata: JSON.parse(row.metadata || '{}'),
|
|
283
|
+
error: row.error,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
mapSpan(row) {
|
|
287
|
+
return {
|
|
288
|
+
id: row.id,
|
|
289
|
+
traceId: row.trace_id,
|
|
290
|
+
parentSpanId: row.parent_span_id,
|
|
291
|
+
name: row.name,
|
|
292
|
+
status: row.status,
|
|
293
|
+
startedAt: row.started_at,
|
|
294
|
+
endedAt: row.ended_at,
|
|
295
|
+
durationMs: row.duration_ms,
|
|
296
|
+
tokens: row.tokens,
|
|
297
|
+
cost: row.cost,
|
|
298
|
+
metadata: JSON.parse(row.metadata || '{}'),
|
|
299
|
+
error: row.error,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=trace-collector.js.map
|