@kkelly-offical/kkcode 0.2.3-preview.1 → 0.2.3-preview.2
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 +4 -2
- package/package.json +3 -3
- package/src/session/compaction.mjs +150 -42
- package/src/session/store.mjs +14 -5
- package/src/version.mjs +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# kkcode
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@kkelly-offical/kkcode)
|
|
4
4
|
[](https://github.com/kkelly-offical/kkcode/releases)
|
|
5
5
|

|
|
6
6
|

|
|
@@ -376,15 +376,17 @@ update:
|
|
|
376
376
|
<a id="release-status"></a>
|
|
377
377
|
## Release Status / 发布状态
|
|
378
378
|
|
|
379
|
-
**Current preview / 当前预览版本**: `v0.2.3-preview.
|
|
379
|
+
**Current preview / 当前预览版本**: `v0.2.3-preview.2`
|
|
380
380
|
**Latest releases / 最新发布**: [GitHub Releases](https://github.com/kkelly-offical/kkcode/releases)
|
|
381
381
|
**Package / 包地址**: [npm](https://www.npmjs.com/package/@kkelly-offical/kkcode)
|
|
382
382
|
|
|
383
383
|
**English**
|
|
384
|
+
- `0.2.3-preview.2` is the Preview V2 context release: compaction now merges prior context state, keeps evidence before pruning, and preserves turn metadata for reliable recent-turn retention.
|
|
384
385
|
- `0.2.3-preview.1` is the Preview V1 updater release: kkcode can check npm dist-tags at startup and exposes `kkcode update` for manual upgrades.
|
|
385
386
|
- `0.2.1` rebuilt kkcode around Assistant as the default general-purpose lane, with dedicated Agent and LongAgent modes for coding work.
|
|
386
387
|
|
|
387
388
|
**中文**
|
|
389
|
+
- `0.2.3-preview.2` 是 Preview V2 上下文版本:压缩会合并旧上下文状态,在裁剪前保留关键证据,并保留 turn 元数据以稳定保留最近对话。
|
|
388
390
|
- `0.2.3-preview.1` 是 Preview V1 更新器版本:kkcode 可在启动时检查 npm dist-tag,并提供 `kkcode update` 手动升级入口。
|
|
389
391
|
- `0.2.1` 将 kkcode 重构为以 Assistant 为默认入口的通用个人助手,同时保留专门面向代码工作的 Agent 和 LongAgent 模式。
|
|
390
392
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kkelly-offical/kkcode",
|
|
3
|
-
"version": "0.2.3-preview.
|
|
3
|
+
"version": "0.2.3-preview.2",
|
|
4
4
|
"description": "CLI-first personal assistant with dedicated coding and LongAgent modes for governed terminal workflows, MCP integrations, and extensible automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.5.2",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"author": "kkelly-offical",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/kkelly-offical/kkcode.git"
|
|
11
|
+
"url": "git+https://github.com/kkelly-offical/kkcode.git"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/kkelly-offical/kkcode",
|
|
14
14
|
"bugs": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"mcp"
|
|
26
26
|
],
|
|
27
27
|
"bin": {
|
|
28
|
-
"kkcode": "
|
|
28
|
+
"kkcode": "src/index.mjs"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"src/",
|
|
@@ -5,36 +5,36 @@ import { saveCheckpoint } from "./checkpoint.mjs"
|
|
|
5
5
|
import { recordTurn } from "../usage/usage-meter.mjs"
|
|
6
6
|
import { loadPricing, calculateCost } from "../usage/pricing.mjs"
|
|
7
7
|
|
|
8
|
-
const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured summary preserving all critical information for continued work.
|
|
8
|
+
const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured, merge-safe summary preserving all critical information for continued work.
|
|
9
9
|
|
|
10
10
|
## Output Format
|
|
11
11
|
|
|
12
|
+
Return exactly:
|
|
13
|
+
|
|
14
|
+
<context-state>
|
|
15
|
+
{
|
|
16
|
+
"goal": "The user's current overall goal",
|
|
17
|
+
"completed": ["Completed work with specific file paths, function names, and line numbers"],
|
|
18
|
+
"in_progress": ["Current work being done"],
|
|
19
|
+
"files_modified": [{"path":"path/to/file","changes":["specific change"]}],
|
|
20
|
+
"key_decisions": ["Decision, constraint, or user preference"],
|
|
21
|
+
"errors_resolved": ["Error -> fix applied"],
|
|
22
|
+
"evidence": ["Important command output, test result, provider error, or exact failure detail"],
|
|
23
|
+
"next_steps": ["Specific next action item"]
|
|
24
|
+
}
|
|
25
|
+
</context-state>
|
|
26
|
+
|
|
12
27
|
<summary>
|
|
13
|
-
|
|
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>
|
|
28
|
+
Concise human-readable continuation summary.
|
|
31
29
|
</summary>
|
|
32
30
|
|
|
33
31
|
Rules:
|
|
34
32
|
- Use the SAME LANGUAGE as the conversation
|
|
33
|
+
- Merge the prior context state with the new conversation delta; do not summarize the prior state as another chat message
|
|
35
34
|
- Preserve ALL file paths, function names, variable names, and technical identifiers exactly
|
|
36
35
|
- Include specific code changes, not just "modified file X"
|
|
37
36
|
- Omit tool call metadata and message formatting details
|
|
37
|
+
- Preserve exact errors, failing test names, package versions, release labels, and user constraints in evidence
|
|
38
38
|
- Be concise but never drop actionable information`
|
|
39
39
|
|
|
40
40
|
const DEFAULT_THRESHOLD_MESSAGES = 50
|
|
@@ -42,6 +42,118 @@ const DEFAULT_THRESHOLD_RATIO = 0.7
|
|
|
42
42
|
const DEFAULT_KEEP_RECENT = 6
|
|
43
43
|
const DEFAULT_KEEP_RECENT_TURNS = 3
|
|
44
44
|
const TOOL_RESULT_PREVIEW_LIMIT = 200
|
|
45
|
+
const EVIDENCE_PREVIEW_LIMIT = 900
|
|
46
|
+
const PATH_RE = /(?:^|\s)([A-Za-z0-9_.@~/-]+\.(?:mjs|js|ts|tsx|jsx|json|yaml|yml|md|txt|rs|go|py|sh|toml|lock))(?:[:\s]|$)/g
|
|
47
|
+
const IMPORTANT_LINE_RE = /(error|failed|failure|exception|traceback|assert|reject|denied|unauthorized|context|compact|version|publish|npm|test|lint|typecheck|diff|modified)/i
|
|
48
|
+
|
|
49
|
+
export function isCompactionSummaryMessage(msg) {
|
|
50
|
+
const content = msg?.content
|
|
51
|
+
if (typeof content === "string") return content.includes("<compaction-summary")
|
|
52
|
+
if (Array.isArray(content)) {
|
|
53
|
+
return content.some((block) => {
|
|
54
|
+
if (typeof block === "string") return block.includes("<compaction-summary")
|
|
55
|
+
return block?.type === "text" && typeof block.text === "string" && block.text.includes("<compaction-summary")
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function extractCompactionSummary(content) {
|
|
62
|
+
const text = Array.isArray(content)
|
|
63
|
+
? content.map((block) => typeof block === "string" ? block : (block?.text || block?.content || "")).join("\n")
|
|
64
|
+
: String(content || "")
|
|
65
|
+
const match = text.match(/<compaction-summary(?:\s[^>]*)?>\s*([\s\S]*?)\s*<\/compaction-summary>/i)
|
|
66
|
+
return (match ? match[1] : "").trim()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clip(text, limit = EVIDENCE_PREVIEW_LIMIT) {
|
|
70
|
+
const raw = String(text || "")
|
|
71
|
+
if (raw.length <= limit) return raw
|
|
72
|
+
return raw.slice(0, limit) + "... [truncated " + raw.length + " chars]"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractPaths(text) {
|
|
76
|
+
const paths = new Set()
|
|
77
|
+
let match
|
|
78
|
+
PATH_RE.lastIndex = 0
|
|
79
|
+
while ((match = PATH_RE.exec(String(text || ""))) !== null) {
|
|
80
|
+
paths.add(match[1])
|
|
81
|
+
if (paths.size >= 12) break
|
|
82
|
+
}
|
|
83
|
+
return [...paths]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function importantLines(text, limit = 12) {
|
|
87
|
+
return String(text || "")
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.map((line) => line.trim())
|
|
90
|
+
.filter((line) => line && IMPORTANT_LINE_RE.test(line))
|
|
91
|
+
.slice(0, limit)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function collectEvidenceLedger(messages, previewLimit = EVIDENCE_PREVIEW_LIMIT) {
|
|
95
|
+
const evidence = []
|
|
96
|
+
for (const msg of messages) {
|
|
97
|
+
const content = msg.content
|
|
98
|
+
const blocks = Array.isArray(content) ? content : [{ type: "text", text: content }]
|
|
99
|
+
for (const block of blocks) {
|
|
100
|
+
const raw = String(block?.content || block?.text || "")
|
|
101
|
+
if (!raw) continue
|
|
102
|
+
if (block.type === "tool_result") {
|
|
103
|
+
const lines = importantLines(raw)
|
|
104
|
+
const paths = extractPaths(raw)
|
|
105
|
+
if (block.is_error || lines.length || paths.length) {
|
|
106
|
+
evidence.push([
|
|
107
|
+
"- role=" + msg.role + " tool_result" + (block.is_error ? " ERROR" : ""),
|
|
108
|
+
paths.length ? " paths: " + paths.join(", ") : "",
|
|
109
|
+
lines.length ? " key_lines:\n" + lines.map((line) => " " + clip(line, 220)).join("\n") : " preview: " + clip(raw, previewLimit)
|
|
110
|
+
].filter(Boolean).join("\n"))
|
|
111
|
+
}
|
|
112
|
+
} else if (typeof content === "string" && raw.length > 1000) {
|
|
113
|
+
const lines = importantLines(raw)
|
|
114
|
+
const paths = extractPaths(raw)
|
|
115
|
+
if (lines.length || paths.length) {
|
|
116
|
+
evidence.push([
|
|
117
|
+
"- role=" + msg.role + " long_text",
|
|
118
|
+
paths.length ? " paths: " + paths.join(", ") : "",
|
|
119
|
+
lines.length ? " key_lines:\n" + lines.map((line) => " " + clip(line, 220)).join("\n") : ""
|
|
120
|
+
].filter(Boolean).join("\n"))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (evidence.length >= 20) return evidence
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return evidence
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildCompactionPrompt({ previousSummary = "", messages, evidence = [] }) {
|
|
130
|
+
const transcript = messages.map((m) => {
|
|
131
|
+
const content = m.content
|
|
132
|
+
if (Array.isArray(content)) {
|
|
133
|
+
return "[" + m.role + "]: " + content.map((b) => {
|
|
134
|
+
if (b.type === "text") return b.text || ""
|
|
135
|
+
if (b.type === "tool_use") return "[tool_use:" + b.name + "(" + JSON.stringify(b.input || {}).slice(0, 120) + ")]"
|
|
136
|
+
if (b.type === "tool_result") return "[tool_result:" + (b.is_error ? "ERROR " : "") + (b.content || "") + "]"
|
|
137
|
+
return ""
|
|
138
|
+
}).filter(Boolean).join("\n")
|
|
139
|
+
}
|
|
140
|
+
return "[" + m.role + "]: " + content
|
|
141
|
+
}).join("\n\n")
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
"<prior-context-state>",
|
|
145
|
+
previousSummary || "No prior compacted context.",
|
|
146
|
+
"</prior-context-state>",
|
|
147
|
+
"",
|
|
148
|
+
"<evidence-ledger>",
|
|
149
|
+
evidence.length ? evidence.join("\n") : "No extracted evidence ledger.",
|
|
150
|
+
"</evidence-ledger>",
|
|
151
|
+
"",
|
|
152
|
+
"<conversation-delta>",
|
|
153
|
+
transcript,
|
|
154
|
+
"</conversation-delta>"
|
|
155
|
+
].join("\n")
|
|
156
|
+
}
|
|
45
157
|
|
|
46
158
|
// Estimate tokens from a string, accounting for CJK characters (~1.5 chars/token vs ~4 for Latin)
|
|
47
159
|
export function estimateStringTokens(str) {
|
|
@@ -193,8 +305,12 @@ export async function compactSession({
|
|
|
193
305
|
baseUrl = null,
|
|
194
306
|
apiKeyEnv = null
|
|
195
307
|
}) {
|
|
196
|
-
const history = await getConversationHistory(sessionId, 9999)
|
|
308
|
+
const history = await getConversationHistory(sessionId, 9999, { includeMetadata: true })
|
|
197
309
|
if (history.length <= keepRecent + 2) return { compacted: false, reason: "too few messages" }
|
|
310
|
+
const previousSummary = isCompactionSummaryMessage(history[0])
|
|
311
|
+
? extractCompactionSummary(history[0].content)
|
|
312
|
+
: ""
|
|
313
|
+
const workingHistory = previousSummary ? history.slice(1) : history
|
|
198
314
|
|
|
199
315
|
// Turn-based split: keep last keepRecentTurns complete turns
|
|
200
316
|
// A "turn" = one user interaction cycle (user msg + model response + all tool calls)
|
|
@@ -202,7 +318,7 @@ export async function compactSession({
|
|
|
202
318
|
let splitIdx
|
|
203
319
|
const turnIds = []
|
|
204
320
|
const seenTurns = new Set()
|
|
205
|
-
for (const msg of
|
|
321
|
+
for (const msg of workingHistory) {
|
|
206
322
|
if (msg.turnId && !seenTurns.has(msg.turnId)) {
|
|
207
323
|
seenTurns.add(msg.turnId)
|
|
208
324
|
turnIds.push(msg.turnId)
|
|
@@ -210,29 +326,19 @@ export async function compactSession({
|
|
|
210
326
|
}
|
|
211
327
|
if (turnIds.length > keepRecentTurns) {
|
|
212
328
|
const keepFromTurnId = turnIds[turnIds.length - keepRecentTurns]
|
|
213
|
-
splitIdx =
|
|
214
|
-
if (splitIdx < 0) splitIdx =
|
|
329
|
+
splitIdx = workingHistory.findIndex(msg => msg.turnId === keepFromTurnId)
|
|
330
|
+
if (splitIdx < 0) splitIdx = workingHistory.length - keepRecent
|
|
215
331
|
} else {
|
|
216
332
|
// Fallback: not enough turns, use message count
|
|
217
|
-
splitIdx =
|
|
333
|
+
splitIdx = workingHistory.length - keepRecent
|
|
218
334
|
}
|
|
219
|
-
const toSummarize =
|
|
220
|
-
const kept =
|
|
335
|
+
const toSummarize = workingHistory.slice(0, splitIdx)
|
|
336
|
+
const kept = workingHistory.slice(splitIdx)
|
|
221
337
|
|
|
222
|
-
// Layer 1: prune large tool outputs before sending to LLM
|
|
338
|
+
// Layer 1: extract exact evidence, then prune large tool outputs before sending to LLM
|
|
339
|
+
const evidence = collectEvidenceLedger(toSummarize)
|
|
223
340
|
const pruned = pruneForSummary(toSummarize)
|
|
224
|
-
const summaryPrompt =
|
|
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")
|
|
341
|
+
const summaryPrompt = buildCompactionPrompt({ previousSummary, messages: pruned, evidence })
|
|
236
342
|
|
|
237
343
|
const hookPayload = await HookBus.sessionCompacting({
|
|
238
344
|
sessionId,
|
|
@@ -266,7 +372,7 @@ export async function compactSession({
|
|
|
266
372
|
// Replace all messages with: [summary] + [kept recent messages]
|
|
267
373
|
const summaryMessage = {
|
|
268
374
|
role: "user",
|
|
269
|
-
content: `<compaction-summary>\n${summaryText}\n</compaction-summary>`
|
|
375
|
+
content: `<compaction-summary version="2">\n${summaryText}\n</compaction-summary>`
|
|
270
376
|
}
|
|
271
377
|
await replaceMessages(sessionId, [summaryMessage, ...kept])
|
|
272
378
|
|
|
@@ -285,8 +391,10 @@ export async function compactSession({
|
|
|
285
391
|
compactedAt: Date.now(),
|
|
286
392
|
summarizeCount: toSummarize.length,
|
|
287
393
|
keepCount: kept.length,
|
|
288
|
-
summaryVersion:
|
|
289
|
-
summaryLength: summaryText.length
|
|
394
|
+
summaryVersion: 2,
|
|
395
|
+
summaryLength: summaryText.length,
|
|
396
|
+
previousSummaryLength: previousSummary.length,
|
|
397
|
+
evidenceCount: evidence.length
|
|
290
398
|
})
|
|
291
399
|
|
|
292
400
|
return {
|
package/src/session/store.mjs
CHANGED
|
@@ -323,7 +323,7 @@ export async function listSessions({ cwd = null, limit = 100, includeChildren =
|
|
|
323
323
|
})
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
export async function getConversationHistory(sessionId, limit = 30) {
|
|
326
|
+
export async function getConversationHistory(sessionId, limit = 30, options = {}) {
|
|
327
327
|
return withLock(async () => {
|
|
328
328
|
await ensureLoadedUnsafe()
|
|
329
329
|
const data = await loadSessionDataUnsafe(sessionId)
|
|
@@ -339,10 +339,19 @@ export async function getConversationHistory(sessionId, limit = 30) {
|
|
|
339
339
|
const sliced = firstIsCompaction
|
|
340
340
|
? [msgs[0], ...msgs.slice(1).slice(-limit)]
|
|
341
341
|
: msgs.slice(-limit)
|
|
342
|
-
return sliced.map((msg) =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
342
|
+
return sliced.map((msg) => {
|
|
343
|
+
const base = {
|
|
344
|
+
role: msg.role,
|
|
345
|
+
content: msg.content
|
|
346
|
+
}
|
|
347
|
+
if (!options.includeMetadata) return base
|
|
348
|
+
return {
|
|
349
|
+
...base,
|
|
350
|
+
turnId: msg.turnId,
|
|
351
|
+
step: msg.step,
|
|
352
|
+
synthetic: msg.synthetic
|
|
353
|
+
}
|
|
354
|
+
})
|
|
346
355
|
})
|
|
347
356
|
}
|
|
348
357
|
|
package/src/version.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "@kkelly-offical/kkcode"
|
|
2
|
-
export const PACKAGE_VERSION = "0.2.3-preview.
|
|
3
|
-
export const RELEASE_LABEL = "0.2.3 Preview
|
|
2
|
+
export const PACKAGE_VERSION = "0.2.3-preview.2"
|
|
3
|
+
export const RELEASE_LABEL = "0.2.3 Preview V2"
|