agentic-kdd 3.2.3 → 3.5.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/.cursorrules +169 -0
- package/AGENTS.md +173 -0
- package/CLAUDE.md +375 -0
- package/autonomous-decision.cjs +915 -0
- package/bin/akdd.js +56 -0
- package/collab-manager.cjs +62 -15
- package/kdd-memory.cjs +579 -0
- package/knowledge-validator.cjs +408 -0
- package/package.json +1 -1
- package/telemetry.cjs +400 -0
package/telemetry.cjs
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentic KDD — Telemetry v1.0
|
|
3
|
+
* Brecha (c): Observabilidad — requisito duro de L4
|
|
4
|
+
*
|
|
5
|
+
* Sin telemetría no hay L4. L4 (Approver) exige que el usuario pueda
|
|
6
|
+
* auditar la ejecución. La aprobación a nivel de objetivo requiere trazas
|
|
7
|
+
* inmutables de qué hizo el agente y por qué.
|
|
8
|
+
*
|
|
9
|
+
* Implementa:
|
|
10
|
+
* - JSONL append-only en .agentic/telemetria/ (auditable en git)
|
|
11
|
+
* - Spans con: timestamp, agente, fase, acción, resultado, duración
|
|
12
|
+
* - Export opcional a Langfuse (self-hosted MIT, ~21k ★)
|
|
13
|
+
* - STOPs siempre registrados con razón completa
|
|
14
|
+
* - Memory reads/writes registrados (cuándo se usó recall, qué se recordó)
|
|
15
|
+
*
|
|
16
|
+
* La filosofía: cada ciclo genera un archivo trace_[ciclo_id].jsonl
|
|
17
|
+
* Cada línea es un span. El archivo completo es la auditoría del ciclo.
|
|
18
|
+
* Git lo versiona. El equipo lo puede revisar. El EU AI Act lo exige.
|
|
19
|
+
*
|
|
20
|
+
* Uso:
|
|
21
|
+
* const { startSpan, endSpan, recordStop, recordMemoryRead } = require('./telemetry.cjs');
|
|
22
|
+
* const span = startSpan({ agent: 'Analista', phase: 'plan', task: 'fix auth' });
|
|
23
|
+
* // ... work ...
|
|
24
|
+
* endSpan(span, { outcome: 'PASS', files_touched: ['auth.ts'] });
|
|
25
|
+
*
|
|
26
|
+
* CLI:
|
|
27
|
+
* node telemetry.cjs view [ciclo_id] — ver trazas de un ciclo
|
|
28
|
+
* node telemetry.cjs summary — resumen de telemetría
|
|
29
|
+
* node telemetry.cjs export langfuse — exportar a Langfuse
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const path = require('path');
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const crypto = require('crypto');
|
|
37
|
+
|
|
38
|
+
const TELEMETRIA_DIR = '.agentic/telemetria';
|
|
39
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB por archivo
|
|
40
|
+
|
|
41
|
+
// ─── ESTRUCTURA DE UN SPAN ────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Span de telemetría. Inmutable una vez cerrado.
|
|
44
|
+
*
|
|
45
|
+
* {
|
|
46
|
+
* span_id: "abc123",
|
|
47
|
+
* trace_id: "ciclo_xyz", // = ciclo_id del ciclo actual
|
|
48
|
+
* parent_id: "def456", // null si es root span
|
|
49
|
+
* timestamp: "2026-06-24T...", // ISO8601 UTC
|
|
50
|
+
* duration_ms: 1234, // null si el span no cerró
|
|
51
|
+
* agent: "Analista", // Orquestador|Analista|Dev|QA|Memoria|ContractGuard
|
|
52
|
+
* phase: "plan", // plan|build|tdd|qa|preservation|review|memory|creative
|
|
53
|
+
* action: "recall", // recall|remember|decide|execute|verify|stop|warn
|
|
54
|
+
* input: { query: "..." }, // lo que recibió
|
|
55
|
+
* output: { results: 3 }, // lo que produjo
|
|
56
|
+
* outcome: "PASS", // PASS|FAIL|STOP|WARN|SKIP
|
|
57
|
+
* metadata: {}
|
|
58
|
+
* }
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
// ─── GESTIÓN DE ARCHIVOS ──────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function ensureDir(projectRoot) {
|
|
64
|
+
const dir = path.join(projectRoot, TELEMETRIA_DIR);
|
|
65
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
return dir;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getTraceFile(projectRoot, traceId) {
|
|
70
|
+
const dir = ensureDir(projectRoot);
|
|
71
|
+
return path.join(dir, `trace_${traceId || 'session'}.jsonl`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function appendLine(filePath, obj) {
|
|
75
|
+
try {
|
|
76
|
+
const line = JSON.stringify(obj) + '\n';
|
|
77
|
+
fs.appendFileSync(filePath, line, 'utf8');
|
|
78
|
+
return true;
|
|
79
|
+
} catch { return false; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readTrace(filePath) {
|
|
83
|
+
if (!fs.existsSync(filePath)) return [];
|
|
84
|
+
try {
|
|
85
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
86
|
+
.split('\n')
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.map(line => JSON.parse(line));
|
|
89
|
+
} catch { return []; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── SPAN MANAGEMENT ─────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
const activeSpans = {};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Inicia un span de telemetría.
|
|
98
|
+
* @returns span object con span_id
|
|
99
|
+
*/
|
|
100
|
+
function startSpan(params = {}, projectRoot) {
|
|
101
|
+
const spanId = crypto.randomBytes(4).toString('hex');
|
|
102
|
+
const traceId = params.trace_id || params.ciclo_id || 'session';
|
|
103
|
+
|
|
104
|
+
const span = {
|
|
105
|
+
span_id: spanId,
|
|
106
|
+
trace_id: traceId,
|
|
107
|
+
parent_id: params.parent_id || null,
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
_start_ms: Date.now(),
|
|
110
|
+
agent: params.agent || 'unknown',
|
|
111
|
+
phase: params.phase || 'unknown',
|
|
112
|
+
action: params.action || 'execute',
|
|
113
|
+
input: params.input || {},
|
|
114
|
+
outcome: null,
|
|
115
|
+
output: null,
|
|
116
|
+
metadata: params.metadata || {},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
activeSpans[spanId] = { span, projectRoot: projectRoot || process.cwd() };
|
|
120
|
+
|
|
121
|
+
// Escribir span abierto (para auditoría de crashes)
|
|
122
|
+
const file = getTraceFile(span._start_ms && projectRoot, traceId);
|
|
123
|
+
try {
|
|
124
|
+
const { _start_ms, ...logSpan } = span;
|
|
125
|
+
appendLine(file, { ...logSpan, status: 'open' });
|
|
126
|
+
} catch {}
|
|
127
|
+
|
|
128
|
+
return span;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Cierra un span con resultado.
|
|
133
|
+
*/
|
|
134
|
+
function endSpan(span, result = {}, projectRoot) {
|
|
135
|
+
if (!span || !span.span_id) return;
|
|
136
|
+
|
|
137
|
+
const root = projectRoot || activeSpans[span.span_id]?.projectRoot || process.cwd();
|
|
138
|
+
const durationMs = Date.now() - (span._start_ms || Date.now());
|
|
139
|
+
|
|
140
|
+
const closedSpan = {
|
|
141
|
+
span_id: span.span_id,
|
|
142
|
+
trace_id: span.trace_id,
|
|
143
|
+
parent_id: span.parent_id,
|
|
144
|
+
timestamp: span.timestamp,
|
|
145
|
+
closed_at: new Date().toISOString(),
|
|
146
|
+
duration_ms: durationMs,
|
|
147
|
+
agent: span.agent,
|
|
148
|
+
phase: span.phase,
|
|
149
|
+
action: span.action,
|
|
150
|
+
input: span.input,
|
|
151
|
+
output: result.output || {},
|
|
152
|
+
outcome: result.outcome || 'PASS',
|
|
153
|
+
files_touched: result.files_touched || [],
|
|
154
|
+
metadata: { ...span.metadata, ...result.metadata },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const file = getTraceFile(root, span.trace_id);
|
|
158
|
+
appendLine(file, closedSpan);
|
|
159
|
+
|
|
160
|
+
delete activeSpans[span.span_id];
|
|
161
|
+
return closedSpan;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── HELPERS PARA EVENTOS ESPECÍFICOS ────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function recordStop(reason, context = {}, projectRoot) {
|
|
167
|
+
const root = projectRoot || process.cwd();
|
|
168
|
+
const file = getTraceFile(root, context.ciclo_id || 'session');
|
|
169
|
+
|
|
170
|
+
const stopEvent = {
|
|
171
|
+
span_id: crypto.randomBytes(4).toString('hex'),
|
|
172
|
+
trace_id: context.ciclo_id || 'session',
|
|
173
|
+
timestamp: new Date().toISOString(),
|
|
174
|
+
agent: context.agent || 'Harness',
|
|
175
|
+
phase: context.phase || 'gate',
|
|
176
|
+
action: 'stop',
|
|
177
|
+
outcome: 'STOP',
|
|
178
|
+
reason,
|
|
179
|
+
context,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
appendLine(file, stopEvent);
|
|
183
|
+
return stopEvent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function recordMemoryRead(query, results = [], context = {}, projectRoot) {
|
|
187
|
+
const root = projectRoot || process.cwd();
|
|
188
|
+
const file = getTraceFile(root, context.ciclo_id || 'session');
|
|
189
|
+
|
|
190
|
+
appendLine(file, {
|
|
191
|
+
span_id: crypto.randomBytes(4).toString('hex'),
|
|
192
|
+
trace_id: context.ciclo_id || 'session',
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
agent: context.agent || 'Analista',
|
|
195
|
+
phase: context.phase || 'plan',
|
|
196
|
+
action: 'recall',
|
|
197
|
+
outcome: 'PASS',
|
|
198
|
+
input: { query },
|
|
199
|
+
output: { results_count: results.length, top_result: results[0]?.titulo },
|
|
200
|
+
tokens_saved: context.tokens_saved || null,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function recordMemoryWrite(entry, result = {}, context = {}, projectRoot) {
|
|
205
|
+
const root = projectRoot || process.cwd();
|
|
206
|
+
const file = getTraceFile(root, context.ciclo_id || 'session');
|
|
207
|
+
|
|
208
|
+
appendLine(file, {
|
|
209
|
+
span_id: crypto.randomBytes(4).toString('hex'),
|
|
210
|
+
trace_id: context.ciclo_id || 'session',
|
|
211
|
+
timestamp: new Date().toISOString(),
|
|
212
|
+
agent: context.agent || 'Memoria',
|
|
213
|
+
phase: context.phase || 'memory',
|
|
214
|
+
action: 'remember',
|
|
215
|
+
outcome: result.ok ? 'PASS' : 'FAIL',
|
|
216
|
+
input: { entry: (entry || '').substring(0, 100) },
|
|
217
|
+
output: { id: result.id, reason: result.reason },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function recordDecision(decision, context = {}, projectRoot) {
|
|
222
|
+
const root = projectRoot || process.cwd();
|
|
223
|
+
const file = getTraceFile(root, context.ciclo_id || 'session');
|
|
224
|
+
|
|
225
|
+
appendLine(file, {
|
|
226
|
+
span_id: crypto.randomBytes(4).toString('hex'),
|
|
227
|
+
trace_id: context.ciclo_id || 'session',
|
|
228
|
+
timestamp: new Date().toISOString(),
|
|
229
|
+
agent: 'AutonomousDecision',
|
|
230
|
+
phase: context.phase || 'analysis',
|
|
231
|
+
action: 'decide',
|
|
232
|
+
outcome: decision.decision,
|
|
233
|
+
input: { files: context.files, task: context.task },
|
|
234
|
+
output: { decision: decision.decision, summary: decision.summary },
|
|
235
|
+
blast_radius: decision.blast?.level,
|
|
236
|
+
reasons: decision.reasons,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── EXPORT A LANGFUSE ────────────────────────────────────────────────────────
|
|
241
|
+
/**
|
|
242
|
+
* Export opcional a Langfuse self-hosted.
|
|
243
|
+
* Config en .agentic/config.md: langfuse_host, langfuse_pk, langfuse_sk
|
|
244
|
+
*/
|
|
245
|
+
async function exportToLangfuse(traceId, projectRoot) {
|
|
246
|
+
projectRoot = projectRoot || process.cwd();
|
|
247
|
+
|
|
248
|
+
// Leer config de Langfuse
|
|
249
|
+
let langfuseConfig = null;
|
|
250
|
+
try {
|
|
251
|
+
const config = fs.readFileSync(path.join(projectRoot, '.agentic/config.md'), 'utf8');
|
|
252
|
+
const host = config.match(/langfuse_host:\s*(.+)/)?.[1]?.trim();
|
|
253
|
+
const pk = config.match(/langfuse_pk:\s*(.+)/)?.[1]?.trim();
|
|
254
|
+
const sk = config.match(/langfuse_sk:\s*(.+)/)?.[1]?.trim();
|
|
255
|
+
if (host && pk && sk) langfuseConfig = { host, pk, sk };
|
|
256
|
+
} catch {}
|
|
257
|
+
|
|
258
|
+
if (!langfuseConfig) {
|
|
259
|
+
return { ok: false, reason: 'Langfuse not configured. Add langfuse_host/pk/sk to .agentic/config.md' };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const dir = ensureDir(projectRoot);
|
|
263
|
+
const file = path.join(dir, `trace_${traceId}.jsonl`);
|
|
264
|
+
const spans = readTrace(file);
|
|
265
|
+
|
|
266
|
+
if (spans.length === 0) return { ok: false, reason: 'No spans found' };
|
|
267
|
+
|
|
268
|
+
// Enviar a Langfuse API (OTEL format)
|
|
269
|
+
try {
|
|
270
|
+
const body = {
|
|
271
|
+
batch: spans.map(span => ({
|
|
272
|
+
id: span.span_id,
|
|
273
|
+
timestamp: span.timestamp,
|
|
274
|
+
type: 'span',
|
|
275
|
+
body: {
|
|
276
|
+
id: span.span_id,
|
|
277
|
+
traceId: span.trace_id,
|
|
278
|
+
name: `${span.agent}.${span.action}`,
|
|
279
|
+
startTime: span.timestamp,
|
|
280
|
+
endTime: span.closed_at || span.timestamp,
|
|
281
|
+
metadata: { ...span.metadata, outcome: span.outcome },
|
|
282
|
+
input: span.input,
|
|
283
|
+
output: span.output,
|
|
284
|
+
statusMessage:span.outcome === 'STOP' ? span.reason : null,
|
|
285
|
+
},
|
|
286
|
+
})),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const resp = await fetch(`${langfuseConfig.host}/api/public/ingestion`, {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: {
|
|
292
|
+
'Content-Type': 'application/json',
|
|
293
|
+
'Authorization': `Basic ${Buffer.from(`${langfuseConfig.pk}:${langfuseConfig.sk}`).toString('base64')}`,
|
|
294
|
+
},
|
|
295
|
+
body: JSON.stringify(body),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return { ok: resp.ok, status: resp.status, spans_exported: spans.length };
|
|
299
|
+
} catch (e) {
|
|
300
|
+
return { ok: false, error: e.message };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── VIEW & SUMMARY ───────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function viewTrace(traceId, projectRoot) {
|
|
307
|
+
projectRoot = projectRoot || process.cwd();
|
|
308
|
+
const dir = path.join(projectRoot, TELEMETRIA_DIR);
|
|
309
|
+
|
|
310
|
+
if (!fs.existsSync(dir)) return [];
|
|
311
|
+
|
|
312
|
+
let file;
|
|
313
|
+
if (traceId) {
|
|
314
|
+
file = path.join(dir, `trace_${traceId}.jsonl`);
|
|
315
|
+
} else {
|
|
316
|
+
// Último archivo
|
|
317
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort().reverse();
|
|
318
|
+
if (!files.length) return [];
|
|
319
|
+
file = path.join(dir, files[0]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return readTrace(file);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getSummary(projectRoot) {
|
|
326
|
+
projectRoot = projectRoot || process.cwd();
|
|
327
|
+
const dir = path.join(projectRoot, TELEMETRIA_DIR);
|
|
328
|
+
|
|
329
|
+
if (!fs.existsSync(dir)) return { traces: 0, total_spans: 0, stops: 0 };
|
|
330
|
+
|
|
331
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
332
|
+
let totalSpans = 0, stops = 0, recalls = 0, remembers = 0;
|
|
333
|
+
|
|
334
|
+
files.forEach(f => {
|
|
335
|
+
const spans = readTrace(path.join(dir, f));
|
|
336
|
+
totalSpans += spans.length;
|
|
337
|
+
stops += spans.filter(s => s.outcome === 'STOP').length;
|
|
338
|
+
recalls += spans.filter(s => s.action === 'recall').length;
|
|
339
|
+
remembers += spans.filter(s => s.action === 'remember').length;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
traces: files.length,
|
|
344
|
+
total_spans: totalSpans,
|
|
345
|
+
stops,
|
|
346
|
+
recalls,
|
|
347
|
+
remembers,
|
|
348
|
+
avg_spans_per_trace: files.length > 0 ? Math.round(totalSpans / files.length) : 0,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
if (require.main === module) {
|
|
355
|
+
const [,, cmd, arg] = process.argv;
|
|
356
|
+
const projectRoot = process.cwd();
|
|
357
|
+
|
|
358
|
+
switch (cmd) {
|
|
359
|
+
case 'view': {
|
|
360
|
+
const spans = viewTrace(arg, projectRoot);
|
|
361
|
+
if (!spans.length) { console.log('\n No traces found.\n'); break; }
|
|
362
|
+
console.log(`\n Trace ${arg || 'latest'} — ${spans.length} spans\n`);
|
|
363
|
+
spans.forEach(s => {
|
|
364
|
+
const icon = s.outcome === 'STOP' ? '🛑' : s.outcome === 'WARN' ? '⚠️' : '✅';
|
|
365
|
+
const dur = s.duration_ms ? `${s.duration_ms}ms` : '';
|
|
366
|
+
console.log(` ${icon} ${s.agent}.${s.action} [${s.phase}] ${dur}`);
|
|
367
|
+
if (s.outcome === 'STOP') console.log(` STOP: ${s.reason}`);
|
|
368
|
+
});
|
|
369
|
+
console.log('');
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
case 'summary': {
|
|
374
|
+
const s = getSummary(projectRoot);
|
|
375
|
+
console.log('\n Telemetry Summary');
|
|
376
|
+
console.log(` Trace files: ${s.traces}`);
|
|
377
|
+
console.log(` Total spans: ${s.total_spans}`);
|
|
378
|
+
console.log(` STOPs: ${s.stops}`);
|
|
379
|
+
console.log(` Recalls: ${s.recalls}`);
|
|
380
|
+
console.log(` Remembers: ${s.remembers}\n`);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
case 'export':
|
|
385
|
+
if (arg === 'langfuse') {
|
|
386
|
+
exportToLangfuse(process.argv[4], projectRoot)
|
|
387
|
+
.then(r => console.log(r.ok ? `✅ Exported ${r.spans_exported} spans` : `❌ ${r.reason || r.error}`));
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
|
|
391
|
+
default:
|
|
392
|
+
console.log('Uso: node telemetry.cjs [view [trace_id] | summary | export langfuse <trace_id>]');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
startSpan, endSpan,
|
|
398
|
+
recordStop, recordMemoryRead, recordMemoryWrite, recordDecision,
|
|
399
|
+
exportToLangfuse, viewTrace, getSummary,
|
|
400
|
+
};
|