@yeaft/webchat-agent 0.1.399 → 0.1.409
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/crew/role-query.js +10 -6
- package/package.json +3 -1
- package/sdk/query.js +3 -1
- package/unify/cli.js +735 -0
- package/unify/config.js +269 -0
- package/unify/conversation/persist.js +436 -0
- package/unify/conversation/search.js +65 -0
- package/unify/debug-trace.js +398 -0
- package/unify/engine.js +511 -0
- package/unify/index.js +27 -0
- package/unify/init.js +147 -0
- package/unify/llm/adapter.js +186 -0
- package/unify/llm/anthropic.js +322 -0
- package/unify/llm/chat-completions.js +315 -0
- package/unify/memory/consolidate.js +187 -0
- package/unify/memory/extract.js +97 -0
- package/unify/memory/recall.js +243 -0
- package/unify/memory/store.js +507 -0
- package/unify/models.js +167 -0
- package/unify/prompts.js +109 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search.js — Conversation history search
|
|
3
|
+
*
|
|
4
|
+
* Simple keyword search across hot and cold messages.
|
|
5
|
+
* Reference: yeaft-unify-core-systems.md §4.3
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { parseMessage } from './persist.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Search messages in a directory for a keyword.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} dir — messages directory
|
|
16
|
+
* @param {string} keyword — search term (case-insensitive)
|
|
17
|
+
* @param {number} limit — max results
|
|
18
|
+
* @returns {object[]} — matching messages
|
|
19
|
+
*/
|
|
20
|
+
function searchDir(dir, keyword, limit) {
|
|
21
|
+
if (!existsSync(dir)) return [];
|
|
22
|
+
|
|
23
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
24
|
+
const files = readdirSync(dir)
|
|
25
|
+
.filter(f => f.endsWith('.md'))
|
|
26
|
+
.sort()
|
|
27
|
+
.reverse(); // newest first
|
|
28
|
+
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
if (results.length >= limit) break;
|
|
32
|
+
|
|
33
|
+
const raw = readFileSync(join(dir, file), 'utf8');
|
|
34
|
+
if (raw.toLowerCase().includes(lowerKeyword)) {
|
|
35
|
+
const msg = parseMessage(raw);
|
|
36
|
+
if (msg) results.push(msg);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Search conversation history (hot + cold) for a keyword.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} dir — Yeaft root directory (e.g. ~/.yeaft)
|
|
47
|
+
* @param {string} keyword — search term
|
|
48
|
+
* @param {number} [limit=20] — max results
|
|
49
|
+
* @returns {object[]} — matching messages, newest first
|
|
50
|
+
*/
|
|
51
|
+
export function searchMessages(dir, keyword, limit = 20) {
|
|
52
|
+
if (!keyword || !keyword.trim()) return [];
|
|
53
|
+
|
|
54
|
+
const msgDir = join(dir, 'conversation', 'messages');
|
|
55
|
+
const coldDir = join(dir, 'conversation', 'cold');
|
|
56
|
+
|
|
57
|
+
// Search hot first (more recent), then cold
|
|
58
|
+
const hotResults = searchDir(msgDir, keyword, limit);
|
|
59
|
+
const remaining = limit - hotResults.length;
|
|
60
|
+
const coldResults = remaining > 0
|
|
61
|
+
? searchDir(coldDir, keyword, remaining)
|
|
62
|
+
: [];
|
|
63
|
+
|
|
64
|
+
return [...hotResults, ...coldResults];
|
|
65
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* debug-trace.js — SQLite-backed debug trace for Yeaft
|
|
3
|
+
*
|
|
4
|
+
* Records every LLM turn, tool call, and event for debugging and analytics.
|
|
5
|
+
* When disabled, uses NullTrace (same interface, zero overhead).
|
|
6
|
+
*
|
|
7
|
+
* Reference: server/db/connection.js — Database(path), pragma WAL
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Database from 'better-sqlite3';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { statSync } from 'fs';
|
|
13
|
+
|
|
14
|
+
/** Schema DDL — 3 tables + indexes */
|
|
15
|
+
const SCHEMA = `
|
|
16
|
+
CREATE TABLE IF NOT EXISTS trace_turns (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
trace_id TEXT NOT NULL,
|
|
19
|
+
message_id TEXT,
|
|
20
|
+
mode TEXT,
|
|
21
|
+
turn_number INTEGER,
|
|
22
|
+
model TEXT,
|
|
23
|
+
input_tokens INTEGER,
|
|
24
|
+
output_tokens INTEGER,
|
|
25
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
26
|
+
cache_write_tokens INTEGER DEFAULT 0,
|
|
27
|
+
stop_reason TEXT,
|
|
28
|
+
latency_ms INTEGER,
|
|
29
|
+
response_text TEXT,
|
|
30
|
+
started_at INTEGER NOT NULL,
|
|
31
|
+
ended_at INTEGER
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS trace_tools (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
turn_id TEXT NOT NULL,
|
|
37
|
+
tool_name TEXT NOT NULL,
|
|
38
|
+
tool_input TEXT,
|
|
39
|
+
tool_output TEXT,
|
|
40
|
+
duration_ms INTEGER,
|
|
41
|
+
is_error INTEGER DEFAULT 0,
|
|
42
|
+
created_at INTEGER NOT NULL,
|
|
43
|
+
FOREIGN KEY (turn_id) REFERENCES trace_turns(id)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS trace_events (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
trace_id TEXT NOT NULL,
|
|
49
|
+
event_type TEXT NOT NULL,
|
|
50
|
+
event_data TEXT,
|
|
51
|
+
created_at INTEGER NOT NULL
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_turns_trace_id ON trace_turns(trace_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_turns_message_id ON trace_turns(message_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_turns_started_at ON trace_turns(started_at);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_turns_model ON trace_turns(model);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_tools_turn_id ON trace_tools(turn_id);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_tools_name ON trace_tools(tool_name);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_events_trace_id ON trace_events(trace_id);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON trace_events(event_type);
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
/** Max tool_output size stored (10KB). Longer outputs are truncated. */
|
|
65
|
+
const MAX_TOOL_OUTPUT = 10240;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Truncate a string to a max length, appending "... [truncated]" if needed.
|
|
69
|
+
* @param {string|null|undefined} str
|
|
70
|
+
* @param {number} max
|
|
71
|
+
* @returns {string|null}
|
|
72
|
+
*/
|
|
73
|
+
function truncate(str, max) {
|
|
74
|
+
if (!str) return str ?? null;
|
|
75
|
+
if (str.length <= max) return str;
|
|
76
|
+
return str.slice(0, max) + '... [truncated]';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* DebugTrace — SQLite-backed debug trace.
|
|
81
|
+
*/
|
|
82
|
+
export class DebugTrace {
|
|
83
|
+
/** @type {Database.Database} */
|
|
84
|
+
#db;
|
|
85
|
+
|
|
86
|
+
/** @type {string} */
|
|
87
|
+
#dbPath;
|
|
88
|
+
|
|
89
|
+
// Prepared statements (created lazily)
|
|
90
|
+
#stmts = {};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} dbPath — Path to the SQLite database file.
|
|
94
|
+
*/
|
|
95
|
+
constructor(dbPath) {
|
|
96
|
+
this.#dbPath = dbPath;
|
|
97
|
+
this.#db = new Database(dbPath);
|
|
98
|
+
this.#db.pragma('journal_mode = WAL');
|
|
99
|
+
this.#db.pragma('foreign_keys = ON');
|
|
100
|
+
this.#db.exec(SCHEMA);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Write API ───────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Start a new turn.
|
|
107
|
+
* @param {{ traceId: string, messageId?: string, mode?: string, turnNumber?: number }} opts
|
|
108
|
+
* @returns {string} — turnId
|
|
109
|
+
*/
|
|
110
|
+
startTurn({ traceId, messageId = null, mode = null, turnNumber = null }) {
|
|
111
|
+
const id = randomUUID();
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
this.#prepare('insertTurn', `
|
|
114
|
+
INSERT INTO trace_turns (id, trace_id, message_id, mode, turn_number, started_at)
|
|
115
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
116
|
+
`).run(id, traceId, messageId, mode, turnNumber, now);
|
|
117
|
+
return id;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* End a turn with model response info.
|
|
122
|
+
* @param {string} turnId
|
|
123
|
+
* @param {{ model?: string, inputTokens?: number, outputTokens?: number, cacheReadTokens?: number, cacheWriteTokens?: number, stopReason?: string, latencyMs?: number, responseText?: string }} info
|
|
124
|
+
*/
|
|
125
|
+
endTurn(turnId, {
|
|
126
|
+
model = null,
|
|
127
|
+
inputTokens = null,
|
|
128
|
+
outputTokens = null,
|
|
129
|
+
cacheReadTokens = 0,
|
|
130
|
+
cacheWriteTokens = 0,
|
|
131
|
+
stopReason = null,
|
|
132
|
+
latencyMs = null,
|
|
133
|
+
responseText = null,
|
|
134
|
+
} = {}) {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
this.#prepare('endTurn', `
|
|
137
|
+
UPDATE trace_turns SET
|
|
138
|
+
model = ?, input_tokens = ?, output_tokens = ?,
|
|
139
|
+
cache_read_tokens = ?, cache_write_tokens = ?,
|
|
140
|
+
stop_reason = ?, latency_ms = ?, response_text = ?, ended_at = ?
|
|
141
|
+
WHERE id = ?
|
|
142
|
+
`).run(
|
|
143
|
+
model, inputTokens, outputTokens,
|
|
144
|
+
cacheReadTokens, cacheWriteTokens,
|
|
145
|
+
stopReason, latencyMs, truncate(responseText, MAX_TOOL_OUTPUT),
|
|
146
|
+
now, turnId,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Log a tool call within a turn.
|
|
152
|
+
* @param {string} turnId
|
|
153
|
+
* @param {{ toolName: string, toolInput?: string, toolOutput?: string, durationMs?: number, isError?: boolean }} info
|
|
154
|
+
* @returns {string} — tool record id
|
|
155
|
+
*/
|
|
156
|
+
logTool(turnId, {
|
|
157
|
+
toolName,
|
|
158
|
+
toolInput = null,
|
|
159
|
+
toolOutput = null,
|
|
160
|
+
durationMs = null,
|
|
161
|
+
isError = false,
|
|
162
|
+
}) {
|
|
163
|
+
const id = randomUUID();
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
this.#prepare('insertTool', `
|
|
166
|
+
INSERT INTO trace_tools (id, turn_id, tool_name, tool_input, tool_output, duration_ms, is_error, created_at)
|
|
167
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
168
|
+
`).run(
|
|
169
|
+
id, turnId, toolName,
|
|
170
|
+
truncate(toolInput, MAX_TOOL_OUTPUT),
|
|
171
|
+
truncate(toolOutput, MAX_TOOL_OUTPUT),
|
|
172
|
+
durationMs, isError ? 1 : 0, now,
|
|
173
|
+
);
|
|
174
|
+
return id;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Log a freeform event.
|
|
179
|
+
* @param {{ traceId: string, eventType: string, eventData?: unknown }} info
|
|
180
|
+
* @returns {string} — event id
|
|
181
|
+
*/
|
|
182
|
+
logEvent({ traceId, eventType, eventData = null }) {
|
|
183
|
+
const id = randomUUID();
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const data = eventData != null ? JSON.stringify(eventData) : null;
|
|
186
|
+
this.#prepare('insertEvent', `
|
|
187
|
+
INSERT INTO trace_events (id, trace_id, event_type, event_data, created_at)
|
|
188
|
+
VALUES (?, ?, ?, ?, ?)
|
|
189
|
+
`).run(id, traceId, eventType, data, now);
|
|
190
|
+
return id;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Read API ────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Query all data for a specific message.
|
|
197
|
+
* @param {string} messageId
|
|
198
|
+
* @returns {{ turns: object[], tools: object[], events: object[] }}
|
|
199
|
+
*/
|
|
200
|
+
queryByMessage(messageId) {
|
|
201
|
+
const turns = this.#prepare('turnsByMessage', `
|
|
202
|
+
SELECT * FROM trace_turns WHERE message_id = ? ORDER BY started_at
|
|
203
|
+
`).all(messageId);
|
|
204
|
+
return this.#expandTurns(turns);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Query all data for a trace.
|
|
209
|
+
* @param {string} traceId
|
|
210
|
+
* @returns {{ turns: object[], tools: object[], events: object[] }}
|
|
211
|
+
*/
|
|
212
|
+
queryByTrace(traceId) {
|
|
213
|
+
const turns = this.#prepare('turnsByTrace', `
|
|
214
|
+
SELECT * FROM trace_turns WHERE trace_id = ? ORDER BY started_at
|
|
215
|
+
`).all(traceId);
|
|
216
|
+
const events = this.#prepare('eventsByTrace', `
|
|
217
|
+
SELECT * FROM trace_events WHERE trace_id = ? ORDER BY created_at
|
|
218
|
+
`).all(traceId);
|
|
219
|
+
const turnIds = turns.map(t => t.id);
|
|
220
|
+
const tools = turnIds.length > 0
|
|
221
|
+
? this.#db.prepare(
|
|
222
|
+
`SELECT * FROM trace_tools WHERE turn_id IN (${turnIds.map(() => '?').join(',')}) ORDER BY created_at`
|
|
223
|
+
).all(...turnIds)
|
|
224
|
+
: [];
|
|
225
|
+
return { turns, tools, events };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Query recent turns.
|
|
230
|
+
* @param {number} [limit=20]
|
|
231
|
+
* @returns {object[]}
|
|
232
|
+
*/
|
|
233
|
+
queryRecent(limit = 20) {
|
|
234
|
+
return this.#prepare('recentTurns', `
|
|
235
|
+
SELECT * FROM trace_turns ORDER BY started_at DESC LIMIT ?
|
|
236
|
+
`).all(limit);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Query tool calls with optional filters.
|
|
241
|
+
* @param {{ name?: string, since?: number }} [filters={}]
|
|
242
|
+
* @returns {object[]}
|
|
243
|
+
*/
|
|
244
|
+
queryTools({ name = null, since = null } = {}) {
|
|
245
|
+
if (name && since) {
|
|
246
|
+
return this.#prepare('toolsByNameSince', `
|
|
247
|
+
SELECT * FROM trace_tools WHERE tool_name = ? AND created_at >= ? ORDER BY created_at DESC
|
|
248
|
+
`).all(name, since);
|
|
249
|
+
}
|
|
250
|
+
if (name) {
|
|
251
|
+
return this.#prepare('toolsByName', `
|
|
252
|
+
SELECT * FROM trace_tools WHERE tool_name = ? ORDER BY created_at DESC
|
|
253
|
+
`).all(name);
|
|
254
|
+
}
|
|
255
|
+
if (since) {
|
|
256
|
+
return this.#prepare('toolsSince', `
|
|
257
|
+
SELECT * FROM trace_tools WHERE created_at >= ? ORDER BY created_at DESC
|
|
258
|
+
`).all(since);
|
|
259
|
+
}
|
|
260
|
+
return this.#prepare('allTools', `
|
|
261
|
+
SELECT * FROM trace_tools ORDER BY created_at DESC LIMIT 100
|
|
262
|
+
`).all();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Full-text search across response_text and tool_output.
|
|
267
|
+
* @param {string} keyword
|
|
268
|
+
* @returns {object[]}
|
|
269
|
+
*/
|
|
270
|
+
search(keyword) {
|
|
271
|
+
const like = `%${keyword}%`;
|
|
272
|
+
return this.#prepare('search', `
|
|
273
|
+
SELECT DISTINCT t.* FROM trace_turns t
|
|
274
|
+
LEFT JOIN trace_tools tt ON tt.turn_id = t.id
|
|
275
|
+
WHERE t.response_text LIKE ? OR tt.tool_output LIKE ?
|
|
276
|
+
ORDER BY t.started_at DESC LIMIT 50
|
|
277
|
+
`).all(like, like);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get trace statistics.
|
|
282
|
+
* @returns {{ turnCount: number, toolCount: number, eventCount: number, dbSizeBytes: number }}
|
|
283
|
+
*/
|
|
284
|
+
stats() {
|
|
285
|
+
const turnCount = this.#db.prepare('SELECT COUNT(*) as c FROM trace_turns').get().c;
|
|
286
|
+
const toolCount = this.#db.prepare('SELECT COUNT(*) as c FROM trace_tools').get().c;
|
|
287
|
+
const eventCount = this.#db.prepare('SELECT COUNT(*) as c FROM trace_events').get().c;
|
|
288
|
+
let dbSizeBytes = 0;
|
|
289
|
+
try {
|
|
290
|
+
dbSizeBytes = statSync(this.#dbPath).size;
|
|
291
|
+
} catch { /* ignore */ }
|
|
292
|
+
return { turnCount, toolCount, eventCount, dbSizeBytes };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Maintenance ─────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Delete data older than retentionDays.
|
|
299
|
+
* @param {number} [retentionDays=30]
|
|
300
|
+
* @returns {{ deletedTurns: number, deletedTools: number, deletedEvents: number }}
|
|
301
|
+
*/
|
|
302
|
+
cleanup(retentionDays = 30) {
|
|
303
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
304
|
+
const deletedTools = this.#db.prepare(`
|
|
305
|
+
DELETE FROM trace_tools WHERE turn_id IN (
|
|
306
|
+
SELECT id FROM trace_turns WHERE started_at < ?
|
|
307
|
+
)
|
|
308
|
+
`).run(cutoff).changes;
|
|
309
|
+
const deletedTurns = this.#db.prepare(`
|
|
310
|
+
DELETE FROM trace_turns WHERE started_at < ?
|
|
311
|
+
`).run(cutoff).changes;
|
|
312
|
+
const deletedEvents = this.#db.prepare(`
|
|
313
|
+
DELETE FROM trace_events WHERE created_at < ?
|
|
314
|
+
`).run(cutoff).changes;
|
|
315
|
+
return { deletedTurns, deletedTools, deletedEvents };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Delete all trace data. */
|
|
319
|
+
purge() {
|
|
320
|
+
this.#db.exec('DELETE FROM trace_tools');
|
|
321
|
+
this.#db.exec('DELETE FROM trace_turns');
|
|
322
|
+
this.#db.exec('DELETE FROM trace_events');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Close the database connection. */
|
|
326
|
+
close() {
|
|
327
|
+
this.#db.close();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Internal ────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get or create a prepared statement.
|
|
334
|
+
* @param {string} key
|
|
335
|
+
* @param {string} sql
|
|
336
|
+
* @returns {Database.Statement}
|
|
337
|
+
*/
|
|
338
|
+
#prepare(key, sql) {
|
|
339
|
+
if (!this.#stmts[key]) {
|
|
340
|
+
this.#stmts[key] = this.#db.prepare(sql);
|
|
341
|
+
}
|
|
342
|
+
return this.#stmts[key];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Expand turns with their tools.
|
|
347
|
+
* @param {object[]} turns
|
|
348
|
+
* @returns {{ turns: object[], tools: object[], events: object[] }}
|
|
349
|
+
*/
|
|
350
|
+
#expandTurns(turns) {
|
|
351
|
+
const turnIds = turns.map(t => t.id);
|
|
352
|
+
const tools = turnIds.length > 0
|
|
353
|
+
? this.#db.prepare(
|
|
354
|
+
`SELECT * FROM trace_tools WHERE turn_id IN (${turnIds.map(() => '?').join(',')}) ORDER BY created_at`
|
|
355
|
+
).all(...turnIds)
|
|
356
|
+
: [];
|
|
357
|
+
// Events need trace_ids from turns
|
|
358
|
+
const traceIds = [...new Set(turns.map(t => t.trace_id))];
|
|
359
|
+
const events = traceIds.length > 0
|
|
360
|
+
? this.#db.prepare(
|
|
361
|
+
`SELECT * FROM trace_events WHERE trace_id IN (${traceIds.map(() => '?').join(',')}) ORDER BY created_at`
|
|
362
|
+
).all(...traceIds)
|
|
363
|
+
: [];
|
|
364
|
+
return { turns, tools, events };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* NullTrace — No-op implementation with the same interface.
|
|
370
|
+
* Used when debug is disabled. Zero overhead.
|
|
371
|
+
*/
|
|
372
|
+
export class NullTrace {
|
|
373
|
+
startTurn() { return 'null'; }
|
|
374
|
+
endTurn() {}
|
|
375
|
+
logTool() { return 'null'; }
|
|
376
|
+
logEvent() { return 'null'; }
|
|
377
|
+
queryByMessage() { return { turns: [], tools: [], events: [] }; }
|
|
378
|
+
queryByTrace() { return { turns: [], tools: [], events: [] }; }
|
|
379
|
+
queryRecent() { return []; }
|
|
380
|
+
queryTools() { return []; }
|
|
381
|
+
search() { return []; }
|
|
382
|
+
stats() { return { turnCount: 0, toolCount: 0, eventCount: 0, dbSizeBytes: 0 }; }
|
|
383
|
+
cleanup() { return { deletedTurns: 0, deletedTools: 0, deletedEvents: 0 }; }
|
|
384
|
+
purge() {}
|
|
385
|
+
close() {}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create a DebugTrace or NullTrace based on config.
|
|
390
|
+
* @param {{ enabled: boolean, dbPath?: string }} opts
|
|
391
|
+
* @returns {DebugTrace | NullTrace}
|
|
392
|
+
*/
|
|
393
|
+
export function createTrace({ enabled, dbPath }) {
|
|
394
|
+
if (!enabled || !dbPath) {
|
|
395
|
+
return new NullTrace();
|
|
396
|
+
}
|
|
397
|
+
return new DebugTrace(dbPath);
|
|
398
|
+
}
|