@jmtrin/opencode-kevin 0.3.0 → 0.5.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 +117 -9
- package/dist/migrations/005_v04_signal.sql +57 -0
- package/dist/migrations/006_v05_glassbox.sql +118 -0
- package/dist/plugin/Archiver.d.ts +42 -0
- package/dist/plugin/Archiver.js +98 -0
- package/dist/plugin/Archiver.js.map +1 -0
- package/dist/plugin/CausalChain.d.ts +13 -2
- package/dist/plugin/CausalChain.js +83 -13
- package/dist/plugin/CausalChain.js.map +1 -1
- package/dist/plugin/ContextInjector.d.ts +152 -4
- package/dist/plugin/ContextInjector.js +400 -93
- package/dist/plugin/ContextInjector.js.map +1 -1
- package/dist/plugin/Feedback.d.ts +67 -0
- package/dist/plugin/Feedback.js +137 -0
- package/dist/plugin/Feedback.js.map +1 -0
- package/dist/plugin/InjectionLedger.d.ts +92 -0
- package/dist/plugin/InjectionLedger.js +243 -0
- package/dist/plugin/InjectionLedger.js.map +1 -0
- package/dist/plugin/LessonFixer.d.ts +44 -0
- package/dist/plugin/LessonFixer.js +46 -0
- package/dist/plugin/LessonFixer.js.map +1 -0
- package/dist/plugin/MemoryService.d.ts +81 -1
- package/dist/plugin/MemoryService.js +321 -54
- package/dist/plugin/MemoryService.js.map +1 -1
- package/dist/plugin/Migrate.js +31 -0
- package/dist/plugin/Migrate.js.map +1 -1
- package/dist/plugin/QualityGate.d.ts +110 -0
- package/dist/plugin/QualityGate.js +108 -0
- package/dist/plugin/QualityGate.js.map +1 -0
- package/dist/plugin/Reflector.d.ts +17 -0
- package/dist/plugin/Reflector.js +81 -26
- package/dist/plugin/Reflector.js.map +1 -1
- package/dist/plugin/Retrospective.js +22 -1
- package/dist/plugin/Retrospective.js.map +1 -1
- package/dist/plugin/ToolCallObserver.d.ts +1 -0
- package/dist/plugin/ToolCallObserver.js +15 -8
- package/dist/plugin/ToolCallObserver.js.map +1 -1
- package/dist/plugin/confidence.d.ts +8 -0
- package/dist/plugin/confidence.js +35 -0
- package/dist/plugin/confidence.js.map +1 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +368 -44
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/kevin_audit.d.ts +49 -0
- package/dist/plugin/kevin_audit.js +141 -0
- package/dist/plugin/kevin_audit.js.map +1 -0
- package/dist/plugin/kevin_why.d.ts +4 -0
- package/dist/plugin/kevin_why.js +72 -35
- package/dist/plugin/kevin_why.js.map +1 -1
- package/dist/plugin/memory-format.d.ts +12 -0
- package/dist/plugin/memory-format.js +45 -5
- package/dist/plugin/memory-format.js.map +1 -1
- package/dist/plugin/metrics.d.ts +27 -1
- package/dist/plugin/metrics.js +61 -0
- package/dist/plugin/metrics.js.map +1 -1
- package/dist/plugin/okf-export.js +63 -22
- package/dist/plugin/okf-export.js.map +1 -1
- package/dist/plugin/okf-import.d.ts +4 -1
- package/dist/plugin/okf-import.js +45 -12
- package/dist/plugin/okf-import.js.map +1 -1
- package/dist/plugin/query-tokenizer.d.ts +13 -0
- package/dist/plugin/query-tokenizer.js +86 -0
- package/dist/plugin/query-tokenizer.js.map +1 -0
- package/dist/plugin/replay-types.d.ts +327 -0
- package/dist/plugin/replay-types.js +65 -0
- package/dist/plugin/replay-types.js.map +1 -0
- package/dist/plugin/replay.d.ts +36 -0
- package/dist/plugin/replay.js +193 -0
- package/dist/plugin/replay.js.map +1 -0
- package/migrations/005_v04_signal.sql +57 -0
- package/migrations/006_v05_glassbox.sql +118 -0
- package/package.json +3 -2
package/dist/plugin/index.js
CHANGED
|
@@ -3,8 +3,11 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { tool } from "@opencode-ai/plugin";
|
|
6
|
+
import { Archiver } from "./Archiver.js";
|
|
6
7
|
import { CausalChain } from "./CausalChain.js";
|
|
7
8
|
import { ContextInjector } from "./ContextInjector.js";
|
|
9
|
+
import { Feedback } from "./Feedback.js";
|
|
10
|
+
import { InjectionLedger } from "./InjectionLedger.js";
|
|
8
11
|
import { MemoryService, } from "./MemoryService.js";
|
|
9
12
|
import { Migrate } from "./Migrate.js";
|
|
10
13
|
import { PatternMiner } from "./PatternMiner.js";
|
|
@@ -12,13 +15,30 @@ import { ERROR_LINE_RE, Reflector, STRONG_ERROR_RE } from "./Reflector.js";
|
|
|
12
15
|
import { Retrospective } from "./Retrospective.js";
|
|
13
16
|
import { Store } from "./Store.js";
|
|
14
17
|
import { ToolCallObserver } from "./ToolCallObserver.js";
|
|
18
|
+
import { fingerprint } from "./fingerprint.js";
|
|
19
|
+
import { buildAudit } from "./kevin_audit.js";
|
|
15
20
|
import { kevinWhy } from "./kevin_why.js";
|
|
16
|
-
import {
|
|
17
|
-
import { Metrics, estimateTokens } from "./metrics.js";
|
|
21
|
+
import { Metrics } from "./metrics.js";
|
|
18
22
|
import { exportMarkdown, exportOkf } from "./okf-export.js";
|
|
19
23
|
import { importOkf } from "./okf-import.js";
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
/**
|
|
25
|
+
* v0.4.0 (K4-021) — settings surfaced by `kevin_config` (plan §8.8).
|
|
26
|
+
* Unknown keys are rejected on `set` unless `strict: false`.
|
|
27
|
+
*/
|
|
28
|
+
export const KEVIN_CONFIG_KEYS = [
|
|
29
|
+
"quality_gate_enabled",
|
|
30
|
+
"lesson_snippet_injection",
|
|
31
|
+
"patternminer_enabled",
|
|
32
|
+
"cross_project_enabled",
|
|
33
|
+
"llm_reflection_enabled",
|
|
34
|
+
"tool_calls_dedup_enabled",
|
|
35
|
+
// v0.5.0 (K5-003 / plan §8.13) — omitting these makes `kevin_config set`
|
|
36
|
+
// return { error: "unknown_key" } while `kevin_config list` still shows
|
|
37
|
+
// the keys seeded by migration 006.
|
|
38
|
+
"deterministic_retrieval",
|
|
39
|
+
"pre_prompt_budget_tokens",
|
|
40
|
+
"archive_after_days",
|
|
41
|
+
];
|
|
22
42
|
function resolveMigrationsDir() {
|
|
23
43
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
44
|
return join(here, "..", "migrations");
|
|
@@ -35,6 +55,10 @@ export const KevinPlugin = async (input, options) => {
|
|
|
35
55
|
const migrationsDir = opts.migrationsDir ?? resolveMigrationsDir();
|
|
36
56
|
await new Migrate(store, migrationsDir).run();
|
|
37
57
|
const metrics = new Metrics(store);
|
|
58
|
+
// v0.4.0 (K4-019): the plugin hooks expose no project field, so the
|
|
59
|
+
// project id is derived once from the plugin host's working directory
|
|
60
|
+
// (plan §5.7 fallback; D2-11 project scoping wired into the live path).
|
|
61
|
+
const projectId = fingerprint(process.cwd());
|
|
38
62
|
const memoryService = new MemoryService(store, metrics);
|
|
39
63
|
const observer = new ToolCallObserver(store, metrics);
|
|
40
64
|
// v0.3.0 fix — Prepared UPDATE used by Reflector.onLinkError to stamp
|
|
@@ -53,14 +77,31 @@ export const KevinPlugin = async (input, options) => {
|
|
|
53
77
|
linkErrorStmt.run(fp, callID);
|
|
54
78
|
},
|
|
55
79
|
}, metrics);
|
|
56
|
-
|
|
80
|
+
// v0.4.0 (K4-017): the single injection path. The inline transform/
|
|
81
|
+
// compacting logic was removed; the ContextInjector now owns the whole
|
|
82
|
+
// pipeline (QualityGate admission + snippet payload + ledger rows),
|
|
83
|
+
// so ledger and injector share the same store.
|
|
84
|
+
const ledger = new InjectionLedger(store, metrics);
|
|
85
|
+
const injector = new ContextInjector(memoryService, metrics, ledger);
|
|
86
|
+
// v0.5.0 (K5-009/011 / plan §5.3) — human feedback component behind the
|
|
87
|
+
// `kevin_feedback` tool; 'ignore' verdicts also stamp memories.ignored.
|
|
88
|
+
const feedback = new Feedback(store, metrics);
|
|
89
|
+
// v0.5.0 (K5-012 / plan §5.4) — retires stale memories at session.idle.
|
|
90
|
+
const archiver = new Archiver(store, memoryService, metrics);
|
|
57
91
|
const retrospective = new Retrospective(store, memoryService, {
|
|
58
92
|
dir: opts.retrospectivesDir,
|
|
59
93
|
}, metrics);
|
|
60
94
|
const patternMiner = new PatternMiner(store, memoryService, metrics);
|
|
61
95
|
const causalChain = new CausalChain(store, memoryService, metrics);
|
|
62
96
|
let currentSessionId = null;
|
|
97
|
+
// BUG-011 — process-global last derived query. Cleared on
|
|
98
|
+
// `session.idle`; the per-session map below is the preferred source.
|
|
63
99
|
let lastUserQuery = null;
|
|
100
|
+
// v0.4.0 (K4-018) — compaction may fire without a recent chat.message
|
|
101
|
+
// (auto-compact after a long tool turn, resumed sessions). Keep the
|
|
102
|
+
// last derived query PER SESSION so the compacting hook can always
|
|
103
|
+
// resolve a query for the session it runs in.
|
|
104
|
+
const lastUserQueryBySession = new Map();
|
|
64
105
|
const pending = new Set();
|
|
65
106
|
const toolCache = new Map();
|
|
66
107
|
const TOOL_CACHE_MAX = 500;
|
|
@@ -105,6 +146,7 @@ export const KevinPlugin = async (input, options) => {
|
|
|
105
146
|
errorType,
|
|
106
147
|
sessionId: sessionID,
|
|
107
148
|
callID,
|
|
149
|
+
projectId,
|
|
108
150
|
}));
|
|
109
151
|
}
|
|
110
152
|
return {
|
|
@@ -172,6 +214,10 @@ export const KevinPlugin = async (input, options) => {
|
|
|
172
214
|
type: args.type,
|
|
173
215
|
limit: args.limit,
|
|
174
216
|
full: args.full === true,
|
|
217
|
+
// BUG-001 — pass the flag through so the slim mapper
|
|
218
|
+
// can include the evidence fields (the old code cast
|
|
219
|
+
// SlimMemory rows to Memory and read undefined fields).
|
|
220
|
+
evidence: args.evidence === true,
|
|
175
221
|
});
|
|
176
222
|
const rows = args.full === true
|
|
177
223
|
? memories.map((m) => ({
|
|
@@ -180,7 +226,7 @@ export const KevinPlugin = async (input, options) => {
|
|
|
180
226
|
content: m.content,
|
|
181
227
|
scope: m.scope,
|
|
182
228
|
}))
|
|
183
|
-
: memories.map((m
|
|
229
|
+
: memories.map((m) => {
|
|
184
230
|
const base = {
|
|
185
231
|
id: m.id,
|
|
186
232
|
type: m.type,
|
|
@@ -189,12 +235,14 @@ export const KevinPlugin = async (input, options) => {
|
|
|
189
235
|
snippet: m.snippet,
|
|
190
236
|
};
|
|
191
237
|
if (args.evidence === true) {
|
|
192
|
-
|
|
238
|
+
// SlimMemoryWithEvidence — carried by the
|
|
239
|
+
// mapper, no per-row getById needed.
|
|
240
|
+
const ev = m;
|
|
193
241
|
return {
|
|
194
242
|
...base,
|
|
195
|
-
confidence:
|
|
196
|
-
evidence_count:
|
|
197
|
-
last_verified_at:
|
|
243
|
+
confidence: ev.confidence ?? null,
|
|
244
|
+
evidence_count: ev.evidence_count ?? null,
|
|
245
|
+
last_verified_at: ev.last_verified_at ?? null,
|
|
198
246
|
};
|
|
199
247
|
}
|
|
200
248
|
return base;
|
|
@@ -238,6 +286,15 @@ export const KevinPlugin = async (input, options) => {
|
|
|
238
286
|
fingerprint: mem.fingerprint ?? null,
|
|
239
287
|
origin: mem.origin ?? null,
|
|
240
288
|
metadata: mem.metadata ?? null,
|
|
289
|
+
// BUG-010 — v0.3/v0.4 evidence fields so kevin_get
|
|
290
|
+
// is the full-fidelity read path (mapRow already
|
|
291
|
+
// computes confidence via computeConfidence).
|
|
292
|
+
confidence: mem.confidence ?? null,
|
|
293
|
+
evidence_count: mem.evidenceCount ?? null,
|
|
294
|
+
recurrence_count: mem.recurrenceCount ?? null,
|
|
295
|
+
last_verified_at: mem.lastVerifiedAt ?? null,
|
|
296
|
+
status: mem.status ?? "active",
|
|
297
|
+
fix_args: mem.fixArgs ?? null,
|
|
241
298
|
}),
|
|
242
299
|
};
|
|
243
300
|
},
|
|
@@ -296,6 +353,45 @@ export const KevinPlugin = async (input, options) => {
|
|
|
296
353
|
const memoriesAgent = byOrigin.agent ?? 0;
|
|
297
354
|
const memoriesPattern = byOrigin.pattern ?? 0;
|
|
298
355
|
const memoriesCausal = byOrigin.causal ?? 0;
|
|
356
|
+
// v0.4.0 (K4-024): per-origin recurrence_count totals
|
|
357
|
+
// (plan §8.8 — honest negative-evidence breakdown).
|
|
358
|
+
// Best-effort: DBs pre-005 lack the column; the rest of
|
|
359
|
+
// the block must still work (same pattern as
|
|
360
|
+
// ledger.settle in session.idle).
|
|
361
|
+
let recurrenceByOrigin = {};
|
|
362
|
+
try {
|
|
363
|
+
const recRows = store
|
|
364
|
+
.prepare(`SELECT origin, SUM(recurrence_count) as c
|
|
365
|
+
FROM memories
|
|
366
|
+
GROUP BY origin`)
|
|
367
|
+
.all();
|
|
368
|
+
for (const r of recRows) {
|
|
369
|
+
recurrenceByOrigin[r.origin ?? "agent"] = r.c;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
recurrenceByOrigin = {};
|
|
374
|
+
}
|
|
375
|
+
// v0.5.0 (K5-021 / plan §5.7) — lifecycle counts; best-effort
|
|
376
|
+
// on pre-006 DBs (columns may be missing).
|
|
377
|
+
let memoriesIgnored = 0;
|
|
378
|
+
let memoriesArchived = 0;
|
|
379
|
+
try {
|
|
380
|
+
memoriesIgnored = store
|
|
381
|
+
.prepare("SELECT COUNT(*) as c FROM memories WHERE ignored = 1")
|
|
382
|
+
.get().c;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
memoriesIgnored = 0;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
memoriesArchived = store
|
|
389
|
+
.prepare("SELECT COUNT(*) as c FROM memories WHERE status = 'archived'")
|
|
390
|
+
.get().c;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
memoriesArchived = 0;
|
|
394
|
+
}
|
|
299
395
|
return {
|
|
300
396
|
title: "Estado de Kevin",
|
|
301
397
|
output: JSON.stringify({
|
|
@@ -307,10 +403,48 @@ export const KevinPlugin = async (input, options) => {
|
|
|
307
403
|
tool_calls: toolCallCount.c,
|
|
308
404
|
retrospectives: retroCount.c,
|
|
309
405
|
metrics: metrics.snapshot(),
|
|
406
|
+
// v0.4.0 (K4-008): precision block. The
|
|
407
|
+
// human-facing "patterns promoted" reading is
|
|
408
|
+
// `patterns_promoted_new` (K4-009 corrected the
|
|
409
|
+
// inflated `patterns_causal` counter; its key
|
|
410
|
+
// stays frozen in metrics for compatibility).
|
|
411
|
+
precision_rate: metrics.precisionRate(),
|
|
412
|
+
injections_total: metrics.get("injections_total"),
|
|
413
|
+
injections_effective: metrics.get("injections_effective"),
|
|
414
|
+
injections_ineffective: metrics.get("injections_ineffective"),
|
|
415
|
+
patterns_promoted_new: metrics.get("patterns_promoted_new"),
|
|
416
|
+
// v0.4.0 (K4-024): per-origin recurrence totals.
|
|
417
|
+
recurrence_by_origin: recurrenceByOrigin,
|
|
418
|
+
// v0.5.0 (K5-021 / plan §5.7) — glassbox fields.
|
|
419
|
+
injections_inconclusive: metrics.get("injections_inconclusive"),
|
|
420
|
+
coverage_rate: metrics.coverageRate(),
|
|
421
|
+
blocked: metrics.blockedSnapshot(),
|
|
422
|
+
memories_ignored: memoriesIgnored,
|
|
423
|
+
memories_archived: memoriesArchived,
|
|
424
|
+
feedback: {
|
|
425
|
+
positive: metrics.get("feedback_positive_total"),
|
|
426
|
+
negative: metrics.get("feedback_negative_total"),
|
|
427
|
+
},
|
|
310
428
|
}),
|
|
311
429
|
};
|
|
312
430
|
},
|
|
313
431
|
}),
|
|
432
|
+
kevin_audit: tool({
|
|
433
|
+
description: 'Auditoria de solo lectura de la DB (v0.5.0, glassbox): memorias por status/origin/type, salud de inyecciones (precision/coverage), contadores bloqueados, feedback por verdicto, tokens inyectados. Sin writes, sin LLM. En DBs pre-006 devuelve los bloques computables con "partial": true. verbose añade el bloque settings.',
|
|
434
|
+
args: {
|
|
435
|
+
verbose: tool.schema.boolean().default(false),
|
|
436
|
+
},
|
|
437
|
+
async execute(args) {
|
|
438
|
+
const report = buildAudit(store, metrics);
|
|
439
|
+
const payload = args.verbose
|
|
440
|
+
? report
|
|
441
|
+
: { ...report, settings: undefined };
|
|
442
|
+
return {
|
|
443
|
+
title: "Auditoria de Kevin",
|
|
444
|
+
output: JSON.stringify(payload),
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
}),
|
|
314
448
|
kevin_retrospective: tool({
|
|
315
449
|
description: "Genera un retrospective markdown para una sesion (resume tools que fallaron y lecciones aprendidas).",
|
|
316
450
|
args: {
|
|
@@ -361,6 +495,90 @@ export const KevinPlugin = async (input, options) => {
|
|
|
361
495
|
};
|
|
362
496
|
},
|
|
363
497
|
}),
|
|
498
|
+
kevin_feedback: tool({
|
|
499
|
+
description: "Reporta juicio humano sobre una memoria (v0.5.0). verdict: useful | wrong | outdated | ignore. 'ignore' excluye la memoria de retrieval e inyeccion (D5-07); los demas ajustan confidence via computeConfidence (D5-02).",
|
|
500
|
+
args: {
|
|
501
|
+
memory_id: tool.schema.string().min(1),
|
|
502
|
+
verdict: tool.schema
|
|
503
|
+
.enum(["useful", "wrong", "outdated", "ignore"])
|
|
504
|
+
.describe("useful: la memoria ayudo. wrong: era incorrecta. outdated: ya no aplica. ignore: no volver a mostrarla."),
|
|
505
|
+
note: tool.schema.string().optional(),
|
|
506
|
+
session_id: tool.schema.string().optional(),
|
|
507
|
+
},
|
|
508
|
+
async execute(args) {
|
|
509
|
+
const memory = memoryService.getById(args.memory_id);
|
|
510
|
+
if (!memory) {
|
|
511
|
+
return {
|
|
512
|
+
title: "Feedback no registrado",
|
|
513
|
+
output: JSON.stringify({
|
|
514
|
+
message: `No memory found for id "${args.memory_id}".`,
|
|
515
|
+
}),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const feedbackId = feedback.record({
|
|
520
|
+
memoryId: args.memory_id,
|
|
521
|
+
verdict: args.verdict,
|
|
522
|
+
sessionId: args.session_id ?? currentSessionId ?? null,
|
|
523
|
+
note: args.note,
|
|
524
|
+
});
|
|
525
|
+
const counts = feedback.countsFor(args.memory_id);
|
|
526
|
+
return {
|
|
527
|
+
title: "Feedback registrado",
|
|
528
|
+
output: JSON.stringify({
|
|
529
|
+
feedback_id: feedbackId,
|
|
530
|
+
memory_id: args.memory_id,
|
|
531
|
+
verdict: args.verdict,
|
|
532
|
+
counters: counts,
|
|
533
|
+
ignored: args.verdict === "ignore" ? true : memory.ignored === true,
|
|
534
|
+
}),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
return {
|
|
539
|
+
title: "Feedback no registrado",
|
|
540
|
+
output: JSON.stringify({
|
|
541
|
+
message: err instanceof Error ? err.message : String(err),
|
|
542
|
+
}),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
kevin_trace: tool({
|
|
548
|
+
description: "Predice que memorias se inyectarian en el prompt para una query (v0.5.0, dry-run estricto). Retorna el plan: items admitidos/bloqueados con su razon, tokens totales estimados. NO inyecta, NO mueve contadores, NO muta el seen-set, NO hace bump de relevancia (D5-08). Si omites query, usa la ultima query derivada de la sesion.",
|
|
549
|
+
args: {
|
|
550
|
+
query: tool.schema.string().optional(),
|
|
551
|
+
session_id: tool.schema.string().optional(),
|
|
552
|
+
tag: tool.schema
|
|
553
|
+
.enum(["context", "memory"])
|
|
554
|
+
.default("context")
|
|
555
|
+
.describe("context = pre-prompt (cap por setting pre_prompt_budget_tokens, default 900), memory = compacting (cap 2000)"),
|
|
556
|
+
cap: tool.schema.number().int().positive().optional(),
|
|
557
|
+
},
|
|
558
|
+
async execute(args) {
|
|
559
|
+
const sid = args.session_id ?? currentSessionId ?? "";
|
|
560
|
+
const query = args.query ??
|
|
561
|
+
(sid ? (lastUserQueryBySession.get(sid) ?? null) : null) ??
|
|
562
|
+
lastUserQuery;
|
|
563
|
+
if (!query) {
|
|
564
|
+
return {
|
|
565
|
+
title: "Trace sin query",
|
|
566
|
+
output: JSON.stringify({
|
|
567
|
+
message: "No query derivada disponible. Pasa query explicitamente.",
|
|
568
|
+
}),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const plan = injector.plan(query, {
|
|
572
|
+
tag: args.tag,
|
|
573
|
+
cap: args.cap,
|
|
574
|
+
sessionId: sid,
|
|
575
|
+
});
|
|
576
|
+
return {
|
|
577
|
+
title: "Plan de inyeccion (dry-run)",
|
|
578
|
+
output: JSON.stringify(plan),
|
|
579
|
+
};
|
|
580
|
+
},
|
|
581
|
+
}),
|
|
364
582
|
kevin_export: tool({
|
|
365
583
|
description: "Exporta memorias curadas (decision, rule, pattern) como markdown o formato OKF. v0.3.0.",
|
|
366
584
|
args: {
|
|
@@ -392,6 +610,68 @@ export const KevinPlugin = async (input, options) => {
|
|
|
392
610
|
};
|
|
393
611
|
},
|
|
394
612
|
}),
|
|
613
|
+
kevin_config: tool({
|
|
614
|
+
description: "Lee o modifica settings de Kevin sin SQL (v0.4.0). list: todas las settings de kevin_settings. set: upsert de una key conocida (rechaza keys desconocidas salvo strict:false).",
|
|
615
|
+
args: {
|
|
616
|
+
action: tool.schema.enum(["list", "set"]).describe("list | set"),
|
|
617
|
+
key: tool.schema
|
|
618
|
+
.string()
|
|
619
|
+
.optional()
|
|
620
|
+
.describe("Clave a modificar (action=set)."),
|
|
621
|
+
value: tool.schema
|
|
622
|
+
.string()
|
|
623
|
+
.optional()
|
|
624
|
+
.describe("Nuevo valor (action=set). Default '1'."),
|
|
625
|
+
strict: tool.schema
|
|
626
|
+
.boolean()
|
|
627
|
+
.default(true)
|
|
628
|
+
.optional()
|
|
629
|
+
.describe("Con strict=true (default) se rechazan keys desconocidas."),
|
|
630
|
+
},
|
|
631
|
+
async execute(args) {
|
|
632
|
+
if (args.action === "list") {
|
|
633
|
+
const rows = store
|
|
634
|
+
.prepare("SELECT key, value FROM kevin_settings ORDER BY key")
|
|
635
|
+
.all();
|
|
636
|
+
const settings = {};
|
|
637
|
+
for (const r of rows)
|
|
638
|
+
settings[r.key] = r.value;
|
|
639
|
+
return {
|
|
640
|
+
title: "Configuracion de Kevin",
|
|
641
|
+
output: JSON.stringify(settings),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (!args.key) {
|
|
645
|
+
return {
|
|
646
|
+
title: "kevin_config",
|
|
647
|
+
output: JSON.stringify({
|
|
648
|
+
error: "missing_key",
|
|
649
|
+
action: "set",
|
|
650
|
+
}),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
const known = KEVIN_CONFIG_KEYS.includes(args.key);
|
|
654
|
+
if (!known && args.strict !== false) {
|
|
655
|
+
return {
|
|
656
|
+
title: "kevin_config",
|
|
657
|
+
output: JSON.stringify({
|
|
658
|
+
error: "unknown_key",
|
|
659
|
+
key: args.key,
|
|
660
|
+
known_keys: [...KEVIN_CONFIG_KEYS],
|
|
661
|
+
}),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
const value = args.value ?? "1";
|
|
665
|
+
store
|
|
666
|
+
.prepare(`INSERT INTO kevin_settings (key, value) VALUES (?, ?)
|
|
667
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
668
|
+
.run(args.key, value);
|
|
669
|
+
return {
|
|
670
|
+
title: "kevin_config",
|
|
671
|
+
output: JSON.stringify({ ok: true, key: args.key, value }),
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
}),
|
|
395
675
|
},
|
|
396
676
|
"tool.execute.before": async (hookInput, output) => {
|
|
397
677
|
rememberToolCall(hookInput.callID, hookInput.tool, output.args);
|
|
@@ -400,6 +680,7 @@ export const KevinPlugin = async (input, options) => {
|
|
|
400
680
|
args: output.args,
|
|
401
681
|
sessionId: hookInput.sessionID,
|
|
402
682
|
callID: hookInput.callID,
|
|
683
|
+
projectId,
|
|
403
684
|
}, {});
|
|
404
685
|
},
|
|
405
686
|
"tool.execute.after": async (hookInput, output) => {
|
|
@@ -427,6 +708,7 @@ export const KevinPlugin = async (input, options) => {
|
|
|
427
708
|
args: hookInput.args,
|
|
428
709
|
sessionId: hookInput.sessionID,
|
|
429
710
|
callID: hookInput.callID,
|
|
711
|
+
projectId,
|
|
430
712
|
}, { success, stdout, stderr, exitCode });
|
|
431
713
|
if (!success) {
|
|
432
714
|
const errorType = observer.inferErrorType(stderr, stdout, exitCode);
|
|
@@ -439,13 +721,14 @@ export const KevinPlugin = async (input, options) => {
|
|
|
439
721
|
errorType,
|
|
440
722
|
sessionId: hookInput.sessionID,
|
|
441
723
|
callID: hookInput.callID,
|
|
724
|
+
projectId,
|
|
442
725
|
}));
|
|
443
726
|
}
|
|
444
727
|
else {
|
|
445
|
-
causalChain.onSuccess(hookInput.tool, hookInput.args,
|
|
728
|
+
causalChain.onSuccess(hookInput.tool, hookInput.args, projectId, hookInput.sessionID);
|
|
446
729
|
}
|
|
447
730
|
},
|
|
448
|
-
"chat.message": async (
|
|
731
|
+
"chat.message": async (hookInput, output) => {
|
|
449
732
|
const text = output.parts
|
|
450
733
|
.map((p) => p)
|
|
451
734
|
.filter((p) => p.type === "text")
|
|
@@ -454,63 +737,104 @@ export const KevinPlugin = async (input, options) => {
|
|
|
454
737
|
if (text.trim()) {
|
|
455
738
|
const derived = injector.deriveQuery([{ role: "user", content: text }]);
|
|
456
739
|
lastUserQuery = derived.length > 0 ? derived : null;
|
|
740
|
+
if (hookInput.sessionID && lastUserQuery) {
|
|
741
|
+
lastUserQueryBySession.set(hookInput.sessionID, lastUserQuery);
|
|
742
|
+
}
|
|
457
743
|
}
|
|
458
744
|
},
|
|
459
|
-
"experimental.chat.system.transform": async (
|
|
460
|
-
|
|
745
|
+
"experimental.chat.system.transform": async (hookInput, output) => {
|
|
746
|
+
// BUG-011 — prefer the per-session query so a new session whose
|
|
747
|
+
// first transform fires before any chat.message cannot reuse the
|
|
748
|
+
// previous session's query (the global is cleared on idle).
|
|
749
|
+
const query = lastUserQueryBySession.get(hookInput.sessionID ?? "") ?? lastUserQuery;
|
|
750
|
+
if (!query)
|
|
461
751
|
return;
|
|
462
752
|
const suggestion = injector.generateSuggestion();
|
|
463
753
|
if (suggestion)
|
|
464
754
|
output.system.push(suggestion);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
});
|
|
470
|
-
const block = formatMemories(memories, "context");
|
|
471
|
-
if (block) {
|
|
472
|
-
output.system.push(block);
|
|
473
|
-
metrics.incr("tokens_injected_pre_prompt", estimateTokens(block));
|
|
474
|
-
}
|
|
755
|
+
injector.onSystemTransform({
|
|
756
|
+
sessionID: hookInput.sessionID ?? undefined,
|
|
757
|
+
messages: [{ role: "user", content: query }],
|
|
758
|
+
}, output);
|
|
475
759
|
},
|
|
476
|
-
"experimental.session.compacting": async (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
760
|
+
"experimental.session.compacting": async (hookInput, output) => {
|
|
761
|
+
// v0.4.0 (K4-018) — plan §5.6: compaction often fires with no
|
|
762
|
+
// recent chat.message (auto-compact after a long tool turn,
|
|
763
|
+
// resumed sessions). Resolve a query per session first, then
|
|
764
|
+
// the global fallback, then any messages the runtime may
|
|
765
|
+
// provide (defensive — the current SDK contract only exposes
|
|
766
|
+
// sessionID).
|
|
767
|
+
// BUG-012 — the HITL suggestion fires AT MOST ONCE per session:
|
|
768
|
+
// whichever hook (transform or compacting) runs first consumes
|
|
769
|
+
// the pending recurrence signal (generateSuggestion resets it).
|
|
480
770
|
const suggestion = injector.generateSuggestion();
|
|
481
771
|
if (suggestion)
|
|
482
772
|
output.context.push(suggestion);
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
773
|
+
const sid = hookInput.sessionID;
|
|
774
|
+
const sessionQuery = lastUserQueryBySession.get(sid) ?? lastUserQuery;
|
|
775
|
+
const runtimeMessages = hookInput
|
|
776
|
+
.messages;
|
|
777
|
+
const messages = sessionQuery != null
|
|
778
|
+
? [{ role: "user", content: sessionQuery }]
|
|
779
|
+
: (runtimeMessages ?? []);
|
|
780
|
+
injector.onCompacting({
|
|
781
|
+
sessionID: sid,
|
|
782
|
+
messages,
|
|
783
|
+
}, output);
|
|
493
784
|
},
|
|
494
785
|
event: async ({ event }) => {
|
|
495
786
|
const type = event.type;
|
|
496
787
|
const props = event.properties ?? {};
|
|
497
788
|
if (type === "session.created") {
|
|
498
789
|
const info = props.info;
|
|
499
|
-
if (info?.id)
|
|
790
|
+
if (info?.id) {
|
|
500
791
|
currentSessionId = info.id;
|
|
792
|
+
// BUG-011 — a fresh session must not inherit the
|
|
793
|
+
// previous session's derived query (the global may
|
|
794
|
+
// still hold it if no idle fired).
|
|
795
|
+
lastUserQueryBySession.delete(info.id);
|
|
796
|
+
// v0.4.0 (K4-017) — plan §5.1 rule 3: the per-session
|
|
797
|
+
// seen-set resets when a session is created.
|
|
798
|
+
injector.onSessionCreated(info.id);
|
|
799
|
+
}
|
|
501
800
|
}
|
|
502
801
|
else if (type === "session.idle") {
|
|
503
802
|
const sid = props.sessionID;
|
|
504
803
|
if (sid) {
|
|
505
804
|
toolCache.clear();
|
|
805
|
+
// BUG-011 — the session is done: drop the global query
|
|
806
|
+
// so the next session cannot reuse it.
|
|
807
|
+
lastUserQuery = null;
|
|
808
|
+
// v0.4.0 (K4-024) — plan §5.2: settle the session's
|
|
809
|
+
// unmeasured injections (effective/ineffective +
|
|
810
|
+
// recurrence_count charges) at idle. Best-effort: a
|
|
811
|
+
// legacy DB without migration 005 has no ledger table.
|
|
812
|
+
try {
|
|
813
|
+
ledger.settle(sid);
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
// best-effort: a legacy DB without the ledger
|
|
817
|
+
// table must not break the idle path
|
|
818
|
+
}
|
|
819
|
+
// v0.5.0 (K5-012 / plan §5.4) — retire stale lessons at
|
|
820
|
+
// idle; pre-006 DBs degrade to a no-op.
|
|
821
|
+
try {
|
|
822
|
+
archiver.run();
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
// best-effort, same pattern as ledger.settle
|
|
826
|
+
}
|
|
506
827
|
fireAndForget(retrospective.generate(sid));
|
|
507
828
|
memoryService.boostPositiveReflectors(sid);
|
|
508
829
|
const recurred = memoryService.penalizeRecurringReflectors(sid);
|
|
509
|
-
injector.setRecurrences(recurred);
|
|
510
|
-
patternMiner.mine();
|
|
511
|
-
fireAndForget(Promise.resolve()
|
|
512
|
-
causalChain.onSessionIdle(sid)
|
|
513
|
-
|
|
830
|
+
injector.setRecurrences(recurred, sid);
|
|
831
|
+
patternMiner.mine(projectId);
|
|
832
|
+
fireAndForget(Promise.resolve()
|
|
833
|
+
.then(() => causalChain.onSessionIdle(sid))
|
|
834
|
+
// non-blocking — promote is a best-effort pass
|
|
835
|
+
// (legacy DBs pre-005 lack the recurrence_count
|
|
836
|
+
// column)
|
|
837
|
+
.catch(() => { }));
|
|
514
838
|
}
|
|
515
839
|
metrics.flush();
|
|
516
840
|
}
|