smartcompact 0.1.5 → 0.1.6
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/adapters/pi/recognize.ts +1 -0
- package/package.json +1 -1
- package/plugin/src/core/decide.ts +4 -2
- package/plugin/src/core/report.ts +5 -1
- package/plugin/src/core/signals.ts +9 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
Every request re-reads the whole context, so a long context makes every step expensive. Native auto-compaction waits until the context is nearly full (close to 1M tokens in Claude Code, up to 800k in Codex if configured), and it ignores timing, so it often lands in the middle of a task. SmartCompact works with Claude Code (CLI and desktop app), [Pi](https://github.com/earendil-works/pi) and the Codex CLI:
|
|
15
15
|
|
|
16
|
-
- **Compact at a good moment.** Once the context passes the **allow line** (200k tokens by default), it compacts when a turn ends, if a piece of work has been finished since the last compaction (a commit, tests passing, a task completed).
|
|
16
|
+
- **Compact at a good moment.** Once the context passes the **allow line** (200k tokens by default), it compacts when a turn ends, if a piece of work has been finished since the last compaction (a commit, tests passing, a task completed) and no file has been changed since.
|
|
17
17
|
- **Never in the middle of work.** It only decides when a turn ends, never mid-turn, and it waits while tests or the build are failing or errors keep repeating.
|
|
18
18
|
- **No limit of its own.** If no piece of work gets finished, the agent is usually deep in one large task, which is when a large context pays off. SmartCompact leaves it alone, and the platform's native compaction applies as it would without the plugin.
|
|
19
19
|
- **A progress ledger bridges the gap.** Files changed, the latest commit and test result, and decisions the agent recorded are handed back to the model after compaction.
|
|
@@ -80,7 +80,7 @@ Codex hooks cannot postpone a compaction in the middle of a turn, so SmartCompac
|
|
|
80
80
|
|
|
81
81
|
## FAQ
|
|
82
82
|
|
|
83
|
-
**How does it know a piece of work is finished?** From the commands the agent runs. Any finished piece of work since the last compaction counts, not only one in the current turn: the end of a turn is already a pause, so waiting for the next commit would mostly just cost more. A commit counts if `git commit` actually ran and the tool call did not fail. A test command piped into something else (`| tail`) without `pipefail` does not count, since its exit code cannot be trusted.
|
|
83
|
+
**How does it know a piece of work is finished?** From the commands the agent runs. Any finished piece of work since the last compaction counts, not only one in the current turn: the end of a turn is already a pause, so waiting for the next commit would mostly just cost more. Once files are changed again after it, the next piece of work has started, and SmartCompact waits for that one to finish. A commit counts if `git commit` actually ran and the tool call did not fail. A test command piped into something else (`| tail`) without `pipefail` does not count, since its exit code cannot be trusted.
|
|
84
84
|
|
|
85
85
|
**Will the model forget things after compaction?** Sometimes, for details mentioned in passing. The ledger only records files, commits, tests and decisions the model chose to record. Details the user mentions in passing are kept by asking the compaction summary to keep every specific fact the user stated. On Pi this clearly reduced losses, but it is not a guarantee.
|
|
86
86
|
|
package/README.zh-CN.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
agent 每发一次请求,都要把整段上下文重新读一遍,上下文越长,每一步越贵。原生的自动压缩要等到快满了才触发(Claude Code 接近 100 万,Codex 可以配到 80 万),而且不看时机,常常发生在一件事做到一半的时候。SmartCompact 支持 Claude Code(终端和桌面 App)、[Pi](https://github.com/earendil-works/pi) 和 Codex 命令行:
|
|
15
15
|
|
|
16
|
-
- **挑对时机压缩。** 上下文超过**允许压缩线**(默认 20
|
|
16
|
+
- **挑对时机压缩。** 上下文超过**允许压缩线**(默认 20 万)以后,只要上次压缩以来做完过一件事(一次提交、测试通过、任务完成),之后又没开始改文件,就在这一轮结束时压缩。
|
|
17
17
|
- **不在做到一半时压缩。** 只在一轮结束时判断,从不在一轮中间主动压缩;测试失败、构建失败或连续报错时先等着。
|
|
18
18
|
- **不设自己的上限。** 一直没做完一件事,多半是在干一件大活,这时正需要大上下文。插件不去打断,压缩按平台原生规则来,和没装插件时一样。
|
|
19
19
|
- **用进度笔记接上。** 改过的文件、最近的提交和测试结果、记下的决定,压缩后补回给模型。
|
|
@@ -80,7 +80,7 @@ Codex 的 hook 不能推迟一轮中间的压缩,所以插件在一轮结束
|
|
|
80
80
|
|
|
81
81
|
## 常见问题
|
|
82
82
|
|
|
83
|
-
**怎么判断"做完一件事"?** 靠识别 agent
|
|
83
|
+
**怎么判断"做完一件事"?** 靠识别 agent 执行的命令。看的是上次压缩以来有没有做完过一件事,不要求正好在这一轮:一轮结束本身就是停顿点,非要等下一次提交,多半只是多花钱。但做完之后又开始改文件,就算在做下一件事,要等它做完。提交只要真的执行了 `git commit` 且工具调用没报错就算;测试命令接了管道(`| tail`)又没开 `pipefail` 时退出码不可信,不算测试结果。
|
|
84
84
|
|
|
85
85
|
**压缩后模型会不会忘事?** 随口提到的细节有时会丢。进度笔记只记文件、提交、测试和模型主动记下的决定。用户在对话里随口交代的细节,靠压缩时要求摘要原样保留用户说过的具体信息。Pi 上这样做明显减少了丢失,但不能保证不丢。
|
|
86
86
|
|
package/adapters/pi/recognize.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function recognizePi(outcome: PiToolOutcome, ledger: Ledger): { signals:
|
|
|
20
20
|
const { path, command } = outcome.input
|
|
21
21
|
if (EDIT_TOOLS.has(outcome.toolName) && !outcome.isError && typeof path === 'string' && path) {
|
|
22
22
|
next = recordFileChanged(next, path)
|
|
23
|
+
signals.push({ kind: 'file-changed' })
|
|
23
24
|
}
|
|
24
25
|
if (outcome.toolName === 'bash' && typeof command === 'string' && command) {
|
|
25
26
|
const classified = classifyCommand(command, outcome.isError, outcome.text, next)
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@ export type DecisionInput = {
|
|
|
17
17
|
|
|
18
18
|
export type TurnEndDecision =
|
|
19
19
|
| { action: 'none'; reason: 'paused' | 'no-reading' | 'below-line' }
|
|
20
|
-
| { action: 'wait'; reason: HardSignal | 'insufficient-benefit' | 'waiting-for-milestone' }
|
|
20
|
+
| { action: 'wait'; reason: HardSignal | 'insufficient-benefit' | 'changed-since-milestone' | 'waiting-for-milestone' }
|
|
21
21
|
| { action: 'compact'; reason: MilestoneSource }
|
|
22
22
|
|
|
23
23
|
export type MidTurnDecision =
|
|
@@ -36,7 +36,9 @@ export function decideAtTurnEnd(input: DecisionInput, params: Params): TurnEndDe
|
|
|
36
36
|
const hard = hardSignal(work, params)
|
|
37
37
|
if (hard) return { action: 'wait', reason: hard }
|
|
38
38
|
if (!freesEnough(tokens, input, params)) return { action: 'wait', reason: 'insufficient-benefit' }
|
|
39
|
-
if (work.milestone) return { action: 'compact', reason: work.milestone }
|
|
39
|
+
if (work.milestone && !work.changedSinceMilestone) return { action: 'compact', reason: work.milestone }
|
|
40
|
+
// 做完一件事之后又改了文件,已经在做下一件事,等它做完。
|
|
41
|
+
if (work.milestone) return { action: 'wait', reason: 'changed-since-milestone' }
|
|
40
42
|
// 不设兜底线:一直等不到做完一件事,多半是在做一件大活,正需要大上下文;交给原生在上限处压缩。
|
|
41
43
|
return { action: 'wait', reason: 'waiting-for-milestone' }
|
|
42
44
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// /smartcompact report 的内容:当前状态、决策次数、信号次数、成本估算。只排版,不读写。
|
|
2
2
|
|
|
3
3
|
import type { Lines } from './lines.ts'
|
|
4
|
+
import type { WorkState } from './signals.ts'
|
|
4
5
|
import type { DecisionLogEntry, SessionState } from './state.ts'
|
|
5
6
|
import { savingsRatio } from './usage.ts'
|
|
6
7
|
|
|
@@ -18,6 +19,9 @@ export function countDecisions(entries: readonly DecisionLogEntry[]): DecisionCo
|
|
|
18
19
|
|
|
19
20
|
const k = (tokens: number | undefined): string => (tokens === undefined ? '—' : `${Math.round(tokens / 1000)}k`)
|
|
20
21
|
|
|
22
|
+
const milestoneStatus = (work: WorkState): string =>
|
|
23
|
+
!work.milestone ? '等待阶段节点' : work.changedSinceMilestone ? `上一件事已做完(${work.milestone}),之后又改了文件,等这件事做完` : `已到阶段节点(${work.milestone})`
|
|
24
|
+
|
|
21
25
|
export function renderReport(input: {
|
|
22
26
|
state: SessionState
|
|
23
27
|
lines: Lines
|
|
@@ -33,7 +37,7 @@ export function renderReport(input: {
|
|
|
33
37
|
`- 本会话压缩 ${state.compactions} 次;最近一次压缩后约 ${k(state.measuredAfterTokens)}`,
|
|
34
38
|
`- 本会话决策:插件压缩 ${(counts['compacted:plugin'] ?? 0) + (counts['compacted:plugin-queued'] ?? 0)},自动压缩放行 ${counts['compacted:auto'] ?? 0},手动压缩 ${counts['compacted:manual'] ?? 0},轮中否决 ${counts['mid-turn:veto'] ?? 0},轮末等待 ${counts['turn-end:wait'] ?? 0}`,
|
|
35
39
|
`- 信号:提交 ${seen.commit},测试通过 ${seen['tests-passed']},任务完成 ${seen['task-completed']},checkpoint 完成 ${seen.checkpoint}`,
|
|
36
|
-
`- 当前状态:${state.work
|
|
40
|
+
`- 当前状态:${milestoneStatus(state.work)}${state.work.testsFailing ? ',测试失败中' : ''}${state.work.buildFailing ? ',构建失败中' : ''}`,
|
|
37
41
|
ratio === undefined
|
|
38
42
|
? '- 成本估算:还没有读数'
|
|
39
43
|
: `- 成本估算:上下文成本约节省 ${Math.round(ratio * 100)}%(${state.usage.samples} 次读数,已扣除压缩本身的开销;对比的是没有插件、一直涨到原生压缩点的情况)${
|
|
@@ -9,6 +9,7 @@ export type Signal =
|
|
|
9
9
|
| { kind: 'task-completed' }
|
|
10
10
|
| { kind: 'checkpoint'; milestone: boolean }
|
|
11
11
|
| { kind: 'tool-result'; isError: boolean }
|
|
12
|
+
| { kind: 'file-changed' }
|
|
12
13
|
|
|
13
14
|
export type MilestoneSource = 'commit' | 'tests-passed' | 'task-completed' | 'checkpoint'
|
|
14
15
|
|
|
@@ -19,6 +20,8 @@ export type WorkState = {
|
|
|
19
20
|
milestone: MilestoneSource | null
|
|
20
21
|
/** 最近的阶段节点出现时的上下文用量,用来看它到压缩时隔了多远 */
|
|
21
22
|
milestoneTokens?: number
|
|
23
|
+
/** 最近的阶段节点之后又改过文件:已经开始下一件事,这个阶段节点不再算数 */
|
|
24
|
+
changedSinceMilestone?: boolean
|
|
22
25
|
testsFailing: boolean
|
|
23
26
|
buildFailing: boolean
|
|
24
27
|
errorStreak: number
|
|
@@ -26,6 +29,7 @@ export type WorkState = {
|
|
|
26
29
|
|
|
27
30
|
export const INITIAL_WORK_STATE: WorkState = Object.freeze({
|
|
28
31
|
milestone: null,
|
|
32
|
+
changedSinceMilestone: false,
|
|
29
33
|
testsFailing: false,
|
|
30
34
|
buildFailing: false,
|
|
31
35
|
errorStreak: 0,
|
|
@@ -50,7 +54,7 @@ export function milestoneOf(signal: Signal): MilestoneSource | null {
|
|
|
50
54
|
export function applySignal(state: WorkState, signal: Signal): WorkState {
|
|
51
55
|
const milestone = milestoneOf(signal)
|
|
52
56
|
// 失败之后又做完了一件事(比如提交、checkpoint),那段失败就算过去了,不再挡着压缩。
|
|
53
|
-
const next = milestone ? { ...state, milestone, testsFailing: false, buildFailing: false } : state
|
|
57
|
+
const next = milestone ? { ...state, milestone, changedSinceMilestone: false, testsFailing: false, buildFailing: false } : state
|
|
54
58
|
switch (signal.kind) {
|
|
55
59
|
case 'tests':
|
|
56
60
|
return { ...next, testsFailing: !signal.passed }
|
|
@@ -58,6 +62,9 @@ export function applySignal(state: WorkState, signal: Signal): WorkState {
|
|
|
58
62
|
return { ...next, buildFailing: !signal.passed }
|
|
59
63
|
case 'tool-result':
|
|
60
64
|
return { ...next, errorStreak: signal.isError ? state.errorStreak + 1 : 0 }
|
|
65
|
+
// 做完一件事之后又改文件,说明已经开始下一件事,要等它做完。
|
|
66
|
+
case 'file-changed':
|
|
67
|
+
return next.milestone ? { ...next, changedSinceMilestone: true } : next
|
|
61
68
|
default:
|
|
62
69
|
return next
|
|
63
70
|
}
|
|
@@ -65,7 +72,7 @@ export function applySignal(state: WorkState, signal: Signal): WorkState {
|
|
|
65
72
|
|
|
66
73
|
/** 压缩后清空阶段节点;失败这些仍然成立的事实保留。 */
|
|
67
74
|
export function afterCompaction(state: WorkState): WorkState {
|
|
68
|
-
return { ...state, milestone: null, milestoneTokens: undefined }
|
|
75
|
+
return { ...state, milestone: null, milestoneTokens: undefined, changedSinceMilestone: false }
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
/** 表明工作做到一半的硬信号,没有则返回 null。 */
|