@hawon/nexus 0.2.0 → 0.3.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.
Files changed (37) hide show
  1. package/dist/cli/index.js +44 -71
  2. package/dist/index.js +15 -24
  3. package/dist/mcp/server.js +7 -6
  4. package/package.json +1 -1
  5. package/scripts/benchmark.ts +13 -35
  6. package/src/cli/index.ts +45 -84
  7. package/src/index.ts +17 -27
  8. package/src/mcp/server.ts +8 -7
  9. package/src/memory-engine/index.ts +4 -6
  10. package/src/memory-engine/semantic.ts +38 -11
  11. package/src/promptguard/advanced-rules.ts +122 -5
  12. package/src/promptguard/entropy.ts +21 -2
  13. package/src/promptguard/rules.ts +87 -2
  14. package/src/promptguard/scanner.test.ts +1 -4
  15. package/src/promptguard/scanner.ts +1 -1
  16. package/src/promptguard/semantic.ts +19 -4
  17. package/src/promptguard/token-analysis.ts +17 -5
  18. package/src/review/analyzer.ts +106 -23
  19. package/src/skills/index.ts +11 -27
  20. package/src/testing/health-check.ts +19 -2
  21. package/src/memory-engine/compressor.ts +0 -97
  22. package/src/memory-engine/context-window.ts +0 -113
  23. package/src/memory-engine/store.ts +0 -371
  24. package/src/memory-engine/types.ts +0 -32
  25. package/src/skills/context-engine.ts +0 -863
  26. package/src/skills/extractor.ts +0 -224
  27. package/src/skills/global-context.ts +0 -731
  28. package/src/skills/library.ts +0 -189
  29. package/src/skills/pattern-engine.ts +0 -715
  30. package/src/skills/render-evolved.ts +0 -160
  31. package/src/skills/skill-reconciler.ts +0 -699
  32. package/src/skills/smart-extractor.ts +0 -849
  33. package/src/skills/types.ts +0 -18
  34. package/src/skills/wisdom-extractor.ts +0 -737
  35. package/src/superdev-evolution/index.ts +0 -3
  36. package/src/superdev-evolution/skill-manager.ts +0 -266
  37. package/src/superdev-evolution/types.ts +0 -20
@@ -1,863 +0,0 @@
1
- /**
2
- * Context Engine — Algorithmic Language Understanding
3
- *
4
- * A mini-LLM built with pure algorithms. No neural networks, no API calls.
5
- * Understands conversation context, intent, and flow through:
6
- *
7
- * 1. CONTEXTUAL STATE MACHINE
8
- * Tracks conversation phase: greeting → problem_statement → exploration →
9
- * execution → verification → completion
10
- *
11
- * 2. BAYESIAN INTENT CLASSIFIER
12
- * P(intent | words, state, history) using Bayes theorem
13
- * Prior: conversation state, recent intents
14
- * Likelihood: word probabilities per intent class
15
- * Evidence: full message + surrounding context
16
- *
17
- * 3. SLIDING ATTENTION WINDOW
18
- * Dynamically weights which past messages matter most for current understanding
19
- * Recency bias + relevance scoring + topic coherence
20
- *
21
- * 4. CONCEPT GRAPH
22
- * Builds a graph of entities and relationships mentioned in conversation
23
- * Tracks: what files, what errors, what goals, what tools, what decisions
24
- *
25
- * 5. AUTO-TRIGGER
26
- * Automatically detects when a skill-worthy pattern has completed
27
- * No explicit commands needed — watches the conversation flow
28
- */
29
-
30
- import type { ParsedMessage, ToolCall } from "../parser/types.js";
31
-
32
- // ═══════════════════════════════════════════════════════════════════
33
- // TYPES
34
- // ═══════════════════════════════════════════════════════════════════
35
-
36
- /** Conversation phase — tracked by the state machine. */
37
- export type ConversationPhase =
38
- | "idle" // No active task
39
- | "problem_stated" // User described a problem/goal
40
- | "exploring" // Discussing approaches, asking questions
41
- | "executing" // Claude is running tools, making changes
42
- | "verifying" // Checking if the result works
43
- | "completed" // Task done, user acknowledged
44
- | "failed" // Task didn't work
45
- | "pivoting"; // Changing approach mid-task
46
-
47
- /** Fine-grained intent — what the user wants RIGHT NOW. */
48
- export type GranularIntent = {
49
- primary: IntentType;
50
- confidence: number;
51
- /** Secondary intents (e.g., fix_bug + security). */
52
- secondary: IntentType[];
53
- /** What specifically (extracted from context). */
54
- target: string;
55
- /** Why (inferred from conversation history). */
56
- motivation: string;
57
- /** How urgent/important (from language intensity). */
58
- urgency: "low" | "medium" | "high" | "critical";
59
- };
60
-
61
- export type IntentType =
62
- | "fix_bug" | "build_feature" | "refactor" | "investigate"
63
- | "configure" | "deploy" | "test" | "security_audit"
64
- | "code_review" | "explain" | "optimize" | "migrate"
65
- | "debug" | "cleanup" | "document" | "monitor"
66
- | "acknowledge" | "redirect" | "clarify" | "continue";
67
-
68
- /** A node in the concept graph. */
69
- export type ConceptNode = {
70
- id: string;
71
- type: "file" | "error" | "tool" | "goal" | "decision" | "entity" | "concept";
72
- label: string;
73
- /** How many times mentioned. */
74
- weight: number;
75
- /** When first/last mentioned. */
76
- firstSeen: number;
77
- lastSeen: number;
78
- /** Sentiment: positive/negative/neutral. */
79
- sentiment: number; // -1 to 1
80
- };
81
-
82
- export type ConceptEdge = {
83
- from: string;
84
- to: string;
85
- relation: "causes" | "fixes" | "modifies" | "requires" | "produces" | "related";
86
- weight: number;
87
- };
88
-
89
- export type ConceptGraph = {
90
- nodes: Map<string, ConceptNode>;
91
- edges: ConceptEdge[];
92
- };
93
-
94
- /** Attention-weighted message. */
95
- export type AttentionMessage = {
96
- message: ParsedMessage;
97
- /** How relevant this message is to the current context. 0-1. */
98
- attention: number;
99
- /** What phase the conversation was in when this was said. */
100
- phase: ConversationPhase;
101
- /** Intent at this point. */
102
- intent: GranularIntent;
103
- };
104
-
105
- /** Auto-detected skill trigger event. */
106
- export type SkillTrigger = {
107
- type: "task_completed" | "pattern_repeated" | "lesson_learned" | "pivot_point";
108
- /** Episode messages that form this skill. */
109
- messages: AttentionMessage[];
110
- /** Why this was triggered. */
111
- reason: string;
112
- /** Quality estimate. */
113
- quality: number;
114
- };
115
-
116
- /** Full engine state. */
117
- export type EngineState = {
118
- phase: ConversationPhase;
119
- phaseHistory: ConversationPhase[];
120
- currentIntent: GranularIntent;
121
- intentHistory: GranularIntent[];
122
- attentionWindow: AttentionMessage[];
123
- conceptGraph: ConceptGraph;
124
- triggers: SkillTrigger[];
125
- /** Running topic summary. */
126
- topicStack: string[];
127
- /** Turn counter. */
128
- turnCount: number;
129
- };
130
-
131
- // ═══════════════════════════════════════════════════════════════════
132
- // BAYESIAN INTENT CLASSIFIER
133
- // ═══════════════════════════════════════════════════════════════════
134
-
135
- /**
136
- * Word-intent probability table.
137
- * P(word | intent) — how likely each word is given an intent.
138
- * Built from domain knowledge, not training data.
139
- */
140
- const INTENT_WORD_PROBS: Record<IntentType, Record<string, number>> = {
141
- fix_bug: {
142
- "fix": 0.9, "bug": 0.9, "error": 0.85, "broken": 0.85, "crash": 0.8,
143
- "고치": 0.9, "수정": 0.85, "에러": 0.85, "버그": 0.9, "오류": 0.8,
144
- "안돼": 0.7, "안되": 0.7, "doesn": 0.6, "work": 0.3, "fail": 0.8,
145
- "wrong": 0.7, "issue": 0.6, "problem": 0.6, "문제": 0.7, "해결": 0.7,
146
- "resolve": 0.7, "debug": 0.7, "디버그": 0.7, "왜": 0.3,
147
- },
148
- build_feature: {
149
- "만들": 0.9, "생성": 0.85, "create": 0.9, "build": 0.85, "add": 0.7,
150
- "추가": 0.8, "implement": 0.9, "구현": 0.85, "new": 0.5, "새로": 0.7,
151
- "write": 0.6, "작성": 0.7, "개발": 0.8, "develop": 0.8, "feature": 0.8,
152
- "기능": 0.8, "만들어": 0.9, "setup": 0.5, "init": 0.5,
153
- },
154
- refactor: {
155
- "refactor": 0.95, "리팩토": 0.95, "정리": 0.7, "clean": 0.6,
156
- "improve": 0.6, "개선": 0.7, "restructure": 0.9, "simplify": 0.8,
157
- "organize": 0.7, "reduce": 0.5, "optimize": 0.4, "rename": 0.6,
158
- },
159
- investigate: {
160
- "찾아": 0.7, "find": 0.5, "search": 0.6, "조사": 0.8, "분석": 0.8,
161
- "analyze": 0.8, "check": 0.5, "확인": 0.6, "look": 0.4, "examine": 0.8,
162
- "inspect": 0.8, "어디": 0.5, "뭐가": 0.5, "what": 0.3, "scan": 0.7,
163
- },
164
- configure: {
165
- "설정": 0.9, "setup": 0.85, "install": 0.8, "설치": 0.85,
166
- "config": 0.9, "configure": 0.9, "init": 0.7, "초기화": 0.7,
167
- "환경": 0.6, "environment": 0.5, "연결": 0.5, "connect": 0.5,
168
- },
169
- deploy: {
170
- "배포": 0.95, "deploy": 0.95, "publish": 0.9, "push": 0.5,
171
- "release": 0.85, "올려": 0.7, "npm": 0.5, "ship": 0.7,
172
- },
173
- test: {
174
- "테스트": 0.95, "test": 0.9, "검증": 0.8, "verify": 0.8,
175
- "assert": 0.8, "spec": 0.7, "coverage": 0.7, "확인": 0.4,
176
- },
177
- security_audit: {
178
- "보안": 0.95, "security": 0.95, "취약점": 0.95, "vulnerability": 0.9,
179
- "exploit": 0.9, "hack": 0.7, "audit": 0.8, "감사": 0.5,
180
- "injection": 0.85, "xss": 0.9, "csrf": 0.9, "ssrf": 0.9,
181
- },
182
- code_review: {
183
- "리뷰": 0.9, "review": 0.85, "검토": 0.8, "봐봐": 0.6,
184
- "봐줘": 0.6, "체크": 0.5, "pr": 0.6, "diff": 0.5,
185
- },
186
- explain: {
187
- "설명": 0.9, "explain": 0.9, "알려": 0.8, "뭐야": 0.7,
188
- "what": 0.4, "how": 0.5, "어떻게": 0.6, "why": 0.5, "왜": 0.5,
189
- "이해": 0.8, "understand": 0.7, "mean": 0.5, "뜻": 0.7,
190
- },
191
- optimize: {
192
- "최적화": 0.95, "optimize": 0.95, "빠르게": 0.7, "faster": 0.7,
193
- "performance": 0.8, "성능": 0.8, "speed": 0.7, "slow": 0.6,
194
- "느려": 0.6, "memory": 0.4, "efficient": 0.7,
195
- },
196
- migrate: {
197
- "마이그레이션": 0.95, "migrate": 0.95, "이전": 0.5, "변환": 0.7,
198
- "convert": 0.7, "upgrade": 0.7, "업그레이드": 0.7, "port": 0.6,
199
- },
200
- debug: {
201
- "디버그": 0.95, "debug": 0.95, "로그": 0.5, "log": 0.4,
202
- "trace": 0.7, "breakpoint": 0.8, "원인": 0.5, "cause": 0.5,
203
- },
204
- cleanup: {
205
- "정리": 0.7, "cleanup": 0.8, "삭제": 0.6, "remove": 0.5,
206
- "delete": 0.5, "unused": 0.7, "불필요": 0.7, "쓸데없": 0.6,
207
- },
208
- document: {
209
- "문서": 0.85, "document": 0.8, "readme": 0.9, "docs": 0.8,
210
- "comment": 0.5, "주석": 0.6, "api": 0.3, "설명": 0.4,
211
- },
212
- monitor: {
213
- "모니터": 0.9, "monitor": 0.9, "watch": 0.6, "감시": 0.8,
214
- "alert": 0.7, "status": 0.5, "health": 0.6, "확인": 0.3,
215
- },
216
- acknowledge: {
217
- "ㅇㅇ": 0.95, "ㅇㅋ": 0.95, "ㄱㄱ": 0.9, "응": 0.8,
218
- "ok": 0.7, "okay": 0.7, "sure": 0.7, "알겠": 0.8, "good": 0.6,
219
- "네": 0.7, "yes": 0.6, "그래": 0.7,
220
- },
221
- redirect: {
222
- "아니": 0.8, "말고": 0.9, "대신": 0.85, "아닌데": 0.9,
223
- "no": 0.5, "wait": 0.7, "stop": 0.7, "instead": 0.85,
224
- "잠깐": 0.8, "다시": 0.6, "바꿔": 0.8, "actually": 0.7,
225
- "근데": 0.5, "그거": 0.3, "scratch": 0.7,
226
- },
227
- clarify: {
228
- "뭐": 0.5, "무슨": 0.6, "어떤": 0.5, "which": 0.5,
229
- "what": 0.4, "?": 0.3, "정확히": 0.7, "specifically": 0.7,
230
- "구체적": 0.7, "어떻게": 0.4,
231
- },
232
- continue: {
233
- "계속": 0.9, "continue": 0.85, "go": 0.4, "ㄱㄱ": 0.7,
234
- "next": 0.6, "다음": 0.6, "진행": 0.8, "이어서": 0.85,
235
- "해줘": 0.4, "하자": 0.5,
236
- },
237
- };
238
-
239
- // Prior probabilities based on conversation phase
240
- const PHASE_INTENT_PRIOR: Record<ConversationPhase, Partial<Record<IntentType, number>>> = {
241
- idle: { build_feature: 0.2, investigate: 0.15, explain: 0.15, configure: 0.1 },
242
- problem_stated: { fix_bug: 0.3, investigate: 0.2, debug: 0.15, explain: 0.1 },
243
- exploring: { investigate: 0.2, clarify: 0.2, explain: 0.15, redirect: 0.1 },
244
- executing: { continue: 0.2, acknowledge: 0.15, redirect: 0.15, clarify: 0.1 },
245
- verifying: { acknowledge: 0.2, fix_bug: 0.15, redirect: 0.15, continue: 0.1 },
246
- completed: { build_feature: 0.15, continue: 0.15, acknowledge: 0.15, deploy: 0.1 },
247
- failed: { fix_bug: 0.25, redirect: 0.2, debug: 0.15, investigate: 0.1 },
248
- pivoting: { redirect: 0.2, build_feature: 0.15, fix_bug: 0.15, investigate: 0.1 },
249
- };
250
-
251
- const DEFAULT_PRIOR = 0.02;
252
-
253
- function classifyIntentBayesian(
254
- text: string,
255
- phase: ConversationPhase,
256
- recentIntents: IntentType[],
257
- conceptGraph: ConceptGraph,
258
- ): GranularIntent {
259
- const words = tokenize(text);
260
- const scores: Record<string, number> = {};
261
- const phasePrior = PHASE_INTENT_PRIOR[phase] ?? {};
262
-
263
- for (const [intent, wordProbs] of Object.entries(INTENT_WORD_PROBS)) {
264
- // Prior: from phase + recent intent momentum
265
- let prior = phasePrior[intent as IntentType] ?? DEFAULT_PRIOR;
266
-
267
- // Intent momentum: if same intent appeared recently, boost slightly
268
- const recentCount = recentIntents.slice(-3).filter((i) => i === intent).length;
269
- prior += recentCount * 0.05;
270
-
271
- // Likelihood: P(words | intent)
272
- let logLikelihood = 0;
273
- let matchedWords = 0;
274
- for (const word of words) {
275
- const prob = wordProbs[word];
276
- if (prob !== undefined) {
277
- logLikelihood += Math.log(prob);
278
- matchedWords++;
279
- } else {
280
- logLikelihood += Math.log(0.01); // Smoothing for unknown words
281
- }
282
- }
283
-
284
- // Context boost: if concept graph has related entities
285
- let contextBoost = 0;
286
- if (intent === "fix_bug" && hasConceptType(conceptGraph, "error")) contextBoost += 0.3;
287
- if (intent === "code_review" && hasConceptType(conceptGraph, "file")) contextBoost += 0.2;
288
- if (intent === "security_audit" && hasConceptSentiment(conceptGraph, -0.5)) contextBoost += 0.3;
289
-
290
- // Message length heuristic
291
- let lengthBoost = 0;
292
- if (intent === "acknowledge" && text.length < 10) lengthBoost += 0.5;
293
- if (intent === "redirect" && text.length < 30) lengthBoost += 0.2;
294
- if (intent === "explain" && text.includes("?")) lengthBoost += 0.3;
295
-
296
- // Combine: log(prior) + logLikelihood + boosts
297
- scores[intent] = Math.log(Math.max(prior, 0.001)) + logLikelihood +
298
- (matchedWords > 0 ? contextBoost + lengthBoost : 0);
299
- }
300
-
301
- // Softmax-like normalization
302
- const maxScore = Math.max(...Object.values(scores));
303
- const expScores: Record<string, number> = {};
304
- let sumExp = 0;
305
- for (const [intent, score] of Object.entries(scores)) {
306
- const exp = Math.exp(score - maxScore);
307
- expScores[intent] = exp;
308
- sumExp += exp;
309
- }
310
- if (sumExp === 0) sumExp = 1; // Prevent division by zero
311
-
312
- // Sort by probability
313
- const ranked = Object.entries(expScores)
314
- .map(([intent, exp]) => ({ intent: intent as IntentType, prob: exp / sumExp }))
315
- .sort((a, b) => b.prob - a.prob);
316
-
317
- const primary = ranked[0];
318
- const secondary = ranked.slice(1, 4).filter((r) => r.prob > 0.1).map((r) => r.intent);
319
-
320
- // Extract target from context
321
- const target = extractTarget(text, primary.intent, conceptGraph);
322
- const motivation = inferMotivation(phase, recentIntents, conceptGraph);
323
- const urgency = detectUrgency(text);
324
-
325
- return {
326
- primary: primary.intent,
327
- confidence: primary.prob,
328
- secondary,
329
- target,
330
- motivation,
331
- urgency,
332
- };
333
- }
334
-
335
- // ═══════════════════════════════════════════════════════════════════
336
- // CONTEXTUAL STATE MACHINE
337
- // ═══════════════════════════════════════════════════════════════════
338
-
339
- function transitionPhase(
340
- current: ConversationPhase,
341
- message: ParsedMessage,
342
- intent: GranularIntent,
343
- ): ConversationPhase {
344
- const hasTools = (message.toolCalls?.length ?? 0) > 0;
345
- const isUser = message.role === "user";
346
-
347
- // Transition rules (state × signal → new state)
348
- if (isUser) {
349
- switch (current) {
350
- case "idle":
351
- if (["fix_bug", "build_feature", "investigate", "security_audit", "debug"].includes(intent.primary)) {
352
- return "problem_stated";
353
- }
354
- if (intent.primary === "explain" || intent.primary === "clarify") return "exploring";
355
- return "idle";
356
-
357
- case "problem_stated":
358
- if (intent.primary === "redirect") return "pivoting";
359
- if (intent.primary === "clarify") return "exploring";
360
- if (intent.primary === "acknowledge" || intent.primary === "continue") return "executing";
361
- return "problem_stated";
362
-
363
- case "exploring":
364
- if (intent.primary === "acknowledge" || intent.primary === "continue") return "executing";
365
- if (intent.primary === "redirect") return "pivoting";
366
- return "exploring";
367
-
368
- case "executing":
369
- if (intent.primary === "redirect") return "pivoting";
370
- if (intent.primary === "acknowledge") return "verifying";
371
- if (["fix_bug", "build_feature"].includes(intent.primary)) return "problem_stated";
372
- return "executing";
373
-
374
- case "verifying":
375
- if (intent.primary === "acknowledge") return "completed";
376
- if (intent.primary === "redirect" || intent.primary === "fix_bug") return "failed";
377
- return "verifying";
378
-
379
- case "completed":
380
- if (["fix_bug", "build_feature", "investigate", "configure", "deploy"].includes(intent.primary)) {
381
- return "problem_stated"; // New task
382
- }
383
- return "idle";
384
-
385
- case "failed":
386
- if (intent.primary === "redirect") return "pivoting";
387
- if (intent.primary === "fix_bug") return "problem_stated";
388
- if (intent.primary === "acknowledge") return "executing"; // Try again
389
- return "failed";
390
-
391
- case "pivoting":
392
- if (["fix_bug", "build_feature", "investigate"].includes(intent.primary)) return "problem_stated";
393
- if (intent.primary === "acknowledge") return "executing";
394
- return "exploring";
395
- }
396
- } else {
397
- // Assistant messages
398
- if (hasTools) return "executing";
399
- if (current === "executing" && !hasTools) return "verifying";
400
- }
401
-
402
- return current;
403
- }
404
-
405
- // ═══════════════════════════════════════════════════════════════════
406
- // SLIDING ATTENTION WINDOW
407
- // ═══════════════════════════════════════════════════════════════════
408
-
409
- function computeAttention(
410
- messages: AttentionMessage[],
411
- currentMessage: ParsedMessage,
412
- conceptGraph: ConceptGraph,
413
- ): void {
414
- if (messages.length === 0) return;
415
-
416
- const currentKeywords = new Set(tokenize(currentMessage.content));
417
- const now = messages.length;
418
-
419
- for (let i = 0; i < messages.length; i++) {
420
- const msg = messages[i];
421
- const age = now - i;
422
-
423
- // Recency: exponential decay
424
- const recency = Math.exp(-age * 0.15);
425
-
426
- // Relevance: keyword overlap with current message
427
- const msgKeywords = tokenize(msg.message.content);
428
- const overlap = msgKeywords.filter((w) => currentKeywords.has(w)).length;
429
- const relevance = msgKeywords.length > 0 ? overlap / Math.sqrt(msgKeywords.length) : 0;
430
-
431
- // Phase importance: decision points and pivots are always important
432
- const phaseBoost = (msg.phase === "pivoting" || msg.phase === "failed") ? 0.3 : 0;
433
-
434
- // User messages are slightly more important than assistant
435
- const roleBoost = msg.message.role === "user" ? 0.1 : 0;
436
-
437
- // Tool calls are important context
438
- const toolBoost = (msg.message.toolCalls?.length ?? 0) > 0 ? 0.15 : 0;
439
-
440
- msg.attention = Math.min(1,
441
- recency * 0.4 +
442
- relevance * 0.3 +
443
- phaseBoost +
444
- roleBoost +
445
- toolBoost,
446
- );
447
- }
448
- }
449
-
450
- // ═══════════════════════════════════════════════════════════════════
451
- // CONCEPT GRAPH
452
- // ═══════════════════════════════════════════════════════════════════
453
-
454
- function updateConceptGraph(
455
- graph: ConceptGraph,
456
- message: ParsedMessage,
457
- turnCount: number,
458
- ): void {
459
- const text = message.content;
460
-
461
- // Extract file paths
462
- const filePaths = text.match(/[\w./\\-]+\.\w{1,5}/g) ?? [];
463
- for (const fp of filePaths) {
464
- upsertConcept(graph, fp, "file", turnCount);
465
- }
466
-
467
- // Extract error patterns
468
- const errorPatterns = text.match(/(?:error|Error|ERROR|에러|오류)[:\s].*?(?:\n|$)/g) ?? [];
469
- for (const err of errorPatterns) {
470
- const label = err.slice(0, 60).trim();
471
- upsertConcept(graph, label, "error", turnCount, -0.7);
472
- }
473
-
474
- // Extract backtick entities
475
- const backticks = text.match(/`([^`]+)`/g) ?? [];
476
- for (const bt of backticks) {
477
- const label = bt.replace(/`/g, "");
478
- if (label.length > 1 && label.length < 50) {
479
- upsertConcept(graph, label, "entity", turnCount);
480
- }
481
- }
482
-
483
- // Extract tool calls as concepts
484
- if (message.toolCalls) {
485
- for (const tc of message.toolCalls) {
486
- upsertConcept(graph, tc.name, "tool", turnCount);
487
-
488
- // Link tool to files it operates on
489
- const fp = tc.input["file_path"] ?? tc.input["path"];
490
- if (typeof fp === "string") {
491
- upsertConcept(graph, fp, "file", turnCount);
492
- addEdge(graph, tc.name, fp, "modifies");
493
- }
494
-
495
- // Check tool results for errors
496
- if (tc.result && /error|fail|denied|not found/i.test(tc.result)) {
497
- const errLabel = `${tc.name} failed`;
498
- upsertConcept(graph, errLabel, "error", turnCount, -0.8);
499
- addEdge(graph, tc.name, errLabel, "causes");
500
- }
501
- }
502
- }
503
-
504
- // Extract goals from user messages
505
- if (message.role === "user") {
506
- const goalPatterns = [
507
- /(?:want|need|should|must|해줘|하자|해봐|만들|고쳐)\s+(.{5,40})/i,
508
- ];
509
- for (const pattern of goalPatterns) {
510
- const match = pattern.exec(text);
511
- if (match) {
512
- upsertConcept(graph, match[1].trim(), "goal", turnCount, 0.5);
513
- }
514
- }
515
- }
516
- }
517
-
518
- function upsertConcept(
519
- graph: ConceptGraph,
520
- label: string,
521
- type: ConceptNode["type"],
522
- turn: number,
523
- sentiment = 0,
524
- ): void {
525
- const id = `${type}:${label}`;
526
- const existing = graph.nodes.get(id);
527
- if (existing) {
528
- existing.weight++;
529
- existing.lastSeen = turn;
530
- existing.sentiment = (existing.sentiment + sentiment) / 2;
531
- } else {
532
- graph.nodes.set(id, {
533
- id,
534
- type,
535
- label,
536
- weight: 1,
537
- firstSeen: turn,
538
- lastSeen: turn,
539
- sentiment,
540
- });
541
- }
542
- }
543
-
544
- function addEdge(graph: ConceptGraph, from: string, to: string, relation: ConceptEdge["relation"]): void {
545
- const fromId = findNodeId(graph, from);
546
- const toId = findNodeId(graph, to);
547
- if (!fromId || !toId) return;
548
-
549
- const existing = graph.edges.find((e) => e.from === fromId && e.to === toId);
550
- if (existing) {
551
- existing.weight++;
552
- } else {
553
- graph.edges.push({ from: fromId, to: toId, relation, weight: 1 });
554
- }
555
- }
556
-
557
- function findNodeId(graph: ConceptGraph, label: string): string | undefined {
558
- for (const [id, node] of graph.nodes) {
559
- if (node.label === label) return id;
560
- }
561
- return undefined;
562
- }
563
-
564
- function hasConceptType(graph: ConceptGraph, type: ConceptNode["type"]): boolean {
565
- for (const node of graph.nodes.values()) {
566
- if (node.type === type && node.lastSeen >= 0) return true;
567
- }
568
- return false;
569
- }
570
-
571
- function hasConceptSentiment(graph: ConceptGraph, threshold: number): boolean {
572
- for (const node of graph.nodes.values()) {
573
- if (node.sentiment <= threshold) return true;
574
- }
575
- return false;
576
- }
577
-
578
- // ═══════════════════════════════════════════════════════════════════
579
- // AUTO-TRIGGER — Detects when to extract a skill
580
- // ═══════════════════════════════════════════════════════════════════
581
-
582
- function checkAutoTrigger(state: EngineState): SkillTrigger | null {
583
- const { phase, phaseHistory, attentionWindow, conceptGraph } = state;
584
-
585
- // Trigger 1: Task completed (phase went problem_stated → ... → completed)
586
- if (phase === "completed" && phaseHistory.length >= 3) {
587
- const recentPhases = phaseHistory.slice(-6);
588
- const hadExecution = recentPhases.includes("executing");
589
- const hadProblem = recentPhases.includes("problem_stated");
590
-
591
- if (hadExecution && hadProblem) {
592
- const relevantMessages = attentionWindow
593
- .filter((m) => m.attention > 0.2)
594
- .sort((a, b) => b.attention - a.attention);
595
-
596
- if (relevantMessages.length >= 3) {
597
- return {
598
- type: "task_completed",
599
- messages: relevantMessages,
600
- reason: `Task completed: ${state.currentIntent.target}`,
601
- quality: computeTriggerQuality(relevantMessages, conceptGraph),
602
- };
603
- }
604
- }
605
- }
606
-
607
- // Trigger 2: Pivot point (user changed approach — valuable lesson)
608
- if (phase === "pivoting") {
609
- const pivotMessages = attentionWindow.slice(-5);
610
- if (pivotMessages.length >= 2) {
611
- return {
612
- type: "pivot_point",
613
- messages: pivotMessages,
614
- reason: `Approach changed: ${state.currentIntent.motivation}`,
615
- quality: 0.6,
616
- };
617
- }
618
- }
619
-
620
- // Trigger 3: Lesson learned (failure → recovery → success)
621
- if (phase === "completed" && phaseHistory.includes("failed")) {
622
- const journeyMessages = attentionWindow.filter((m) => m.attention > 0.15);
623
- return {
624
- type: "lesson_learned",
625
- messages: journeyMessages,
626
- reason: "Recovered from failure — approach evolution captured",
627
- quality: 0.8,
628
- };
629
- }
630
-
631
- return null;
632
- }
633
-
634
- function computeTriggerQuality(
635
- messages: AttentionMessage[],
636
- graph: ConceptGraph,
637
- ): number {
638
- let quality = 0;
639
-
640
- // More high-attention messages = higher quality
641
- const highAttention = messages.filter((m) => m.attention > 0.4).length;
642
- quality += highAttention * 0.1;
643
-
644
- // More concept nodes = richer context
645
- quality += Math.min(0.3, graph.nodes.size * 0.03);
646
-
647
- // Has files = concrete work
648
- const fileNodes = [...graph.nodes.values()].filter((n) => n.type === "file");
649
- if (fileNodes.length > 0) quality += 0.15;
650
-
651
- // Has tools = actual execution
652
- const toolNodes = [...graph.nodes.values()].filter((n) => n.type === "tool");
653
- if (toolNodes.length > 0) quality += 0.15;
654
-
655
- return Math.min(1, quality);
656
- }
657
-
658
- // ═══════════════════════════════════════════════════════════════════
659
- // MAIN ENGINE
660
- // ═══════════════════════════════════════════════════════════════════
661
-
662
- export function createContextEngine(): {
663
- /** Process a new message. Returns any triggered skills. */
664
- process: (message: ParsedMessage) => SkillTrigger | null;
665
- /** Get current engine state. */
666
- getState: () => EngineState;
667
- /** Get attention-weighted context summary. */
668
- getContextSummary: () => string;
669
- /** Get the concept graph. */
670
- getConceptGraph: () => ConceptGraph;
671
- } {
672
- const state: EngineState = {
673
- phase: "idle",
674
- phaseHistory: ["idle"],
675
- currentIntent: {
676
- primary: "continue",
677
- confidence: 0,
678
- secondary: [],
679
- target: "",
680
- motivation: "",
681
- urgency: "low",
682
- },
683
- intentHistory: [],
684
- attentionWindow: [],
685
- conceptGraph: { nodes: new Map(), edges: [] },
686
- triggers: [],
687
- topicStack: [],
688
- turnCount: 0,
689
- };
690
-
691
- return {
692
- process(message: ParsedMessage): SkillTrigger | null {
693
- state.turnCount++;
694
-
695
- // 1. Classify intent
696
- const intent = message.role === "user"
697
- ? classifyIntentBayesian(
698
- message.content,
699
- state.phase,
700
- state.intentHistory.map((i) => i.primary),
701
- state.conceptGraph,
702
- )
703
- : state.currentIntent; // Keep current intent for assistant messages
704
-
705
- state.currentIntent = intent;
706
- state.intentHistory.push(intent);
707
-
708
- // 2. Transition phase
709
- const newPhase = transitionPhase(state.phase, message, intent);
710
- if (newPhase !== state.phase) {
711
- state.phaseHistory.push(newPhase);
712
- }
713
- state.phase = newPhase;
714
-
715
- // 3. Update concept graph
716
- updateConceptGraph(state.conceptGraph, message, state.turnCount);
717
-
718
- // 4. Add to attention window
719
- const attMsg: AttentionMessage = {
720
- message,
721
- attention: 1.0, // Will be recalculated
722
- phase: state.phase,
723
- intent,
724
- };
725
- state.attentionWindow.push(attMsg);
726
-
727
- // 5. Recompute attention weights
728
- computeAttention(state.attentionWindow, message, state.conceptGraph);
729
-
730
- // 6. Prune old messages (keep max 50)
731
- if (state.attentionWindow.length > 50) {
732
- // Remove lowest-attention messages beyond window
733
- state.attentionWindow.sort((a, b) => b.attention - a.attention);
734
- state.attentionWindow = state.attentionWindow.slice(0, 50);
735
- }
736
-
737
- // 7. Update topic stack
738
- if (message.role === "user" && intent.target) {
739
- if (state.topicStack[state.topicStack.length - 1] !== intent.target) {
740
- state.topicStack.push(intent.target);
741
- if (state.topicStack.length > 10) state.topicStack.shift();
742
- }
743
- }
744
-
745
- // 8. Check auto-trigger
746
- const trigger = checkAutoTrigger(state);
747
- if (trigger) {
748
- state.triggers.push(trigger);
749
- // Reset phase after trigger
750
- state.phase = "idle";
751
- state.phaseHistory.push("idle");
752
- }
753
-
754
- return trigger;
755
- },
756
-
757
- getState(): EngineState {
758
- return state;
759
- },
760
-
761
- getContextSummary(): string {
762
- const topConcepts = [...state.conceptGraph.nodes.values()]
763
- .sort((a, b) => b.weight - a.weight)
764
- .slice(0, 10)
765
- .map((n) => `${n.type}:${n.label}(${n.weight})`);
766
-
767
- return [
768
- `Phase: ${state.phase}`,
769
- `Intent: ${state.currentIntent.primary} (${(state.currentIntent.confidence * 100).toFixed(0)}%)`,
770
- `Target: ${state.currentIntent.target}`,
771
- `Topics: ${state.topicStack.slice(-5).join(" → ")}`,
772
- `Concepts: ${topConcepts.join(", ")}`,
773
- `Triggers: ${state.triggers.length}`,
774
- ].join("\n");
775
- },
776
-
777
- getConceptGraph(): ConceptGraph {
778
- return state.conceptGraph;
779
- },
780
- };
781
- }
782
-
783
- // ═══════════════════════════════════════════════════════════════════
784
- // HELPERS
785
- // ═══════════════════════════════════════════════════════════════════
786
-
787
- function tokenize(text: string): string[] {
788
- return text
789
- .toLowerCase()
790
- .replace(/[^a-z가-힣0-9\s]/g, " ")
791
- .split(/\s+/)
792
- .filter((w) => w.length > 1);
793
- }
794
-
795
- function extractTarget(
796
- text: string,
797
- intent: IntentType,
798
- graph: ConceptGraph,
799
- ): string {
800
- // Try to find the most relevant concept for this intent
801
- const recentFiles = [...graph.nodes.values()]
802
- .filter((n) => n.type === "file")
803
- .sort((a, b) => b.lastSeen - a.lastSeen);
804
-
805
- const recentErrors = [...graph.nodes.values()]
806
- .filter((n) => n.type === "error")
807
- .sort((a, b) => b.lastSeen - a.lastSeen);
808
-
809
- if (intent === "fix_bug" && recentErrors.length > 0) {
810
- return recentErrors[0].label;
811
- }
812
- if (["code_review", "refactor", "optimize"].includes(intent) && recentFiles.length > 0) {
813
- return recentFiles[0].label;
814
- }
815
-
816
- // Fall back to extracting from the message itself
817
- const backtick = text.match(/`([^`]+)`/);
818
- if (backtick) return backtick[1];
819
-
820
- const quoted = text.match(/"([^"]+)"/);
821
- if (quoted) return quoted[1];
822
-
823
- // First noun-like phrase after the intent verb
824
- const words = text.split(/\s+/).slice(0, 8);
825
- return words.slice(1, 4).join(" ").slice(0, 40) || "unknown";
826
- }
827
-
828
- function inferMotivation(
829
- phase: ConversationPhase,
830
- recentIntents: IntentType[],
831
- graph: ConceptGraph,
832
- ): string {
833
- if (phase === "failed") return "Previous approach failed, trying alternative";
834
- if (phase === "pivoting") return "User redirected to a different approach";
835
-
836
- const errors = [...graph.nodes.values()].filter((n) => n.type === "error");
837
- if (errors.length > 0) return `Responding to error: ${errors[errors.length - 1].label.slice(0, 50)}`;
838
-
839
- const goals = [...graph.nodes.values()].filter((n) => n.type === "goal");
840
- if (goals.length > 0) return goals[goals.length - 1].label;
841
-
842
- if (recentIntents.length > 0) {
843
- const last = recentIntents[recentIntents.length - 1];
844
- return `Continuation of ${last} task`;
845
- }
846
-
847
- return "User initiated new task";
848
- }
849
-
850
- function detectUrgency(text: string): "low" | "medium" | "high" | "critical" {
851
- const lower = text.toLowerCase();
852
-
853
- // Critical: exclamation, urgent words
854
- if (/급해|urgent|critical|asap|지금 당장|immediately|!{2,}/.test(lower)) return "critical";
855
-
856
- // High: strong language, errors mentioned
857
- if (/빨리|quickly|error|crash|broken|안돼|망했/.test(lower)) return "high";
858
-
859
- // Medium: requests with some emphasis
860
- if (/please|좀|부탁|important|need/.test(lower)) return "medium";
861
-
862
- return "low";
863
- }