flint-agent 1.14.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/.env.example +108 -0
- package/CHANGELOG.md +55 -0
- package/FEATURES.md +298 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/bin/flint.js +47 -0
- package/config/classifier-prompt.md +218 -0
- package/config/models-curated.json +4 -0
- package/config/providers.json +74 -0
- package/package.json +92 -0
- package/patches/ink+6.8.0.patch +78 -0
- package/profiles/desktop.md +65 -0
- package/profiles/generic.md +20 -0
- package/profiles/marketer.md +20 -0
- package/profiles/profiles.json +34 -0
- package/profiles/ux-reviewer.md +25 -0
- package/src/agent/agent.js +1743 -0
- package/src/agent/auto.js +346 -0
- package/src/agent/backoff.js +143 -0
- package/src/agent/compression.js +310 -0
- package/src/agent/content-resolver.js +180 -0
- package/src/agent/flow-controller.js +309 -0
- package/src/agent/intent-manifest.js +231 -0
- package/src/agent/intent-timeout.js +46 -0
- package/src/agent/intent.js +633 -0
- package/src/agent/knowledge.js +114 -0
- package/src/agent/learning.js +180 -0
- package/src/agent/modes.js +187 -0
- package/src/agent/outcome-ask.js +91 -0
- package/src/agent/project-context.js +76 -0
- package/src/agent/prompt-budget.js +117 -0
- package/src/agent/reflection-extractor.js +140 -0
- package/src/agent/steering.js +86 -0
- package/src/agent/supervisor.js +430 -0
- package/src/agent/swap.js +443 -0
- package/src/agent/system-prompt.js +446 -0
- package/src/agent/time-stamp.js +48 -0
- package/src/agent/tool-guard.js +201 -0
- package/src/agent/toolcall-text.js +162 -0
- package/src/agent/usage.js +297 -0
- package/src/agent/vision.js +94 -0
- package/src/agent/watchdog.js +139 -0
- package/src/agent/workspace-changes.js +177 -0
- package/src/api/address.js +14 -0
- package/src/api/client.js +280 -0
- package/src/api/server.js +535 -0
- package/src/api/stream-pipe.js +113 -0
- package/src/app-state.js +39 -0
- package/src/bootstrap.js +501 -0
- package/src/bus/drain-loop.js +497 -0
- package/src/bus/index.js +270 -0
- package/src/bus/plugins.js +65 -0
- package/src/child-idle.js +14 -0
- package/src/cli.js +118 -0
- package/src/commands/commands.js +1297 -0
- package/src/commands/registry.js +132 -0
- package/src/components/App.js +491 -0
- package/src/components/CarefulMenu.js +145 -0
- package/src/components/HistoryWriter.js +86 -0
- package/src/components/LineInput.js +69 -0
- package/src/components/LiveZone.js +294 -0
- package/src/components/OverlayMenu.js +179 -0
- package/src/components/SystemPanel.js +156 -0
- package/src/components/Table.js +54 -0
- package/src/config.js +249 -0
- package/src/free-models.js +230 -0
- package/src/index.js +1111 -0
- package/src/input-handler.js +13 -0
- package/src/input-text.js +123 -0
- package/src/launcher.js +129 -0
- package/src/logging/api-log.js +95 -0
- package/src/logging/chat-log-follower.js +113 -0
- package/src/logging/chat-log.js +15 -0
- package/src/logging/log-collector.js +182 -0
- package/src/logging/logger.js +112 -0
- package/src/logging/tool-log.js +20 -0
- package/src/mcp-client.js +314 -0
- package/src/memory/conversation-digest.js +113 -0
- package/src/memory/extract-facts.js +98 -0
- package/src/memory/facts.js +181 -0
- package/src/memory/inbox.js +63 -0
- package/src/memory/markdown.js +38 -0
- package/src/memory/patterns.js +185 -0
- package/src/memory/project.js +66 -0
- package/src/memory/reflections.js +74 -0
- package/src/memory/retrieval.js +84 -0
- package/src/memory/rules.js +105 -0
- package/src/memory/session-facts.js +125 -0
- package/src/memory/skills.js +191 -0
- package/src/memory/sqlite-store.js +653 -0
- package/src/memory/store.js +208 -0
- package/src/memory/tools.js +196 -0
- package/src/memory/user-model.js +86 -0
- package/src/message-handler.js +775 -0
- package/src/model-check.js +218 -0
- package/src/plugins/loader.js +120 -0
- package/src/plugins/manager.js +88 -0
- package/src/production-env.js +22 -0
- package/src/profiles.js +42 -0
- package/src/providers/adapters/anthropic.js +270 -0
- package/src/providers/adapters/openai.js +120 -0
- package/src/providers/keys-dpapi.js +41 -0
- package/src/providers/keys-fallback.js +31 -0
- package/src/providers/keys.js +132 -0
- package/src/providers/models.js +154 -0
- package/src/providers/registry.js +56 -0
- package/src/providers/state.js +56 -0
- package/src/registry.js +96 -0
- package/src/restart.js +29 -0
- package/src/sandbox/backend.js +130 -0
- package/src/security/api-auth.js +132 -0
- package/src/security/audit.js +98 -0
- package/src/security/child-policy.js +41 -0
- package/src/security/command-guard.js +173 -0
- package/src/security/content-fence.js +250 -0
- package/src/security/content-validator.js +132 -0
- package/src/security/index.js +143 -0
- package/src/security/network-guard.js +126 -0
- package/src/security/pairing.js +180 -0
- package/src/security/path-guard.js +140 -0
- package/src/security/persona-guard.js +67 -0
- package/src/security/policies.js +452 -0
- package/src/security/safety-constants.js +34 -0
- package/src/security/watchdog.js +107 -0
- package/src/sessions.js +130 -0
- package/src/spend.js +97 -0
- package/src/startup-watchdog.js +59 -0
- package/src/stdio/args.js +71 -0
- package/src/stdio/guard.js +59 -0
- package/src/stdio/protocol.js +167 -0
- package/src/stdio/run.js +106 -0
- package/src/stdio/session.js +180 -0
- package/src/store/agent-slice.js +306 -0
- package/src/store/dataset-slice.js +73 -0
- package/src/store/index.js +22 -0
- package/src/store/process-slice.js +135 -0
- package/src/store/session-slice.js +191 -0
- package/src/store/ui-slice.js +119 -0
- package/src/tasks/db.js +184 -0
- package/src/tasks/queries.js +589 -0
- package/src/tools/agent-tools.js +473 -0
- package/src/tools/checkpoint.js +152 -0
- package/src/tools/command-approvals.js +180 -0
- package/src/tools/dataset.js +50 -0
- package/src/tools/filesystem.js +682 -0
- package/src/tools/inbox-tools.js +48 -0
- package/src/tools/mesh.js +135 -0
- package/src/tools/own-env.js +136 -0
- package/src/tools/permissions.js +681 -0
- package/src/tools/plugin-tools.js +123 -0
- package/src/tools/process-tools.js +595 -0
- package/src/tools/registry.js +307 -0
- package/src/tools/swap-tools.js +72 -0
- package/src/tools/system.js +662 -0
- package/src/tools/tasks.js +532 -0
- package/src/tools/tool-search.js +171 -0
- package/src/ui/header.js +140 -0
- package/src/ui/input-cursor.js +23 -0
- package/src/ui/last-line.js +25 -0
- package/src/ui/line-edit.js +135 -0
- package/src/ui/output.js +399 -0
- package/src/ui/paste-tokens.js +131 -0
- package/src/ui/prompt-attention.js +134 -0
- package/src/ui/render-options.js +13 -0
- package/src/ui/replay.js +94 -0
- package/src/ui/splash.js +49 -0
- package/src/ui/status-level.js +36 -0
- package/src/ui/tool-ledger.js +203 -0
- package/src/ui/window-title.js +150 -0
- package/src/update.js +205 -0
- package/system.md +63 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, chmodSync } from "node:fs";
|
|
2
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
|
|
6
|
+
const MEMORY_DIR = path.join(config.projectRoot, "memory");
|
|
7
|
+
const MEMORIES_FILE = path.join(MEMORY_DIR, "memories.jsonl");
|
|
8
|
+
const HMAC_FILE = path.join(MEMORY_DIR, "memories.hmac");
|
|
9
|
+
const KEY_FILE = path.join(MEMORY_DIR, ".hmac-key");
|
|
10
|
+
|
|
11
|
+
const MAX_CONTENT_LENGTH = 10000;
|
|
12
|
+
const MAX_IMPORTANCE = 3;
|
|
13
|
+
const MIN_IMPORTANCE = 1;
|
|
14
|
+
|
|
15
|
+
// ── HMAC Integrity ──────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function getHmacKey() {
|
|
18
|
+
// Key from env takes priority (for CI/deployment)
|
|
19
|
+
if (process.env.AGENT_MEMORY_HMAC_KEY) {
|
|
20
|
+
return process.env.AGENT_MEMORY_HMAC_KEY;
|
|
21
|
+
}
|
|
22
|
+
// Otherwise generate and persist a key per project
|
|
23
|
+
ensureDir();
|
|
24
|
+
if (existsSync(KEY_FILE)) {
|
|
25
|
+
return readFileSync(KEY_FILE, "utf-8").trim();
|
|
26
|
+
}
|
|
27
|
+
const key = randomBytes(32).toString("hex");
|
|
28
|
+
writeFileSync(KEY_FILE, key, { encoding: "utf-8", mode: 0o600 });
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function computeHmac(data) {
|
|
33
|
+
return createHmac("sha256", getHmacKey()).update(data, "utf-8").digest("hex");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function verifyHmac(data) {
|
|
37
|
+
if (!existsSync(HMAC_FILE)) {
|
|
38
|
+
// No HMAC file: only trust if data file is also empty/missing (true first run)
|
|
39
|
+
// If data exists without HMAC → possible tampering (attacker deleted .hmac)
|
|
40
|
+
if (existsSync(MEMORIES_FILE) && data.trim().length > 0) {
|
|
41
|
+
console.error("[SECURITY] Memory data exists without HMAC file — possible tampering (HMAC deleted)");
|
|
42
|
+
// Auto-heal: create HMAC for existing data (migration from pre-HMAC version)
|
|
43
|
+
// This is a one-time migration — after this, HMAC is required
|
|
44
|
+
saveHmac(data);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return true; // genuinely empty first run
|
|
48
|
+
}
|
|
49
|
+
const stored = readFileSync(HMAC_FILE, "utf-8").trim();
|
|
50
|
+
const computed = computeHmac(data);
|
|
51
|
+
// Timing-safe comparison to prevent timing attacks
|
|
52
|
+
try {
|
|
53
|
+
const storedBuf = Buffer.from(stored, "hex");
|
|
54
|
+
const computedBuf = Buffer.from(computed, "hex");
|
|
55
|
+
if (storedBuf.length !== computedBuf.length || !timingSafeEqual(storedBuf, computedBuf)) {
|
|
56
|
+
console.error("[SECURITY] Memory file integrity check FAILED — possible tampering detected");
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
console.error("[SECURITY] Memory HMAC comparison error — rejecting data");
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function saveHmac(data) {
|
|
67
|
+
writeFileSync(HMAC_FILE, computeHmac(data), "utf-8");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Core ─────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
// PERF-01: In-process cache — load file once, invalidate on write
|
|
73
|
+
let _cache = null;
|
|
74
|
+
|
|
75
|
+
/** Invalidate in-memory cache (call after external file modification) */
|
|
76
|
+
export function invalidateCache() { _cache = null; }
|
|
77
|
+
|
|
78
|
+
function ensureDir() {
|
|
79
|
+
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function loadAll() {
|
|
83
|
+
if (_cache !== null) return _cache;
|
|
84
|
+
ensureDir();
|
|
85
|
+
if (!existsSync(MEMORIES_FILE)) { _cache = []; return _cache; }
|
|
86
|
+
const raw = readFileSync(MEMORIES_FILE, "utf-8");
|
|
87
|
+
|
|
88
|
+
// Verify integrity before parsing
|
|
89
|
+
if (!verifyHmac(raw)) {
|
|
90
|
+
// Tampering detected — return empty to prevent poisoned memory from loading
|
|
91
|
+
_cache = [];
|
|
92
|
+
return _cache;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
96
|
+
_cache = lines.map((line) => {
|
|
97
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
98
|
+
}).filter(Boolean);
|
|
99
|
+
return _cache;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function atomicWrite(data) {
|
|
103
|
+
ensureDir();
|
|
104
|
+
const tmp = MEMORIES_FILE + ".tmp";
|
|
105
|
+
writeFileSync(tmp, data, "utf-8");
|
|
106
|
+
renameSync(tmp, MEMORIES_FILE);
|
|
107
|
+
// Update HMAC after successful write
|
|
108
|
+
saveHmac(data);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function saveAll(memories) {
|
|
112
|
+
const data = memories.map((m) => JSON.stringify(m)).join("\n") + (memories.length ? "\n" : "");
|
|
113
|
+
atomicWrite(data);
|
|
114
|
+
_cache = memories; // update cache after write
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function nextId(memories) {
|
|
118
|
+
if (!memories.length) return 1;
|
|
119
|
+
let max = 0;
|
|
120
|
+
for (const m of memories) {
|
|
121
|
+
if (m.id > max) max = m.id;
|
|
122
|
+
}
|
|
123
|
+
return max + 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function clampImportance(val) {
|
|
127
|
+
const n = typeof val === "number" ? val : MIN_IMPORTANCE;
|
|
128
|
+
return Math.max(MIN_IMPORTANCE, Math.min(MAX_IMPORTANCE, Math.round(n)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function insertMemory({ content, category = "general", importance = 1, sessionId = null }) {
|
|
132
|
+
if (!content || typeof content !== "string") {
|
|
133
|
+
throw new Error("Memory content must be a non-empty string");
|
|
134
|
+
}
|
|
135
|
+
const trimmed = content.slice(0, MAX_CONTENT_LENGTH);
|
|
136
|
+
const memories = loadAll();
|
|
137
|
+
const entry = {
|
|
138
|
+
id: nextId(memories),
|
|
139
|
+
content: trimmed,
|
|
140
|
+
category: String(category || "general"),
|
|
141
|
+
importance: clampImportance(importance),
|
|
142
|
+
session_id: sessionId,
|
|
143
|
+
created_at: new Date().toISOString(),
|
|
144
|
+
};
|
|
145
|
+
memories.push(entry);
|
|
146
|
+
saveAll(memories);
|
|
147
|
+
return entry;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function searchMemories(query, limit = 10) {
|
|
151
|
+
if (!query || typeof query !== "string") return [];
|
|
152
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
153
|
+
if (!terms.length) return [];
|
|
154
|
+
|
|
155
|
+
const memories = loadAll();
|
|
156
|
+
const scored = [];
|
|
157
|
+
|
|
158
|
+
for (const m of memories) {
|
|
159
|
+
const text = `${m.content} ${m.category}`.toLowerCase();
|
|
160
|
+
let score = 0;
|
|
161
|
+
for (const term of terms) {
|
|
162
|
+
if (text.includes(term)) score++;
|
|
163
|
+
}
|
|
164
|
+
if (score === 0) continue;
|
|
165
|
+
score += (m.importance - 1) * 0.5;
|
|
166
|
+
scored.push({ ...m, score });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
scored.sort((a, b) => b.score - a.score);
|
|
170
|
+
return scored.slice(0, limit);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getMemory(id) {
|
|
174
|
+
const memories = loadAll();
|
|
175
|
+
return memories.find((m) => m.id === id) || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function listRecentMemories(limit = 10) {
|
|
179
|
+
const memories = loadAll();
|
|
180
|
+
return memories.slice(-limit).reverse();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function deleteMemory(id) {
|
|
184
|
+
const memories = loadAll();
|
|
185
|
+
const idx = memories.findIndex((m) => m.id === id);
|
|
186
|
+
if (idx === -1) return false;
|
|
187
|
+
memories.splice(idx, 1);
|
|
188
|
+
saveAll(memories);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getMemoryStats() {
|
|
193
|
+
const memories = loadAll();
|
|
194
|
+
const byCategory = {};
|
|
195
|
+
for (const m of memories) {
|
|
196
|
+
byCategory[m.category] = (byCategory[m.category] || 0) + 1;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
total: memories.length,
|
|
200
|
+
byCategory,
|
|
201
|
+
oldest: memories[0]?.created_at || null,
|
|
202
|
+
newest: memories[memories.length - 1]?.created_at || null,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function clearAllMemories() {
|
|
207
|
+
saveAll([]);
|
|
208
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { insertMemory, searchMemories, getMemory, listRecentMemories, deleteMemory } from "./store.js";
|
|
2
|
+
import { updateMemoryMd } from "./markdown.js";
|
|
3
|
+
import { createSkill, updateSkill, removeSkill, listSkillSummaries } from "./skills.js";
|
|
4
|
+
import { searchFts, getSkill as getSkillById } from "./sqlite-store.js";
|
|
5
|
+
|
|
6
|
+
export const tools = [
|
|
7
|
+
{
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: "memory_write",
|
|
11
|
+
description: "Save a fact, decision, or observation to persistent memory. Use this when you learn something important that should be remembered across sessions.",
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
content: { type: "string", description: "The information to remember" },
|
|
16
|
+
category: { type: "string", description: "Category: facts, decisions, preferences, bugs, patterns", default: "general" },
|
|
17
|
+
importance: { type: "integer", description: "1-3, where 3 is critical", default: 1 },
|
|
18
|
+
},
|
|
19
|
+
required: ["content"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: "function",
|
|
25
|
+
function: {
|
|
26
|
+
name: "memory_search",
|
|
27
|
+
description: "Search persistent memory for relevant information. Use this to recall previously saved knowledge.",
|
|
28
|
+
parameters: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
query: { type: "string", description: "Search query" },
|
|
32
|
+
limit: { type: "integer", description: "Max results", default: 10 },
|
|
33
|
+
},
|
|
34
|
+
required: ["query"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "function",
|
|
40
|
+
function: {
|
|
41
|
+
name: "memory_get",
|
|
42
|
+
description: "Get a specific memory by ID or list recent memories.",
|
|
43
|
+
parameters: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
id: { type: "integer", description: "Memory ID to retrieve" },
|
|
47
|
+
last: { type: "integer", description: "Get last N memories instead" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "function",
|
|
54
|
+
function: {
|
|
55
|
+
name: "memory_delete",
|
|
56
|
+
description: "Delete a memory by ID.",
|
|
57
|
+
parameters: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
id: { type: "integer", description: "Memory ID to delete" },
|
|
61
|
+
},
|
|
62
|
+
required: ["id"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "function",
|
|
68
|
+
function: {
|
|
69
|
+
name: "skill_add",
|
|
70
|
+
description: "Create a reusable skill — a named procedure the agent remembers permanently. Use when you learn a multi-step workflow the user might need again (e.g. 'how we deploy', 'how to run tests for this project').",
|
|
71
|
+
parameters: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
name: { type: "string", description: "Short name for the skill, e.g. 'Deploy to staging'" },
|
|
75
|
+
content: { type: "string", description: "Full procedure in markdown. Include steps, commands, and caveats." },
|
|
76
|
+
tags: { type: "string", description: "Comma-separated tags, e.g. 'deploy, aws, staging'" },
|
|
77
|
+
},
|
|
78
|
+
required: ["name", "content"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: "function",
|
|
84
|
+
function: {
|
|
85
|
+
name: "skill_update",
|
|
86
|
+
description: "Update the content of an existing skill.",
|
|
87
|
+
parameters: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
id: { type: "string", description: "Skill ID (from skill_list)" },
|
|
91
|
+
content: { type: "string", description: "New content replacing the old" },
|
|
92
|
+
},
|
|
93
|
+
required: ["id", "content"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "function",
|
|
99
|
+
function: {
|
|
100
|
+
name: "skill_remove",
|
|
101
|
+
description: "Delete a skill permanently.",
|
|
102
|
+
parameters: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
id: { type: "string", description: "Skill ID to delete" },
|
|
106
|
+
},
|
|
107
|
+
required: ["id"],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: "function",
|
|
113
|
+
function: {
|
|
114
|
+
name: "memory_expand",
|
|
115
|
+
description: "Retrieve full content of a skill or memory entry by ID. Use when the system prompt shows a skill index (§ heading) and you need the full procedure. Alias: load_skill.",
|
|
116
|
+
parameters: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
id: { type: "string", description: "Entry ID (skill-xxx or memory ID)" },
|
|
120
|
+
layer: { type: "string", description: "Layer: skills, reflections, patterns, facts, user_model", default: "skills" },
|
|
121
|
+
},
|
|
122
|
+
required: ["id"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
export const handlers = {
|
|
129
|
+
memory_write(args) {
|
|
130
|
+
const entry = insertMemory({
|
|
131
|
+
content: args.content,
|
|
132
|
+
category: args.category ?? "general",
|
|
133
|
+
importance: args.importance ?? 1,
|
|
134
|
+
});
|
|
135
|
+
updateMemoryMd();
|
|
136
|
+
return `Saved memory #${entry.id} [${entry.category}]: ${entry.content}`;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
memory_search(args) {
|
|
140
|
+
const results = searchMemories(args.query, args.limit ?? 10);
|
|
141
|
+
if (!results.length) return "No memories found matching your query.";
|
|
142
|
+
return results.map((m) => {
|
|
143
|
+
const date = m.created_at?.slice(0, 10) || "";
|
|
144
|
+
return `[#${m.id}] (${m.category}) ${date}: ${m.content} [score: ${m.score.toFixed(1)}]`;
|
|
145
|
+
}).join("\n");
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
memory_get(args) {
|
|
149
|
+
if (args.id != null) {
|
|
150
|
+
const m = getMemory(args.id);
|
|
151
|
+
if (!m) return `Memory #${args.id} not found.`;
|
|
152
|
+
return `[#${m.id}] (${m.category}) ${m.created_at}: ${m.content} [importance: ${m.importance}]`;
|
|
153
|
+
}
|
|
154
|
+
const recent = listRecentMemories(args.last ?? 5);
|
|
155
|
+
if (!recent.length) return "No memories stored yet.";
|
|
156
|
+
return recent.map((m) => `[#${m.id}] (${m.category}) ${m.created_at?.slice(0, 10)}: ${m.content}`).join("\n");
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
memory_delete(args) {
|
|
160
|
+
const ok = deleteMemory(args.id);
|
|
161
|
+
if (ok) {
|
|
162
|
+
updateMemoryMd();
|
|
163
|
+
return `Deleted memory #${args.id}.`;
|
|
164
|
+
}
|
|
165
|
+
return `Memory #${args.id} not found.`;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
skill_add(args) {
|
|
169
|
+
const tags = args.tags ? args.tags.split(",").map(t => t.trim()).filter(Boolean) : [];
|
|
170
|
+
const id = createSkill(args.name, args.content, tags);
|
|
171
|
+
return `Created skill "${args.name}" (${id}). Stored as markdown in ~/.flint/memory/skills/ and indexed for search.`;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
skill_update(args) {
|
|
175
|
+
const ok = updateSkill(args.id, args.content);
|
|
176
|
+
return ok ? `Updated skill ${args.id}.` : `Skill ${args.id} not found.`;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
skill_remove(args) {
|
|
180
|
+
const ok = removeSkill(args.id);
|
|
181
|
+
return ok ? `Removed skill ${args.id} (file deleted).` : `Skill ${args.id} not found.`;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
memory_expand(args) {
|
|
185
|
+
const layer = args.layer || "skills";
|
|
186
|
+
if (layer === "skills") {
|
|
187
|
+
const skill = getSkillById(args.id);
|
|
188
|
+
if (!skill) return `Skill ${args.id} not found.`;
|
|
189
|
+
return `# ${skill.name}\n\n${skill.content}`;
|
|
190
|
+
}
|
|
191
|
+
// For other layers — FTS search by id
|
|
192
|
+
const results = searchFts(args.id, { limit: 1 });
|
|
193
|
+
if (results.length) return results[0].text;
|
|
194
|
+
return `Entry ${args.id} not found in layer ${layer}.`;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// User Model — Layer 5 of memory architecture (dialectic).
|
|
2
|
+
//
|
|
3
|
+
// Builds a profile of the user over time by observing conversations.
|
|
4
|
+
// Traits: communication style, expertise level, preferences, recurring topics.
|
|
5
|
+
// Not explicit "user said X" (that's facts L4). This is "agent OBSERVED X".
|
|
6
|
+
//
|
|
7
|
+
// Inspired by Honcho (Plastic Labs) but implemented in own SQLite, no dependency.
|
|
8
|
+
// Internal task reference removed.
|
|
9
|
+
|
|
10
|
+
import { upsertTrait, getAllTraits, indexEntry } from "./sqlite-store.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Observe a user message and update traits.
|
|
14
|
+
* Called on each user turn. Lightweight language-neutral observations only.
|
|
15
|
+
*
|
|
16
|
+
* - Communication style (terse / verbose) from word count
|
|
17
|
+
* - Language (ru / en / mixed) from unicode block presence
|
|
18
|
+
*
|
|
19
|
+
* Expertise level and role hint used to be guessed from hardcoded RU/EN keyword
|
|
20
|
+
* regex (git rebase|API|CSS|...). Removed 2026-04-19 — biased to English,
|
|
21
|
+
* misses Ukrainian/German/Chinese users, and is the LLM's job anyway. Those
|
|
22
|
+
* traits are inferred by the LLM-based fact extractor at session end
|
|
23
|
+
* (see src/memory/extract-facts.js, category "preference" / "person").
|
|
24
|
+
*
|
|
25
|
+
* @param {string} userMessage
|
|
26
|
+
* @param {object} context — { toolsUsed, responseLanguage, sessionLength }
|
|
27
|
+
*/
|
|
28
|
+
export function observeUser(userMessage, context = {}) {
|
|
29
|
+
if (!userMessage || userMessage.length < 5) return;
|
|
30
|
+
|
|
31
|
+
// Communication style — word count is language-neutral
|
|
32
|
+
const words = userMessage.split(/\s+/).length;
|
|
33
|
+
if (words <= 5) {
|
|
34
|
+
updateTrait("communication_style", "terse", `message: ${words} words`, 0.6);
|
|
35
|
+
} else if (words > 50) {
|
|
36
|
+
updateTrait("communication_style", "verbose", `message: ${words} words`, 0.6);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Language — unicode block observation (ru / en / mixed).
|
|
40
|
+
// This is observation of the user's script, not interpretation of intent.
|
|
41
|
+
const hasCyrillic = /\p{Script=Cyrillic}/u.test(userMessage);
|
|
42
|
+
const hasLatin = /\p{Script=Latin}/u.test(userMessage);
|
|
43
|
+
const hasCjk = /\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Hangul}/u.test(userMessage);
|
|
44
|
+
const hasArabic = /\p{Script=Arabic}/u.test(userMessage);
|
|
45
|
+
let lang = null;
|
|
46
|
+
if (hasCjk) lang = hasLatin ? "cjk+latin" : "cjk";
|
|
47
|
+
else if (hasArabic) lang = hasLatin ? "arabic+latin" : "arabic";
|
|
48
|
+
else if (hasCyrillic && !hasLatin) lang = "cyrillic";
|
|
49
|
+
else if (hasLatin && !hasCyrillic) lang = "latin";
|
|
50
|
+
else if (hasCyrillic && hasLatin) lang = "cyrillic+latin";
|
|
51
|
+
if (lang) updateTrait("language", lang, "script observation", 0.7);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function updateTrait(trait, value, evidence, confidence) {
|
|
55
|
+
upsertTrait(trait, value, evidence, confidence);
|
|
56
|
+
indexEntry("user_model", `trait-${trait}`, `${trait}: ${value}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get current user profile as structured object.
|
|
61
|
+
* @returns {Record<string, {value, confidence, evidence}>}
|
|
62
|
+
*/
|
|
63
|
+
export function getProfile() {
|
|
64
|
+
const traits = getAllTraits();
|
|
65
|
+
const profile = {};
|
|
66
|
+
for (const t of traits) {
|
|
67
|
+
profile[t.trait] = { value: t.value, confidence: t.confidence, evidence: t.evidence };
|
|
68
|
+
}
|
|
69
|
+
return profile;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format user model as system-prompt-ready block.
|
|
74
|
+
* Only includes traits with confidence >= threshold.
|
|
75
|
+
* @param {number} minConfidence
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function formatForPrompt(minConfidence = 0.5) {
|
|
79
|
+
const traits = getAllTraits().filter(t => t.confidence >= minConfidence);
|
|
80
|
+
if (!traits.length) return "";
|
|
81
|
+
const lines = ["User profile (observed, adapt your behavior):"];
|
|
82
|
+
for (const t of traits) {
|
|
83
|
+
lines.push(` § ${t.trait}: ${t.value} (${Math.round(t.confidence * 100)}%)`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|