tiger-agent 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.
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import glob
4
+ import json
5
+ import os
6
+ import sqlite3
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def run(cmd):
13
+ return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
14
+
15
+
16
+ def discover_extension_path() -> str:
17
+ try:
18
+ import sqlite_vec # type: ignore
19
+ except Exception:
20
+ return ""
21
+
22
+ # Preferred API if present.
23
+ loadable_fn = getattr(sqlite_vec, "loadable_path", None)
24
+ if callable(loadable_fn):
25
+ try:
26
+ path = str(loadable_fn() or "").strip()
27
+ if path and os.path.exists(path):
28
+ return path
29
+ except Exception:
30
+ pass
31
+
32
+ module_file = Path(getattr(sqlite_vec, "__file__", "")).resolve()
33
+ if not module_file.exists():
34
+ return ""
35
+ base = module_file.parent
36
+ patterns = ["*vec*.so", "*vec*.dylib", "*vec*.dll", "*.so", "*.dylib", "*.dll"]
37
+ for pat in patterns:
38
+ for candidate in glob.glob(str(base / pat)):
39
+ if os.path.isfile(candidate):
40
+ return candidate
41
+ return ""
42
+
43
+
44
+ def test_load(path: str):
45
+ if not path:
46
+ return False, "extension path is empty"
47
+ conn = sqlite3.connect(":memory:")
48
+ try:
49
+ conn.enable_load_extension(True)
50
+ conn.load_extension(path)
51
+ return True, ""
52
+ except Exception as err:
53
+ return False, str(err)
54
+ finally:
55
+ try:
56
+ conn.enable_load_extension(False)
57
+ except Exception:
58
+ pass
59
+ conn.close()
60
+
61
+
62
+ def set_env_sqlite_vec(env_path: str, extension_path: str):
63
+ p = Path(env_path)
64
+ text = p.read_text(encoding="utf-8") if p.exists() else ""
65
+ lines = text.splitlines()
66
+ replaced = False
67
+ out = []
68
+ for line in lines:
69
+ if line.startswith("SQLITE_VEC_EXTENSION="):
70
+ out.append(f"SQLITE_VEC_EXTENSION={extension_path}")
71
+ replaced = True
72
+ else:
73
+ out.append(line)
74
+ if not replaced:
75
+ out.append(f"SQLITE_VEC_EXTENSION={extension_path}")
76
+ p.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
77
+
78
+
79
+ def main() -> int:
80
+ parser = argparse.ArgumentParser()
81
+ parser.add_argument("--install", action="store_true", help="Attempt pip install sqlite-vec first")
82
+ parser.add_argument("--write-env", action="store_true", help="Write SQLITE_VEC_EXTENSION to .env")
83
+ parser.add_argument("--env-path", default=".env")
84
+ parser.add_argument("--extension-path", default="")
85
+ args = parser.parse_args()
86
+
87
+ install_error = ""
88
+ if args.install:
89
+ res = run([sys.executable, "-m", "pip", "install", "sqlite-vec"])
90
+ if res.returncode != 0:
91
+ install_error = (res.stderr or res.stdout or "").strip()
92
+
93
+ extension_path = args.extension_path.strip() or discover_extension_path()
94
+ ok, load_error = test_load(extension_path)
95
+
96
+ if ok and args.write_env:
97
+ set_env_sqlite_vec(args.env_path, extension_path)
98
+
99
+ out = {
100
+ "ok": ok,
101
+ "sqlite_version": sqlite3.sqlite_version,
102
+ "extension_path": extension_path,
103
+ "loaded": ok,
104
+ "load_error": load_error,
105
+ "install_error": install_error,
106
+ }
107
+ sys.stdout.write(json.dumps(out))
108
+ return 0 if ok else 1
109
+
110
+
111
+ if __name__ == "__main__":
112
+ raise SystemExit(main())
@@ -0,0 +1,30 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { dataDir } = require('../config');
4
+ const { ensureDir } = require('../utils');
5
+
6
+ const files = ['soul.md', 'human.md', 'human2.md', 'ownskill.md'];
7
+
8
+ function ensureContextFiles() {
9
+ ensureDir(dataDir);
10
+ for (const name of files) {
11
+ const full = path.join(dataDir, name);
12
+ if (!fs.existsSync(full)) {
13
+ fs.writeFileSync(full, `# ${name.replace('.md', '')}\n\n`, 'utf8');
14
+ }
15
+ }
16
+ }
17
+
18
+ function loadContextFiles() {
19
+ ensureContextFiles();
20
+ return files.map((name) => {
21
+ const full = path.join(dataDir, name);
22
+ const content = fs.readFileSync(full, 'utf8');
23
+ return { name, full, content };
24
+ });
25
+ }
26
+
27
+ module.exports = {
28
+ ensureContextFiles,
29
+ loadContextFiles
30
+ };
@@ -0,0 +1,349 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFileSync } = require('child_process');
4
+ const { ensureDir, cosineSimilarity } = require('../utils');
5
+ const { dbPath, maxMessages, recentMessages, vectorDbPath, sqliteVecExtension } = require('../config');
6
+
7
+ ensureDir(path.dirname(dbPath));
8
+ ensureDir(path.dirname(vectorDbPath));
9
+
10
+ const sqliteMemoryScript = path.resolve(process.cwd(), 'scripts', 'sqlite_memory.py');
11
+ let sqliteMemoryReady = false;
12
+ let sqliteVecLoaded = false;
13
+ let sqliteInitError = '';
14
+
15
+ function runSqliteMemory(args) {
16
+ if (!fs.existsSync(sqliteMemoryScript)) {
17
+ throw new Error('sqlite memory helper script is missing');
18
+ }
19
+ const out = execFileSync('python3', [sqliteMemoryScript, ...args], {
20
+ encoding: 'utf8',
21
+ stdio: ['ignore', 'pipe', 'pipe']
22
+ });
23
+ const parsed = JSON.parse(String(out || '{}'));
24
+ if (parsed && parsed.ok === false) {
25
+ throw new Error(String(parsed.error || 'sqlite memory helper failed'));
26
+ }
27
+ return parsed;
28
+ }
29
+
30
+ function ensureSqliteMemoryReady() {
31
+ if (sqliteMemoryReady) return;
32
+ try {
33
+ const args = ['init', '--db', vectorDbPath];
34
+ if (sqliteVecExtension) {
35
+ args.push('--vec-ext', sqliteVecExtension);
36
+ }
37
+ const initResult = runSqliteMemory(args);
38
+ sqliteVecLoaded = Boolean(initResult.vec_loaded);
39
+ sqliteInitError = String(initResult.vec_error || '');
40
+ sqliteMemoryReady = true;
41
+ } catch (err) {
42
+ sqliteMemoryReady = false;
43
+ sqliteVecLoaded = false;
44
+ sqliteInitError = String(err.message || err);
45
+ }
46
+ }
47
+
48
+ function initVectorMemory() {
49
+ ensureSqliteMemoryReady();
50
+ if (sqliteMemoryReady) {
51
+ let counts = null;
52
+ try {
53
+ const statsResult = runSqliteMemory(['stats', '--db', vectorDbPath]);
54
+ counts = statsResult && statsResult.counts ? statsResult.counts : null;
55
+ } catch (err) {
56
+ counts = null;
57
+ }
58
+ return {
59
+ ok: true,
60
+ backend: 'sqlite',
61
+ dbPath: vectorDbPath,
62
+ sqliteVecLoaded,
63
+ sqliteVecExtension,
64
+ sqliteInitError,
65
+ counts
66
+ };
67
+ }
68
+ return {
69
+ ok: false,
70
+ backend: 'json-fallback',
71
+ dbPath: dbPath,
72
+ sqliteVecLoaded: false,
73
+ sqliteVecExtension,
74
+ sqliteInitError,
75
+ counts: null
76
+ };
77
+ }
78
+
79
+ function defaultState() {
80
+ return {
81
+ conversations: {},
82
+ messages: [],
83
+ memories: [],
84
+ meta: {}
85
+ };
86
+ }
87
+
88
+ function loadState() {
89
+ if (!fs.existsSync(dbPath)) {
90
+ return defaultState();
91
+ }
92
+ try {
93
+ const parsed = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
94
+ if (!parsed || typeof parsed !== 'object') return defaultState();
95
+ return {
96
+ conversations: parsed.conversations || {},
97
+ messages: Array.isArray(parsed.messages) ? parsed.messages : [],
98
+ memories: Array.isArray(parsed.memories) ? parsed.memories : [],
99
+ meta: parsed.meta && typeof parsed.meta === 'object' ? parsed.meta : {}
100
+ };
101
+ } catch (err) {
102
+ return defaultState();
103
+ }
104
+ }
105
+
106
+ const state = loadState();
107
+
108
+ function saveState() {
109
+ fs.writeFileSync(dbPath, JSON.stringify(state, null, 2), 'utf8');
110
+ }
111
+
112
+ function now() {
113
+ return Date.now();
114
+ }
115
+
116
+ function conversationId(platform, userId) {
117
+ return `${platform}:${userId}`;
118
+ }
119
+
120
+ function nextId(rows) {
121
+ if (!rows.length) return 1;
122
+ return rows[rows.length - 1].id + 1;
123
+ }
124
+
125
+ function ensureConversation(platform, userId) {
126
+ const id = conversationId(platform, userId);
127
+ const ts = now();
128
+ const existing = state.conversations[id];
129
+ if (existing) {
130
+ existing.updated_at = ts;
131
+ } else {
132
+ state.conversations[id] = {
133
+ id,
134
+ platform,
135
+ user_id: userId,
136
+ created_at: ts,
137
+ updated_at: ts
138
+ };
139
+ }
140
+ saveState();
141
+ return id;
142
+ }
143
+
144
+ function addMessage(conversationIdValue, role, content) {
145
+ state.messages.push({
146
+ id: nextId(state.messages),
147
+ conversation_id: conversationIdValue,
148
+ role,
149
+ content,
150
+ created_at: now()
151
+ });
152
+
153
+ if (state.conversations[conversationIdValue]) {
154
+ state.conversations[conversationIdValue].updated_at = now();
155
+ }
156
+ saveState();
157
+ }
158
+
159
+ function getRecentMessages(conversationIdValue, limit = recentMessages) {
160
+ return state.messages
161
+ .filter((m) => m.conversation_id === conversationIdValue)
162
+ .slice(-limit)
163
+ .map((m) => ({ role: m.role, content: m.content, created_at: m.created_at }));
164
+ }
165
+
166
+ function getMessageCount(conversationIdValue) {
167
+ return state.messages.filter((m) => m.conversation_id === conversationIdValue).length;
168
+ }
169
+
170
+ function getMessagesForCompaction(conversationIdValue) {
171
+ const count = getMessageCount(conversationIdValue);
172
+ if (count <= maxMessages) return [];
173
+ const toCompact = Math.max(0, count - recentMessages);
174
+ if (!toCompact) return [];
175
+ return state.messages
176
+ .filter((m) => m.conversation_id === conversationIdValue)
177
+ .slice(0, toCompact)
178
+ .map((m) => ({ id: m.id, role: m.role, content: m.content }));
179
+ }
180
+
181
+ function deleteMessagesUpTo(conversationIdValue, maxId) {
182
+ state.messages = state.messages.filter((m) => {
183
+ if (m.conversation_id !== conversationIdValue) return true;
184
+ return m.id > maxId;
185
+ });
186
+ saveState();
187
+ }
188
+
189
+ function addMemory(conversationIdValue, source, content, embedding) {
190
+ const createdAt = now();
191
+ ensureSqliteMemoryReady();
192
+
193
+ if (sqliteMemoryReady) {
194
+ try {
195
+ runSqliteMemory([
196
+ 'add',
197
+ '--db',
198
+ vectorDbPath,
199
+ '--conversation-id',
200
+ String(conversationIdValue || ''),
201
+ '--source',
202
+ String(source || ''),
203
+ '--content',
204
+ String(content || ''),
205
+ '--embedding-json',
206
+ JSON.stringify(Array.isArray(embedding) ? embedding : []),
207
+ '--created-at',
208
+ String(createdAt)
209
+ ]);
210
+ return;
211
+ } catch (err) {
212
+ // Fall back to legacy JSON memory if sqlite path is unavailable.
213
+ }
214
+ }
215
+
216
+ state.memories.push({
217
+ id: nextId(state.memories),
218
+ conversation_id: conversationIdValue,
219
+ source,
220
+ content,
221
+ embedding,
222
+ created_at: createdAt
223
+ });
224
+ saveState();
225
+ }
226
+
227
+ function getMeta(key, fallback = null) {
228
+ if (!key) return fallback;
229
+ if (!Object.prototype.hasOwnProperty.call(state.meta, key)) return fallback;
230
+ return state.meta[key];
231
+ }
232
+
233
+ function setMeta(key, value) {
234
+ if (!key) return;
235
+ state.meta[key] = value;
236
+ saveState();
237
+ }
238
+
239
+ function getRecentMessagesAll(limit = 200) {
240
+ return state.messages.slice(-limit).map((m) => ({
241
+ conversation_id: m.conversation_id,
242
+ role: m.role,
243
+ content: m.content,
244
+ created_at: m.created_at
245
+ }));
246
+ }
247
+
248
+ function getMessagesSince(sinceTs, limit = 500) {
249
+ const threshold = Number(sinceTs || 0);
250
+ return state.messages
251
+ .filter((m) => Number(m.created_at || 0) > threshold)
252
+ .slice(-limit)
253
+ .map((m) => ({
254
+ conversation_id: m.conversation_id,
255
+ role: m.role,
256
+ content: m.content,
257
+ created_at: m.created_at
258
+ }));
259
+ }
260
+
261
+ function getRelevantMemories(conversationIdValue, queryEmbedding, limit = 6) {
262
+ ensureSqliteMemoryReady();
263
+ if (sqliteMemoryReady) {
264
+ try {
265
+ const result = runSqliteMemory([
266
+ 'search',
267
+ '--db',
268
+ vectorDbPath,
269
+ '--conversation-id',
270
+ String(conversationIdValue || ''),
271
+ '--query-embedding-json',
272
+ JSON.stringify(Array.isArray(queryEmbedding) ? queryEmbedding : []),
273
+ '--limit',
274
+ String(limit),
275
+ '--min-score',
276
+ '0.1',
277
+ '--window',
278
+ '600'
279
+ ]);
280
+ const rows = Array.isArray(result.rows) ? result.rows : [];
281
+ if (rows.length) {
282
+ return rows;
283
+ }
284
+ } catch (err) {
285
+ // Fall through to JSON fallback.
286
+ }
287
+ }
288
+
289
+ return state.memories
290
+ .filter(
291
+ (m) =>
292
+ m.conversation_id === conversationIdValue ||
293
+ m.conversation_id === 'global' ||
294
+ m.source === 'self_reflection'
295
+ )
296
+ .slice(-300)
297
+ .map((m) => ({
298
+ id: m.id,
299
+ source: m.source,
300
+ content: m.content,
301
+ created_at: m.created_at,
302
+ score: cosineSimilarity(queryEmbedding, m.embedding || [])
303
+ }))
304
+ .filter((m) => m.score > 0.1)
305
+ .sort((a, b) => b.score - a.score)
306
+ .slice(0, limit);
307
+ }
308
+
309
+ function recordSkillUsage(name, provider = 'tool') {
310
+ const skillName = String(name || '').trim();
311
+ if (!skillName) return;
312
+ ensureSqliteMemoryReady();
313
+ if (!sqliteMemoryReady) return;
314
+ try {
315
+ runSqliteMemory([
316
+ 'upsert-skill',
317
+ '--db',
318
+ vectorDbPath,
319
+ '--name',
320
+ skillName,
321
+ '--provider',
322
+ String(provider || 'tool'),
323
+ '--enabled',
324
+ '1',
325
+ '--updated-at',
326
+ String(now())
327
+ ]);
328
+ } catch (err) {
329
+ // Non-blocking telemetry.
330
+ }
331
+ }
332
+
333
+ module.exports = {
334
+ db: state,
335
+ ensureConversation,
336
+ addMessage,
337
+ getRecentMessages,
338
+ getMessageCount,
339
+ getMessagesForCompaction,
340
+ deleteMessagesUpTo,
341
+ addMemory,
342
+ getRelevantMemories,
343
+ getMeta,
344
+ setMeta,
345
+ getRecentMessagesAll,
346
+ getMessagesSince,
347
+ initVectorMemory,
348
+ recordSkillUsage
349
+ };