@jmtrin/opencode-kevin 0.1.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/README.md +248 -0
- package/dist/migrations/001_initial.sql +92 -0
- package/dist/migrations/002_indexes.sql +14 -0
- package/dist/plugin/ContextInjector.d.ts +27 -0
- package/dist/plugin/ContextInjector.js +131 -0
- package/dist/plugin/ContextInjector.js.map +1 -0
- package/dist/plugin/MemoryService.d.ts +49 -0
- package/dist/plugin/MemoryService.js +228 -0
- package/dist/plugin/MemoryService.js.map +1 -0
- package/dist/plugin/Migrate.d.ts +13 -0
- package/dist/plugin/Migrate.js +54 -0
- package/dist/plugin/Migrate.js.map +1 -0
- package/dist/plugin/Reflector.d.ts +30 -0
- package/dist/plugin/Reflector.js +117 -0
- package/dist/plugin/Reflector.js.map +1 -0
- package/dist/plugin/Retrospective.d.ts +13 -0
- package/dist/plugin/Retrospective.js +77 -0
- package/dist/plugin/Retrospective.js.map +1 -0
- package/dist/plugin/Store.d.ts +14 -0
- package/dist/plugin/Store.js +36 -0
- package/dist/plugin/Store.js.map +1 -0
- package/dist/plugin/ToolCallObserver.d.ts +28 -0
- package/dist/plugin/ToolCallObserver.js +150 -0
- package/dist/plugin/ToolCallObserver.js.map +1 -0
- package/dist/plugin/index.d.ts +9 -0
- package/dist/plugin/index.js +340 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/redact.d.ts +1 -0
- package/dist/plugin/redact.js +12 -0
- package/dist/plugin/redact.js.map +1 -0
- package/dist/plugin/sqlite-adapter.d.ts +12 -0
- package/dist/plugin/sqlite-adapter.js +27 -0
- package/dist/plugin/sqlite-adapter.js.map +1 -0
- package/dist/plugin/uuid.d.ts +1 -0
- package/dist/plugin/uuid.js +51 -0
- package/dist/plugin/uuid.js.map +1 -0
- package/migrations/001_initial.sql +92 -0
- package/migrations/002_indexes.sql +14 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Kevin
|
|
2
|
+
|
|
3
|
+
> Observe and learn: the learning layer OpenCode was missing.
|
|
4
|
+
|
|
5
|
+
Kevin is an [OpenCode](https://opencode.ai) plugin that **observes** every agent tool call, **learns** from failures by generating lessons, and **shares** what it learned proactively in future sessions. It does not plan, orchestrate, or compete with the plugin ecosystem. It only learns.
|
|
6
|
+
|
|
7
|
+
- **Local-first**: SQLite + FTS5, no external services, no network calls.
|
|
8
|
+
- **Global memory**: a single `~/.opencode-kevin/kevin.db` shared across all your projects (WAL mode → safe for concurrent sessions). No per-project folders.
|
|
9
|
+
- **Standalone**: works without any other plugin. With the ecosystem, it learns more richly.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### 1. Declare the plugin
|
|
16
|
+
|
|
17
|
+
Add Kevin to your OpenCode config. For **all projects** (global):
|
|
18
|
+
|
|
19
|
+
```jsonc
|
|
20
|
+
// ~/.config/opencode/opencode.jsonc
|
|
21
|
+
{
|
|
22
|
+
"$schema": "https://opencode.ai/config.json",
|
|
23
|
+
"plugin": [
|
|
24
|
+
"@jmtrin/opencode-kevin@latest"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For a **single project**, put the same `plugin` array in `./opencode.json` or `.opencode/opencode.json` at the project root.
|
|
30
|
+
|
|
31
|
+
### 2. Restart OpenCode
|
|
32
|
+
|
|
33
|
+
Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, OpenCode resolves the npm spec, caches the plugin in `~/.cache/opencode/packages/@jmtrin/opencode-kevin/`, and exposes five tools: `kevin_save`, `kevin_query`, `kevin_recall`, `kevin_status`, `kevin_retrospective`.
|
|
34
|
+
|
|
35
|
+
### 3. Where data lives
|
|
36
|
+
|
|
37
|
+
Kevin stores everything in a single **global, shared** location under your home directory — no per-project `.kevin/` folders:
|
|
38
|
+
|
|
39
|
+
| Path | Content |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `~/.opencode-kevin/kevin.db` | SQLite database (memories, tool calls, retrospectives). WAL mode → safe for concurrent OpenCode sessions across projects. |
|
|
42
|
+
| `~/.opencode-kevin/retrospectives/<session>.md` | Per-session retrospective markdown. |
|
|
43
|
+
|
|
44
|
+
Migrations run automatically on startup.
|
|
45
|
+
|
|
46
|
+
### Requirements
|
|
47
|
+
|
|
48
|
+
- Node.js >= 20
|
|
49
|
+
- OpenCode with plugin support (`@opencode-ai/plugin` >= 1.17)
|
|
50
|
+
|
|
51
|
+
### Verification
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run verify
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Checks Node version, SQLite, migration, MemoryService save/query, Reflector, ContextInjector, and TypeScript strict mode.
|
|
58
|
+
|
|
59
|
+
### Advanced (optional)
|
|
60
|
+
|
|
61
|
+
Override defaults via the plugin tuple form:
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
{
|
|
65
|
+
"plugin": [
|
|
66
|
+
["@jmtrin/opencode-kevin", {
|
|
67
|
+
"dbPath": "/custom/path/kevin.db",
|
|
68
|
+
"retrospectivesDir": "/custom/path/retrospectives",
|
|
69
|
+
"throttleMs": 120000
|
|
70
|
+
}]
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use `:memory:` for `dbPath` in tests.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Cycle: Observe → Learn → Share
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Tool call (success or failure)
|
|
83
|
+
│
|
|
84
|
+
▼
|
|
85
|
+
┌─────────────────┐
|
|
86
|
+
│ OBSERVE │ ToolCallObserver records every call
|
|
87
|
+
│ ToolCallObserver│ (tool, args redacted, success, duration, error_type)
|
|
88
|
+
└────────┬────────┘
|
|
89
|
+
│ on failure
|
|
90
|
+
▼
|
|
91
|
+
┌─────────────────┐
|
|
92
|
+
│ LEARN │ Reflector generates a heuristic lesson
|
|
93
|
+
│ Reflector │ redacts paths/secrets, throttled 1/min,
|
|
94
|
+
└────────┬────────┘ persists type:error memory
|
|
95
|
+
│
|
|
96
|
+
▼
|
|
97
|
+
┌─────────────────┐
|
|
98
|
+
│ SHARE │ ContextInjector injects relevant lessons
|
|
99
|
+
│ ContextInjector │ pre-prompt (1500 tokens) and on compacting (2000 tokens)
|
|
100
|
+
└────────┬────────┘
|
|
101
|
+
│ session.idle
|
|
102
|
+
▼
|
|
103
|
+
┌─────────────────┐
|
|
104
|
+
│ RETROSPECTIVE │ Retrospective generates ~/.opencode-kevin/retrospectives/<session>.md
|
|
105
|
+
└─────────────────┘ with summary of failures and lessons
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Tools
|
|
111
|
+
|
|
112
|
+
Kevin exposes 5 tools callable by the agent:
|
|
113
|
+
|
|
114
|
+
### `kevin_save`
|
|
115
|
+
|
|
116
|
+
Saves an explicit memory.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
kevin_save({ type: "decision", content: "We use vitest for tests", scope: "project" })
|
|
120
|
+
// → { "id": "0195a3b2-..." }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`type`: `error` | `pattern` | `decision` | `context`. `scope`: `project` (persists) | `session` (TTL 24h).
|
|
124
|
+
|
|
125
|
+
### `kevin_query`
|
|
126
|
+
|
|
127
|
+
Searches memories by text (FTS5 + bm25).
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
kevin_query({ query: "typecheck", type: "error", limit: 5 })
|
|
131
|
+
// → [{ "id": "...", "type": "error", "content": "...", "scope": "project" }, ...]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `kevin_recall`
|
|
135
|
+
|
|
136
|
+
Retrieves relevant memories (greedy fill by relevance). Without `query`, returns all memories in scope.
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
kevin_recall({ query: "auth", limit: 3 })
|
|
140
|
+
// → [{ "id": "...", "type": "decision", ... }, ...]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### `kevin_status`
|
|
144
|
+
|
|
145
|
+
Global counts.
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
kevin_status({})
|
|
149
|
+
// → { "memories": 42, "tool_calls": 318, "retrospectives": 7 }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `kevin_retrospective`
|
|
153
|
+
|
|
154
|
+
Generates a retrospective for a session (uses current session if `session_id` is omitted).
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
kevin_retrospective({ session_id: "sess-abc" })
|
|
158
|
+
// → { "file_path": "~/.opencode-kevin/retrospectives/sess-abc.md" }
|
|
159
|
+
// or → { "message": "No failures in session sess-abc." }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Hooks
|
|
165
|
+
|
|
166
|
+
Kevin subscribes to 6 OpenCode hooks:
|
|
167
|
+
|
|
168
|
+
| Hook | What Kevin does |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `tool.execute.before` | Records tool call start (callID + redacted args) |
|
|
171
|
+
| `tool.execute.after` | Records result; on failure → Reflector.invoke async (throttled) |
|
|
172
|
+
| `experimental.chat.system.transform` | Injects relevant lessons in `<kevin-context>` (1500 tokens) |
|
|
173
|
+
| `experimental.session.compacting` | Re-injects lessons in `<kevin-memory>` after compacting (2000 tokens) |
|
|
174
|
+
| `event` (`session.created`) | Captures current `sessionID` |
|
|
175
|
+
| `event` (`session.idle`) | Generates retrospective.md for the session |
|
|
176
|
+
|
|
177
|
+
**Redaction**: absolute paths (`C:\Users\...`, `/home/...`) → `<path>` and secrets (`API_KEY=`, `Bearer`, `token`) → `<redacted>` before persisting anything.
|
|
178
|
+
|
|
179
|
+
**Throttle**: Reflector generates at most 1 lesson per minute (configurable via `throttleMs`).
|
|
180
|
+
|
|
181
|
+
**Truncation**: content > 4KB keeps the lesson searchable; only the additional context is truncated (`metadata.truncated = true`).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Configuration
|
|
186
|
+
|
|
187
|
+
Kevin accepts options via the plugin's tuple form (see Installation → Advanced). Programmatic defaults:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { KevinPlugin } from "@jmtrin/opencode-kevin";
|
|
191
|
+
|
|
192
|
+
// defaults
|
|
193
|
+
KevinPlugin(input, {
|
|
194
|
+
dbPath: "~/.opencode-kevin/kevin.db", // or ":memory:" for tests
|
|
195
|
+
migrationsDir: "<package>/dist/migrations", // resolved automatically
|
|
196
|
+
retrospectivesDir: "~/.opencode-kevin/retrospectives",
|
|
197
|
+
throttleMs: 60_000,
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
git clone https://github.com/jmtrin/opencode-kevin.git
|
|
207
|
+
cd opencode-kevin
|
|
208
|
+
npm install
|
|
209
|
+
npm run typecheck # tsc --noEmit (strict)
|
|
210
|
+
npm run lint # biome check .
|
|
211
|
+
npm test # vitest run (unit + integration + e2e)
|
|
212
|
+
npm run verify # post-install verification
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Publishing (maintainer)
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npm login # as the jmtrin account that owns the @jmtrin scope
|
|
219
|
+
npm publish --access public
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`prepublishOnly` runs `npm run build` (tsc + copy migrations) automatically. The `files` field ships only `dist/plugin`, `dist/migrations`, and `migrations`. `dist/` is gitignored and rebuilt on publish.
|
|
223
|
+
|
|
224
|
+
### Structure
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
plugin/
|
|
228
|
+
index.ts # Entry point: KevinPlugin
|
|
229
|
+
Store.ts # Wrapper better-sqlite3 (WAL, FK, transactions)
|
|
230
|
+
Migrate.ts # Idempotent migrations
|
|
231
|
+
MemoryService.ts # save/query/getRelevant (FTS5 + bm25)
|
|
232
|
+
ToolCallObserver.ts # onBefore/onAfter + redact + inferErrorType
|
|
233
|
+
Reflector.ts # Heuristic lessons + throttle + truncation
|
|
234
|
+
ContextInjector.ts # deriveQuery + pre-prompt/compacting injection
|
|
235
|
+
Retrospective.ts # Generates retrospective.md + table insert
|
|
236
|
+
migrations/
|
|
237
|
+
001_initial.sql # schema: memories, tool_calls, retrospectives
|
|
238
|
+
002_indexes.sql # FTS5 + indexes
|
|
239
|
+
tests/{unit,integration,e2e}/
|
|
240
|
+
scripts/
|
|
241
|
+
copy-migrations.mjs # build step: copies *.sql to dist/migrations
|
|
242
|
+
verify-install.ts # npm run verify
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Kevin 0.1.0 — Schema inicial
|
|
3
|
+
-- ============================================================
|
|
4
|
+
|
|
5
|
+
-- Tabla de versiones para migraciones
|
|
6
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
7
|
+
version TEXT PRIMARY KEY,
|
|
8
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- ============================================================
|
|
12
|
+
-- memories: lecciones aprendidas
|
|
13
|
+
-- ============================================================
|
|
14
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
type TEXT NOT NULL CHECK(type IN ('error', 'pattern', 'decision', 'context')),
|
|
17
|
+
content TEXT NOT NULL,
|
|
18
|
+
scope TEXT NOT NULL DEFAULT 'project' CHECK(scope IN ('project', 'session')),
|
|
19
|
+
relevance_score REAL DEFAULT 0.5,
|
|
20
|
+
source_tool TEXT,
|
|
21
|
+
source_session TEXT,
|
|
22
|
+
metadata TEXT,
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
expires_at TEXT
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_memories_relevance ON memories(relevance_score DESC);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
|
|
32
|
+
|
|
33
|
+
-- FTS5: búsqueda full-text con remoción de diacríticos (mejor para español)
|
|
34
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
35
|
+
content,
|
|
36
|
+
content='memories',
|
|
37
|
+
tokenize='unicode61 remove_diacritics 1'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
-- Triggers para mantener FTS5 sincronizado (FTS5 external-content se indexa por rowid)
|
|
41
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
42
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
43
|
+
END;
|
|
44
|
+
|
|
45
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
46
|
+
INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
|
|
47
|
+
END;
|
|
48
|
+
|
|
49
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
50
|
+
INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
|
|
51
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
52
|
+
END;
|
|
53
|
+
|
|
54
|
+
-- ============================================================
|
|
55
|
+
-- tool_calls: observación de tool calls del agente
|
|
56
|
+
-- ============================================================
|
|
57
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
session_id TEXT NOT NULL,
|
|
60
|
+
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
|
61
|
+
tool TEXT NOT NULL,
|
|
62
|
+
args_summary TEXT,
|
|
63
|
+
success INTEGER NOT NULL CHECK(success IN (0,1)),
|
|
64
|
+
duration_ms INTEGER,
|
|
65
|
+
agent TEXT,
|
|
66
|
+
error_type TEXT,
|
|
67
|
+
metadata TEXT
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_ts ON tool_calls(ts);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_success ON tool_calls(success);
|
|
74
|
+
|
|
75
|
+
-- ============================================================
|
|
76
|
+
-- retrospectives: resúmenes de sesión
|
|
77
|
+
-- ============================================================
|
|
78
|
+
CREATE TABLE IF NOT EXISTS retrospectives (
|
|
79
|
+
id TEXT PRIMARY KEY,
|
|
80
|
+
session_id TEXT NOT NULL,
|
|
81
|
+
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
|
82
|
+
failure_count INTEGER DEFAULT 0,
|
|
83
|
+
success_count INTEGER DEFAULT 0,
|
|
84
|
+
lessons_count INTEGER DEFAULT 0,
|
|
85
|
+
file_path TEXT,
|
|
86
|
+
metadata TEXT
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
-- ============================================================
|
|
90
|
+
-- Seed: versión inicial
|
|
91
|
+
-- ============================================================
|
|
92
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES ('001');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Kevin 0.1.1 — Migration 002: indexes adicionales
|
|
3
|
+
-- ============================================================
|
|
4
|
+
|
|
5
|
+
-- F#29: uniqueness on retrospectives.session_id
|
|
6
|
+
-- Prevents duplicate retrospective rows under concurrent session.idle events.
|
|
7
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_retrospectives_session
|
|
8
|
+
ON retrospectives(session_id);
|
|
9
|
+
|
|
10
|
+
-- F#31: index on memories.expires_at
|
|
11
|
+
-- Every query/queryRelevant/loadAll filters WHERE (expires_at IS NULL OR expires_at > datetime('now')).
|
|
12
|
+
-- Without this index the filter is a linear scan on large tables.
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_memories_expires
|
|
14
|
+
ON memories(expires_at);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MemoryService } from "./MemoryService.js";
|
|
2
|
+
export interface ChatMessage {
|
|
3
|
+
role: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SystemTransformInput {
|
|
7
|
+
sessionID?: string;
|
|
8
|
+
messages: ChatMessage[];
|
|
9
|
+
}
|
|
10
|
+
export interface SystemTransformOutput {
|
|
11
|
+
system: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface CompactingInput {
|
|
14
|
+
sessionID: string;
|
|
15
|
+
messages: ChatMessage[];
|
|
16
|
+
}
|
|
17
|
+
export interface CompactingOutput {
|
|
18
|
+
context: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare class ContextInjector {
|
|
21
|
+
private memoryService;
|
|
22
|
+
constructor(memoryService: MemoryService);
|
|
23
|
+
deriveQuery(messages: ChatMessage[]): string;
|
|
24
|
+
onSystemTransform(input: SystemTransformInput, output: SystemTransformOutput): void;
|
|
25
|
+
onCompacting(input: CompactingInput, output: CompactingOutput): void;
|
|
26
|
+
private formatMemories;
|
|
27
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const SYSTEM_TRANSFORM_TOKENS = 1500;
|
|
2
|
+
const COMPACTING_TOKENS = 2000;
|
|
3
|
+
const STOP_WORDS = new Set([
|
|
4
|
+
"a",
|
|
5
|
+
"an",
|
|
6
|
+
"and",
|
|
7
|
+
"are",
|
|
8
|
+
"at",
|
|
9
|
+
"be",
|
|
10
|
+
"been",
|
|
11
|
+
"but",
|
|
12
|
+
"by",
|
|
13
|
+
"did",
|
|
14
|
+
"do",
|
|
15
|
+
"does",
|
|
16
|
+
"el",
|
|
17
|
+
"eso",
|
|
18
|
+
"for",
|
|
19
|
+
"how",
|
|
20
|
+
"i",
|
|
21
|
+
"if",
|
|
22
|
+
"in",
|
|
23
|
+
"is",
|
|
24
|
+
"it",
|
|
25
|
+
"la",
|
|
26
|
+
"las",
|
|
27
|
+
"los",
|
|
28
|
+
"mi",
|
|
29
|
+
"my",
|
|
30
|
+
"o",
|
|
31
|
+
"of",
|
|
32
|
+
"on",
|
|
33
|
+
"or",
|
|
34
|
+
"para",
|
|
35
|
+
"por",
|
|
36
|
+
"que",
|
|
37
|
+
"she",
|
|
38
|
+
"su",
|
|
39
|
+
"that",
|
|
40
|
+
"the",
|
|
41
|
+
"this",
|
|
42
|
+
"to",
|
|
43
|
+
"tu",
|
|
44
|
+
"un",
|
|
45
|
+
"una",
|
|
46
|
+
"we",
|
|
47
|
+
"were",
|
|
48
|
+
"what",
|
|
49
|
+
"when",
|
|
50
|
+
"where",
|
|
51
|
+
"which",
|
|
52
|
+
"who",
|
|
53
|
+
"why",
|
|
54
|
+
"with",
|
|
55
|
+
"y",
|
|
56
|
+
"you",
|
|
57
|
+
"como",
|
|
58
|
+
"con",
|
|
59
|
+
"de",
|
|
60
|
+
"en",
|
|
61
|
+
"he",
|
|
62
|
+
"they",
|
|
63
|
+
"was",
|
|
64
|
+
"sin",
|
|
65
|
+
]);
|
|
66
|
+
function isWordChar(ch) {
|
|
67
|
+
return /[a-z0-9áéíóúüñ]/i.test(ch);
|
|
68
|
+
}
|
|
69
|
+
export class ContextInjector {
|
|
70
|
+
memoryService;
|
|
71
|
+
constructor(memoryService) {
|
|
72
|
+
this.memoryService = memoryService;
|
|
73
|
+
}
|
|
74
|
+
deriveQuery(messages) {
|
|
75
|
+
let lastUserContent = "";
|
|
76
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
77
|
+
if (messages[i].role === "user") {
|
|
78
|
+
lastUserContent = messages[i].content;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!lastUserContent)
|
|
83
|
+
return "";
|
|
84
|
+
const tokens = lastUserContent
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.split(/\s+/)
|
|
87
|
+
.map((t) => {
|
|
88
|
+
let out = "";
|
|
89
|
+
for (const ch of t) {
|
|
90
|
+
if (isWordChar(ch))
|
|
91
|
+
out += ch;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
})
|
|
95
|
+
.filter((t) => t.length > 0 && !STOP_WORDS.has(t));
|
|
96
|
+
return tokens.join(" ");
|
|
97
|
+
}
|
|
98
|
+
onSystemTransform(input, output) {
|
|
99
|
+
const query = this.deriveQuery(input.messages);
|
|
100
|
+
if (!query)
|
|
101
|
+
return;
|
|
102
|
+
const memories = this.memoryService.getRelevant({
|
|
103
|
+
query,
|
|
104
|
+
maxTokens: SYSTEM_TRANSFORM_TOKENS,
|
|
105
|
+
});
|
|
106
|
+
if (memories.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
output.system.push(this.formatMemories(memories, "context"));
|
|
109
|
+
}
|
|
110
|
+
onCompacting(input, output) {
|
|
111
|
+
const query = this.deriveQuery(input.messages);
|
|
112
|
+
if (!query)
|
|
113
|
+
return;
|
|
114
|
+
const memories = this.memoryService.getRelevant({
|
|
115
|
+
query,
|
|
116
|
+
maxTokens: COMPACTING_TOKENS,
|
|
117
|
+
});
|
|
118
|
+
if (memories.length === 0)
|
|
119
|
+
return;
|
|
120
|
+
output.context.push(this.formatMemories(memories, "memory"));
|
|
121
|
+
}
|
|
122
|
+
formatMemories(memories, format) {
|
|
123
|
+
const lines = memories.map((m) => `[${m.type}] ${m.content}`);
|
|
124
|
+
const body = lines.join("\n");
|
|
125
|
+
if (format === "context") {
|
|
126
|
+
return `<kevin-context>Lecciones relevantes:\n${body}\n</kevin-context>`;
|
|
127
|
+
}
|
|
128
|
+
return `<kevin-memory>\n${body}\n</kevin-memory>`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=ContextInjector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextInjector.js","sourceRoot":"","sources":["../../plugin/ContextInjector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Store } from "./Store.js";
|
|
2
|
+
export type MemoryType = "error" | "pattern" | "decision" | "context";
|
|
3
|
+
export type MemoryScope = "project" | "session";
|
|
4
|
+
export interface Memory {
|
|
5
|
+
id: string;
|
|
6
|
+
type: MemoryType;
|
|
7
|
+
content: string;
|
|
8
|
+
scope: MemoryScope;
|
|
9
|
+
relevanceScore: number;
|
|
10
|
+
sourceTool?: string | null;
|
|
11
|
+
sourceSession?: string | null;
|
|
12
|
+
metadata?: Record<string, unknown> | null;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
expiresAt?: string | null;
|
|
16
|
+
}
|
|
17
|
+
export interface SaveInput {
|
|
18
|
+
type: MemoryType;
|
|
19
|
+
content: string;
|
|
20
|
+
scope?: MemoryScope;
|
|
21
|
+
relevanceScore?: number;
|
|
22
|
+
sourceTool?: string;
|
|
23
|
+
sourceSession?: string;
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
expiresAt?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface QueryInput {
|
|
28
|
+
text: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
scope?: MemoryScope | "all";
|
|
31
|
+
limit?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface GetRelevantInput {
|
|
34
|
+
query?: string;
|
|
35
|
+
maxTokens?: number;
|
|
36
|
+
scope?: MemoryScope | "all";
|
|
37
|
+
}
|
|
38
|
+
export declare class MemoryService {
|
|
39
|
+
private store;
|
|
40
|
+
constructor(store: Store);
|
|
41
|
+
save(input: SaveInput): string;
|
|
42
|
+
getById(id: string): Memory | null;
|
|
43
|
+
update(id: string, fields: Partial<Memory>): void;
|
|
44
|
+
delete(id: string): void;
|
|
45
|
+
query(input: QueryInput): Memory[];
|
|
46
|
+
private loadAll;
|
|
47
|
+
private queryRelevant;
|
|
48
|
+
getRelevant(input: GetRelevantInput): Memory[];
|
|
49
|
+
}
|