fraude-code 0.1.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 +68 -0
- package/dist/index.js +179297 -0
- package/package.json +88 -0
- package/src/agent/agent.ts +475 -0
- package/src/agent/contextManager.ts +141 -0
- package/src/agent/index.ts +14 -0
- package/src/agent/pendingChanges.ts +270 -0
- package/src/agent/prompts/AskPrompt.txt +10 -0
- package/src/agent/prompts/FastPrompt.txt +40 -0
- package/src/agent/prompts/PlannerPrompt.txt +51 -0
- package/src/agent/prompts/ReviewerPrompt.txt +57 -0
- package/src/agent/prompts/WorkerPrompt.txt +33 -0
- package/src/agent/subagents/askAgent.ts +37 -0
- package/src/agent/subagents/extractionAgent.ts +123 -0
- package/src/agent/subagents/fastAgent.ts +45 -0
- package/src/agent/subagents/managerAgent.ts +36 -0
- package/src/agent/subagents/relationAgent.ts +76 -0
- package/src/agent/subagents/researchSubAgent.ts +79 -0
- package/src/agent/subagents/reviewerSubAgent.ts +42 -0
- package/src/agent/subagents/workerSubAgent.ts +42 -0
- package/src/agent/tools/bashTool.ts +94 -0
- package/src/agent/tools/descriptions/bash.txt +47 -0
- package/src/agent/tools/descriptions/edit.txt +7 -0
- package/src/agent/tools/descriptions/glob.txt +4 -0
- package/src/agent/tools/descriptions/grep.txt +8 -0
- package/src/agent/tools/descriptions/lsp.txt +20 -0
- package/src/agent/tools/descriptions/plan.txt +3 -0
- package/src/agent/tools/descriptions/read.txt +9 -0
- package/src/agent/tools/descriptions/todo.txt +12 -0
- package/src/agent/tools/descriptions/write.txt +8 -0
- package/src/agent/tools/editTool.ts +44 -0
- package/src/agent/tools/globTool.ts +59 -0
- package/src/agent/tools/grepTool.ts +343 -0
- package/src/agent/tools/lspTool.ts +429 -0
- package/src/agent/tools/planTool.ts +118 -0
- package/src/agent/tools/readTool.ts +78 -0
- package/src/agent/tools/rememberTool.ts +91 -0
- package/src/agent/tools/testRunnerTool.ts +77 -0
- package/src/agent/tools/testTool.ts +44 -0
- package/src/agent/tools/todoTool.ts +224 -0
- package/src/agent/tools/writeTool.ts +33 -0
- package/src/commands/COMMANDS.ts +38 -0
- package/src/commands/cerebras/auth.ts +27 -0
- package/src/commands/cerebras/index.ts +31 -0
- package/src/commands/forget.ts +29 -0
- package/src/commands/google/auth.ts +24 -0
- package/src/commands/google/index.ts +31 -0
- package/src/commands/groq/add_model.ts +60 -0
- package/src/commands/groq/auth.ts +24 -0
- package/src/commands/groq/index.ts +33 -0
- package/src/commands/index.ts +65 -0
- package/src/commands/knowledge.ts +92 -0
- package/src/commands/log.ts +32 -0
- package/src/commands/mistral/auth.ts +27 -0
- package/src/commands/mistral/index.ts +31 -0
- package/src/commands/model/index.ts +145 -0
- package/src/commands/models/index.ts +16 -0
- package/src/commands/ollama/index.ts +29 -0
- package/src/commands/openrouter/add_model.ts +64 -0
- package/src/commands/openrouter/auth.ts +24 -0
- package/src/commands/openrouter/index.ts +33 -0
- package/src/commands/remember.ts +48 -0
- package/src/commands/serve.ts +31 -0
- package/src/commands/session/index.ts +21 -0
- package/src/commands/usage.ts +15 -0
- package/src/commands/visualize.ts +773 -0
- package/src/components/App.tsx +55 -0
- package/src/components/IntroComponent.tsx +70 -0
- package/src/components/LoaderComponent.tsx +68 -0
- package/src/components/OutputRenderer.tsx +88 -0
- package/src/components/SettingsRenderer.tsx +23 -0
- package/src/components/input/CommandSuggestions.tsx +41 -0
- package/src/components/input/FileSuggestions.tsx +61 -0
- package/src/components/input/InputBox.tsx +371 -0
- package/src/components/output/CheckpointView.tsx +13 -0
- package/src/components/output/CommandView.tsx +13 -0
- package/src/components/output/CommentView.tsx +12 -0
- package/src/components/output/ConfirmationView.tsx +179 -0
- package/src/components/output/ContextUsage.tsx +62 -0
- package/src/components/output/DiffView.tsx +202 -0
- package/src/components/output/ErrorView.tsx +14 -0
- package/src/components/output/InteractiveServerView.tsx +69 -0
- package/src/components/output/KnowledgeView.tsx +220 -0
- package/src/components/output/MarkdownView.tsx +15 -0
- package/src/components/output/ModelSelectView.tsx +71 -0
- package/src/components/output/ReasoningView.tsx +21 -0
- package/src/components/output/ToolCallView.tsx +45 -0
- package/src/components/settings/ModelList.tsx +250 -0
- package/src/components/settings/TokenUsage.tsx +274 -0
- package/src/config/schema.ts +19 -0
- package/src/config/settings.ts +229 -0
- package/src/index.tsx +100 -0
- package/src/parsers/tree-sitter-python.wasm +0 -0
- package/src/providers/providers.ts +71 -0
- package/src/services/PluginLoader.ts +123 -0
- package/src/services/cerebras.ts +69 -0
- package/src/services/embeddingService.ts +229 -0
- package/src/services/google.ts +65 -0
- package/src/services/graphSerializer.ts +248 -0
- package/src/services/groq.ts +23 -0
- package/src/services/knowledgeOrchestrator.ts +286 -0
- package/src/services/mistral.ts +79 -0
- package/src/services/ollama.ts +109 -0
- package/src/services/openrouter.ts +23 -0
- package/src/services/symbolExtractor.ts +277 -0
- package/src/store/useFraudeStore.ts +123 -0
- package/src/store/useSettingsStore.ts +38 -0
- package/src/theme.ts +26 -0
- package/src/types/Agent.ts +147 -0
- package/src/types/CommandDefinition.ts +8 -0
- package/src/types/Model.ts +94 -0
- package/src/types/OutputItem.ts +24 -0
- package/src/types/PluginContext.ts +55 -0
- package/src/types/TokenUsage.ts +5 -0
- package/src/types/assets.d.ts +4 -0
- package/src/utils/agentCognition.ts +1152 -0
- package/src/utils/fileSuggestions.ts +111 -0
- package/src/utils/index.ts +17 -0
- package/src/utils/initFraude.ts +8 -0
- package/src/utils/logger.ts +24 -0
- package/src/utils/lspClient.ts +1415 -0
- package/src/utils/paths.ts +24 -0
- package/src/utils/queryHandler.ts +227 -0
- package/src/utils/router.ts +278 -0
- package/src/utils/streamHandler.ts +132 -0
- package/src/utils/treeSitterQueries.ts +125 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
import { Database, Connection, QueryResult, PreparedStatement } from "kuzu";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import type { ModelMessage } from "ai";
|
|
5
|
+
import ignore from "ignore";
|
|
6
|
+
import EmbeddingService from "@/services/embeddingService";
|
|
7
|
+
import log from "./logger";
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface Fact {
|
|
14
|
+
id: string;
|
|
15
|
+
type:
|
|
16
|
+
| "fact"
|
|
17
|
+
| "decision"
|
|
18
|
+
| "concept"
|
|
19
|
+
| "reference"
|
|
20
|
+
| "file"
|
|
21
|
+
| "module"
|
|
22
|
+
| "function"
|
|
23
|
+
| "class"
|
|
24
|
+
| "interface"
|
|
25
|
+
| "variable"
|
|
26
|
+
| "symbol";
|
|
27
|
+
content: string;
|
|
28
|
+
data?: {
|
|
29
|
+
file?: string;
|
|
30
|
+
function?: string;
|
|
31
|
+
symbol?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
timestamp: number;
|
|
35
|
+
sessionId: string;
|
|
36
|
+
confidence: number;
|
|
37
|
+
validated?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ValidationResult {
|
|
41
|
+
valid: boolean;
|
|
42
|
+
reason?: "file_missing" | "code_changed" | "stale" | "conflict";
|
|
43
|
+
suggestion?: "delete" | "update" | "keep";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// AgentCognition - Consolidated Knowledge Management
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
class AgentCognition {
|
|
51
|
+
private static instance: AgentCognition;
|
|
52
|
+
private db: Database | null = null;
|
|
53
|
+
private conn: Connection | null = null;
|
|
54
|
+
private dbPath: string;
|
|
55
|
+
private initPromise: Promise<void> | null = null;
|
|
56
|
+
private sessionId: string;
|
|
57
|
+
private embeddings: EmbeddingService;
|
|
58
|
+
|
|
59
|
+
// Staleness threshold: 7 days
|
|
60
|
+
private static STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
61
|
+
|
|
62
|
+
private constructor() {
|
|
63
|
+
this.dbPath = path.join(process.cwd(), ".fraude", "kuzu");
|
|
64
|
+
this.sessionId = crypto.randomUUID();
|
|
65
|
+
this.embeddings = EmbeddingService.getInstance();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static getInstance(): AgentCognition {
|
|
69
|
+
if (!AgentCognition.instance) {
|
|
70
|
+
AgentCognition.instance = new AgentCognition();
|
|
71
|
+
}
|
|
72
|
+
return AgentCognition.instance;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Initialization
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
async init(): Promise<void> {
|
|
80
|
+
if (this.initPromise) return this.initPromise;
|
|
81
|
+
|
|
82
|
+
this.initPromise = (async () => {
|
|
83
|
+
this.ensureDirectory();
|
|
84
|
+
this.db = new Database(this.dbPath);
|
|
85
|
+
this.conn = new Connection(this.db);
|
|
86
|
+
await this.initSchema();
|
|
87
|
+
})();
|
|
88
|
+
|
|
89
|
+
return this.initPromise;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private ensureDirectory(): void {
|
|
93
|
+
const dir = path.dirname(this.dbPath);
|
|
94
|
+
if (!fs.existsSync(dir)) {
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async initSchema(): Promise<void> {
|
|
100
|
+
if (!this.conn) throw new Error("Connection not initialized");
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Create Fact node table
|
|
104
|
+
await this.conn.query(`
|
|
105
|
+
CREATE NODE TABLE IF NOT EXISTS Fact (
|
|
106
|
+
id STRING,
|
|
107
|
+
type STRING,
|
|
108
|
+
content STRING,
|
|
109
|
+
data STRING,
|
|
110
|
+
timestamp INT64,
|
|
111
|
+
sessionId STRING,
|
|
112
|
+
confidence DOUBLE,
|
|
113
|
+
validated INT64,
|
|
114
|
+
PRIMARY KEY (id)
|
|
115
|
+
)
|
|
116
|
+
`);
|
|
117
|
+
|
|
118
|
+
// Create relationship table
|
|
119
|
+
await this.conn.query(`
|
|
120
|
+
CREATE REL TABLE IF NOT EXISTS RELATED_TO (
|
|
121
|
+
FROM Fact TO Fact,
|
|
122
|
+
relation STRING,
|
|
123
|
+
weight DOUBLE
|
|
124
|
+
)
|
|
125
|
+
`);
|
|
126
|
+
} catch (e: unknown) {
|
|
127
|
+
// Tables may already exist, ignore errors
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Graph Operations
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
private async execute(
|
|
136
|
+
cypher: string,
|
|
137
|
+
params: Record<string, any> = {},
|
|
138
|
+
): Promise<QueryResult> {
|
|
139
|
+
await this.init();
|
|
140
|
+
if (!this.conn) throw new Error("Connection not initialized");
|
|
141
|
+
|
|
142
|
+
const stmt: PreparedStatement = await this.conn.prepare(cypher);
|
|
143
|
+
if (!stmt.isSuccess()) {
|
|
144
|
+
throw new Error(`Failed to prepare statement: ${stmt.getErrorMessage()}`);
|
|
145
|
+
}
|
|
146
|
+
const result = await this.conn.execute(stmt, params);
|
|
147
|
+
log("DB EXECUTE RESULT:", result, "IsArray:", Array.isArray(result));
|
|
148
|
+
if (Array.isArray(result)) {
|
|
149
|
+
if (result.length === 0) {
|
|
150
|
+
// Fallback for empty array result
|
|
151
|
+
return {
|
|
152
|
+
getAll: async () => [],
|
|
153
|
+
getNumTuples: () => 0,
|
|
154
|
+
} as unknown as QueryResult;
|
|
155
|
+
}
|
|
156
|
+
return result[0] as QueryResult;
|
|
157
|
+
}
|
|
158
|
+
return result as QueryResult;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async addFact(
|
|
162
|
+
fact: Omit<Fact, "id" | "timestamp" | "sessionId">,
|
|
163
|
+
): Promise<string> {
|
|
164
|
+
// Deduplication: Check if a fact with same type+content already exists
|
|
165
|
+
const existing = await this.findDuplicate(
|
|
166
|
+
fact.type,
|
|
167
|
+
fact.content,
|
|
168
|
+
fact.data,
|
|
169
|
+
);
|
|
170
|
+
if (existing) {
|
|
171
|
+
// Update timestamp to keep it fresh, but don't create duplicate
|
|
172
|
+
await this.execute(
|
|
173
|
+
`
|
|
174
|
+
MATCH (f:Fact {id: $id})
|
|
175
|
+
SET f.validated = $validated,
|
|
176
|
+
f.confidence = CASE WHEN $confidence > f.confidence THEN $confidence ELSE f.confidence END,
|
|
177
|
+
f.content = $content,
|
|
178
|
+
f.data = $data
|
|
179
|
+
RETURN f
|
|
180
|
+
`,
|
|
181
|
+
{
|
|
182
|
+
id: existing.id,
|
|
183
|
+
validated: Date.now(),
|
|
184
|
+
confidence: fact.confidence,
|
|
185
|
+
content: fact.content,
|
|
186
|
+
data: JSON.stringify(fact.data || {}),
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
// Ensure symbol linking even on duplicate (heals missing links)
|
|
190
|
+
if (
|
|
191
|
+
fact.data?.symbol &&
|
|
192
|
+
fact.data?.file &&
|
|
193
|
+
typeof fact.data.file === "string" &&
|
|
194
|
+
typeof fact.data.symbol === "string"
|
|
195
|
+
) {
|
|
196
|
+
try {
|
|
197
|
+
const symbolNode = await this.findSymbolNode(
|
|
198
|
+
fact.data.file,
|
|
199
|
+
fact.data.symbol,
|
|
200
|
+
);
|
|
201
|
+
if (symbolNode && symbolNode.id !== existing.id) {
|
|
202
|
+
await this.addRelation(existing.id, symbolNode.id, "ABOUT", 1.0);
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
/* ignore linking errors on dup update */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return existing.id;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const id = crypto.randomUUID();
|
|
212
|
+
const timestamp = Date.now();
|
|
213
|
+
const dataStr = JSON.stringify(fact.data || {});
|
|
214
|
+
|
|
215
|
+
await this.execute(
|
|
216
|
+
`
|
|
217
|
+
CREATE (f:Fact {
|
|
218
|
+
id: $id,
|
|
219
|
+
type: $type,
|
|
220
|
+
content: $content,
|
|
221
|
+
data: $data,
|
|
222
|
+
timestamp: $timestamp,
|
|
223
|
+
sessionId: $sessionId,
|
|
224
|
+
confidence: $confidence,
|
|
225
|
+
validated: $validated
|
|
226
|
+
})
|
|
227
|
+
RETURN f
|
|
228
|
+
`,
|
|
229
|
+
{
|
|
230
|
+
id,
|
|
231
|
+
type: fact.type,
|
|
232
|
+
content: fact.content,
|
|
233
|
+
data: dataStr,
|
|
234
|
+
timestamp,
|
|
235
|
+
sessionId: this.sessionId,
|
|
236
|
+
confidence: fact.confidence,
|
|
237
|
+
validated: timestamp,
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Also store in vector DB for semantic search
|
|
242
|
+
try {
|
|
243
|
+
await this.embeddings.store({
|
|
244
|
+
id,
|
|
245
|
+
type: fact.type,
|
|
246
|
+
content: fact.content,
|
|
247
|
+
data: dataStr,
|
|
248
|
+
timestamp,
|
|
249
|
+
sessionId: this.sessionId,
|
|
250
|
+
confidence: fact.confidence,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Link to canonical symbol node if this fact contains symbol data
|
|
254
|
+
if (
|
|
255
|
+
fact.data?.symbol &&
|
|
256
|
+
fact.data?.file &&
|
|
257
|
+
typeof fact.data.file === "string" &&
|
|
258
|
+
typeof fact.data.symbol === "string"
|
|
259
|
+
) {
|
|
260
|
+
const symbolNode = await this.findSymbolNode(
|
|
261
|
+
fact.data.file,
|
|
262
|
+
fact.data.symbol,
|
|
263
|
+
);
|
|
264
|
+
if (symbolNode && symbolNode.id !== id) {
|
|
265
|
+
await this.addRelation(id, symbolNode.id, "ABOUT", 1.0);
|
|
266
|
+
}
|
|
267
|
+
} else if (fact.data?.file && typeof fact.data.file === "string") {
|
|
268
|
+
// Fallback: Link to file node if no symbol but file is present
|
|
269
|
+
const fileNode = await this.findFileNode(fact.data.file);
|
|
270
|
+
if (fileNode && fileNode.id !== id) {
|
|
271
|
+
await this.addRelation(id, fileNode.id, "ABOUT", 1.0);
|
|
272
|
+
}
|
|
273
|
+
} else if (fact.data?.file && typeof fact.data.file === "string") {
|
|
274
|
+
// Fallback: Link to file node if no symbol but file is present
|
|
275
|
+
// This catches generic summaries or file-level facts
|
|
276
|
+
const fileNode = await this.findFileNode(fact.data.file);
|
|
277
|
+
if (fileNode && fileNode.id !== id) {
|
|
278
|
+
await this.addRelation(id, fileNode.id, "ABOUT", 1.0);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Discover relations to existing facts
|
|
283
|
+
await this.discoverRelations(id, fact.content);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
// Embedding storage is optional, don't fail if it errors
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return id;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async findDuplicate(
|
|
292
|
+
type: Fact["type"],
|
|
293
|
+
content: string,
|
|
294
|
+
data?: Fact["data"],
|
|
295
|
+
): Promise<Fact | null> {
|
|
296
|
+
// 1. Structural Identity (Stable IDs for code symbols)
|
|
297
|
+
if (data?.file && data?.symbol) {
|
|
298
|
+
const relFile = path.isAbsolute(data.file)
|
|
299
|
+
? path.relative(process.cwd(), data.file)
|
|
300
|
+
: data.file;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const fileFrag = `"file":"${relFile}"`;
|
|
304
|
+
const symbolFrag = `"symbol":"${data.symbol}"`;
|
|
305
|
+
const result = await this.execute(
|
|
306
|
+
`
|
|
307
|
+
MATCH (f:Fact {type: $type})
|
|
308
|
+
WHERE f.data CONTAINS $fileFrag AND f.data CONTAINS $symbolFrag
|
|
309
|
+
RETURN f
|
|
310
|
+
LIMIT 10
|
|
311
|
+
`,
|
|
312
|
+
{ type, fileFrag, symbolFrag },
|
|
313
|
+
);
|
|
314
|
+
const rows = await result.getAll();
|
|
315
|
+
for (const row of rows) {
|
|
316
|
+
const parsed = this.parseFact(row.f as Record<string, unknown>);
|
|
317
|
+
if (
|
|
318
|
+
parsed.data?.file === relFile &&
|
|
319
|
+
parsed.data?.symbol === data.symbol
|
|
320
|
+
) {
|
|
321
|
+
return parsed;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 2. Exact string match (Fast path)
|
|
328
|
+
try {
|
|
329
|
+
const result = await this.execute(
|
|
330
|
+
`
|
|
331
|
+
MATCH (f:Fact {type: $type})
|
|
332
|
+
WHERE f.content = $content
|
|
333
|
+
RETURN f
|
|
334
|
+
LIMIT 1
|
|
335
|
+
`,
|
|
336
|
+
{ type, content },
|
|
337
|
+
);
|
|
338
|
+
const rows = await result.getAll();
|
|
339
|
+
if (rows.length > 0) {
|
|
340
|
+
return this.parseFact((rows[0] as any).f as Record<string, unknown>);
|
|
341
|
+
}
|
|
342
|
+
} catch (e) {}
|
|
343
|
+
|
|
344
|
+
// 3. Semantic similarity check (Vector DB)
|
|
345
|
+
// Use a high threshold (0.92) to establish "essentially the same meaning"
|
|
346
|
+
try {
|
|
347
|
+
const semanticResults = await this.embeddings.search(content, 3, 0.92);
|
|
348
|
+
const match = semanticResults.find((r) => {
|
|
349
|
+
return r.type === type;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (match) {
|
|
353
|
+
// Fetch full fact from Graph DB to get all fields (e.g. validated status)
|
|
354
|
+
const result = await this.execute(
|
|
355
|
+
`
|
|
356
|
+
MATCH (f:Fact {id: $id})
|
|
357
|
+
RETURN f
|
|
358
|
+
`,
|
|
359
|
+
{ id: match.id },
|
|
360
|
+
);
|
|
361
|
+
const rows = await result.getAll();
|
|
362
|
+
if (rows.length > 0) {
|
|
363
|
+
return this.parseFact((rows[0] as any).f as Record<string, unknown>);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch (e) {}
|
|
367
|
+
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async addRelation(
|
|
372
|
+
sourceId: string,
|
|
373
|
+
targetId: string,
|
|
374
|
+
relation: string,
|
|
375
|
+
weight: number = 1.0,
|
|
376
|
+
): Promise<void> {
|
|
377
|
+
await this.execute(
|
|
378
|
+
`
|
|
379
|
+
MATCH (a:Fact {id: $sourceId})
|
|
380
|
+
MATCH (b:Fact {id: $targetId})
|
|
381
|
+
MERGE (a)-[r:RELATED_TO {relation: $relation}]->(b)
|
|
382
|
+
SET r.weight = $weight
|
|
383
|
+
RETURN r
|
|
384
|
+
`,
|
|
385
|
+
{ sourceId, targetId, relation, weight },
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Discover and create relations between a new fact and existing facts.
|
|
391
|
+
* Uses vector similarity for discovery and LLM for relation typing.
|
|
392
|
+
*/
|
|
393
|
+
private async discoverRelations(
|
|
394
|
+
newFactId: string,
|
|
395
|
+
content: string,
|
|
396
|
+
): Promise<void> {
|
|
397
|
+
// Skip for very short content
|
|
398
|
+
if (content.length < 30) return;
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
// Find semantically similar existing facts
|
|
402
|
+
const similar = await this.embeddings.search(content, 5, 0.6);
|
|
403
|
+
|
|
404
|
+
for (const match of similar) {
|
|
405
|
+
// Skip self-references
|
|
406
|
+
if (match.id === newFactId) continue;
|
|
407
|
+
|
|
408
|
+
// High similarity: use LLM to classify relation type
|
|
409
|
+
if (match.score >= 0.85) {
|
|
410
|
+
try {
|
|
411
|
+
const { classifyRelation } =
|
|
412
|
+
await import("@/agent/subagents/relationAgent");
|
|
413
|
+
const relationType = await classifyRelation(content, match.content);
|
|
414
|
+
await this.addRelation(
|
|
415
|
+
newFactId,
|
|
416
|
+
match.id,
|
|
417
|
+
relationType,
|
|
418
|
+
match.score,
|
|
419
|
+
);
|
|
420
|
+
} catch (e) {
|
|
421
|
+
// Fallback if import fails
|
|
422
|
+
await this.addRelation(
|
|
423
|
+
newFactId,
|
|
424
|
+
match.id,
|
|
425
|
+
"RELATED_TO",
|
|
426
|
+
match.score,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
} else if (match.score >= 0.6) {
|
|
430
|
+
// Medium similarity: use generic RELATED_TO
|
|
431
|
+
await this.addRelation(
|
|
432
|
+
newFactId,
|
|
433
|
+
match.id,
|
|
434
|
+
"RELATED_TO",
|
|
435
|
+
match.score,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} catch (e) {
|
|
440
|
+
// Relation discovery is non-critical, log and continue
|
|
441
|
+
log("Relation discovery failed: " + e);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Batch insert facts and relations for atomic knowledge updates.
|
|
447
|
+
* Efficiently handles indexing large amounts of symbol data.
|
|
448
|
+
*/
|
|
449
|
+
async addFactsWithRelations(
|
|
450
|
+
facts: Omit<Fact, "id" | "timestamp" | "sessionId">[],
|
|
451
|
+
relations: { sourceIdx: number; targetIdx: number; type: string }[],
|
|
452
|
+
): Promise<string[]> {
|
|
453
|
+
// 1. Insert all facts first, collecting IDs
|
|
454
|
+
const factIds: string[] = [];
|
|
455
|
+
for (const fact of facts) {
|
|
456
|
+
// Use standard addFact to ensure deduplication and vector embedding
|
|
457
|
+
// Note: This calls discoverRelations() for each fact, which is good for cross-linking
|
|
458
|
+
const id = await this.addFact(fact);
|
|
459
|
+
factIds.push(id);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 2. Create provided relations using collected IDs
|
|
463
|
+
for (const rel of relations) {
|
|
464
|
+
const sourceId = factIds[rel.sourceIdx];
|
|
465
|
+
const targetId = factIds[rel.targetIdx];
|
|
466
|
+
|
|
467
|
+
// Safety check: ensure both ends exist
|
|
468
|
+
if (sourceId && targetId) {
|
|
469
|
+
await this.addRelation(sourceId, targetId, rel.type);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return factIds;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Index a source file to extract symbols and structural relations.
|
|
478
|
+
* Maps code intelligence (DEFINES, CALLS) into the knowledge graph.
|
|
479
|
+
*/
|
|
480
|
+
async indexFile(filePath: string): Promise<void> {
|
|
481
|
+
try {
|
|
482
|
+
const stats = fs.statSync(filePath);
|
|
483
|
+
const mtime = stats.mtimeMs;
|
|
484
|
+
const relPath = path.relative(process.cwd(), filePath);
|
|
485
|
+
|
|
486
|
+
// Check if file has changed since last indexing
|
|
487
|
+
const existingFileFact = await this.findDuplicate(
|
|
488
|
+
"file",
|
|
489
|
+
`File: ${relPath}`,
|
|
490
|
+
{ file: relPath },
|
|
491
|
+
);
|
|
492
|
+
if (existingFileFact && existingFileFact.data?.mtime === mtime) {
|
|
493
|
+
log(`[AgentCognition] Skipping unchanged file: ${relPath}`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const { SymbolExtractor } = await import("@/services/symbolExtractor");
|
|
498
|
+
const extractor = new SymbolExtractor();
|
|
499
|
+
const { facts, relations } = await extractor.analyze(filePath);
|
|
500
|
+
|
|
501
|
+
if (facts.length > 0) {
|
|
502
|
+
// Update file fact with mtime
|
|
503
|
+
const fileFactIdx = facts.findIndex((f) => f.type === "file");
|
|
504
|
+
if (fileFactIdx !== -1) {
|
|
505
|
+
const fileFact = facts[fileFactIdx];
|
|
506
|
+
if (fileFact) {
|
|
507
|
+
fileFact.data = { ...(fileFact.data || {}), mtime };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
log(
|
|
512
|
+
`Indexing ${filePath}: ${facts.length} symbols, ${relations.length} relations`,
|
|
513
|
+
);
|
|
514
|
+
const indexedIds = await this.addFactsWithRelations(facts, relations);
|
|
515
|
+
|
|
516
|
+
// Purge symbols that are no longer in the file
|
|
517
|
+
const currentSymbolNames = facts
|
|
518
|
+
.filter((f) => f.type !== "file" && f.type !== "module")
|
|
519
|
+
.map((f) => (f.data as any).symbol)
|
|
520
|
+
.filter(Boolean);
|
|
521
|
+
|
|
522
|
+
await this.purgeOrphanedSymbols(filePath, currentSymbolNames);
|
|
523
|
+
}
|
|
524
|
+
} catch (e) {
|
|
525
|
+
log(`Failed to index file ${filePath}: ${e}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Remove symbol-related facts for a file that are no longer present.
|
|
531
|
+
* Uses DETACH DELETE to ensure no orphaned relations remain.
|
|
532
|
+
*/
|
|
533
|
+
private async purgeOrphanedSymbols(
|
|
534
|
+
filePath: string,
|
|
535
|
+
currentSymbols: string[],
|
|
536
|
+
): Promise<void> {
|
|
537
|
+
const codeTypes =
|
|
538
|
+
"['function', 'class', 'interface', 'variable', 'symbol']";
|
|
539
|
+
const relFile = path.relative(process.cwd(), filePath);
|
|
540
|
+
try {
|
|
541
|
+
const result = await this.execute(
|
|
542
|
+
`
|
|
543
|
+
MATCH (f:Fact)
|
|
544
|
+
WHERE f.type IN ${codeTypes} AND f.data CONTAINS $file
|
|
545
|
+
RETURN f
|
|
546
|
+
`,
|
|
547
|
+
{ file: `"${relFile}"` },
|
|
548
|
+
);
|
|
549
|
+
const rows = await result.getAll();
|
|
550
|
+
|
|
551
|
+
for (const row of rows) {
|
|
552
|
+
const fact = this.parseFact(row.f as Record<string, unknown>);
|
|
553
|
+
if (
|
|
554
|
+
fact.data?.file === relFile &&
|
|
555
|
+
fact.data?.symbol &&
|
|
556
|
+
!currentSymbols.includes(fact.data.symbol as string)
|
|
557
|
+
) {
|
|
558
|
+
log(
|
|
559
|
+
`[AgentCognition] Purging orphaned symbol: ${fact.data.symbol} from ${relFile}`,
|
|
560
|
+
);
|
|
561
|
+
await this.execute(`MATCH (f:Fact {id: $id}) DETACH DELETE f`, {
|
|
562
|
+
id: fact.id,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} catch (e) {
|
|
567
|
+
log(`Failed to purge orphaned symbols for ${relFile}: ${e}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Remove all symbol-related facts for a file (called before re-indexing).
|
|
573
|
+
* Also cleans up relations to prevent orphaned edges.
|
|
574
|
+
*/
|
|
575
|
+
/**
|
|
576
|
+
* Remove all symbol-related facts for a file (called when cleanup is forced).
|
|
577
|
+
* Also cleans up relations to prevent orphaned edges.
|
|
578
|
+
*/
|
|
579
|
+
private async clearFileSymbols(filePath: string): Promise<void> {
|
|
580
|
+
const codeTypes =
|
|
581
|
+
"['file', 'module', 'function', 'class', 'interface', 'variable', 'symbol']";
|
|
582
|
+
const relFile = path.relative(process.cwd(), filePath);
|
|
583
|
+
try {
|
|
584
|
+
// Use DETACH DELETE for cleaner relation removal
|
|
585
|
+
await this.execute(
|
|
586
|
+
`
|
|
587
|
+
MATCH (f:Fact)
|
|
588
|
+
WHERE f.type IN ${codeTypes} AND f.data CONTAINS $file
|
|
589
|
+
DETACH DELETE f
|
|
590
|
+
`,
|
|
591
|
+
{ file: `"${relFile}"` },
|
|
592
|
+
);
|
|
593
|
+
} catch (e) {
|
|
594
|
+
// Non-critical
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Index all supported files in a directory (background batch indexing).
|
|
600
|
+
* Runs non-blocking and logs progress.
|
|
601
|
+
*/
|
|
602
|
+
async indexDirectory(
|
|
603
|
+
dirPath: string,
|
|
604
|
+
extensions = [".ts", ".tsx", ".js", ".jsx", ".py"],
|
|
605
|
+
): Promise<void> {
|
|
606
|
+
// Load .gitignore patterns
|
|
607
|
+
const ig = ignore();
|
|
608
|
+
const gitignorePath = path.join(dirPath, ".gitignore");
|
|
609
|
+
if (fs.existsSync(gitignorePath)) {
|
|
610
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
|
|
611
|
+
ig.add(gitignoreContent);
|
|
612
|
+
}
|
|
613
|
+
// Always ignore .fraude data folder and common build artifacts
|
|
614
|
+
ig.add([".fraude", "node_modules", "dist", "build", ".git"]);
|
|
615
|
+
|
|
616
|
+
const glob = new Bun.Glob(`**/*{${extensions.join(",")}}`);
|
|
617
|
+
const files: string[] = [];
|
|
618
|
+
|
|
619
|
+
for await (const file of glob.scan({
|
|
620
|
+
cwd: dirPath,
|
|
621
|
+
absolute: true,
|
|
622
|
+
onlyFiles: true,
|
|
623
|
+
dot: false,
|
|
624
|
+
})) {
|
|
625
|
+
// Get relative path for gitignore matching
|
|
626
|
+
const relativePath = path.relative(dirPath, file);
|
|
627
|
+
if (ig.ignores(relativePath)) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
files.push(file);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
log(`Background indexing: ${files.length} files in ${dirPath}`);
|
|
634
|
+
|
|
635
|
+
// Process files with a small delay between each to avoid blocking
|
|
636
|
+
for (const file of files) {
|
|
637
|
+
try {
|
|
638
|
+
await this.indexFile(file);
|
|
639
|
+
} catch (e) {
|
|
640
|
+
// Continue on individual file failures
|
|
641
|
+
}
|
|
642
|
+
// Yield to event loop occasionally
|
|
643
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
log(`Background indexing complete: ${files.length} files processed`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Find a canonical symbol node in the graph.
|
|
651
|
+
* Used to link extracted semantic facts to concrete code symbols.
|
|
652
|
+
*/
|
|
653
|
+
async findSymbolNode(file: string, symbol: string): Promise<Fact | null> {
|
|
654
|
+
const relFile = path.relative(process.cwd(), file);
|
|
655
|
+
try {
|
|
656
|
+
// Precise search using both symbol name and file path
|
|
657
|
+
// This avoids the "floating node" issue where common symbols (e.g. "init")
|
|
658
|
+
// were missed due to LIMIT clauses on broad queries
|
|
659
|
+
const symbolFrag = `"symbol":"${symbol}"`;
|
|
660
|
+
const fileFrag = `"file":"${relFile}"`;
|
|
661
|
+
|
|
662
|
+
const result = await this.execute(
|
|
663
|
+
`
|
|
664
|
+
MATCH (f:Fact)
|
|
665
|
+
WHERE f.data CONTAINS $symbolFrag AND f.data CONTAINS $fileFrag
|
|
666
|
+
RETURN f
|
|
667
|
+
LIMIT 1
|
|
668
|
+
`,
|
|
669
|
+
{ symbolFrag, fileFrag },
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const rows = await result.getAll();
|
|
673
|
+
|
|
674
|
+
if (rows.length > 0) {
|
|
675
|
+
return this.parseFact((rows[0] as any).f as Record<string, unknown>);
|
|
676
|
+
}
|
|
677
|
+
} catch (e) {
|
|
678
|
+
log(`findSymbolNode error: ${e}`);
|
|
679
|
+
}
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Find a file node by path, handling normalization.
|
|
685
|
+
*/
|
|
686
|
+
async findFileNode(filePath: string): Promise<Fact | null> {
|
|
687
|
+
const relFile = path.isAbsolute(filePath)
|
|
688
|
+
? path.relative(process.cwd(), filePath)
|
|
689
|
+
: filePath;
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
const fileFrag = `"file":"${relFile}"`;
|
|
693
|
+
const result = await this.execute(
|
|
694
|
+
`
|
|
695
|
+
MATCH (f:Fact {type: 'file'})
|
|
696
|
+
WHERE f.data CONTAINS $fileFrag
|
|
697
|
+
RETURN f
|
|
698
|
+
LIMIT 1
|
|
699
|
+
`,
|
|
700
|
+
{ fileFrag },
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const rows = await result.getAll();
|
|
704
|
+
if (rows.length > 0) {
|
|
705
|
+
return this.parseFact((rows[0] as any).f as Record<string, unknown>);
|
|
706
|
+
}
|
|
707
|
+
} catch (e) {
|
|
708
|
+
log(`findFileNode error: ${e}`);
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async query(
|
|
714
|
+
cypher: string,
|
|
715
|
+
params: Record<string, unknown> = {},
|
|
716
|
+
): Promise<unknown[]> {
|
|
717
|
+
const result = await this.execute(cypher, params);
|
|
718
|
+
return result.getAll();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ============================================================================
|
|
722
|
+
// Retrieval
|
|
723
|
+
// ============================================================================
|
|
724
|
+
|
|
725
|
+
async findByType(type: Fact["type"]): Promise<Fact[]> {
|
|
726
|
+
const result = await this.execute(`MATCH (f:Fact {type: $type}) RETURN f`, {
|
|
727
|
+
type,
|
|
728
|
+
});
|
|
729
|
+
const rows = await result.getAll();
|
|
730
|
+
return rows.map((row: Record<string, unknown>) =>
|
|
731
|
+
this.parseFact(row.f as Record<string, unknown>),
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async findRelated(factId: string, depth: number = 1): Promise<Fact[]> {
|
|
736
|
+
const result = await this.execute(
|
|
737
|
+
`
|
|
738
|
+
MATCH (start:Fact {id: $factId})-[r*1..${depth}]->(related:Fact)
|
|
739
|
+
WHERE NOT start.id = related.id
|
|
740
|
+
RETURN DISTINCT related
|
|
741
|
+
`,
|
|
742
|
+
{ factId },
|
|
743
|
+
);
|
|
744
|
+
const rows = await result.getAll();
|
|
745
|
+
return rows.map((row: Record<string, unknown>) =>
|
|
746
|
+
this.parseFact(row.related as Record<string, unknown>),
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async searchByContent(query: string, limit: number = 10): Promise<Fact[]> {
|
|
751
|
+
// Simple contains search - could be enhanced with embeddings later
|
|
752
|
+
const result = await this.execute(
|
|
753
|
+
`
|
|
754
|
+
MATCH (f:Fact)
|
|
755
|
+
WHERE f.content CONTAINS $query
|
|
756
|
+
RETURN f
|
|
757
|
+
LIMIT $limit
|
|
758
|
+
`,
|
|
759
|
+
{ query, limit },
|
|
760
|
+
);
|
|
761
|
+
const rows = await result.getAll();
|
|
762
|
+
return rows.map((row: Record<string, unknown>) =>
|
|
763
|
+
this.parseFact(row.f as Record<string, unknown>),
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// async getPrimingContext(): Promise<string> {
|
|
768
|
+
// // Get key project knowledge: recent summaries and high-confidence decisions
|
|
769
|
+
// const summaries = await this.findByType("summary");
|
|
770
|
+
// const decisions = await this.findByType("decision");
|
|
771
|
+
|
|
772
|
+
// const validSummaries = await this.filterValid(summaries.slice(0, 3));
|
|
773
|
+
// const validDecisions = await this.filterValid(decisions.slice(0, 5));
|
|
774
|
+
|
|
775
|
+
// const parts: string[] = [];
|
|
776
|
+
|
|
777
|
+
// if (validSummaries.length > 0) {
|
|
778
|
+
// parts.push("<previous_session_context>");
|
|
779
|
+
// validSummaries.forEach((s) => parts.push(`- ${s.content}`));
|
|
780
|
+
// parts.push("</previous_session_context>");
|
|
781
|
+
// }
|
|
782
|
+
|
|
783
|
+
// if (validDecisions.length > 0) {
|
|
784
|
+
// parts.push("<project_decisions>");
|
|
785
|
+
// validDecisions.forEach((d) => parts.push(`- ${d.content}`));
|
|
786
|
+
// parts.push("</project_decisions>");
|
|
787
|
+
// }
|
|
788
|
+
|
|
789
|
+
// return parts.join("\n");
|
|
790
|
+
// }
|
|
791
|
+
|
|
792
|
+
async retrieveRelevant(query: string, limit: number = 5): Promise<Fact[]> {
|
|
793
|
+
// Hybrid search: combine vector similarity + graph relationships
|
|
794
|
+
const scored = new Map<string, { fact: Fact; score: number }>();
|
|
795
|
+
|
|
796
|
+
// Phase 1: Vector search for semantic similarity
|
|
797
|
+
try {
|
|
798
|
+
const semanticResults = await this.embeddings.search(
|
|
799
|
+
query,
|
|
800
|
+
limit * 2,
|
|
801
|
+
0.3,
|
|
802
|
+
);
|
|
803
|
+
for (const r of semanticResults) {
|
|
804
|
+
const fact: Fact = {
|
|
805
|
+
id: r.id,
|
|
806
|
+
type: r.type as Fact["type"],
|
|
807
|
+
content: r.content,
|
|
808
|
+
data: r.data ? JSON.parse(r.data) : undefined,
|
|
809
|
+
timestamp: r.timestamp,
|
|
810
|
+
sessionId: r.sessionId,
|
|
811
|
+
confidence: r.confidence,
|
|
812
|
+
};
|
|
813
|
+
// Vector score: similarity (0-1) weighted heavily
|
|
814
|
+
scored.set(r.id, { fact, score: r.score * 0.7 });
|
|
815
|
+
}
|
|
816
|
+
} catch (e) {
|
|
817
|
+
// Continue with graph-only if embeddings fail
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Phase 2: Graph expansion - find related facts for top vector hits
|
|
821
|
+
const topVectorIds = [...scored.entries()]
|
|
822
|
+
.sort((a, b) => b[1].score - a[1].score)
|
|
823
|
+
.slice(0, 3)
|
|
824
|
+
.map(([id]) => id);
|
|
825
|
+
|
|
826
|
+
for (const factId of topVectorIds) {
|
|
827
|
+
try {
|
|
828
|
+
const related = await this.findRelated(factId, 1);
|
|
829
|
+
for (const rel of related) {
|
|
830
|
+
const existing = scored.get(rel.id);
|
|
831
|
+
if (existing) {
|
|
832
|
+
// Boost score for facts found via both vector AND graph
|
|
833
|
+
existing.score += 0.2;
|
|
834
|
+
} else {
|
|
835
|
+
// Add graph-discovered facts with lower base score
|
|
836
|
+
scored.set(rel.id, { fact: rel, score: 0.3 });
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} catch (e) {
|
|
840
|
+
// Continue if graph query fails
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Phase 3: Text fallback if no results yet
|
|
845
|
+
if (scored.size === 0) {
|
|
846
|
+
const textResults = await this.searchByContent(query, limit * 2);
|
|
847
|
+
for (const fact of textResults) {
|
|
848
|
+
scored.set(fact.id, { fact, score: 0.4 });
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Phase 4: Rank, validate, and return
|
|
853
|
+
const ranked = [...scored.values()]
|
|
854
|
+
.sort((a, b) => b.score - a.score)
|
|
855
|
+
.slice(0, limit * 2)
|
|
856
|
+
.map((s) => s.fact);
|
|
857
|
+
|
|
858
|
+
return this.filterValid(ranked.slice(0, limit));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private parseFact(raw: Record<string, unknown>): Fact {
|
|
862
|
+
return {
|
|
863
|
+
id: raw.id as string,
|
|
864
|
+
type: raw.type as Fact["type"],
|
|
865
|
+
content: raw.content as string,
|
|
866
|
+
data: raw.data ? JSON.parse(raw.data as string) : undefined,
|
|
867
|
+
timestamp: raw.timestamp as number,
|
|
868
|
+
sessionId: raw.sessionId as string,
|
|
869
|
+
confidence: raw.confidence as number,
|
|
870
|
+
validated: raw.validated as number,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ============================================================================
|
|
875
|
+
// Validation
|
|
876
|
+
// ============================================================================
|
|
877
|
+
|
|
878
|
+
async validateFact(fact: Fact): Promise<ValidationResult> {
|
|
879
|
+
// 1. File reference check
|
|
880
|
+
if (fact.data?.file) {
|
|
881
|
+
// Normalize to absolute for existence check
|
|
882
|
+
const filePath = path.isAbsolute(fact.data.file)
|
|
883
|
+
? fact.data.file
|
|
884
|
+
: path.resolve(process.cwd(), fact.data.file);
|
|
885
|
+
|
|
886
|
+
if (fact.type === "file" && !fs.existsSync(filePath)) {
|
|
887
|
+
// Strict check for FactType.FILE
|
|
888
|
+
return { valid: false, reason: "file_missing", suggestion: "delete" };
|
|
889
|
+
} else if (!fs.existsSync(filePath) && !fact.data.isExternal) {
|
|
890
|
+
// Loose check for other types (might be external libs)
|
|
891
|
+
return { valid: false, reason: "file_missing", suggestion: "delete" };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// 2. Code presence check (function/symbol)
|
|
896
|
+
if (fact.data?.function || fact.data?.symbol) {
|
|
897
|
+
const exists = await this.checkCodeExists(fact.data);
|
|
898
|
+
if (!exists) {
|
|
899
|
+
return { valid: false, reason: "code_changed", suggestion: "delete" };
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// 3. Staleness check
|
|
904
|
+
const age = Date.now() - fact.timestamp;
|
|
905
|
+
if (age > AgentCognition.STALE_THRESHOLD_MS) {
|
|
906
|
+
// If it references files, check if they changed
|
|
907
|
+
if (fact.data?.file) {
|
|
908
|
+
const filePath = path.isAbsolute(fact.data.file)
|
|
909
|
+
? fact.data.file
|
|
910
|
+
: path.resolve(process.cwd(), fact.data.file);
|
|
911
|
+
|
|
912
|
+
if (fs.existsSync(filePath)) {
|
|
913
|
+
const stat = fs.statSync(filePath);
|
|
914
|
+
if (stat.mtimeMs > fact.timestamp) {
|
|
915
|
+
return { valid: false, reason: "stale", suggestion: "update" };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
// Generic staleness for old facts without file refs
|
|
920
|
+
if (age > AgentCognition.STALE_THRESHOLD_MS * 4) {
|
|
921
|
+
return { valid: false, reason: "stale", suggestion: "update" };
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return { valid: true };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private async checkCodeExists(data: Fact["data"]): Promise<boolean> {
|
|
929
|
+
if (!data?.file) return true;
|
|
930
|
+
|
|
931
|
+
const filePath = path.isAbsolute(data.file)
|
|
932
|
+
? data.file
|
|
933
|
+
: path.resolve(process.cwd(), data.file);
|
|
934
|
+
|
|
935
|
+
if (!fs.existsSync(filePath)) return false;
|
|
936
|
+
|
|
937
|
+
// Check if function/symbol exists in file
|
|
938
|
+
const target = data.function || data.symbol;
|
|
939
|
+
if (!target) return true;
|
|
940
|
+
|
|
941
|
+
try {
|
|
942
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
943
|
+
// Simple check: does the symbol name appear in the file?
|
|
944
|
+
return content.includes(target);
|
|
945
|
+
} catch {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async filterValid(facts: Fact[]): Promise<Fact[]> {
|
|
951
|
+
const results: Fact[] = [];
|
|
952
|
+
for (const fact of facts) {
|
|
953
|
+
const validation = await this.validateFact(fact);
|
|
954
|
+
if (validation.valid) {
|
|
955
|
+
results.push(fact);
|
|
956
|
+
} else if (validation.suggestion === "delete") {
|
|
957
|
+
await this.deleteFact(fact.id);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return results;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
async deleteFact(id: string): Promise<void> {
|
|
964
|
+
await this.execute(`MATCH (f:Fact {id: $id}) DELETE f`, { id });
|
|
965
|
+
try {
|
|
966
|
+
await this.embeddings.delete(id);
|
|
967
|
+
} catch (e) {
|
|
968
|
+
// Vector deletion is best-effort, don't fail if it errors
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async pruneStale(): Promise<number> {
|
|
973
|
+
const allFacts = await this.query(`MATCH (f:Fact) RETURN f`);
|
|
974
|
+
let pruned = 0;
|
|
975
|
+
|
|
976
|
+
for (const row of allFacts) {
|
|
977
|
+
const fact = this.parseFact(
|
|
978
|
+
(row as Record<string, unknown>).f as Record<string, unknown>,
|
|
979
|
+
);
|
|
980
|
+
const validation = await this.validateFact(fact);
|
|
981
|
+
if (!validation.valid && validation.suggestion === "delete") {
|
|
982
|
+
await this.deleteFact(fact.id);
|
|
983
|
+
pruned++;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return pruned;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// ============================================================================
|
|
991
|
+
// Session Management
|
|
992
|
+
// ============================================================================
|
|
993
|
+
|
|
994
|
+
async summarizeSession(
|
|
995
|
+
messages: { role: string; content: string }[],
|
|
996
|
+
): Promise<string> {
|
|
997
|
+
// Simple extraction: look for key patterns in messages
|
|
998
|
+
// Could be enhanced with LLM-based summarization
|
|
999
|
+
const userMessages = messages
|
|
1000
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
1001
|
+
.map((m) => m.content)
|
|
1002
|
+
.filter((c) => c.length > 0);
|
|
1003
|
+
|
|
1004
|
+
if (userMessages.length === 0) return "";
|
|
1005
|
+
|
|
1006
|
+
// Create a simple summary from first and last user messages
|
|
1007
|
+
const first = userMessages[0] ?? "";
|
|
1008
|
+
const last = userMessages[userMessages.length - 1] ?? "";
|
|
1009
|
+
|
|
1010
|
+
return `"${first.slice(0, 100)}"${userMessages.length > 1 ? ` → "${last.slice(0, 100)}"` : ""}`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async extractFromSession(
|
|
1014
|
+
messages: { role: string; content: string }[],
|
|
1015
|
+
): Promise<Fact[]> {
|
|
1016
|
+
log("Extracting facts from session");
|
|
1017
|
+
// Extract facts from session using LLM-based analysis
|
|
1018
|
+
const recentMessages = messages.slice(-15);
|
|
1019
|
+
const conversationText = recentMessages
|
|
1020
|
+
.map((m) => `${m.role.toUpperCase()}:\n${m.content}`)
|
|
1021
|
+
.join("\n---\n");
|
|
1022
|
+
|
|
1023
|
+
const assistantMessages = messages
|
|
1024
|
+
.filter((m) => m.role === "assistant" || m.role === "user")
|
|
1025
|
+
.map((m) => m.content);
|
|
1026
|
+
|
|
1027
|
+
if (conversationText.length < 50 && assistantMessages.length === 0)
|
|
1028
|
+
return [];
|
|
1029
|
+
|
|
1030
|
+
// Try LLM-based extraction first
|
|
1031
|
+
try {
|
|
1032
|
+
if (conversationText.length >= 50) {
|
|
1033
|
+
const extracted = await this.extractWithLLM(conversationText);
|
|
1034
|
+
if (extracted.length > 0) return extracted;
|
|
1035
|
+
}
|
|
1036
|
+
} catch (e) {}
|
|
1037
|
+
|
|
1038
|
+
// Fallback: regex-based extraction
|
|
1039
|
+
return this.extractWithPatterns(assistantMessages);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private async extractWithLLM(content: string): Promise<Fact[]> {
|
|
1043
|
+
const { extractFacts } = await import("@/agent/subagents/extractionAgent");
|
|
1044
|
+
|
|
1045
|
+
const extracted = await extractFacts(content, this.sessionId);
|
|
1046
|
+
log("Extracted facts: " + JSON.stringify(extracted, null, 2));
|
|
1047
|
+
|
|
1048
|
+
return extracted.map((f) => ({
|
|
1049
|
+
...f,
|
|
1050
|
+
id: crypto.randomUUID(),
|
|
1051
|
+
timestamp: Date.now(),
|
|
1052
|
+
})) as Fact[];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private extractWithPatterns(assistantMessages: string[]): Fact[] {
|
|
1056
|
+
const facts: Fact[] = [];
|
|
1057
|
+
|
|
1058
|
+
for (const content of assistantMessages) {
|
|
1059
|
+
const decisionPatterns = [
|
|
1060
|
+
/(?:decided|choosing|using|implementing)\s+([^.]+)/gi,
|
|
1061
|
+
/(?:will use|should use|recommend)\s+([^.]+)/gi,
|
|
1062
|
+
];
|
|
1063
|
+
|
|
1064
|
+
for (const pattern of decisionPatterns) {
|
|
1065
|
+
let match;
|
|
1066
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1067
|
+
const captured = match[1];
|
|
1068
|
+
if (captured && captured.length > 10 && captured.length < 200) {
|
|
1069
|
+
facts.push({
|
|
1070
|
+
id: crypto.randomUUID(),
|
|
1071
|
+
type: "decision",
|
|
1072
|
+
content: captured.trim(),
|
|
1073
|
+
timestamp: Date.now(),
|
|
1074
|
+
sessionId: this.sessionId,
|
|
1075
|
+
confidence: 0.7,
|
|
1076
|
+
validated: Date.now(),
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Look for fact patterns
|
|
1083
|
+
const factPatterns = [
|
|
1084
|
+
/(?:the project|this codebase|the app)\s+([^.]+)/gi,
|
|
1085
|
+
/(?:note that|remember that|important:)\s+([^.]+)/gi,
|
|
1086
|
+
];
|
|
1087
|
+
|
|
1088
|
+
for (const pattern of factPatterns) {
|
|
1089
|
+
let match;
|
|
1090
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1091
|
+
const captured = match[1];
|
|
1092
|
+
if (captured && captured.length > 10 && captured.length < 200) {
|
|
1093
|
+
facts.push({
|
|
1094
|
+
id: crypto.randomUUID(),
|
|
1095
|
+
type: "fact",
|
|
1096
|
+
content: captured.trim(),
|
|
1097
|
+
timestamp: Date.now(),
|
|
1098
|
+
sessionId: this.sessionId,
|
|
1099
|
+
confidence: 0.6,
|
|
1100
|
+
validated: Date.now(),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return facts;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
getSessionId(): string {
|
|
1111
|
+
return this.sessionId;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ============================================================================
|
|
1115
|
+
// Cleanup
|
|
1116
|
+
// ============================================================================
|
|
1117
|
+
|
|
1118
|
+
async reset(): Promise<void> {
|
|
1119
|
+
await this.init();
|
|
1120
|
+
|
|
1121
|
+
// Clear Graph DB - drop relation tables first (they depend on Fact)
|
|
1122
|
+
if (this.conn) {
|
|
1123
|
+
try {
|
|
1124
|
+
await this.conn.query("DROP TABLE RELATED_TO");
|
|
1125
|
+
} catch (e) {}
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
await this.conn.query("DROP TABLE Fact");
|
|
1129
|
+
} catch (e) {}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Clear Vector DB
|
|
1133
|
+
await this.embeddings.clear();
|
|
1134
|
+
|
|
1135
|
+
// Re-initialize schema
|
|
1136
|
+
await this.initSchema();
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async close(): Promise<void> {
|
|
1140
|
+
if (this.conn) {
|
|
1141
|
+
await this.conn.close();
|
|
1142
|
+
this.conn = null;
|
|
1143
|
+
}
|
|
1144
|
+
if (this.db) {
|
|
1145
|
+
await this.db.close();
|
|
1146
|
+
this.db = null;
|
|
1147
|
+
}
|
|
1148
|
+
this.initPromise = null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
export default AgentCognition;
|