@hawon/nexus 0.1.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.
- package/README.md +60 -38
- package/dist/cli/index.js +76 -145
- package/dist/index.js +15 -26
- package/dist/mcp/server.js +61 -32
- package/package.json +2 -1
- package/scripts/auto-skill.sh +54 -0
- package/scripts/auto-sync.sh +11 -0
- package/scripts/benchmark.ts +444 -0
- package/scripts/scan-tool-result.sh +46 -0
- package/src/cli/index.ts +79 -172
- package/src/index.ts +17 -29
- package/src/mcp/server.ts +67 -41
- package/src/memory-engine/index.ts +4 -6
- package/src/memory-engine/nexus-memory.test.ts +437 -0
- package/src/memory-engine/nexus-memory.ts +631 -0
- package/src/memory-engine/semantic.ts +380 -0
- package/src/parser/parse.ts +1 -21
- package/src/promptguard/advanced-rules.ts +129 -12
- package/src/promptguard/entropy.ts +21 -2
- package/src/promptguard/evolution/auto-update.ts +16 -6
- package/src/promptguard/multilingual-rules.ts +68 -0
- package/src/promptguard/rules.ts +87 -2
- package/src/promptguard/scanner.test.ts +262 -0
- package/src/promptguard/scanner.ts +1 -1
- package/src/promptguard/semantic.ts +19 -4
- package/src/promptguard/token-analysis.ts +17 -5
- package/src/review/analyzer.test.ts +279 -0
- package/src/review/analyzer.ts +112 -28
- package/src/shared/stop-words.ts +21 -0
- package/src/skills/index.ts +11 -27
- package/src/skills/memory-skill-engine.ts +1044 -0
- package/src/testing/health-check.ts +19 -2
- package/src/cost/index.ts +0 -3
- package/src/cost/tracker.ts +0 -290
- package/src/cost/types.ts +0 -34
- package/src/memory-engine/compressor.ts +0 -97
- package/src/memory-engine/context-window.ts +0 -113
- package/src/memory-engine/store.ts +0 -371
- package/src/memory-engine/types.ts +0 -32
- package/src/skills/context-engine.ts +0 -863
- package/src/skills/extractor.ts +0 -224
- package/src/skills/global-context.ts +0 -726
- package/src/skills/library.ts +0 -189
- package/src/skills/pattern-engine.ts +0 -712
- package/src/skills/render-evolved.ts +0 -160
- package/src/skills/skill-reconciler.ts +0 -703
- package/src/skills/smart-extractor.ts +0 -843
- package/src/skills/types.ts +0 -18
- package/src/skills/wisdom-extractor.ts +0 -737
- package/src/superdev-evolution/index.ts +0 -3
- package/src/superdev-evolution/skill-manager.ts +0 -266
- 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
|
-
}
|