@kb-labs/agent-tracing 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/README.md +86 -0
- package/dist/index.d.ts +582 -0
- package/dist/index.js +873 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, promises, mkdirSync, statSync } from 'fs';
|
|
2
|
+
import * as path3 from 'path';
|
|
3
|
+
import path3__default from 'path';
|
|
4
|
+
import * as fs2 from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
// src/incremental-trace-writer.ts
|
|
7
|
+
|
|
8
|
+
// src/privacy-redactor.ts
|
|
9
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
10
|
+
// API keys
|
|
11
|
+
/sk-[A-Za-z0-9]{20,}/g,
|
|
12
|
+
// OpenAI API keys
|
|
13
|
+
/sk_live_[A-Za-z0-9]{24,}/g,
|
|
14
|
+
// Stripe API keys
|
|
15
|
+
/ghp_[A-Za-z0-9]{36}/g,
|
|
16
|
+
// GitHub personal access tokens
|
|
17
|
+
// Authentication tokens
|
|
18
|
+
/Bearer\s+[A-Za-z0-9\-._~+/]+=*/g,
|
|
19
|
+
/token[=:]\s*[A-Za-z0-9\-._~+/]+=*/gi,
|
|
20
|
+
// Passwords
|
|
21
|
+
/password[=:]\s*["']?[^"'\s]+["']?/gi,
|
|
22
|
+
/passwd[=:]\s*["']?[^"'\s]+["']?/gi,
|
|
23
|
+
/pwd[=:]\s*["']?[^"'\s]+["']?/gi,
|
|
24
|
+
// Connection strings
|
|
25
|
+
/mongodb(\+srv)?:\/\/[^@]+@[^\s]+/g,
|
|
26
|
+
/postgres(ql)?:\/\/[^@]+@[^\s]+/g,
|
|
27
|
+
/mysql:\/\/[^@]+@[^\s]+/g,
|
|
28
|
+
// Email addresses (PII)
|
|
29
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g
|
|
30
|
+
// Note: Generic long alphanumeric pattern removed to avoid false positives
|
|
31
|
+
// (would match file hashes, UUIDs, legitimate data)
|
|
32
|
+
// Credit card numbers removed - too many false positives
|
|
33
|
+
];
|
|
34
|
+
function redactSecretsFromString(str, patterns = DEFAULT_SECRET_PATTERNS) {
|
|
35
|
+
let result = str;
|
|
36
|
+
for (const pattern of patterns) {
|
|
37
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function redactPaths(str, replacements) {
|
|
42
|
+
let result = str;
|
|
43
|
+
for (const [absolute, relative] of Object.entries(replacements)) {
|
|
44
|
+
result = result.replace(new RegExp(absolute.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), relative);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function redactValue(value, config, depth = 0) {
|
|
49
|
+
if (depth > 10) {
|
|
50
|
+
return "[REDACTED:TOO_DEEP]";
|
|
51
|
+
}
|
|
52
|
+
if (value === null || value === void 0) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
let result = value;
|
|
57
|
+
if (config.redactSecrets) {
|
|
58
|
+
const patterns = config.secretPatterns.map((p) => new RegExp(p, "g"));
|
|
59
|
+
result = redactSecretsFromString(result, [...DEFAULT_SECRET_PATTERNS, ...patterns]);
|
|
60
|
+
}
|
|
61
|
+
if (config.redactPaths) {
|
|
62
|
+
result = redactPaths(result, config.pathReplacements);
|
|
63
|
+
}
|
|
64
|
+
return result === value ? value : result;
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
let changed = false;
|
|
71
|
+
const result = value.map((item) => {
|
|
72
|
+
const redacted = redactValue(item, config, depth + 1);
|
|
73
|
+
if (redacted !== item) {
|
|
74
|
+
changed = true;
|
|
75
|
+
}
|
|
76
|
+
return redacted;
|
|
77
|
+
});
|
|
78
|
+
return changed ? result : value;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === "object") {
|
|
81
|
+
let changed = false;
|
|
82
|
+
const result = {};
|
|
83
|
+
for (const [key, val] of Object.entries(value)) {
|
|
84
|
+
const redacted = redactValue(val, config, depth + 1);
|
|
85
|
+
result[key] = redacted;
|
|
86
|
+
if (redacted !== val) {
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return changed ? result : value;
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
function redactTraceEvent(event, config) {
|
|
95
|
+
if (!config.redactSecrets && !config.redactPaths) {
|
|
96
|
+
return event;
|
|
97
|
+
}
|
|
98
|
+
const eventStr = JSON.stringify(event);
|
|
99
|
+
const MAX_STRING_SIZE = 1e5;
|
|
100
|
+
const testStr = eventStr.length > MAX_STRING_SIZE ? eventStr.substring(0, MAX_STRING_SIZE) : eventStr;
|
|
101
|
+
let needsRedaction = false;
|
|
102
|
+
if (config.redactSecrets) {
|
|
103
|
+
const patterns = config.secretPatterns.map((p) => new RegExp(p, "g"));
|
|
104
|
+
needsRedaction = [...DEFAULT_SECRET_PATTERNS, ...patterns].some((p) => p.test(testStr));
|
|
105
|
+
}
|
|
106
|
+
if (!needsRedaction && config.redactPaths) {
|
|
107
|
+
needsRedaction = Object.keys(config.pathReplacements).some((path4) => testStr.includes(path4));
|
|
108
|
+
}
|
|
109
|
+
if (!needsRedaction) {
|
|
110
|
+
return event;
|
|
111
|
+
}
|
|
112
|
+
return redactValue(event, config, 0);
|
|
113
|
+
}
|
|
114
|
+
function createDefaultPrivacyConfig() {
|
|
115
|
+
return {
|
|
116
|
+
redactSecrets: true,
|
|
117
|
+
redactPaths: true,
|
|
118
|
+
secretPatterns: [],
|
|
119
|
+
pathReplacements: {
|
|
120
|
+
"/Users": "~",
|
|
121
|
+
"/home": "~",
|
|
122
|
+
"C:\\Users": "~"
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/incremental-trace-writer.ts
|
|
128
|
+
var DEFAULT_TRACE_CONFIG = {
|
|
129
|
+
version: "1.0.0",
|
|
130
|
+
enabled: true,
|
|
131
|
+
level: "detailed",
|
|
132
|
+
incremental: {
|
|
133
|
+
enabled: true,
|
|
134
|
+
flushIntervalMs: 100,
|
|
135
|
+
maxBufferSize: 10,
|
|
136
|
+
format: "ndjson"
|
|
137
|
+
},
|
|
138
|
+
capture: {
|
|
139
|
+
prompts: true,
|
|
140
|
+
toolOutputs: true,
|
|
141
|
+
memorySnapshots: true,
|
|
142
|
+
decisions: true
|
|
143
|
+
},
|
|
144
|
+
retention: {
|
|
145
|
+
maxTraces: 30,
|
|
146
|
+
maxDays: 30,
|
|
147
|
+
cleanupOnFinalize: true,
|
|
148
|
+
archiveOlderThan: 7,
|
|
149
|
+
compressArchived: true
|
|
150
|
+
},
|
|
151
|
+
privacy: {
|
|
152
|
+
redactSecrets: true,
|
|
153
|
+
redactPaths: true,
|
|
154
|
+
secretPatterns: [
|
|
155
|
+
"sk-[a-zA-Z0-9]{20,}",
|
|
156
|
+
// OpenAI API keys
|
|
157
|
+
"Bearer\\s+[a-zA-Z0-9_-]+",
|
|
158
|
+
// Bearer tokens
|
|
159
|
+
`password['"]?\\s*[:=]\\s*['"][^'"]+(['"]})`,
|
|
160
|
+
// Passwords
|
|
161
|
+
`api[_-]?key['"]?\\s*[:=]\\s*['"][^'"]+['"]`
|
|
162
|
+
// Generic API keys
|
|
163
|
+
],
|
|
164
|
+
pathReplacements: {
|
|
165
|
+
"/Users/": "~/",
|
|
166
|
+
"/home/": "~/",
|
|
167
|
+
"\\Users\\": "~\\"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
storage: {
|
|
171
|
+
path: ".kb/traces/incremental",
|
|
172
|
+
indexPath: ".kb/traces/incremental"
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var IncrementalTraceWriter = class {
|
|
176
|
+
seq = 0;
|
|
177
|
+
filepath;
|
|
178
|
+
indexPath;
|
|
179
|
+
config;
|
|
180
|
+
taskId;
|
|
181
|
+
startTime;
|
|
182
|
+
constructor(taskId, config = {}, outputDir) {
|
|
183
|
+
this.taskId = taskId;
|
|
184
|
+
this.config = { ...DEFAULT_TRACE_CONFIG, ...config };
|
|
185
|
+
this.startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
186
|
+
const dir = outputDir || this.config.storage.path;
|
|
187
|
+
this.filepath = path3__default.join(dir, `${taskId}.ndjson`);
|
|
188
|
+
this.indexPath = path3__default.join(dir, `${taskId}-index.json`);
|
|
189
|
+
this.ensureDirectoryExists(dir);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Record a trace entry — writes synchronously to disk.
|
|
193
|
+
*
|
|
194
|
+
* Every call = one appendFileSync. No buffering, no async, no lost events.
|
|
195
|
+
* If the agent crashes on iteration 5, you have all events from iterations 1-5.
|
|
196
|
+
*/
|
|
197
|
+
trace(entry) {
|
|
198
|
+
try {
|
|
199
|
+
const seq = ++this.seq;
|
|
200
|
+
const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
201
|
+
const fullEntry = {
|
|
202
|
+
...entry,
|
|
203
|
+
seq,
|
|
204
|
+
timestamp
|
|
205
|
+
};
|
|
206
|
+
const redacted = this.redact(fullEntry);
|
|
207
|
+
appendFileSync(this.filepath, JSON.stringify(redacted) + "\n", "utf-8");
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error("[IncrementalTraceWriter] trace() error:", error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get all trace entries (reads from NDJSON file to avoid memory leak)
|
|
214
|
+
* Returns TraceEntry[] for backward compatibility, but actual format is DetailedTraceEntry[]
|
|
215
|
+
*/
|
|
216
|
+
getEntries() {
|
|
217
|
+
try {
|
|
218
|
+
const content = readFileSync(this.filepath, "utf-8");
|
|
219
|
+
const lines = content.split("\n").filter(Boolean);
|
|
220
|
+
return lines.map((line) => JSON.parse(line));
|
|
221
|
+
} catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Save trace to file (backward compat — no-op, already written synchronously)
|
|
227
|
+
*/
|
|
228
|
+
async save(_filePath) {
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Clear all entries
|
|
232
|
+
*/
|
|
233
|
+
clear() {
|
|
234
|
+
this.seq = 0;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Finalize trace (generate index, cleanup old traces)
|
|
238
|
+
*/
|
|
239
|
+
async finalize() {
|
|
240
|
+
try {
|
|
241
|
+
await this.createIndex();
|
|
242
|
+
if (this.config.retention.cleanupOnFinalize) {
|
|
243
|
+
await this.cleanupOldTraces();
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error("[IncrementalTraceWriter] finalize() error:", error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Create index file for fast CLI queries
|
|
251
|
+
*/
|
|
252
|
+
async createIndex() {
|
|
253
|
+
try {
|
|
254
|
+
const entries = this.getEntries();
|
|
255
|
+
if (entries.length === 0) {
|
|
256
|
+
console.warn("[IncrementalTraceWriter] No entries to index");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const stats = this.calculateIndexStatistics(entries);
|
|
260
|
+
const index = {
|
|
261
|
+
version: "1.0.0",
|
|
262
|
+
taskId: this.taskId,
|
|
263
|
+
createdAt: this.startTime,
|
|
264
|
+
finalizedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
265
|
+
summary: {
|
|
266
|
+
totalEvents: entries.length,
|
|
267
|
+
iterations: stats.iterations.size,
|
|
268
|
+
status: stats.errors > 0 ? "failed" : "success",
|
|
269
|
+
eventCounts: stats.eventCounts
|
|
270
|
+
},
|
|
271
|
+
timing: {
|
|
272
|
+
startedAt: this.startTime,
|
|
273
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
274
|
+
totalDurationMs: Date.now() - new Date(this.startTime).getTime()
|
|
275
|
+
},
|
|
276
|
+
cost: {
|
|
277
|
+
totalCost: stats.totalCost,
|
|
278
|
+
currency: "USD"
|
|
279
|
+
},
|
|
280
|
+
errors: stats.errors,
|
|
281
|
+
memory: stats.memory,
|
|
282
|
+
iterations: Array.from(stats.iterations.entries()).map(([iteration, iterStats]) => ({ iteration, ...iterStats })).sort((a, b) => a.iteration - b.iteration)
|
|
283
|
+
};
|
|
284
|
+
const indexDir = path3__default.dirname(this.indexPath);
|
|
285
|
+
this.ensureDirectoryExists(indexDir);
|
|
286
|
+
await promises.writeFile(this.indexPath, JSON.stringify(index, null, 2), "utf-8");
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error("[IncrementalTraceWriter] createIndex() error:", error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
292
|
+
// Private Methods
|
|
293
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
294
|
+
/**
|
|
295
|
+
* Cleanup old traces (keep last N traces)
|
|
296
|
+
*/
|
|
297
|
+
async cleanupOldTraces() {
|
|
298
|
+
try {
|
|
299
|
+
const dir = path3__default.dirname(this.filepath);
|
|
300
|
+
const files = await promises.readdir(dir);
|
|
301
|
+
const traceFiles = files.filter((f) => f.endsWith(".ndjson"));
|
|
302
|
+
if (traceFiles.length <= this.config.retention.maxTraces) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const filesWithStats = await Promise.all(
|
|
306
|
+
traceFiles.map(async (file) => {
|
|
307
|
+
const filepath = path3__default.join(dir, file);
|
|
308
|
+
const stats = await promises.stat(filepath);
|
|
309
|
+
return { file, mtime: stats.mtime.getTime(), filepath };
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
313
|
+
const toDelete = filesWithStats.slice(this.config.retention.maxTraces);
|
|
314
|
+
await Promise.allSettled(
|
|
315
|
+
toDelete.map(async ({ file, filepath }) => {
|
|
316
|
+
try {
|
|
317
|
+
await promises.unlink(filepath);
|
|
318
|
+
const indexFile = filepath.replace(".ndjson", "-index.json");
|
|
319
|
+
try {
|
|
320
|
+
await promises.unlink(indexFile);
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(`[IncrementalTraceWriter] Failed to delete ${file}:`, error);
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
if (toDelete.length > 0) {
|
|
329
|
+
console.log(
|
|
330
|
+
`[IncrementalTraceWriter] Cleaned up ${toDelete.length} old traces (kept last ${this.config.retention.maxTraces})`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error("[IncrementalTraceWriter] cleanupOldTraces() error:", error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Redact sensitive data from entry using optimized privacy-redactor
|
|
339
|
+
*
|
|
340
|
+
* Uses shallow clone optimization - only clones objects that need redaction,
|
|
341
|
+
* not the entire trace event tree. Returns original if no secrets found.
|
|
342
|
+
*/
|
|
343
|
+
redact(entry) {
|
|
344
|
+
if (!this.config.privacy.redactSecrets && !this.config.privacy.redactPaths) {
|
|
345
|
+
return entry;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
return redactTraceEvent(entry, this.config.privacy);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.warn("[IncrementalTraceWriter] redact() error:", error);
|
|
351
|
+
return entry;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Calculate index statistics from trace entries
|
|
356
|
+
*/
|
|
357
|
+
calculateIndexStatistics(entries) {
|
|
358
|
+
const eventCounts = {};
|
|
359
|
+
const iterations = /* @__PURE__ */ new Map();
|
|
360
|
+
let totalCost = 0;
|
|
361
|
+
let errors = 0;
|
|
362
|
+
let totalFactsAdded = 0;
|
|
363
|
+
let totalArchiveStores = 0;
|
|
364
|
+
let summarizationRuns = 0;
|
|
365
|
+
let compressionRatioSum = 0;
|
|
366
|
+
let newFactRateSum = 0;
|
|
367
|
+
let lastFactSheetSize = 0;
|
|
368
|
+
let lastFactSheetTokens = 0;
|
|
369
|
+
let lastArchiveEntries = 0;
|
|
370
|
+
let lastArchiveUniqueFiles = 0;
|
|
371
|
+
for (const entry of entries) {
|
|
372
|
+
eventCounts[entry.type] = (eventCounts[entry.type] || 0) + 1;
|
|
373
|
+
if (entry.iteration !== void 0) {
|
|
374
|
+
if (!iterations.has(entry.iteration)) {
|
|
375
|
+
iterations.set(entry.iteration, { eventCount: 0, llmCalls: 0, toolCalls: 0 });
|
|
376
|
+
}
|
|
377
|
+
const iter = iterations.get(entry.iteration);
|
|
378
|
+
iter.eventCount++;
|
|
379
|
+
if (entry.type === "llm:end") {
|
|
380
|
+
iter.llmCalls++;
|
|
381
|
+
if ("cost" in entry && entry.cost) {
|
|
382
|
+
totalCost += entry.cost.totalCost || 0;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (entry.type === "tool:end") {
|
|
386
|
+
iter.toolCalls++;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (entry.type === "error:captured") {
|
|
390
|
+
errors++;
|
|
391
|
+
}
|
|
392
|
+
if (entry.type === "memory:fact_added") {
|
|
393
|
+
totalFactsAdded++;
|
|
394
|
+
if (entry.factSheetStats) {
|
|
395
|
+
lastFactSheetSize = entry.factSheetStats.totalFacts || 0;
|
|
396
|
+
lastFactSheetTokens = entry.factSheetStats.estimatedTokens || 0;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (entry.type === "memory:archive_store") {
|
|
400
|
+
totalArchiveStores++;
|
|
401
|
+
if (entry.archiveStats) {
|
|
402
|
+
lastArchiveEntries = entry.archiveStats.totalEntries || 0;
|
|
403
|
+
lastArchiveUniqueFiles = entry.archiveStats.uniqueFiles || 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (entry.type === "memory:summarization_result") {
|
|
407
|
+
summarizationRuns++;
|
|
408
|
+
if (entry.efficiency) {
|
|
409
|
+
compressionRatioSum += entry.efficiency.compressionRatio || 0;
|
|
410
|
+
newFactRateSum += entry.efficiency.newFactRate || 0;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const hasMemoryEvents = totalFactsAdded > 0 || totalArchiveStores > 0 || summarizationRuns > 0;
|
|
415
|
+
return {
|
|
416
|
+
eventCounts,
|
|
417
|
+
iterations,
|
|
418
|
+
totalCost,
|
|
419
|
+
errors,
|
|
420
|
+
memory: hasMemoryEvents ? {
|
|
421
|
+
totalFactsAdded,
|
|
422
|
+
totalArchiveStores,
|
|
423
|
+
summarizationRuns,
|
|
424
|
+
avgCompressionRatio: summarizationRuns > 0 ? compressionRatioSum / summarizationRuns : 0,
|
|
425
|
+
avgNewFactRate: summarizationRuns > 0 ? newFactRateSum / summarizationRuns : 0,
|
|
426
|
+
finalFactSheetSize: lastFactSheetSize,
|
|
427
|
+
finalFactSheetTokens: lastFactSheetTokens,
|
|
428
|
+
finalArchiveEntries: lastArchiveEntries,
|
|
429
|
+
finalArchiveUniqueFiles: lastArchiveUniqueFiles
|
|
430
|
+
} : void 0
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Ensure directory exists
|
|
435
|
+
*/
|
|
436
|
+
ensureDirectoryExists(dir) {
|
|
437
|
+
try {
|
|
438
|
+
mkdirSync(dir, { recursive: true });
|
|
439
|
+
} catch (error) {
|
|
440
|
+
try {
|
|
441
|
+
const stats = statSync(dir);
|
|
442
|
+
if (!stats.isDirectory()) {
|
|
443
|
+
throw new Error(`Path exists but is not a directory: ${dir}`);
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
throw new Error(`Failed to create trace directory: ${dir}. Error: ${error}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var FileTracer = class {
|
|
452
|
+
entries = [];
|
|
453
|
+
taskId;
|
|
454
|
+
sessionId;
|
|
455
|
+
constructor(taskId, sessionId) {
|
|
456
|
+
this.taskId = taskId;
|
|
457
|
+
this.sessionId = sessionId;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Record a trace entry
|
|
461
|
+
*/
|
|
462
|
+
trace(entry) {
|
|
463
|
+
this.entries.push(entry);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Get all trace entries
|
|
467
|
+
*/
|
|
468
|
+
getEntries() {
|
|
469
|
+
return this.entries;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Save trace to file
|
|
473
|
+
*/
|
|
474
|
+
async save(filePath) {
|
|
475
|
+
const dir = path3.dirname(filePath);
|
|
476
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
477
|
+
const traceData = {
|
|
478
|
+
taskId: this.taskId,
|
|
479
|
+
sessionId: this.sessionId,
|
|
480
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
481
|
+
totalEntries: this.entries.length,
|
|
482
|
+
entries: this.entries
|
|
483
|
+
};
|
|
484
|
+
await fs2.writeFile(filePath, JSON.stringify(traceData, null, 2), "utf-8");
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Clear all entries
|
|
488
|
+
*/
|
|
489
|
+
clear() {
|
|
490
|
+
this.entries = [];
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Get trace summary statistics
|
|
494
|
+
*/
|
|
495
|
+
getSummary() {
|
|
496
|
+
const llmCalls = this.entries.filter((e) => e.type === "llm:call");
|
|
497
|
+
const toolCalls = this.entries.filter((e) => e.type === "tool:execution");
|
|
498
|
+
const getDuration = (e) => {
|
|
499
|
+
if (e.type === "llm:call" || e.type === "tool:execution") {
|
|
500
|
+
return e.timing?.durationMs ?? 0;
|
|
501
|
+
}
|
|
502
|
+
return 0;
|
|
503
|
+
};
|
|
504
|
+
const totalDuration = this.entries.reduce((sum, e) => sum + getDuration(e), 0);
|
|
505
|
+
const llmDuration = llmCalls.reduce((sum, e) => sum + getDuration(e), 0);
|
|
506
|
+
const toolDuration = toolCalls.reduce((sum, e) => sum + getDuration(e), 0);
|
|
507
|
+
return {
|
|
508
|
+
totalEntries: this.entries.length,
|
|
509
|
+
llmCalls: llmCalls.length,
|
|
510
|
+
toolCalls: toolCalls.length,
|
|
511
|
+
totalDuration,
|
|
512
|
+
avgLLMDuration: llmCalls.length > 0 ? llmDuration / llmCalls.length : 0,
|
|
513
|
+
avgToolDuration: toolCalls.length > 0 ? toolDuration / toolCalls.length : 0
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
var TRACE_DIR_RELATIVE = path3__default.join(".kb", "traces", "incremental");
|
|
518
|
+
var TASK_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
519
|
+
var MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
|
|
520
|
+
async function loadTrace(taskId, workingDir = process.cwd()) {
|
|
521
|
+
if (!taskId) {
|
|
522
|
+
return { ok: false, error: { kind: "invalid_task_id", message: "Missing required --task-id" } };
|
|
523
|
+
}
|
|
524
|
+
if (!TASK_ID_PATTERN.test(taskId)) {
|
|
525
|
+
return {
|
|
526
|
+
ok: false,
|
|
527
|
+
error: {
|
|
528
|
+
kind: "invalid_task_id",
|
|
529
|
+
message: "Task ID must contain only alphanumeric characters, hyphens, and underscores"
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const traceDir = path3__default.join(workingDir, TRACE_DIR_RELATIVE);
|
|
534
|
+
const filePath = path3__default.join(traceDir, `${taskId}.ndjson`);
|
|
535
|
+
const resolvedFile = path3__default.resolve(filePath);
|
|
536
|
+
const resolvedDir = path3__default.resolve(traceDir);
|
|
537
|
+
if (!resolvedFile.startsWith(resolvedDir + path3__default.sep) && resolvedFile !== resolvedDir) {
|
|
538
|
+
return { ok: false, error: { kind: "invalid_task_id", message: "Path traversal detected" } };
|
|
539
|
+
}
|
|
540
|
+
let stat;
|
|
541
|
+
try {
|
|
542
|
+
stat = await promises.stat(filePath);
|
|
543
|
+
} catch {
|
|
544
|
+
return { ok: false, error: { kind: "not_found", taskId } };
|
|
545
|
+
}
|
|
546
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
547
|
+
return { ok: false, error: { kind: "too_large", sizeBytes: stat.size } };
|
|
548
|
+
}
|
|
549
|
+
let content;
|
|
550
|
+
try {
|
|
551
|
+
content = await promises.readFile(filePath, "utf-8");
|
|
552
|
+
} catch (e) {
|
|
553
|
+
return { ok: false, error: { kind: "io_error", message: e.message } };
|
|
554
|
+
}
|
|
555
|
+
const events = [];
|
|
556
|
+
for (const line of content.split("\n")) {
|
|
557
|
+
if (!line.trim()) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
events.push(JSON.parse(line));
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (events.length === 0) {
|
|
566
|
+
return { ok: false, error: { kind: "empty", taskId } };
|
|
567
|
+
}
|
|
568
|
+
return { ok: true, events, taskId, filePath };
|
|
569
|
+
}
|
|
570
|
+
function formatTraceLoadError(error) {
|
|
571
|
+
switch (error.kind) {
|
|
572
|
+
case "invalid_task_id":
|
|
573
|
+
return `Invalid task ID: ${error.message}`;
|
|
574
|
+
case "not_found":
|
|
575
|
+
return `Trace not found: ${error.taskId}`;
|
|
576
|
+
case "too_large":
|
|
577
|
+
return `Trace file too large: ${Math.round(error.sizeBytes / 1024 / 1024)} MB (limit 100 MB)`;
|
|
578
|
+
case "empty":
|
|
579
|
+
return `Trace file is empty: ${error.taskId}`;
|
|
580
|
+
case "io_error":
|
|
581
|
+
return `IO error reading trace: ${error.message}`;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/trace-helpers.ts
|
|
586
|
+
function createIterationDetailEvent(params) {
|
|
587
|
+
return {
|
|
588
|
+
type: "iteration:detail",
|
|
589
|
+
iteration: params.iteration,
|
|
590
|
+
config: {
|
|
591
|
+
maxIterations: params.maxIterations,
|
|
592
|
+
mode: params.mode,
|
|
593
|
+
temperature: params.temperature
|
|
594
|
+
},
|
|
595
|
+
availableTools: {
|
|
596
|
+
total: params.availableTools.length,
|
|
597
|
+
tools: params.availableTools
|
|
598
|
+
},
|
|
599
|
+
context: {
|
|
600
|
+
messagesCount: params.messages.length,
|
|
601
|
+
totalTokens: params.totalTokens,
|
|
602
|
+
conversationSummary: params.messages.slice(-2).map((m) => `${m.role}: ${m.content?.substring(0, 100) || ""}`).join(" | ")
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function createLLMCallEvent(params) {
|
|
607
|
+
const durationMs = params.endTime - params.startTime;
|
|
608
|
+
const inputCost = (params.response.usage?.promptTokens || 0) * 3e-6;
|
|
609
|
+
const outputCost = (params.response.usage?.completionTokens || 0) * 15e-6;
|
|
610
|
+
return {
|
|
611
|
+
type: "llm:call",
|
|
612
|
+
iteration: params.iteration,
|
|
613
|
+
request: {
|
|
614
|
+
model: params.model,
|
|
615
|
+
temperature: params.temperature,
|
|
616
|
+
maxTokens: params.maxTokens,
|
|
617
|
+
tools: params.tools
|
|
618
|
+
},
|
|
619
|
+
response: {
|
|
620
|
+
content: params.response.content || null,
|
|
621
|
+
toolCalls: params.response.toolCalls?.map((tc) => ({
|
|
622
|
+
id: tc.id,
|
|
623
|
+
name: tc.name,
|
|
624
|
+
input: tc.input
|
|
625
|
+
})),
|
|
626
|
+
stopReason: params.response.toolCalls && params.response.toolCalls.length > 0 ? "tool_use" : "end_turn",
|
|
627
|
+
usage: {
|
|
628
|
+
inputTokens: params.response.usage.promptTokens,
|
|
629
|
+
outputTokens: params.response.usage.completionTokens,
|
|
630
|
+
totalTokens: params.response.usage.promptTokens + params.response.usage.completionTokens
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
timing: {
|
|
634
|
+
startedAt: new Date(params.startTime).toISOString(),
|
|
635
|
+
completedAt: new Date(params.endTime).toISOString(),
|
|
636
|
+
durationMs
|
|
637
|
+
},
|
|
638
|
+
cost: {
|
|
639
|
+
inputCost,
|
|
640
|
+
outputCost,
|
|
641
|
+
totalCost: inputCost + outputCost,
|
|
642
|
+
currency: "USD"
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function createToolExecutionEvent(params) {
|
|
647
|
+
const durationMs = params.endTime - params.startTime;
|
|
648
|
+
const resultStr = typeof params.output.result === "string" ? params.output.result : JSON.stringify(params.output.result || "");
|
|
649
|
+
const TRACE_OUTPUT_LIMIT = 5e4;
|
|
650
|
+
const truncated = resultStr.length > TRACE_OUTPUT_LIMIT;
|
|
651
|
+
return {
|
|
652
|
+
type: "tool:execution",
|
|
653
|
+
iteration: params.iteration,
|
|
654
|
+
tool: {
|
|
655
|
+
name: params.toolName,
|
|
656
|
+
callId: params.callId
|
|
657
|
+
},
|
|
658
|
+
input: params.input,
|
|
659
|
+
output: {
|
|
660
|
+
success: params.output.success,
|
|
661
|
+
result: truncated ? resultStr.substring(0, TRACE_OUTPUT_LIMIT) + `
|
|
662
|
+
... (truncated from ${resultStr.length} chars)` : params.output.result,
|
|
663
|
+
error: params.output.error,
|
|
664
|
+
truncated,
|
|
665
|
+
originalLength: resultStr.length
|
|
666
|
+
},
|
|
667
|
+
timing: {
|
|
668
|
+
startedAt: new Date(params.startTime).toISOString(),
|
|
669
|
+
completedAt: new Date(params.endTime).toISOString(),
|
|
670
|
+
durationMs
|
|
671
|
+
},
|
|
672
|
+
metadata: params.metadata
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function createMemorySnapshotEvent(params) {
|
|
676
|
+
return {
|
|
677
|
+
type: "memory:snapshot",
|
|
678
|
+
iteration: params.iteration,
|
|
679
|
+
sessionMemory: {
|
|
680
|
+
conversationHistory: params.conversationHistory,
|
|
681
|
+
userPreferences: params.userPreferences
|
|
682
|
+
},
|
|
683
|
+
sharedMemory: {
|
|
684
|
+
facts: params.facts,
|
|
685
|
+
findings: params.findings
|
|
686
|
+
},
|
|
687
|
+
executionMemory: {
|
|
688
|
+
filesRead: params.filesRead,
|
|
689
|
+
searchesMade: params.searchesMade,
|
|
690
|
+
toolsUsed: params.toolsUsed
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function createDecisionPointEvent(params) {
|
|
695
|
+
return {
|
|
696
|
+
type: "decision:point",
|
|
697
|
+
iteration: params.iteration,
|
|
698
|
+
decision: params.decision,
|
|
699
|
+
toolSelection: params.toolSelection,
|
|
700
|
+
stoppingCondition: params.stoppingCondition
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function createSynthesisForcedEvent(params) {
|
|
704
|
+
return {
|
|
705
|
+
type: "synthesis:forced",
|
|
706
|
+
iteration: params.iteration,
|
|
707
|
+
trigger: {
|
|
708
|
+
reason: params.reason,
|
|
709
|
+
lastIteration: params.lastIteration,
|
|
710
|
+
lastToolCall: params.lastToolCall
|
|
711
|
+
},
|
|
712
|
+
synthesisPrompt: params.synthesisPrompt,
|
|
713
|
+
synthesisResponse: params.synthesisResponse
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function createErrorCapturedEvent(params) {
|
|
717
|
+
return {
|
|
718
|
+
type: "error:captured",
|
|
719
|
+
iteration: params.iteration,
|
|
720
|
+
error: {
|
|
721
|
+
message: params.error.message,
|
|
722
|
+
stack: params.error.stack || "",
|
|
723
|
+
code: params.error.code,
|
|
724
|
+
name: params.error.name
|
|
725
|
+
},
|
|
726
|
+
context: {
|
|
727
|
+
lastLLMCall: params.lastLLMCall,
|
|
728
|
+
lastToolCall: params.lastToolCall,
|
|
729
|
+
currentMessages: params.currentMessages.slice(-5).map((m) => ({
|
|
730
|
+
role: m.role === "tool" ? "assistant" : m.role,
|
|
731
|
+
contentPreview: m.content?.substring(0, 100) || ""
|
|
732
|
+
})),
|
|
733
|
+
memoryState: params.memoryState,
|
|
734
|
+
availableTools: params.availableTools
|
|
735
|
+
},
|
|
736
|
+
agentStack: params.agentStack
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function createPromptDiffEvent(params) {
|
|
740
|
+
return {
|
|
741
|
+
type: "prompt:diff",
|
|
742
|
+
iteration: params.iteration,
|
|
743
|
+
diff: {
|
|
744
|
+
messagesAdded: params.messagesAdded,
|
|
745
|
+
messagesRemoved: params.messagesRemoved,
|
|
746
|
+
totalMessages: params.totalMessages,
|
|
747
|
+
changes: params.changes,
|
|
748
|
+
contextGrowth: {
|
|
749
|
+
tokensBefore: params.tokensBefore,
|
|
750
|
+
tokensAfter: params.tokensAfter,
|
|
751
|
+
delta: params.tokensAfter - params.tokensBefore
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function createToolFilterEvent(params) {
|
|
757
|
+
return {
|
|
758
|
+
type: "tool:filter",
|
|
759
|
+
iteration: params.iteration,
|
|
760
|
+
filtering: {
|
|
761
|
+
before: {
|
|
762
|
+
totalTools: params.beforeTools.length,
|
|
763
|
+
tools: params.beforeTools
|
|
764
|
+
},
|
|
765
|
+
after: {
|
|
766
|
+
totalTools: params.afterTools.length,
|
|
767
|
+
tools: params.afterTools
|
|
768
|
+
},
|
|
769
|
+
filtered: params.filtered
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function createContextTrimEvent(params) {
|
|
774
|
+
return {
|
|
775
|
+
type: "context:trim",
|
|
776
|
+
iteration: params.iteration,
|
|
777
|
+
trimming: {
|
|
778
|
+
trigger: params.trigger,
|
|
779
|
+
before: {
|
|
780
|
+
messageCount: params.messageCountBefore,
|
|
781
|
+
estimatedTokens: params.tokensBefore
|
|
782
|
+
},
|
|
783
|
+
after: {
|
|
784
|
+
messageCount: params.messageCountAfter,
|
|
785
|
+
estimatedTokens: params.tokensAfter
|
|
786
|
+
},
|
|
787
|
+
removed: {
|
|
788
|
+
messageCount: params.messagesRemoved,
|
|
789
|
+
tokensRemoved: params.tokensRemoved,
|
|
790
|
+
contentPreview: params.contentPreview
|
|
791
|
+
},
|
|
792
|
+
strategy: params.strategy
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function createStoppingAnalysisEvent(params) {
|
|
797
|
+
return {
|
|
798
|
+
type: "stopping:analysis",
|
|
799
|
+
iteration: params.iteration,
|
|
800
|
+
conditions: params.conditions,
|
|
801
|
+
reasoning: params.reasoning,
|
|
802
|
+
metrics: {
|
|
803
|
+
iterationsUsed: params.iterationsUsed,
|
|
804
|
+
iterationsRemaining: params.iterationsRemaining,
|
|
805
|
+
timeElapsedMs: params.timeElapsedMs,
|
|
806
|
+
timeRemainingMs: params.timeRemainingMs,
|
|
807
|
+
toolCallsInLast3Iterations: params.toolCallsInLast3Iterations,
|
|
808
|
+
confidenceScore: params.confidenceScore
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function createLLMValidationEvent(params) {
|
|
813
|
+
return {
|
|
814
|
+
type: "llm:validation",
|
|
815
|
+
iteration: params.iteration,
|
|
816
|
+
validation: {
|
|
817
|
+
stopReason: params.stopReason,
|
|
818
|
+
isValid: params.isValid,
|
|
819
|
+
checks: {
|
|
820
|
+
hasContent: params.hasContent,
|
|
821
|
+
hasToolCalls: params.hasToolCalls,
|
|
822
|
+
toolCallsValid: params.toolCallsValid,
|
|
823
|
+
jsonParseable: params.jsonParseable,
|
|
824
|
+
schemaValid: params.schemaValid
|
|
825
|
+
},
|
|
826
|
+
issues: params.issues
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function createFactAddedEvent(params) {
|
|
831
|
+
return {
|
|
832
|
+
type: "memory:fact_added",
|
|
833
|
+
iteration: params.iteration,
|
|
834
|
+
fact: params.fact,
|
|
835
|
+
factSheetStats: params.factSheetStats
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function createArchiveStoreEvent(params) {
|
|
839
|
+
return {
|
|
840
|
+
type: "memory:archive_store",
|
|
841
|
+
iteration: params.iteration,
|
|
842
|
+
entry: params.entry,
|
|
843
|
+
archiveStats: params.archiveStats
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function createSummarizationLLMCallEvent(params) {
|
|
847
|
+
return {
|
|
848
|
+
type: "memory:summarization_llm_call",
|
|
849
|
+
iteration: params.iteration,
|
|
850
|
+
prompt: params.prompt,
|
|
851
|
+
rawResponse: params.rawResponse,
|
|
852
|
+
parseSuccess: params.parseSuccess,
|
|
853
|
+
parseError: params.parseError,
|
|
854
|
+
timing: {
|
|
855
|
+
durationMs: params.durationMs,
|
|
856
|
+
outputTokens: params.outputTokens
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function createSummarizationResultEvent(params) {
|
|
861
|
+
return {
|
|
862
|
+
type: "memory:summarization_result",
|
|
863
|
+
iteration: params.iteration,
|
|
864
|
+
input: params.input,
|
|
865
|
+
output: params.output,
|
|
866
|
+
delta: params.delta,
|
|
867
|
+
efficiency: params.efficiency
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export { DEFAULT_TRACE_CONFIG, FileTracer, IncrementalTraceWriter, TRACE_DIR_RELATIVE, createArchiveStoreEvent, createContextTrimEvent, createDecisionPointEvent, createDefaultPrivacyConfig, createErrorCapturedEvent, createFactAddedEvent, createIterationDetailEvent, createLLMCallEvent, createLLMValidationEvent, createMemorySnapshotEvent, createPromptDiffEvent, createStoppingAnalysisEvent, createSummarizationLLMCallEvent, createSummarizationResultEvent, createSynthesisForcedEvent, createToolExecutionEvent, createToolFilterEvent, formatTraceLoadError, loadTrace, redactPaths, redactSecretsFromString, redactTraceEvent, redactValue };
|
|
872
|
+
//# sourceMappingURL=index.js.map
|
|
873
|
+
//# sourceMappingURL=index.js.map
|