@yuaone/core 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/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module context-budget
|
|
3
|
+
* @description 컨텍스트 윈도우 예산 관리자.
|
|
4
|
+
*
|
|
5
|
+
* LLM 컨텍스트 오버플로우를 방지하기 위한 고급 예산 관리 시스템.
|
|
6
|
+
* - 카테고리별 토큰 예산 배분 및 강제
|
|
7
|
+
* - 우선순위 기반 선택적 유지/제거
|
|
8
|
+
* - LLM 기반 요약을 통한 오래된 메시지 압축
|
|
9
|
+
* - 관련성 기반 검색으로 필요한 컨텍스트만 로드
|
|
10
|
+
* - 슬라이딩 윈도우 + 체크포인트로 핵심 결정 보존
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const budget = new ContextBudgetManager({ totalBudget: 128_000 });
|
|
15
|
+
*
|
|
16
|
+
* budget.addItem({
|
|
17
|
+
* category: "systemPrompt",
|
|
18
|
+
* priority: "critical",
|
|
19
|
+
* content: systemPrompt,
|
|
20
|
+
* role: "system",
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* budget.addItem({
|
|
24
|
+
* category: "conversationHistory",
|
|
25
|
+
* priority: "high",
|
|
26
|
+
* content: userMessage,
|
|
27
|
+
* role: "user",
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Auto-manage when nearing limits
|
|
31
|
+
* await budget.autoManage(async (text) => llm.summarize(text));
|
|
32
|
+
*
|
|
33
|
+
* // Build context for LLM call
|
|
34
|
+
* const messages = budget.toMessages();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
import { EventEmitter } from "node:events";
|
|
38
|
+
// ─── Constants ───
|
|
39
|
+
const DEFAULT_ALLOCATION = {
|
|
40
|
+
systemPrompt: 15,
|
|
41
|
+
projectContext: 10,
|
|
42
|
+
conversationHistory: 40,
|
|
43
|
+
toolResults: 25,
|
|
44
|
+
workingMemory: 10,
|
|
45
|
+
};
|
|
46
|
+
const DEFAULT_CONFIG = {
|
|
47
|
+
totalBudget: 128_000,
|
|
48
|
+
allocation: DEFAULT_ALLOCATION,
|
|
49
|
+
enableSummarization: true,
|
|
50
|
+
summarizationThreshold: 0.75,
|
|
51
|
+
minItemsToSummarize: 5,
|
|
52
|
+
maxSummaryLength: 500,
|
|
53
|
+
enableRetrieval: true,
|
|
54
|
+
retrievalTopK: 20,
|
|
55
|
+
enableCheckpoints: true,
|
|
56
|
+
checkpointInterval: 10,
|
|
57
|
+
maxCheckpoints: 5,
|
|
58
|
+
evictionStrategy: "hybrid",
|
|
59
|
+
};
|
|
60
|
+
/** 우선순위 → 숫자 (높을수록 중요) */
|
|
61
|
+
const PRIORITY_SCORES = {
|
|
62
|
+
critical: 5,
|
|
63
|
+
high: 4,
|
|
64
|
+
medium: 3,
|
|
65
|
+
low: 2,
|
|
66
|
+
ephemeral: 1,
|
|
67
|
+
};
|
|
68
|
+
/** 카테고리 간 차용 가능한 최대 오버플로우 (20%) */
|
|
69
|
+
const CATEGORY_OVERFLOW_LIMIT = 0.2;
|
|
70
|
+
// ─── ContextBudgetManager ───
|
|
71
|
+
/**
|
|
72
|
+
* ContextBudgetManager — LLM 컨텍스트 윈도우 예산을 지능적으로 관리.
|
|
73
|
+
*
|
|
74
|
+
* 핵심 기능:
|
|
75
|
+
* - 카테고리별 토큰 예산 배분 및 강제
|
|
76
|
+
* - 우선순위 기반 아이템 제거 (hybrid LRU + priority)
|
|
77
|
+
* - LLM 기반 요약을 통한 오래된 메시지 압축
|
|
78
|
+
* - 관련성 점수 기반 검색으로 필요한 컨텍스트만 로드
|
|
79
|
+
* - 슬라이딩 윈도우 + 체크포인트로 핵심 결정 보존
|
|
80
|
+
*
|
|
81
|
+
* 이벤트:
|
|
82
|
+
* - `budget:warning` — 사용률 > 75%
|
|
83
|
+
* - `budget:critical` — 사용률 > 90%
|
|
84
|
+
* - `budget:overflow` — 사용률 > 100%, 강제 제거 수행
|
|
85
|
+
* - `summarize:start` — 요약 시작
|
|
86
|
+
* - `summarize:complete` — 요약 완료 (압축률 포함)
|
|
87
|
+
* - `evict:items` — 아이템 제거됨
|
|
88
|
+
* - `checkpoint:created` — 체크포인트 저장됨
|
|
89
|
+
*/
|
|
90
|
+
export class ContextBudgetManager extends EventEmitter {
|
|
91
|
+
config;
|
|
92
|
+
items;
|
|
93
|
+
summaries;
|
|
94
|
+
checkpoints;
|
|
95
|
+
idCounter;
|
|
96
|
+
summarizationCount;
|
|
97
|
+
evictionCount;
|
|
98
|
+
constructor(config) {
|
|
99
|
+
super();
|
|
100
|
+
this.config = {
|
|
101
|
+
...DEFAULT_CONFIG,
|
|
102
|
+
...config,
|
|
103
|
+
allocation: {
|
|
104
|
+
...DEFAULT_ALLOCATION,
|
|
105
|
+
...config?.allocation,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
this.items = new Map();
|
|
109
|
+
this.summaries = [];
|
|
110
|
+
this.checkpoints = [];
|
|
111
|
+
this.idCounter = 0;
|
|
112
|
+
this.summarizationCount = 0;
|
|
113
|
+
this.evictionCount = 0;
|
|
114
|
+
}
|
|
115
|
+
// ─── Item Management ───
|
|
116
|
+
/**
|
|
117
|
+
* 컨텍스트 아이템을 추가하고 자동으로 예산을 강제한다.
|
|
118
|
+
* 추가 후 전체 사용률이 90%를 넘으면 `budget:critical` 이벤트를 발생시킨다.
|
|
119
|
+
*
|
|
120
|
+
* @param item - 아이템 메타데이터 (id, tokenCount, timestamp 자동 생성)
|
|
121
|
+
* @param content - 아이템 콘텐츠
|
|
122
|
+
* @returns 생성된 ContextItem
|
|
123
|
+
*/
|
|
124
|
+
addItem(item, content) {
|
|
125
|
+
const id = `ctx_${++this.idCounter}_${Date.now()}`;
|
|
126
|
+
const tokenCount = this.estimateTokens(content);
|
|
127
|
+
const timestamp = Date.now();
|
|
128
|
+
const contextItem = {
|
|
129
|
+
...item,
|
|
130
|
+
id,
|
|
131
|
+
content,
|
|
132
|
+
tokenCount,
|
|
133
|
+
timestamp,
|
|
134
|
+
};
|
|
135
|
+
this.items.set(id, contextItem);
|
|
136
|
+
this.checkBudgetHealth();
|
|
137
|
+
return contextItem;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 기존 아이템의 콘텐츠를 업데이트한다.
|
|
141
|
+
* @param id - 아이템 ID
|
|
142
|
+
* @param content - 새 콘텐츠
|
|
143
|
+
*/
|
|
144
|
+
updateItem(id, content) {
|
|
145
|
+
const item = this.items.get(id);
|
|
146
|
+
if (!item)
|
|
147
|
+
return;
|
|
148
|
+
item.content = content;
|
|
149
|
+
item.tokenCount = this.estimateTokens(content);
|
|
150
|
+
this.checkBudgetHealth();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 아이템을 제거한다.
|
|
154
|
+
* @param id - 제거할 아이템 ID
|
|
155
|
+
*/
|
|
156
|
+
removeItem(id) {
|
|
157
|
+
this.items.delete(id);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 아이템을 고정/해제한다. 고정된 아이템은 절대 제거되지 않는다.
|
|
161
|
+
* @param id - 아이템 ID
|
|
162
|
+
* @param pinned - 고정 여부 (기본 true)
|
|
163
|
+
*/
|
|
164
|
+
pinItem(id, pinned = true) {
|
|
165
|
+
const item = this.items.get(id);
|
|
166
|
+
if (item) {
|
|
167
|
+
item.pinned = pinned;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ─── Budget Queries ───
|
|
171
|
+
/**
|
|
172
|
+
* 현재 예산 현황을 반환한다.
|
|
173
|
+
* @returns 카테고리별 사용량, 건강 상태 등
|
|
174
|
+
*/
|
|
175
|
+
getStatus() {
|
|
176
|
+
const { totalBudget, allocation } = this.config;
|
|
177
|
+
const usedTokens = this.getTotalUsedTokens();
|
|
178
|
+
const usagePercent = usedTokens / totalBudget;
|
|
179
|
+
const categories = Object.keys(allocation);
|
|
180
|
+
const byCategory = {};
|
|
181
|
+
for (const cat of categories) {
|
|
182
|
+
const budget = Math.floor(totalBudget * (allocation[cat] / 100));
|
|
183
|
+
const catItems = this.getItemsByCategory(cat);
|
|
184
|
+
const used = catItems.reduce((sum, it) => sum + it.tokenCount, 0);
|
|
185
|
+
byCategory[cat] = {
|
|
186
|
+
budget,
|
|
187
|
+
used,
|
|
188
|
+
percent: budget > 0 ? used / budget : 0,
|
|
189
|
+
itemCount: catItems.length,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
let health;
|
|
193
|
+
if (usagePercent > 1.0) {
|
|
194
|
+
health = "overflow";
|
|
195
|
+
}
|
|
196
|
+
else if (usagePercent > 0.9) {
|
|
197
|
+
health = "critical";
|
|
198
|
+
}
|
|
199
|
+
else if (usagePercent > 0.75) {
|
|
200
|
+
health = "warning";
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
health = "healthy";
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
totalBudget,
|
|
207
|
+
usedTokens,
|
|
208
|
+
usagePercent,
|
|
209
|
+
byCategory,
|
|
210
|
+
summarizationCount: this.summarizationCount,
|
|
211
|
+
evictionCount: this.evictionCount,
|
|
212
|
+
checkpointCount: this.checkpoints.length,
|
|
213
|
+
health,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 특정 카테고리의 예산 토큰 수를 반환한다.
|
|
218
|
+
* @param category - 카테고리 이름
|
|
219
|
+
* @returns 해당 카테고리의 토큰 예산
|
|
220
|
+
*/
|
|
221
|
+
getCategoryBudget(category) {
|
|
222
|
+
return Math.floor(this.config.totalBudget * (this.config.allocation[category] / 100));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 지정된 토큰 수를 추가할 공간이 있는지 확인한다.
|
|
226
|
+
* @param tokens - 필요한 토큰 수
|
|
227
|
+
* @param category - 특정 카테고리 체크 (미지정 시 전체)
|
|
228
|
+
* @returns 공간 있으면 true
|
|
229
|
+
*/
|
|
230
|
+
hasRoom(tokens, category) {
|
|
231
|
+
if (category) {
|
|
232
|
+
return this.getRemainingTokens(category) >= tokens;
|
|
233
|
+
}
|
|
234
|
+
return this.getTotalUsedTokens() + tokens <= this.config.totalBudget;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 카테고리의 남은 토큰 수를 반환한다.
|
|
238
|
+
* 카테고리 간 차용을 허용하여 소프트 예산의 20% 오버플로우까지 허용한다.
|
|
239
|
+
* @param category - 카테고리 이름
|
|
240
|
+
* @returns 남은 토큰 수
|
|
241
|
+
*/
|
|
242
|
+
getRemainingTokens(category) {
|
|
243
|
+
const budget = this.getCategoryBudget(category);
|
|
244
|
+
const overflowBudget = Math.floor(budget * (1 + CATEGORY_OVERFLOW_LIMIT));
|
|
245
|
+
const used = this.getItemsByCategory(category).reduce((sum, it) => sum + it.tokenCount, 0);
|
|
246
|
+
return Math.max(0, overflowBudget - used);
|
|
247
|
+
}
|
|
248
|
+
// ─── Context Building ───
|
|
249
|
+
/**
|
|
250
|
+
* LLM 호출을 위한 최종 컨텍스트를 구축한다.
|
|
251
|
+
* 예산 내에서 우선순위 순으로 아이템을 정렬하여 반환한다.
|
|
252
|
+
* @returns 예산 내의 ContextItem 배열
|
|
253
|
+
*/
|
|
254
|
+
buildContext() {
|
|
255
|
+
const allItems = Array.from(this.items.values());
|
|
256
|
+
// 우선순위 높은 것 먼저, 같은 우선순위 내에서는 최신 먼저
|
|
257
|
+
allItems.sort((a, b) => {
|
|
258
|
+
const pa = PRIORITY_SCORES[a.priority];
|
|
259
|
+
const pb = PRIORITY_SCORES[b.priority];
|
|
260
|
+
if (pa !== pb)
|
|
261
|
+
return pb - pa;
|
|
262
|
+
return a.timestamp - b.timestamp; // 시간순 (오래된 것 먼저)
|
|
263
|
+
});
|
|
264
|
+
const result = [];
|
|
265
|
+
let totalTokens = 0;
|
|
266
|
+
for (const item of allItems) {
|
|
267
|
+
if (totalTokens + item.tokenCount <= this.config.totalBudget) {
|
|
268
|
+
result.push(item);
|
|
269
|
+
totalTokens += item.tokenCount;
|
|
270
|
+
}
|
|
271
|
+
else if (item.priority === "critical" || item.pinned) {
|
|
272
|
+
// critical/pinned는 예산 초과해도 포함
|
|
273
|
+
result.push(item);
|
|
274
|
+
totalTokens += item.tokenCount;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// 최종적으로 timestamp 순으로 정렬 (대화 순서 유지)
|
|
278
|
+
result.sort((a, b) => a.timestamp - b.timestamp);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 관련성 기반 검색으로 컨텍스트를 구축한다.
|
|
283
|
+
* 현재 작업과 관련 없는 아이템은 제외하여 토큰을 절약한다.
|
|
284
|
+
* @param query - 검색 쿼리
|
|
285
|
+
* @returns 검색 결과
|
|
286
|
+
*/
|
|
287
|
+
buildContextWithRetrieval(query) {
|
|
288
|
+
if (!this.config.enableRetrieval) {
|
|
289
|
+
const items = this.buildContext();
|
|
290
|
+
return {
|
|
291
|
+
items,
|
|
292
|
+
totalTokens: items.reduce((s, it) => s + it.tokenCount, 0),
|
|
293
|
+
truncated: false,
|
|
294
|
+
retrievalScore: 1.0,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const allItems = Array.from(this.items.values());
|
|
298
|
+
// 관련성 점수 계산 및 정렬
|
|
299
|
+
const scored = allItems.map((item) => ({
|
|
300
|
+
item,
|
|
301
|
+
score: this.scoreRelevance(item, query),
|
|
302
|
+
}));
|
|
303
|
+
scored.sort((a, b) => {
|
|
304
|
+
// critical은 항상 포함
|
|
305
|
+
if (a.item.priority === "critical" && b.item.priority !== "critical")
|
|
306
|
+
return -1;
|
|
307
|
+
if (b.item.priority === "critical" && a.item.priority !== "critical")
|
|
308
|
+
return 1;
|
|
309
|
+
// pinned는 항상 포함
|
|
310
|
+
if (a.item.pinned && !b.item.pinned)
|
|
311
|
+
return -1;
|
|
312
|
+
if (b.item.pinned && !a.item.pinned)
|
|
313
|
+
return 1;
|
|
314
|
+
// 나머지는 점수순
|
|
315
|
+
return b.score - a.score;
|
|
316
|
+
});
|
|
317
|
+
const result = [];
|
|
318
|
+
let totalTokens = 0;
|
|
319
|
+
let totalScore = 0;
|
|
320
|
+
let truncated = false;
|
|
321
|
+
const topK = this.config.retrievalTopK;
|
|
322
|
+
for (const { item, score } of scored) {
|
|
323
|
+
// critical/pinned는 항상 포함
|
|
324
|
+
if (item.priority === "critical" || item.pinned) {
|
|
325
|
+
result.push(item);
|
|
326
|
+
totalTokens += item.tokenCount;
|
|
327
|
+
totalScore += score;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
// topK 초과 시 중단
|
|
331
|
+
if (result.length >= topK) {
|
|
332
|
+
truncated = true;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
// 예산 초과 시 중단
|
|
336
|
+
if (totalTokens + item.tokenCount > this.config.totalBudget) {
|
|
337
|
+
truncated = true;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
result.push(item);
|
|
341
|
+
totalTokens += item.tokenCount;
|
|
342
|
+
totalScore += score;
|
|
343
|
+
}
|
|
344
|
+
// timestamp 순 정렬
|
|
345
|
+
result.sort((a, b) => a.timestamp - b.timestamp);
|
|
346
|
+
return {
|
|
347
|
+
items: result,
|
|
348
|
+
totalTokens,
|
|
349
|
+
truncated,
|
|
350
|
+
retrievalScore: result.length > 0 ? totalScore / result.length : 0,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* 카테고리별 아이템을 우선순위 → 최신순으로 정렬하여 반환한다.
|
|
355
|
+
* @param category - 카테고리 이름
|
|
356
|
+
* @returns 정렬된 ContextItem 배열
|
|
357
|
+
*/
|
|
358
|
+
getItemsByCategory(category) {
|
|
359
|
+
const result = [];
|
|
360
|
+
for (const item of this.items.values()) {
|
|
361
|
+
if (item.category === category) {
|
|
362
|
+
result.push(item);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
result.sort((a, b) => {
|
|
366
|
+
const pa = PRIORITY_SCORES[a.priority];
|
|
367
|
+
const pb = PRIORITY_SCORES[b.priority];
|
|
368
|
+
if (pa !== pb)
|
|
369
|
+
return pb - pa;
|
|
370
|
+
return b.timestamp - a.timestamp;
|
|
371
|
+
});
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
// ─── Summarization ───
|
|
375
|
+
/**
|
|
376
|
+
* 오래된 medium 우선순위 메시지를 LLM으로 요약한다.
|
|
377
|
+
* 연속된 medium 아이템을 그룹화하여 하나의 요약으로 교체한다.
|
|
378
|
+
*
|
|
379
|
+
* @param summarizeFn - LLM 요약 호출 함수
|
|
380
|
+
* @returns 요약 결과 또는 null (요약 불필요 시)
|
|
381
|
+
*/
|
|
382
|
+
async summarize(summarizeFn) {
|
|
383
|
+
if (!this.config.enableSummarization || !this.needsSummarization()) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
// medium 우선순위 아이템 중 아직 요약되지 않은 것을 timestamp 순으로 수집
|
|
387
|
+
const candidates = [];
|
|
388
|
+
for (const item of this.items.values()) {
|
|
389
|
+
if (item.priority === "medium" &&
|
|
390
|
+
!item.summarized &&
|
|
391
|
+
!item.pinned) {
|
|
392
|
+
candidates.push(item);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
candidates.sort((a, b) => a.timestamp - b.timestamp);
|
|
396
|
+
if (candidates.length < this.config.minItemsToSummarize) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
// 배치 선택: 가장 오래된 minItemsToSummarize 이상의 연속 아이템
|
|
400
|
+
const batch = candidates.slice(0, Math.max(this.config.minItemsToSummarize, Math.floor(candidates.length / 2)));
|
|
401
|
+
const originalTokens = batch.reduce((s, it) => s + it.tokenCount, 0);
|
|
402
|
+
const combinedContent = batch
|
|
403
|
+
.map((it) => {
|
|
404
|
+
const prefix = it.role ? `[${it.role}]` : "[context]";
|
|
405
|
+
return `${prefix} ${it.content}`;
|
|
406
|
+
})
|
|
407
|
+
.join("\n\n---\n\n");
|
|
408
|
+
const prompt = "Summarize the following conversation turns concisely. " +
|
|
409
|
+
"Preserve key decisions, file changes, errors encountered, and important context. " +
|
|
410
|
+
"Be brief but complete.\n\n" +
|
|
411
|
+
combinedContent;
|
|
412
|
+
this.emit("summarize:start", { itemCount: batch.length, tokens: originalTokens });
|
|
413
|
+
let summarizedContent;
|
|
414
|
+
try {
|
|
415
|
+
summarizedContent = await summarizeFn(prompt);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
// 요약 토큰 수 제한
|
|
421
|
+
const maxLen = this.config.maxSummaryLength;
|
|
422
|
+
const summaryTokens = this.estimateTokens(summarizedContent);
|
|
423
|
+
if (summaryTokens > maxLen) {
|
|
424
|
+
// 대략적인 문자 수로 잘라냄
|
|
425
|
+
const charLimit = Math.floor(maxLen * 3.5);
|
|
426
|
+
summarizedContent = summarizedContent.slice(0, charLimit) + "...";
|
|
427
|
+
}
|
|
428
|
+
const summarizedTokens = this.estimateTokens(summarizedContent);
|
|
429
|
+
const originalIds = batch.map((it) => it.id);
|
|
430
|
+
// 원본 아이템 제거
|
|
431
|
+
for (const item of batch) {
|
|
432
|
+
this.items.delete(item.id);
|
|
433
|
+
}
|
|
434
|
+
// 요약 아이템 추가
|
|
435
|
+
const summaryItem = this.addItem({
|
|
436
|
+
category: "conversationHistory",
|
|
437
|
+
priority: "medium",
|
|
438
|
+
content: summarizedContent,
|
|
439
|
+
role: "system",
|
|
440
|
+
summarized: true,
|
|
441
|
+
summaryOf: originalIds,
|
|
442
|
+
}, `[Summary of ${originalIds.length} messages]\n${summarizedContent}`);
|
|
443
|
+
// 토큰 카운트는 addItem에서 자동 계산되므로 summaryItem.tokenCount 사용
|
|
444
|
+
const summary = {
|
|
445
|
+
originalIds,
|
|
446
|
+
originalTokens,
|
|
447
|
+
summarizedContent,
|
|
448
|
+
summarizedTokens: summaryItem.tokenCount,
|
|
449
|
+
compressionRatio: summaryItem.tokenCount / originalTokens,
|
|
450
|
+
createdAt: Date.now(),
|
|
451
|
+
};
|
|
452
|
+
this.summaries.push(summary);
|
|
453
|
+
// Bound summaries array to prevent unbounded growth
|
|
454
|
+
const MAX_SUMMARIES = 50;
|
|
455
|
+
while (this.summaries.length > MAX_SUMMARIES) {
|
|
456
|
+
this.summaries.shift();
|
|
457
|
+
}
|
|
458
|
+
this.summarizationCount++;
|
|
459
|
+
this.emit("summarize:complete", {
|
|
460
|
+
compressionRatio: summary.compressionRatio,
|
|
461
|
+
savedTokens: originalTokens - summaryItem.tokenCount,
|
|
462
|
+
});
|
|
463
|
+
return summary;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* 요약이 필요한지 확인한다.
|
|
467
|
+
* 전체 사용률이 summarizationThreshold를 초과하고
|
|
468
|
+
* 요약 가능한 medium 아이템이 충분하면 true.
|
|
469
|
+
* @returns 요약 필요 여부
|
|
470
|
+
*/
|
|
471
|
+
needsSummarization() {
|
|
472
|
+
if (!this.config.enableSummarization)
|
|
473
|
+
return false;
|
|
474
|
+
const usagePercent = this.getTotalUsedTokens() / this.config.totalBudget;
|
|
475
|
+
if (usagePercent < this.config.summarizationThreshold)
|
|
476
|
+
return false;
|
|
477
|
+
let mediumCount = 0;
|
|
478
|
+
for (const item of this.items.values()) {
|
|
479
|
+
if (item.priority === "medium" && !item.summarized && !item.pinned) {
|
|
480
|
+
mediumCount++;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return mediumCount >= this.config.minItemsToSummarize;
|
|
484
|
+
}
|
|
485
|
+
// ─── Eviction ───
|
|
486
|
+
/**
|
|
487
|
+
* 지정된 토큰 수를 확보하기 위해 아이템을 제거한다.
|
|
488
|
+
*
|
|
489
|
+
* Hybrid 전략:
|
|
490
|
+
* 1. ephemeral 아이템 먼저 제거
|
|
491
|
+
* 2. low 우선순위 아이템 제거 (LRU 순)
|
|
492
|
+
* 3. medium 우선순위 아이템 제거 (LRU 순)
|
|
493
|
+
* 4. high는 최후의 수단으로만
|
|
494
|
+
* 5. critical/pinned는 절대 제거 불가
|
|
495
|
+
*
|
|
496
|
+
* @param tokensNeeded - 확보해야 할 토큰 수
|
|
497
|
+
* @returns 제거된 아이템 목록
|
|
498
|
+
*/
|
|
499
|
+
evict(tokensNeeded) {
|
|
500
|
+
const evicted = [];
|
|
501
|
+
let freed = 0;
|
|
502
|
+
// 전략에 따라 제거 순서 결정
|
|
503
|
+
const allItems = Array.from(this.items.values());
|
|
504
|
+
const sortedForEviction = this.sortForEviction(allItems);
|
|
505
|
+
for (const item of sortedForEviction) {
|
|
506
|
+
if (freed >= tokensNeeded)
|
|
507
|
+
break;
|
|
508
|
+
// critical/pinned는 절대 제거 불가
|
|
509
|
+
if (item.priority === "critical" || item.pinned)
|
|
510
|
+
continue;
|
|
511
|
+
this.items.delete(item.id);
|
|
512
|
+
evicted.push(item);
|
|
513
|
+
freed += item.tokenCount;
|
|
514
|
+
}
|
|
515
|
+
if (evicted.length > 0) {
|
|
516
|
+
this.evictionCount += evicted.length;
|
|
517
|
+
this.emit("evict:items", {
|
|
518
|
+
count: evicted.length,
|
|
519
|
+
freedTokens: freed,
|
|
520
|
+
items: evicted.map((it) => ({
|
|
521
|
+
id: it.id,
|
|
522
|
+
priority: it.priority,
|
|
523
|
+
category: it.category,
|
|
524
|
+
tokens: it.tokenCount,
|
|
525
|
+
})),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return evicted;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* 예산을 자동으로 관리한다.
|
|
532
|
+
* 사용률에 따라 요약 → 제거 순서로 토큰을 확보한다.
|
|
533
|
+
*
|
|
534
|
+
* @param summarizeFn - LLM 요약 호출 함수 (선택)
|
|
535
|
+
* @returns 요약/제거된 아이템 수
|
|
536
|
+
*/
|
|
537
|
+
async autoManage(summarizeFn) {
|
|
538
|
+
let summarized = 0;
|
|
539
|
+
let evicted = 0;
|
|
540
|
+
const usagePercent = this.getTotalUsedTokens() / this.config.totalBudget;
|
|
541
|
+
// 90% 이상이면 자동 관리 시작
|
|
542
|
+
if (usagePercent < 0.9) {
|
|
543
|
+
return { summarized, evicted };
|
|
544
|
+
}
|
|
545
|
+
// 1. 먼저 요약 시도
|
|
546
|
+
if (summarizeFn && this.needsSummarization()) {
|
|
547
|
+
const result = await this.summarize(summarizeFn);
|
|
548
|
+
if (result) {
|
|
549
|
+
summarized = result.originalIds.length;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// 2. 여전히 예산 초과이면 제거
|
|
553
|
+
const currentUsed = this.getTotalUsedTokens();
|
|
554
|
+
const targetUsed = Math.floor(this.config.totalBudget * 0.8); // 80%까지 줄이기
|
|
555
|
+
if (currentUsed > targetUsed) {
|
|
556
|
+
const tokensToFree = currentUsed - targetUsed;
|
|
557
|
+
const evictedItems = this.evict(tokensToFree);
|
|
558
|
+
evicted = evictedItems.length;
|
|
559
|
+
}
|
|
560
|
+
return { summarized, evicted };
|
|
561
|
+
}
|
|
562
|
+
// ─── Checkpoints ───
|
|
563
|
+
/**
|
|
564
|
+
* 현재 상태의 체크포인트를 생성한다.
|
|
565
|
+
* 체크포인트는 전체 컨텍스트의 축약 요약 + 핵심 결정 목록을 포함한다.
|
|
566
|
+
*
|
|
567
|
+
* @param messageIndex - 현재 메시지 인덱스
|
|
568
|
+
* @param summarizeFn - LLM 요약 호출 함수
|
|
569
|
+
* @returns 생성된 체크포인트
|
|
570
|
+
*/
|
|
571
|
+
async createCheckpoint(messageIndex, summarizeFn) {
|
|
572
|
+
if (!this.config.enableCheckpoints) {
|
|
573
|
+
throw new Error("Checkpoints are disabled");
|
|
574
|
+
}
|
|
575
|
+
// 핵심 결정 추출 (user/assistant 메시지에서)
|
|
576
|
+
const keyDecisions = [];
|
|
577
|
+
const activeFiles = new Set();
|
|
578
|
+
for (const item of this.items.values()) {
|
|
579
|
+
if (item.file) {
|
|
580
|
+
activeFiles.add(item.file);
|
|
581
|
+
}
|
|
582
|
+
// 사용자 요청 또는 승인/거부 결정 추출
|
|
583
|
+
if (item.role === "user" && item.content) {
|
|
584
|
+
const preview = item.content.slice(0, 120);
|
|
585
|
+
keyDecisions.push(`User: ${preview}${item.content.length > 120 ? "..." : ""}`);
|
|
586
|
+
}
|
|
587
|
+
if (item.role === "assistant" &&
|
|
588
|
+
item.content &&
|
|
589
|
+
(item.content.includes("[APPROVED]") ||
|
|
590
|
+
item.content.includes("[REJECTED]") ||
|
|
591
|
+
item.content.includes("decision:"))) {
|
|
592
|
+
keyDecisions.push(item.content.slice(0, 200));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// 전체 컨텍스트를 축약 요약
|
|
596
|
+
const allContent = Array.from(this.items.values())
|
|
597
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
598
|
+
.map((it) => `[${it.role ?? it.category}] ${it.content?.slice(0, 200) ?? ""}`)
|
|
599
|
+
.join("\n");
|
|
600
|
+
const summaryPrompt = "Create a brief checkpoint summary (2-3 sentences) of this conversation context. " +
|
|
601
|
+
"Focus on: current goal, progress made, files modified, and key decisions.\n\n" +
|
|
602
|
+
allContent.slice(0, 8000);
|
|
603
|
+
let summary;
|
|
604
|
+
try {
|
|
605
|
+
summary = await summarizeFn(summaryPrompt);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
summary = `Checkpoint at message ${messageIndex}. ${this.items.size} context items, ${this.getTotalUsedTokens()} tokens used.`;
|
|
609
|
+
}
|
|
610
|
+
const checkpoint = {
|
|
611
|
+
id: `ckpt_${Date.now()}_${messageIndex}`,
|
|
612
|
+
timestamp: Date.now(),
|
|
613
|
+
messageIndex,
|
|
614
|
+
summary,
|
|
615
|
+
keyDecisions: keyDecisions.slice(0, 10),
|
|
616
|
+
activeFiles: Array.from(activeFiles),
|
|
617
|
+
tokenCount: this.getTotalUsedTokens(),
|
|
618
|
+
};
|
|
619
|
+
this.checkpoints.push(checkpoint);
|
|
620
|
+
// 최대 체크포인트 수 유지
|
|
621
|
+
while (this.checkpoints.length > this.config.maxCheckpoints) {
|
|
622
|
+
this.checkpoints.shift();
|
|
623
|
+
}
|
|
624
|
+
this.emit("checkpoint:created", checkpoint);
|
|
625
|
+
return checkpoint;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* 체크포인트에서 복원한다.
|
|
629
|
+
* 현재 아이템을 모두 지우고 체크포인트 요약을 시스템 메시지로 추가한다.
|
|
630
|
+
*
|
|
631
|
+
* @param checkpoint - 복원할 체크포인트
|
|
632
|
+
*/
|
|
633
|
+
restoreFromCheckpoint(checkpoint) {
|
|
634
|
+
this.items.clear();
|
|
635
|
+
// 체크포인트 요약을 시스템 메시지로 추가
|
|
636
|
+
this.addItem({
|
|
637
|
+
category: "conversationHistory",
|
|
638
|
+
priority: "critical",
|
|
639
|
+
content: checkpoint.summary,
|
|
640
|
+
role: "system",
|
|
641
|
+
summarized: true,
|
|
642
|
+
}, `[Restored from checkpoint ${checkpoint.id}]\n${checkpoint.summary}`);
|
|
643
|
+
// 핵심 결정을 워킹 메모리로 추가
|
|
644
|
+
if (checkpoint.keyDecisions.length > 0) {
|
|
645
|
+
this.addItem({
|
|
646
|
+
category: "workingMemory",
|
|
647
|
+
priority: "high",
|
|
648
|
+
content: checkpoint.keyDecisions.join("\n"),
|
|
649
|
+
role: "system",
|
|
650
|
+
}, `[Key decisions]\n${checkpoint.keyDecisions.join("\n")}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* 모든 체크포인트를 반환한다.
|
|
655
|
+
* @returns 읽기 전용 체크포인트 배열
|
|
656
|
+
*/
|
|
657
|
+
getCheckpoints() {
|
|
658
|
+
return this.checkpoints;
|
|
659
|
+
}
|
|
660
|
+
// ─── Retrieval ───
|
|
661
|
+
/**
|
|
662
|
+
* 아이템의 현재 작업에 대한 관련성 점수를 계산한다.
|
|
663
|
+
*
|
|
664
|
+
* 점수 구성:
|
|
665
|
+
* - fileMatch (0.3): 현재 작업 파일과 일치
|
|
666
|
+
* - keywordMatch (0.25): 키워드 겹침
|
|
667
|
+
* - recencyScore (0.2): 최신일수록 높음
|
|
668
|
+
* - priorityScore (0.15): 높은 우선순위일수록 높음
|
|
669
|
+
* - toolMatch (0.1): 최근 사용 도구와 일치
|
|
670
|
+
*
|
|
671
|
+
* @param item - 평가할 아이템
|
|
672
|
+
* @param query - 검색 쿼리
|
|
673
|
+
* @returns 관련성 점수 (0-1)
|
|
674
|
+
*/
|
|
675
|
+
scoreRelevance(item, query) {
|
|
676
|
+
let score = 0;
|
|
677
|
+
// 1. 파일 매치 (0.3)
|
|
678
|
+
if (item.file && query.currentFiles.length > 0) {
|
|
679
|
+
const fileMatch = query.currentFiles.some((f) => item.file === f || item.content.includes(f));
|
|
680
|
+
if (fileMatch)
|
|
681
|
+
score += 0.3;
|
|
682
|
+
}
|
|
683
|
+
else if (query.currentFiles.length > 0) {
|
|
684
|
+
// 콘텐츠에 파일 경로가 포함되어 있는지 확인
|
|
685
|
+
const contentFileMatch = query.currentFiles.some((f) => item.content.includes(f));
|
|
686
|
+
if (contentFileMatch)
|
|
687
|
+
score += 0.2;
|
|
688
|
+
}
|
|
689
|
+
// 2. 키워드 매치 (0.25)
|
|
690
|
+
if (query.keywords.length > 0) {
|
|
691
|
+
const contentLower = item.content.toLowerCase();
|
|
692
|
+
let matchCount = 0;
|
|
693
|
+
for (const kw of query.keywords) {
|
|
694
|
+
if (contentLower.includes(kw.toLowerCase())) {
|
|
695
|
+
matchCount++;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
score += 0.25 * (matchCount / query.keywords.length);
|
|
699
|
+
}
|
|
700
|
+
// 3. 최신성 점수 (0.2)
|
|
701
|
+
const now = Date.now();
|
|
702
|
+
const age = now - item.timestamp;
|
|
703
|
+
const maxAge = 30 * 60 * 1000; // 30분
|
|
704
|
+
const recency = Math.max(0, 1 - age / maxAge);
|
|
705
|
+
score += 0.2 * recency;
|
|
706
|
+
// 4. 우선순위 점수 (0.15)
|
|
707
|
+
const priorityScore = PRIORITY_SCORES[item.priority] / 5; // 0-1 정규화
|
|
708
|
+
score += 0.15 * priorityScore;
|
|
709
|
+
// 5. 도구 매치 (0.1)
|
|
710
|
+
if (item.toolName && query.recentTools.length > 0) {
|
|
711
|
+
if (query.recentTools.includes(item.toolName)) {
|
|
712
|
+
score += 0.1;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return Math.min(1, score);
|
|
716
|
+
}
|
|
717
|
+
// ─── Serialization ───
|
|
718
|
+
/**
|
|
719
|
+
* JSON 직렬화 — 세션 영속성을 위해 전체 상태를 스냅샷으로 변환한다.
|
|
720
|
+
* Map/Set → 배열 변환을 수행한다.
|
|
721
|
+
*
|
|
722
|
+
* @returns ContextBudgetSnapshot
|
|
723
|
+
*/
|
|
724
|
+
toJSON() {
|
|
725
|
+
return {
|
|
726
|
+
items: Array.from(this.items.entries()).map(([key, item]) => ({ key, item })),
|
|
727
|
+
summaries: [...this.summaries],
|
|
728
|
+
checkpoints: [...this.checkpoints],
|
|
729
|
+
idCounter: this.idCounter,
|
|
730
|
+
summarizationCount: this.summarizationCount,
|
|
731
|
+
evictionCount: this.evictionCount,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* JSON에서 ContextBudgetManager를 복구한다.
|
|
736
|
+
* 기존 config를 유지하면서 저장된 런타임 상태를 복원한다.
|
|
737
|
+
*
|
|
738
|
+
* @param snapshot - 저장된 스냅샷
|
|
739
|
+
* @param config - 예산 설정 (미지정 시 기본값)
|
|
740
|
+
* @returns 복원된 ContextBudgetManager
|
|
741
|
+
*/
|
|
742
|
+
static fromJSON(snapshot, config) {
|
|
743
|
+
const manager = new ContextBudgetManager(config);
|
|
744
|
+
// 아이템 복원
|
|
745
|
+
for (const { key, item } of snapshot.items) {
|
|
746
|
+
manager.items.set(key, item);
|
|
747
|
+
}
|
|
748
|
+
// 요약/체크포인트/카운터 복원
|
|
749
|
+
manager.summaries = [...(snapshot.summaries ?? [])];
|
|
750
|
+
manager.checkpoints = [...(snapshot.checkpoints ?? [])];
|
|
751
|
+
manager.idCounter = snapshot.idCounter ?? 0;
|
|
752
|
+
manager.summarizationCount = snapshot.summarizationCount ?? 0;
|
|
753
|
+
manager.evictionCount = snapshot.evictionCount ?? 0;
|
|
754
|
+
return manager;
|
|
755
|
+
}
|
|
756
|
+
// ─── Token Counting ───
|
|
757
|
+
/**
|
|
758
|
+
* 문자열의 대략적인 토큰 수를 추정한다.
|
|
759
|
+
*
|
|
760
|
+
* CJK 문자가 30% 이상이면 CJK 모드 (~2 chars/token),
|
|
761
|
+
* 그 외에는 영어 모드 (~3.5 chars/token).
|
|
762
|
+
*
|
|
763
|
+
* @param text - 추정할 문자열
|
|
764
|
+
* @returns 추정 토큰 수
|
|
765
|
+
*/
|
|
766
|
+
estimateTokens(text) {
|
|
767
|
+
if (!text)
|
|
768
|
+
return 0;
|
|
769
|
+
const cjkCount = (text.match(/[\u3000-\u9fff\uac00-\ud7af]/g) ?? []).length;
|
|
770
|
+
const nonCjkCount = text.length - cjkCount;
|
|
771
|
+
// CJK 비율이 높으면 전체를 2로 나눔
|
|
772
|
+
if (cjkCount > text.length * 0.3) {
|
|
773
|
+
return Math.ceil(text.length / 2);
|
|
774
|
+
}
|
|
775
|
+
return Math.ceil(nonCjkCount / 3.5 + cjkCount / 2);
|
|
776
|
+
}
|
|
777
|
+
// ─── Conversion ───
|
|
778
|
+
/**
|
|
779
|
+
* 현재 아이템을 LLM 호출용 Message[] 로 변환한다.
|
|
780
|
+
* 예산 내의 아이템만 포함하며, timestamp 순으로 정렬한다.
|
|
781
|
+
*
|
|
782
|
+
* @returns Message 배열
|
|
783
|
+
*/
|
|
784
|
+
toMessages() {
|
|
785
|
+
const items = this.buildContext();
|
|
786
|
+
const messages = [];
|
|
787
|
+
for (const item of items) {
|
|
788
|
+
const role = item.role ?? "system";
|
|
789
|
+
messages.push({
|
|
790
|
+
role,
|
|
791
|
+
content: item.content,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return messages;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* 기존 Message[] 히스토리를 아이템으로 임포트한다.
|
|
798
|
+
* 각 메시지를 적절한 카테고리와 우선순위로 분류한다.
|
|
799
|
+
*
|
|
800
|
+
* @param messages - 임포트할 메시지 배열
|
|
801
|
+
*/
|
|
802
|
+
importMessages(messages) {
|
|
803
|
+
const total = messages.length;
|
|
804
|
+
for (let i = 0; i < total; i++) {
|
|
805
|
+
const msg = messages[i];
|
|
806
|
+
const isRecent = i >= total - 5;
|
|
807
|
+
let category;
|
|
808
|
+
let priority;
|
|
809
|
+
switch (msg.role) {
|
|
810
|
+
case "system":
|
|
811
|
+
category = "systemPrompt";
|
|
812
|
+
priority = "critical";
|
|
813
|
+
break;
|
|
814
|
+
case "user":
|
|
815
|
+
category = "conversationHistory";
|
|
816
|
+
priority = isRecent ? "high" : "medium";
|
|
817
|
+
break;
|
|
818
|
+
case "assistant":
|
|
819
|
+
category = "conversationHistory";
|
|
820
|
+
priority = isRecent ? "high" : "medium";
|
|
821
|
+
break;
|
|
822
|
+
case "tool":
|
|
823
|
+
category = "toolResults";
|
|
824
|
+
priority = isRecent ? "high" : "low";
|
|
825
|
+
break;
|
|
826
|
+
default:
|
|
827
|
+
category = "conversationHistory";
|
|
828
|
+
priority = "medium";
|
|
829
|
+
}
|
|
830
|
+
// 첫 번째 user 메시지는 목표이므로 high
|
|
831
|
+
if (msg.role === "user" && i <= 1) {
|
|
832
|
+
priority = "high";
|
|
833
|
+
}
|
|
834
|
+
this.addItem({
|
|
835
|
+
category,
|
|
836
|
+
priority,
|
|
837
|
+
content: msg.content ?? "",
|
|
838
|
+
role: msg.role,
|
|
839
|
+
}, msg.content ?? "");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// ─── Private Helpers ───
|
|
843
|
+
/**
|
|
844
|
+
* 전체 사용 중인 토큰 수를 계산한다.
|
|
845
|
+
*/
|
|
846
|
+
getTotalUsedTokens() {
|
|
847
|
+
let total = 0;
|
|
848
|
+
for (const item of this.items.values()) {
|
|
849
|
+
total += item.tokenCount;
|
|
850
|
+
}
|
|
851
|
+
return total;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* 예산 건강 상태를 확인하고 필요 시 이벤트를 발생시킨다.
|
|
855
|
+
*/
|
|
856
|
+
checkBudgetHealth() {
|
|
857
|
+
const usagePercent = this.getTotalUsedTokens() / this.config.totalBudget;
|
|
858
|
+
if (usagePercent > 1.0) {
|
|
859
|
+
this.emit("budget:overflow", { usagePercent, usedTokens: this.getTotalUsedTokens() });
|
|
860
|
+
}
|
|
861
|
+
else if (usagePercent > 0.9) {
|
|
862
|
+
this.emit("budget:critical", { usagePercent, usedTokens: this.getTotalUsedTokens() });
|
|
863
|
+
}
|
|
864
|
+
else if (usagePercent > 0.75) {
|
|
865
|
+
this.emit("budget:warning", { usagePercent, usedTokens: this.getTotalUsedTokens() });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* 제거 전략에 따라 아이템을 정렬한다.
|
|
870
|
+
* 제거 순서: ephemeral → low → medium → high (oldest first within each)
|
|
871
|
+
*
|
|
872
|
+
* @param items - 정렬할 아이템 배열
|
|
873
|
+
* @returns 제거 우선순위 순으로 정렬된 배열
|
|
874
|
+
*/
|
|
875
|
+
sortForEviction(items) {
|
|
876
|
+
const { evictionStrategy } = this.config;
|
|
877
|
+
return [...items].sort((a, b) => {
|
|
878
|
+
// pinned/critical은 항상 마지막
|
|
879
|
+
if (a.pinned || a.priority === "critical")
|
|
880
|
+
return 1;
|
|
881
|
+
if (b.pinned || b.priority === "critical")
|
|
882
|
+
return -1;
|
|
883
|
+
switch (evictionStrategy) {
|
|
884
|
+
case "lru":
|
|
885
|
+
// 오래된 것 먼저
|
|
886
|
+
return a.timestamp - b.timestamp;
|
|
887
|
+
case "priority":
|
|
888
|
+
// 낮은 우선순위 먼저
|
|
889
|
+
return PRIORITY_SCORES[a.priority] - PRIORITY_SCORES[b.priority];
|
|
890
|
+
case "hybrid":
|
|
891
|
+
default: {
|
|
892
|
+
// 낮은 우선순위 먼저, 같은 우선순위 내에서 오래된 것 먼저
|
|
893
|
+
const pa = PRIORITY_SCORES[a.priority];
|
|
894
|
+
const pb = PRIORITY_SCORES[b.priority];
|
|
895
|
+
if (pa !== pb)
|
|
896
|
+
return pa - pb;
|
|
897
|
+
return a.timestamp - b.timestamp;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
//# sourceMappingURL=context-budget.js.map
|