smartcompact 0.1.4 → 0.1.5
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/adapters/pi/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { Lines } from '../../plugin/src/core/lines.ts'
|
|
|
12
12
|
import { resolveParams } from '../../plugin/src/core/params.ts'
|
|
13
13
|
import { countDecisions, renderReport } from '../../plugin/src/core/report.ts'
|
|
14
14
|
import {
|
|
15
|
+
cycleReads,
|
|
15
16
|
type DecisionLogEntry,
|
|
16
17
|
expectedAfterTokens,
|
|
17
18
|
growthSinceMilestone,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
parseState,
|
|
20
21
|
type SessionState,
|
|
21
22
|
withCompaction,
|
|
23
|
+
withRead,
|
|
22
24
|
withSample,
|
|
23
25
|
withSignals,
|
|
24
26
|
} from '../../plugin/src/core/state.ts'
|
|
@@ -163,8 +165,9 @@ export default function smartCompact(pi: ExtensionAPI) {
|
|
|
163
165
|
if (event.toolName === CHECKPOINT_TOOL) return
|
|
164
166
|
sample(ctx, lines(ctx))
|
|
165
167
|
const text = event.content.flatMap(block => (block.type === 'text' ? [block.text] : [])).join('\n')
|
|
166
|
-
const { signals, ledger } = recognizePi({ toolName: event.toolName, input: event.input, isError: event.isError, text }, state.ledger)
|
|
168
|
+
const { signals, ledger, read } = recognizePi({ toolName: event.toolName, input: event.input, isError: event.isError, text }, state.ledger)
|
|
167
169
|
state = withSignals({ ...state, ledger }, signals)
|
|
170
|
+
if (read) state = withRead(state, read)
|
|
168
171
|
})
|
|
169
172
|
|
|
170
173
|
pi.on('before_agent_start', async (_event, ctx) => {
|
|
@@ -216,10 +219,11 @@ export default function smartCompact(pi: ExtensionAPI) {
|
|
|
216
219
|
const fromPlugin = event.reason === 'manual' && ours
|
|
217
220
|
ours = false
|
|
218
221
|
const summaryTokens = Math.round(event.compactionEntry.summary.length / 4)
|
|
222
|
+
const reads = cycleReads(state)
|
|
219
223
|
// 原生按阈值的压缩、溢出恢复和用户的 /compact,没有插件时也会发生;只有插件发起的压缩计入节省。
|
|
220
224
|
state = withCompaction(state, event.compactionEntry.tokensBefore, summaryTokens, fromPlugin)
|
|
221
225
|
persist()
|
|
222
|
-
log({ event: 'compacted', trigger: fromPlugin ? 'plugin' : event.reason === 'threshold' ? 'auto' : event.reason, tokensBefore: event.compactionEntry.tokensBefore })
|
|
226
|
+
log({ event: 'compacted', trigger: fromPlugin ? 'plugin' : event.reason === 'threshold' ? 'auto' : event.reason, tokensBefore: event.compactionEntry.tokensBefore, ...reads })
|
|
223
227
|
pi.sendMessage(
|
|
224
228
|
{ customType: LEDGER_MESSAGE, content: renderLedger(state.ledger, params(ctx).ledgerMaxChars), display: false },
|
|
225
229
|
{ deliverAs: ctx.isIdle() ? 'nextTurn' : 'steer' },
|
package/adapters/pi/recognize.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type PiToolOutcome = {
|
|
|
14
14
|
|
|
15
15
|
const EDIT_TOOLS = new Set(['edit', 'write'])
|
|
16
16
|
|
|
17
|
-
export function recognizePi(outcome: PiToolOutcome, ledger: Ledger): { signals: Signal[]; ledger: Ledger } {
|
|
17
|
+
export function recognizePi(outcome: PiToolOutcome, ledger: Ledger): { signals: Signal[]; ledger: Ledger; read?: string } {
|
|
18
18
|
const signals: Signal[] = [{ kind: 'tool-result', isError: outcome.isError }]
|
|
19
19
|
let next = ledger
|
|
20
20
|
const { path, command } = outcome.input
|
|
@@ -26,5 +26,6 @@ export function recognizePi(outcome: PiToolOutcome, ledger: Ledger): { signals:
|
|
|
26
26
|
signals.push(...classified.signals)
|
|
27
27
|
next = classified.ledger
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
const read = outcome.toolName === 'read' && !outcome.isError && typeof path === 'string' && path ? path : undefined
|
|
30
|
+
return { signals, ledger: next, read }
|
|
30
31
|
}
|
package/package.json
CHANGED
|
@@ -128,9 +128,20 @@ export function applyCheckpoint(ledger: Ledger, input: CheckpointInput): Ledger
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/**
|
|
131
|
+
/** 排版成补回上下文的文本,超长时先丢最旧的决定、再丢最早改过的文件,仍超长才截断。 */
|
|
132
132
|
export function renderLedger(ledger: Ledger, maxChars: number): string {
|
|
133
|
-
|
|
133
|
+
let decisions = ledger.notes.decisions
|
|
134
|
+
let files = ledger.facts.filesChanged
|
|
135
|
+
let rendered = layoutLedger(ledger, decisions, files)
|
|
136
|
+
while (rendered.length > maxChars && (decisions.length || files.length)) {
|
|
137
|
+
if (decisions.length) decisions = decisions.slice(1)
|
|
138
|
+
else files = files.slice(1)
|
|
139
|
+
rendered = layoutLedger(ledger, decisions, files)
|
|
140
|
+
}
|
|
141
|
+
return rendered.length <= maxChars ? rendered : `${rendered.slice(0, maxChars - 20)}\n…(truncated)`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function layoutLedger({ facts, notes }: Ledger, decisions: readonly string[], files: readonly string[]): string {
|
|
134
145
|
const lines = [
|
|
135
146
|
LEDGER_MARKER,
|
|
136
147
|
'Generated by the SmartCompact plugin to keep continuity across compaction. It is not a message from the user.',
|
|
@@ -139,19 +150,19 @@ export function renderLedger(ledger: Ledger, maxChars: number): string {
|
|
|
139
150
|
const rows = body.filter((row): row is string => Boolean(row))
|
|
140
151
|
if (rows.length) lines.push('', `## ${title}`, ...rows)
|
|
141
152
|
}
|
|
153
|
+
const omitted = (dropped: number, what: string) => (dropped ? `- …(${dropped} earlier ${what} omitted)` : undefined)
|
|
142
154
|
section('Goal', [notes.goal])
|
|
143
155
|
section('Current state', [notes.current])
|
|
144
156
|
section('Next', [notes.next])
|
|
145
|
-
section('Decisions', notes.decisions.map(decision => `- ${decision}`))
|
|
146
|
-
section('Tasks in progress', facts.tasksInProgress.map(task => `- ${task}`))
|
|
147
|
-
section('Files changed', facts.filesChanged.map(file => `- ${file}`))
|
|
148
157
|
section('Latest results', [
|
|
149
158
|
facts.lastTest && `- tests ${facts.lastTest.passed ? 'passed' : 'FAILED'}: ${facts.lastTest.command}`,
|
|
150
159
|
facts.lastBuild && `- build ${facts.lastBuild.passed ? 'passed' : 'FAILED'}: ${facts.lastBuild.command}`,
|
|
151
160
|
facts.lastCommit && `- last commit: ${facts.lastCommit.summary}`,
|
|
152
161
|
])
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
section('Tasks in progress', facts.tasksInProgress.map(task => `- ${task}`))
|
|
163
|
+
section('Decisions', [omitted(notes.decisions.length - decisions.length, 'decisions'), ...decisions.map(decision => `- ${decision}`)])
|
|
164
|
+
section('Files changed', [omitted(facts.filesChanged.length - files.length, 'files'), ...files.map(file => `- ${file}`)])
|
|
165
|
+
return lines.join('\n')
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
export function isLedgerText(value: string): boolean {
|
package/plugin/src/core/state.ts
CHANGED
|
@@ -20,8 +20,24 @@ export type SessionState = {
|
|
|
20
20
|
/** 各种信号出现过的次数,用于确认识别是否生效 */
|
|
21
21
|
seen: SignalsSeen
|
|
22
22
|
usage: UsageTally
|
|
23
|
+
/** 读文件的统计;平台认不出读文件时没有这一项 */
|
|
24
|
+
reads?: ReadStats
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** 用来看压缩后要花多少次读取重新熟悉代码。 */
|
|
28
|
+
export type ReadStats = {
|
|
29
|
+
/** 上次压缩之前读过或改过的文件 */
|
|
30
|
+
known: string[]
|
|
31
|
+
/** 上次压缩以来读过的文件 */
|
|
32
|
+
current: string[]
|
|
33
|
+
/** 上次压缩以来读文件的次数 */
|
|
34
|
+
reads: number
|
|
35
|
+
/** 其中读的是压缩前已经看过的文件 */
|
|
36
|
+
rereads: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MAX_KNOWN_FILES = 300
|
|
40
|
+
|
|
25
41
|
export const INITIAL_SESSION_STATE: SessionState = Object.freeze({
|
|
26
42
|
work: INITIAL_WORK_STATE,
|
|
27
43
|
ledger: EMPTY_LEDGER,
|
|
@@ -73,8 +89,15 @@ export function withSample(state: SessionState, tokens: number, ceiling: number,
|
|
|
73
89
|
* @param attributed 是否算作插件带来的压缩;用户手动 /compact 没有插件时也会发生,不计入节省
|
|
74
90
|
*/
|
|
75
91
|
export function withCompaction(state: SessionState, before: number, summary: number, attributed = true): SessionState {
|
|
92
|
+
const reads = state.reads && {
|
|
93
|
+
known: [...new Set([...state.reads.known, ...state.reads.current, ...state.ledger.facts.filesChanged])].slice(-MAX_KNOWN_FILES),
|
|
94
|
+
current: [],
|
|
95
|
+
reads: 0,
|
|
96
|
+
rereads: 0,
|
|
97
|
+
}
|
|
76
98
|
return {
|
|
77
99
|
...state,
|
|
100
|
+
...(reads && { reads }),
|
|
78
101
|
work: afterCompaction(state.work),
|
|
79
102
|
compactions: state.compactions + 1,
|
|
80
103
|
notesUpdated: false,
|
|
@@ -83,6 +106,25 @@ export function withCompaction(state: SessionState, before: number, summary: num
|
|
|
83
106
|
}
|
|
84
107
|
}
|
|
85
108
|
|
|
109
|
+
/** 记一次读文件:读的是上次压缩前看过的文件就算重读。 */
|
|
110
|
+
export function withRead(state: SessionState, path: string): SessionState {
|
|
111
|
+
const stats = state.reads ?? { known: [], current: [], reads: 0, rereads: 0 }
|
|
112
|
+
return {
|
|
113
|
+
...state,
|
|
114
|
+
reads: {
|
|
115
|
+
known: stats.known,
|
|
116
|
+
current: [...stats.current.filter(file => file !== path), path].slice(-MAX_KNOWN_FILES),
|
|
117
|
+
reads: stats.reads + 1,
|
|
118
|
+
rereads: stats.rereads + (stats.known.includes(path) ? 1 : 0),
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 压缩时记进日志的本周期读文件次数;平台认不出读文件时为空。 */
|
|
124
|
+
export function cycleReads(state: SessionState): Pick<DecisionLogEntry, 'reads' | 'rereads'> {
|
|
125
|
+
return state.reads ? { reads: state.reads.reads, rereads: state.reads.rereads } : {}
|
|
126
|
+
}
|
|
127
|
+
|
|
86
128
|
export type DecisionLogEntry = {
|
|
87
129
|
at: string
|
|
88
130
|
/** 会话 ID,报告据此只统计当前会话 */
|
|
@@ -95,6 +137,9 @@ export type DecisionLogEntry = {
|
|
|
95
137
|
tokensAfter?: number
|
|
96
138
|
/** 决定压缩时,距离最近的阶段节点上下文涨了多少 */
|
|
97
139
|
sinceMilestoneTokens?: number
|
|
140
|
+
/** 压缩时:上次压缩以来读文件的次数,以及其中重读压缩前看过的文件的次数 */
|
|
141
|
+
reads?: number
|
|
142
|
+
rereads?: number
|
|
98
143
|
trigger?: string
|
|
99
144
|
detail?: string
|
|
100
145
|
}
|