Skip to content

Commit 4eccb76

Browse files
committed
redesign -context
1 parent a669183 commit 4eccb76

29 files changed

Lines changed: 442 additions & 985 deletions

packages/codingcode/src/agent/agent.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -271,14 +271,9 @@ export async function* runReActLoop(
271271
const maxStopContinuations = opts.maxStopContinuations ?? deps.maxStopContinuations;
272272

273273
for (let attempt = 0; attempt <= maxOverflowRetries; attempt++) {
274-
const { messages, newBudgets } = Effect.runSync(
274+
const { messages } = Effect.runSync(
275275
ctx.build(state.sessionId, state.projectPath, llm.modelInfo.maxTokens)
276276
);
277-
if (newBudgets.length > 0) {
278-
for (const ev of newBudgets) {
279-
appendFileSync(state.transcriptPath, JSON.stringify(ev) + '\n', 'utf8');
280-
}
281-
}
282277
let lastResult: Result<string, AgentError> | null = null;
283278
let overflow = false;
284279

@@ -354,11 +349,6 @@ export async function* runReActLoop(
354349
const rebuilt = Effect.runSync(
355350
ctx.build(state.sessionId, state.projectPath, llm.modelInfo.maxTokens)
356351
);
357-
if (rebuilt.newBudgets.length > 0) {
358-
for (const ev of rebuilt.newBudgets) {
359-
appendFileSync(state.transcriptPath, JSON.stringify(ev) + '\n', 'utf8');
360-
}
361-
}
362352
messages.length = 0;
363353
messages.push(...rebuilt.messages);
364354
state.usage = undefined;
@@ -389,15 +379,14 @@ export async function* runReActLoop(
389379
const llmResult = await respPromise;
390380
if (!llmResult.ok) {
391381
if (llmResult.error.code === 'CONTEXT_OVERFLOW' && attempt < maxOverflowRetries) {
392-
const aggressiveConfig = { ...config, keepRecentTurns: config.reactiveCompactKeepTurns };
393382
const compressResult = await Effect.runPromise(
394383
ctx.compress(
395384
state.sessionId,
396385
state.projectPath,
397386
null,
398387
undefined,
399388
llm.modelInfo.maxTokens,
400-
aggressiveConfig
389+
config
401390
)
402391
);
403392
yield {
@@ -540,7 +529,7 @@ export async function* runReActLoop(
540529
: (r.output ?? '');
541530
messages.push({ role: 'tool', content, tool_call_id: r.id, tool_name: r.name });
542531
}
543-
if (!todoPrinted && (r.name === 'todo_write' || r.name === 'todo_read')) {
532+
if (!todoPrinted && r.name === 'todo_write') {
544533
yield { _tag: 'TodoUpdate', items: sharedTodoStore.read(sessionId) };
545534
todoPrinted = true;
546535
}

packages/codingcode/src/approval/presets.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ export const READONLY_TOOL_NAMES: string[] = [
9191
'fetch_url',
9292
'web_search',
9393
'tool_search',
94-
'todo_read',
9594
'todo_write',
9695
];
9796

packages/codingcode/src/context/compressor/index.ts

Lines changed: 128 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { randomUUID } from 'crypto';
2-
import { findSessionIndex, readHistory, resolveSessionDir } from '../../session/io.js';
3-
import { estimateTokens, estimateMessageTokens } from '../utils/tokens.js';
2+
import { resolveSessionDir } from '../../session/io.js';
3+
import {
4+
estimateTokens,
5+
estimateMessageTokens,
6+
estimateTokensForContent,
7+
} from '../utils/tokens.js';
8+
import { applyVisibilityEvents } from '../../session/messages.js';
49
import { resolveCompactionLLM } from './llm-resolver.js';
510
import { COMPACTION_SYSTEM_PROMPT } from './prompt.js';
611
import type { ContextConfig } from '../config.js';
@@ -17,16 +22,6 @@ export interface CompressResult {
1722
promptEstimate: number;
1823
}
1924

20-
interface CompressContext {
21-
sessionId: string;
22-
encodedProjectPath: string;
23-
config: ContextConfig;
24-
llm: LLMClient | null;
25-
currentTurnId: number;
26-
events: SessionEvent[];
27-
hiddenUuids: Set<string>;
28-
}
29-
3025
const compactFailureTracker = new Map<string, { count: number; lastAttempt: number }>();
3126
const FAILURE_TTL_MS = 24 * 60 * 60 * 1000;
3227

@@ -43,10 +38,12 @@ function getFailures(sessionId: string): number {
4338
export async function compactIfNeeded(
4439
sessionId: string,
4540
encodedProjectPath: string,
46-
messages: import('../../core/types.js').Message[],
41+
messages: Message[],
4742
modelMaxTokens: number,
4843
config: ContextConfig,
49-
llm: LLMClient | null
44+
llm: LLMClient | null,
45+
compactedEvents?: SessionEvent[],
46+
currentTurnId?: number
5047
): Promise<CompressResult> {
5148
const promptEstimate = estimateTokens(messages);
5249
const failures = getFailures(sessionId);
@@ -64,6 +61,8 @@ export async function compactIfNeeded(
6461
encodedProjectPath,
6562
config,
6663
llm,
64+
compactedEvents,
65+
currentTurnId,
6766
promptEstimate,
6867
modelMaxTokens
6968
);
@@ -82,91 +81,33 @@ export async function compactWithLLM(
8281
encodedProjectPath: string,
8382
config: ContextConfig,
8483
llm: LLMClient | null,
84+
compactedEvents?: SessionEvent[],
85+
currentTurnId?: number,
8586
usage?: number,
8687
modelMaxTokens?: number
8788
): Promise<CompressResult> {
88-
const idx = findSessionIndex(sessionId);
89-
const currentTurnId = idx?.currentTurnId ?? 0;
90-
const ctx = buildContext(sessionId, encodedProjectPath, config, llm, currentTurnId);
89+
if (!compactedEvents || currentTurnId === undefined) {
90+
const payload = assemblePayload(sessionId, encodedProjectPath, config, modelMaxTokens);
91+
compactedEvents = payload.compactedEvents;
92+
currentTurnId = payload.currentTurnId;
93+
}
9194

9295
let released = 0;
9396

9497
const threshold = modelMaxTokens ? modelMaxTokens * config.compactionThreshold : Infinity;
9598
if (usage === undefined || usage - released > threshold) {
96-
released += await tryL5Compaction(ctx);
99+
released += await tryCompaction(sessionId, config, llm, compactedEvents, currentTurnId);
97100
}
98101

99102
const payload = assemblePayload(sessionId, encodedProjectPath, config, modelMaxTokens);
100-
const promptEstimate = estimateTokens(payload.messages);
101-
102-
return { didCompress: released > 0, released, promptEstimate };
103-
}
104-
105-
// ---------- Context building ----------
106-
107-
function buildContext(
108-
sessionId: string,
109-
encodedProjectPath: string,
110-
config: ContextConfig,
111-
llm: LLMClient | null,
112-
currentTurnId: number
113-
): CompressContext {
114-
const dir = resolveSessionDir(sessionId);
115-
if (!dir) throw new Error(`Session ${sessionId} not found`);
116-
const jsonlPath = join(dir, `${sessionId}.jsonl`);
117-
const events = readHistory(jsonlPath);
118-
119-
// Compute which event uuids are already hidden by prior summary/hide events
120-
const { hidden } = buildFilteredView(events);
121-
122103
return {
123-
sessionId,
124-
encodedProjectPath,
125-
config,
126-
llm,
127-
currentTurnId,
128-
events,
129-
hiddenUuids: hidden,
104+
didCompress: released > 0,
105+
released,
106+
promptEstimate: estimateTokens(payload.messages),
130107
};
131108
}
132109

133-
function buildFilteredView(events: SessionEvent[]): { hidden: Set<string> } {
134-
const hidden = new Set<string>();
135-
const hideEffects = new Map<string, Set<string>>();
136-
137-
for (const ev of events) {
138-
switch (ev.type) {
139-
case 'hide': {
140-
let effect: Set<string>;
141-
if (ev.kind === 'message') {
142-
effect = new Set([ev.targetUuid]);
143-
} else {
144-
effect = new Set<string>();
145-
for (const prior of events) {
146-
if (prior === ev) break;
147-
if ('turnId' in prior && prior.turnId >= ev.throughTurnId && 'uuid' in prior) {
148-
effect.add(prior.uuid);
149-
}
150-
}
151-
}
152-
hideEffects.set(ev.uuid, effect);
153-
for (const u of effect) hidden.add(u);
154-
break;
155-
}
156-
case 'unhide': {
157-
const effect = hideEffects.get(ev.targetHideUuid);
158-
if (effect) for (const u of effect) hidden.delete(u);
159-
break;
160-
}
161-
case 'summary': {
162-
for (const u of ev.replaces) hidden.add(u);
163-
break;
164-
}
165-
}
166-
}
167-
168-
return { hidden };
169-
}
110+
// ---------- Summary persistence ----------
170111

171112
function appendSummaryToSession(sessionId: string, event: SummaryEvent): void {
172113
const dir = resolveSessionDir(sessionId);
@@ -177,69 +118,136 @@ function appendSummaryToSession(sessionId: string, event: SummaryEvent): void {
177118

178119
// ---------- LLM Compaction ----------
179120

180-
async function tryL5Compaction(ctx: CompressContext): Promise<number> {
181-
const { sessionId, config, currentTurnId, events, hiddenUuids } = ctx;
121+
const ESTIMATED_SUMMARY_TOKENS = 5000;
122+
const MAX_TOOL_RESULT_TOKENS = 30000;
182123

183-
const startTurn = 1;
184-
const endTurn = currentTurnId - config.keepRecentTurns;
185-
if (endTurn < startTurn) return 0;
186-
const turnsInRange = endTurn - startTurn + 1;
187-
if (turnsInRange < config.minTurnsBetweenCompactions) return 0;
124+
async function tryCompaction(
125+
sessionId: string,
126+
config: ContextConfig,
127+
llm: LLMClient | null,
128+
compactedEvents: SessionEvent[],
129+
currentTurnId: number
130+
): Promise<number> {
131+
const endTurn = currentTurnId - config.keepRecentTurns - 1;
132+
if (endTurn < 1) return 0;
188133

189-
// Collect visible messages in the range for LLM transcript
190-
const inRange = events.filter((ev) => {
134+
const { hidden } = applyVisibilityEvents(compactedEvents);
135+
136+
const inRange = compactedEvents.filter((ev) => {
191137
if (ev.type === 'session_meta') return false;
192-
if ('uuid' in ev && hiddenUuids.has((ev as any).uuid)) return false;
193-
if ('turnId' in ev && (ev as any).turnId >= startTurn && (ev as any).turnId <= endTurn)
194-
return true;
138+
if ('uuid' in ev && hidden.has((ev as any).uuid)) return false;
139+
if ('turnId' in ev && (ev as any).turnId >= 1 && (ev as any).turnId <= endTurn) return true;
195140
return false;
196141
});
197-
198142
if (inRange.length === 0) return 0;
199143

200-
const transcript: Message[] = [];
144+
const targetEvents = getIncrementalEvents(inRange);
145+
if (targetEvents.length === 0) return 0;
146+
147+
const totalTokens = targetEvents.reduce((sum, e) => sum + estimateEventTokens(e), 0);
148+
149+
let compactionLlm = await resolveCompactionLLM(config, llm);
150+
if (compactionLlm && compactionLlm.modelInfo.maxTokens < totalTokens + 25000) {
151+
compactionLlm = llm;
152+
}
153+
154+
const transcript = buildTranscript(targetEvents);
155+
const summary = await callLLMForCompaction(transcript, compactionLlm, config);
156+
if (!summary) return 0;
157+
201158
const replacedUuids: string[] = [];
202-
for (const ev of inRange) {
203-
if ('uuid' in ev) replacedUuids.push((ev as any).uuid);
159+
for (const ev of targetEvents) {
160+
if ('uuid' in (ev as any)) replacedUuids.push((ev as any).uuid);
161+
}
162+
163+
const lastTurnId = Math.max(
164+
...targetEvents.filter((e) => 'turnId' in e).map((e) => (e as any).turnId),
165+
0
166+
);
167+
168+
const event: SummaryEvent = {
169+
type: 'summary',
170+
uuid: randomUUID(),
171+
replaces: replacedUuids,
172+
summaryText: summary,
173+
lastSummarizedTurnId: lastTurnId,
174+
method: 'auto-compact',
175+
timestamp: new Date().toISOString(),
176+
};
177+
appendSummaryToSession(sessionId, event);
178+
for (const u of replacedUuids) hidden.add(u);
179+
180+
const summaryMsg: Message = { role: 'system', name: 'compacted_history', content: summary };
181+
return Math.max(0, totalTokens - estimateMessageTokens(summaryMsg));
182+
}
183+
184+
function getIncrementalEvents(inRange: SessionEvent[]): SessionEvent[] {
185+
const existingSummary = [...inRange]
186+
.reverse()
187+
.find((e): e is SummaryEvent => e.type === 'summary');
188+
189+
if (!existingSummary) return inRange;
190+
191+
const lastTurn = existingSummary.lastSummarizedTurnId ?? 0;
192+
return inRange.filter((e) => 'turnId' in e && (e as any).turnId > lastTurn);
193+
}
194+
195+
function buildTranscript(events: SessionEvent[]): Message[] {
196+
const transcript: Message[] = [];
197+
for (const ev of events) {
204198
switch (ev.type) {
205199
case 'user':
206200
transcript.push({ role: 'user', content: ev.content });
207201
break;
208202
case 'assistant':
209203
transcript.push({ role: 'assistant', content: ev.content });
210204
break;
211-
case 'tool_result':
205+
case 'tool_result': {
206+
let content = ev.output;
207+
const tokens = estimateTokensForContent(content);
208+
if (tokens > MAX_TOOL_RESULT_TOKENS) {
209+
const ratio = MAX_TOOL_RESULT_TOKENS / tokens;
210+
const keepChars = Math.floor(content.length * ratio);
211+
content =
212+
content.slice(0, keepChars) +
213+
`\n\n[...truncated: ${tokens} tokens total, showing first ${MAX_TOOL_RESULT_TOKENS}]`;
214+
}
212215
transcript.push({
213216
role: 'tool',
214-
content: ev.output,
217+
content,
215218
tool_call_id: ev.toolCallId,
216219
tool_name: ev.toolName,
217220
} as any);
218221
break;
222+
}
219223
case 'summary':
220224
transcript.push({ role: 'system', name: 'compacted_history', content: ev.summaryText });
221225
break;
222226
}
223227
}
228+
return transcript;
229+
}
224230

225-
const summary = await callLLMForCompaction(transcript, ctx.llm, config);
226-
if (!summary) return 0;
227-
228-
const event: SummaryEvent = {
229-
type: 'summary',
230-
uuid: randomUUID(),
231-
replaces: replacedUuids,
232-
summaryText: summary,
233-
method: 'auto-compact',
234-
timestamp: new Date().toISOString(),
235-
};
236-
appendSummaryToSession(sessionId, event);
237-
for (const u of replacedUuids) hiddenUuids.add(u);
238-
239-
const replacedTokens = transcript.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
240-
const summaryMsg: Message = { role: 'system', name: 'compacted_history', content: summary };
241-
const summaryTokens = estimateMessageTokens(summaryMsg);
242-
return Math.max(0, replacedTokens - summaryTokens);
231+
function estimateEventTokens(e: SessionEvent): number {
232+
if (e.type === 'user') return estimateMessageTokens({ role: 'user', content: e.content });
233+
if (e.type === 'assistant')
234+
return estimateMessageTokens({ role: 'assistant', content: e.content });
235+
if (e.type === 'tool_result') {
236+
return estimateMessageTokens({
237+
role: 'tool',
238+
content: e.output,
239+
tool_call_id: e.toolCallId,
240+
tool_name: e.toolName,
241+
} as any);
242+
}
243+
if (e.type === 'summary') {
244+
return estimateMessageTokens({
245+
role: 'system',
246+
name: 'compacted_history',
247+
content: e.summaryText,
248+
});
249+
}
250+
return 0;
243251
}
244252

245253
async function callLLMForCompaction(
@@ -274,5 +282,3 @@ function extractSummary(raw: string): string {
274282
const m = raw.match(/<summary>([\s\S]*?)<\/summary>/);
275283
return (m?.[1] ?? raw).trim();
276284
}
277-
278-
// ---------- Helpers ----------

0 commit comments

Comments
 (0)