@kkelly-offical/kkcode 0.1.3 → 0.1.7
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 +110 -172
- package/package.json +46 -46
- package/src/agent/agent.mjs +220 -170
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +59 -7
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/compaction.mjs +298 -276
- package/src/session/engine.mjs +232 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1097 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +900 -1462
- package/src/session/loop.mjs +65 -40
- package/src/session/project-context.mjs +30 -0
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/plan.txt +31 -9
- package/src/session/rollback.mjs +196 -0
- package/src/session/store.mjs +519 -503
- package/src/session/system-prompt.mjs +273 -260
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/question-prompt.mjs +93 -86
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
|
@@ -1,276 +1,298 @@
|
|
|
1
|
-
import { requestProvider } from "../provider/router.mjs"
|
|
2
|
-
import { getConversationHistory, replaceMessages } from "./store.mjs"
|
|
3
|
-
import { HookBus } from "../plugin/hook-bus.mjs"
|
|
4
|
-
import { saveCheckpoint } from "./checkpoint.mjs"
|
|
5
|
-
import { recordTurn } from "../usage/usage-meter.mjs"
|
|
6
|
-
import { loadPricing, calculateCost } from "../usage/pricing.mjs"
|
|
7
|
-
|
|
8
|
-
const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured summary preserving all critical information for continued work.
|
|
9
|
-
|
|
10
|
-
## Output Format
|
|
11
|
-
|
|
12
|
-
<summary>
|
|
13
|
-
<goal>The user's overall goal or current task</goal>
|
|
14
|
-
<completed>
|
|
15
|
-
- Completed task with specific details (file paths, function names, line numbers)
|
|
16
|
-
</completed>
|
|
17
|
-
<in_progress>Current work being done, if any</in_progress>
|
|
18
|
-
<files_modified>
|
|
19
|
-
- path/to/file: specific change description
|
|
20
|
-
</files_modified>
|
|
21
|
-
<key_decisions>
|
|
22
|
-
- Decision and reasoning
|
|
23
|
-
- User preferences or constraints
|
|
24
|
-
</key_decisions>
|
|
25
|
-
<errors_resolved>
|
|
26
|
-
- Error description → fix applied
|
|
27
|
-
</errors_resolved>
|
|
28
|
-
<next_steps>
|
|
29
|
-
- Specific next action items
|
|
30
|
-
</next_steps>
|
|
31
|
-
</summary>
|
|
32
|
-
|
|
33
|
-
Rules:
|
|
34
|
-
- Use the SAME LANGUAGE as the conversation
|
|
35
|
-
- Preserve ALL file paths, function names, variable names, and technical identifiers exactly
|
|
36
|
-
- Include specific code changes, not just "modified file X"
|
|
37
|
-
- Omit tool call metadata and message formatting details
|
|
38
|
-
- Be concise but never drop actionable information`
|
|
39
|
-
|
|
40
|
-
const DEFAULT_THRESHOLD_MESSAGES = 50
|
|
41
|
-
const DEFAULT_THRESHOLD_RATIO = 0.7
|
|
42
|
-
const DEFAULT_KEEP_RECENT = 6
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
tokens += estimateStringTokens(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* -
|
|
88
|
-
* -
|
|
89
|
-
* - Truncate
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
1
|
+
import { requestProvider } from "../provider/router.mjs"
|
|
2
|
+
import { getConversationHistory, replaceMessages } from "./store.mjs"
|
|
3
|
+
import { HookBus } from "../plugin/hook-bus.mjs"
|
|
4
|
+
import { saveCheckpoint } from "./checkpoint.mjs"
|
|
5
|
+
import { recordTurn } from "../usage/usage-meter.mjs"
|
|
6
|
+
import { loadPricing, calculateCost } from "../usage/pricing.mjs"
|
|
7
|
+
|
|
8
|
+
const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured summary preserving all critical information for continued work.
|
|
9
|
+
|
|
10
|
+
## Output Format
|
|
11
|
+
|
|
12
|
+
<summary>
|
|
13
|
+
<goal>The user's overall goal or current task</goal>
|
|
14
|
+
<completed>
|
|
15
|
+
- Completed task with specific details (file paths, function names, line numbers)
|
|
16
|
+
</completed>
|
|
17
|
+
<in_progress>Current work being done, if any</in_progress>
|
|
18
|
+
<files_modified>
|
|
19
|
+
- path/to/file: specific change description
|
|
20
|
+
</files_modified>
|
|
21
|
+
<key_decisions>
|
|
22
|
+
- Decision and reasoning
|
|
23
|
+
- User preferences or constraints
|
|
24
|
+
</key_decisions>
|
|
25
|
+
<errors_resolved>
|
|
26
|
+
- Error description → fix applied
|
|
27
|
+
</errors_resolved>
|
|
28
|
+
<next_steps>
|
|
29
|
+
- Specific next action items
|
|
30
|
+
</next_steps>
|
|
31
|
+
</summary>
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- Use the SAME LANGUAGE as the conversation
|
|
35
|
+
- Preserve ALL file paths, function names, variable names, and technical identifiers exactly
|
|
36
|
+
- Include specific code changes, not just "modified file X"
|
|
37
|
+
- Omit tool call metadata and message formatting details
|
|
38
|
+
- Be concise but never drop actionable information`
|
|
39
|
+
|
|
40
|
+
const DEFAULT_THRESHOLD_MESSAGES = 50
|
|
41
|
+
const DEFAULT_THRESHOLD_RATIO = 0.7
|
|
42
|
+
const DEFAULT_KEEP_RECENT = 6
|
|
43
|
+
const DEFAULT_KEEP_RECENT_TURNS = 3
|
|
44
|
+
const TOOL_RESULT_PREVIEW_LIMIT = 200
|
|
45
|
+
|
|
46
|
+
// Estimate tokens from a string, accounting for CJK characters (~1.5 chars/token vs ~4 for Latin)
|
|
47
|
+
export function estimateStringTokens(str) {
|
|
48
|
+
if (!str) return 0
|
|
49
|
+
let cjk = 0
|
|
50
|
+
for (let i = 0; i < str.length; i++) {
|
|
51
|
+
const code = str.charCodeAt(i)
|
|
52
|
+
if ((code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3000 && code <= 0x30FF) ||
|
|
53
|
+
(code >= 0xAC00 && code <= 0xD7AF)) cjk++
|
|
54
|
+
}
|
|
55
|
+
const latin = str.length - cjk
|
|
56
|
+
return Math.ceil(latin / 4 + cjk / 1.5)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const MSG_OVERHEAD = 4 // ~4 tokens per message for role/metadata
|
|
60
|
+
|
|
61
|
+
export function estimateTokenCount(messages) {
|
|
62
|
+
let tokens = 0
|
|
63
|
+
for (const msg of messages) {
|
|
64
|
+
tokens += MSG_OVERHEAD
|
|
65
|
+
const content = msg.content
|
|
66
|
+
if (Array.isArray(content)) {
|
|
67
|
+
for (const block of content) {
|
|
68
|
+
if (block.type === "image") {
|
|
69
|
+
tokens += 1600 // conservative estimate for a typical image
|
|
70
|
+
} else if (block.type === "tool_use") {
|
|
71
|
+
tokens += estimateStringTokens(block.name || "")
|
|
72
|
+
tokens += estimateStringTokens(JSON.stringify(block.input || {}))
|
|
73
|
+
} else if (block.type === "tool_result") {
|
|
74
|
+
tokens += estimateStringTokens(String(block.content || ""))
|
|
75
|
+
} else {
|
|
76
|
+
tokens += estimateStringTokens(block.text || block.content || "")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
tokens += estimateStringTokens(content || "")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return tokens
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Pre-prune messages before LLM summarization.
|
|
88
|
+
* - Strip synthetic scaffolding messages (continuation noise)
|
|
89
|
+
* - Truncate large tool_result content with aging: older steps get shorter previews
|
|
90
|
+
* - Keep tool_use blocks intact (they show model intent)
|
|
91
|
+
* - Truncate very long plain-text assistant/user messages
|
|
92
|
+
*/
|
|
93
|
+
export function pruneForSummary(messages, previewLimit = TOOL_RESULT_PREVIEW_LIMIT) {
|
|
94
|
+
// Strip synthetic scaffolding messages (continuation prompts, fake tool_result errors)
|
|
95
|
+
const real = messages.filter(msg => !msg.synthetic)
|
|
96
|
+
|
|
97
|
+
// #2 工具结果老化: find max step to compute relative age per message
|
|
98
|
+
const maxStep = real.reduce((m, msg) => Math.max(m, msg.step || 0), 0)
|
|
99
|
+
|
|
100
|
+
return real.map((msg) => {
|
|
101
|
+
// Aging: older tool_results get more aggressive truncation
|
|
102
|
+
const age = maxStep - (msg.step || 0)
|
|
103
|
+
const effectiveLimit = Math.max(50, previewLimit - age * 15)
|
|
104
|
+
|
|
105
|
+
const content = msg.content
|
|
106
|
+
if (Array.isArray(content)) {
|
|
107
|
+
const pruned = content.map((block) => {
|
|
108
|
+
if (block.type === "tool_result") {
|
|
109
|
+
const raw = String(block.content || "")
|
|
110
|
+
if (raw.length > effectiveLimit) {
|
|
111
|
+
return {
|
|
112
|
+
...block,
|
|
113
|
+
content: `${raw.slice(0, effectiveLimit)}... [truncated ${raw.length} chars, age=${age}]`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return block
|
|
118
|
+
})
|
|
119
|
+
return { ...msg, content: pruned }
|
|
120
|
+
}
|
|
121
|
+
// Truncate very long plain-text messages (e.g. large tool output pasted as text)
|
|
122
|
+
if (typeof content === "string" && content.length > 2000) {
|
|
123
|
+
return { ...msg, content: `${content.slice(0, 2000)}... [truncated ${content.length} chars]` }
|
|
124
|
+
}
|
|
125
|
+
return msg
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const BUILTIN_CONTEXT = {
|
|
130
|
+
"gpt-5": 272000, "o3": 200000, "o1": 200000,
|
|
131
|
+
"claude-opus-4": 200000, "claude-3-5": 200000, "claude-3.5": 200000, "claude": 200000,
|
|
132
|
+
"gemini-2": 1048576, "gemini-1.5": 1048576, "gemini": 128000,
|
|
133
|
+
"gpt-4o": 128000, "gpt-4": 128000, "gpt-3.5": 16000,
|
|
134
|
+
"deepseek": 64000, "qwen": 128000
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function modelContextLimit(model, configState = null) {
|
|
138
|
+
const m = String(model || "").toLowerCase()
|
|
139
|
+
// 1) Check provider-level context_limit for the active provider
|
|
140
|
+
const providerCfg = configState?.config?.provider
|
|
141
|
+
if (providerCfg) {
|
|
142
|
+
// Per-model override from provider.model_context map
|
|
143
|
+
const mc = providerCfg.model_context
|
|
144
|
+
if (mc) {
|
|
145
|
+
if (mc[model]) return mc[model]
|
|
146
|
+
for (const key of Object.keys(mc)) {
|
|
147
|
+
if (m.startsWith(key.toLowerCase())) return mc[key]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Provider-level context_limit
|
|
151
|
+
const active = providerCfg[providerCfg.default]
|
|
152
|
+
if (active?.context_limit > 0) return active.context_limit
|
|
153
|
+
}
|
|
154
|
+
// 2) Builtin prefix match
|
|
155
|
+
for (const [prefix, limit] of Object.entries(BUILTIN_CONTEXT)) {
|
|
156
|
+
if (m.includes(prefix)) return limit
|
|
157
|
+
}
|
|
158
|
+
return 128000
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function contextUtilization(messages, model, configState = null) {
|
|
162
|
+
const tokens = estimateTokenCount(messages)
|
|
163
|
+
const limit = modelContextLimit(model, configState)
|
|
164
|
+
const ratio = limit > 0 ? Math.min(1, tokens / limit) : 0
|
|
165
|
+
return {
|
|
166
|
+
tokens,
|
|
167
|
+
limit,
|
|
168
|
+
ratio,
|
|
169
|
+
percent: Math.round(ratio * 100)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function supportsNativeCompaction(providerType, model) {
|
|
174
|
+
if (providerType !== "anthropic") return false
|
|
175
|
+
const m = String(model || "").toLowerCase()
|
|
176
|
+
return m.includes("claude") && (m.includes("opus") || m.includes("sonnet"))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function shouldCompact({ messages, model, thresholdMessages = DEFAULT_THRESHOLD_MESSAGES, thresholdRatio = DEFAULT_THRESHOLD_RATIO, configState = null, realTokenCount = null }) {
|
|
180
|
+
if (messages.length >= thresholdMessages) return true
|
|
181
|
+
const limit = modelContextLimit(model, configState)
|
|
182
|
+
const tokens = realTokenCount != null ? realTokenCount : estimateTokenCount(messages)
|
|
183
|
+
return tokens >= limit * thresholdRatio
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function compactSession({
|
|
187
|
+
sessionId,
|
|
188
|
+
model,
|
|
189
|
+
providerType,
|
|
190
|
+
configState,
|
|
191
|
+
keepRecent = DEFAULT_KEEP_RECENT,
|
|
192
|
+
keepRecentTurns = DEFAULT_KEEP_RECENT_TURNS,
|
|
193
|
+
baseUrl = null,
|
|
194
|
+
apiKeyEnv = null
|
|
195
|
+
}) {
|
|
196
|
+
const history = await getConversationHistory(sessionId, 9999)
|
|
197
|
+
if (history.length <= keepRecent + 2) return { compacted: false, reason: "too few messages" }
|
|
198
|
+
|
|
199
|
+
// Turn-based split: keep last keepRecentTurns complete turns
|
|
200
|
+
// A "turn" = one user interaction cycle (user msg + model response + all tool calls)
|
|
201
|
+
// Falls back to message-count if no turnId metadata is present
|
|
202
|
+
let splitIdx
|
|
203
|
+
const turnIds = []
|
|
204
|
+
const seenTurns = new Set()
|
|
205
|
+
for (const msg of history) {
|
|
206
|
+
if (msg.turnId && !seenTurns.has(msg.turnId)) {
|
|
207
|
+
seenTurns.add(msg.turnId)
|
|
208
|
+
turnIds.push(msg.turnId)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (turnIds.length > keepRecentTurns) {
|
|
212
|
+
const keepFromTurnId = turnIds[turnIds.length - keepRecentTurns]
|
|
213
|
+
splitIdx = history.findIndex(msg => msg.turnId === keepFromTurnId)
|
|
214
|
+
if (splitIdx < 0) splitIdx = history.length - keepRecent
|
|
215
|
+
} else {
|
|
216
|
+
// Fallback: not enough turns, use message count
|
|
217
|
+
splitIdx = history.length - keepRecent
|
|
218
|
+
}
|
|
219
|
+
const toSummarize = history.slice(0, splitIdx)
|
|
220
|
+
const kept = history.slice(splitIdx)
|
|
221
|
+
|
|
222
|
+
// Layer 1: prune large tool outputs before sending to LLM
|
|
223
|
+
const pruned = pruneForSummary(toSummarize)
|
|
224
|
+
const summaryPrompt = pruned.map((m) => {
|
|
225
|
+
const content = m.content
|
|
226
|
+
if (Array.isArray(content)) {
|
|
227
|
+
return `[${m.role}]: ${content.map((b) => {
|
|
228
|
+
if (b.type === "text") return b.text || ""
|
|
229
|
+
if (b.type === "tool_use") return `[tool_use:${b.name}(${JSON.stringify(b.input || {}).slice(0, 120)})]`
|
|
230
|
+
if (b.type === "tool_result") return `[tool_result:${b.is_error ? "ERROR " : ""}${b.content || ""}]`
|
|
231
|
+
return ""
|
|
232
|
+
}).filter(Boolean).join("\n")}`
|
|
233
|
+
}
|
|
234
|
+
return `[${m.role}]: ${content}`
|
|
235
|
+
}).join("\n\n")
|
|
236
|
+
|
|
237
|
+
const hookPayload = await HookBus.sessionCompacting({
|
|
238
|
+
sessionId,
|
|
239
|
+
messageCount: history.length,
|
|
240
|
+
summarizeCount: toSummarize.length,
|
|
241
|
+
keepCount: kept.length
|
|
242
|
+
})
|
|
243
|
+
if (hookPayload?.skip) return { compacted: false, reason: "skipped by hook" }
|
|
244
|
+
|
|
245
|
+
let summaryText
|
|
246
|
+
let compactionUsage = null
|
|
247
|
+
try {
|
|
248
|
+
const response = await requestProvider({
|
|
249
|
+
configState,
|
|
250
|
+
providerType,
|
|
251
|
+
model,
|
|
252
|
+
system: COMPACTION_SYSTEM,
|
|
253
|
+
messages: [{ role: "user", content: summaryPrompt }],
|
|
254
|
+
tools: [],
|
|
255
|
+
baseUrl,
|
|
256
|
+
apiKeyEnv
|
|
257
|
+
})
|
|
258
|
+
summaryText = (response.text || "").trim()
|
|
259
|
+
compactionUsage = response.usage || null
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return { compacted: false, reason: `compaction LLM call failed: ${error.message}` }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!summaryText) return { compacted: false, reason: "empty summary from LLM" }
|
|
265
|
+
|
|
266
|
+
// Replace all messages with: [summary] + [kept recent messages]
|
|
267
|
+
const summaryMessage = {
|
|
268
|
+
role: "user",
|
|
269
|
+
content: `<compaction-summary>\n${summaryText}\n</compaction-summary>`
|
|
270
|
+
}
|
|
271
|
+
await replaceMessages(sessionId, [summaryMessage, ...kept])
|
|
272
|
+
|
|
273
|
+
// Record compaction LLM usage so it's not "invisible"
|
|
274
|
+
if (compactionUsage) {
|
|
275
|
+
try {
|
|
276
|
+
const { pricing } = await loadPricing(configState)
|
|
277
|
+
const { amount } = calculateCost(pricing, model, compactionUsage)
|
|
278
|
+
await recordTurn({ sessionId, usage: compactionUsage, cost: amount })
|
|
279
|
+
} catch { /* best-effort */ }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await saveCheckpoint(sessionId, {
|
|
283
|
+
kind: "compaction",
|
|
284
|
+
iteration: 0,
|
|
285
|
+
compactedAt: Date.now(),
|
|
286
|
+
summarizeCount: toSummarize.length,
|
|
287
|
+
keepCount: kept.length,
|
|
288
|
+
summaryVersion: 1,
|
|
289
|
+
summaryLength: summaryText.length
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
compacted: true,
|
|
294
|
+
summarizedCount: toSummarize.length,
|
|
295
|
+
keptCount: kept.length,
|
|
296
|
+
summaryLength: summaryText.length
|
|
297
|
+
}
|
|
298
|
+
}
|