Skip to content

Commit 439c9e6

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@d20e58a353428bbb26f9674994da1f18933ba782
1 parent ede39b3 commit 439c9e6

9 files changed

Lines changed: 818 additions & 47 deletions

File tree

‎bun.lock‎

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cli/src/utils/byok.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
createBunByokConnectionStore,
3+
withEffectiveByokLimits,
34
} from '@codebuff/sdk'
45
import { create } from 'zustand'
56

@@ -113,11 +114,13 @@ export function saveSelectedByokConnection(
113114
)
114115
}
115116

116-
/** Resolve only at run start so the secret never reaches chat state or logs. */
117+
/** Resolve only at run start so the secret never reaches chat state or logs.
118+
* Untouched default limits become the provider-reported window (see
119+
* withEffectiveByokLimits), so a run does not compact every few tool calls. */
117120
export async function resolveByokConnection(
118121
selected: SelectedByokConnection,
119122
): Promise<ResolvedByokConnection> {
120-
return getCliByokStore().resolve(selected)
123+
return withEffectiveByokLimits(await getCliByokStore().resolve(selected))
121124
}
122125

123126
export function describeByokConnection(connection: Pick<ByokConnection, 'name' | 'provider' | 'model'>): string {

‎packages/agent-runtime/src/__tests__/compact-context-loop.test.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,70 @@ describe('compactContext in loopAgentSteps', () => {
475475
).toBe(false)
476476
})
477477

478+
// Desktop BYOK, 2026-09-24..27 ("compacts every 3-5 turns"): a 32k-default
479+
// connection's budget is 25,804 tokens and its threshold 20,643, while the
480+
// prompt and tool catalog take ~15k. Compacting at the threshold landed
481+
// right back under it, so the next read or two compacted again.
482+
describe('a small window under a large fixed prompt', () => {
483+
const smallWindow = {
484+
...baseTemplate,
485+
systemPrompt: 'Follow the project conventions carefully. '.repeat(1_100),
486+
compactContext: { maxContextLength: 25_804, cacheExpiryMs: null },
487+
} as AgentTemplate
488+
const withReads = (count: number): Message[] => {
489+
const history: Message[] = [
490+
{ ...userMessage('review the files'), tags: ['USER_PROMPT'] },
491+
]
492+
for (let i = 0; i < count; i++) {
493+
history.push(
494+
{
495+
role: 'assistant',
496+
content: [
497+
{
498+
type: 'tool-call',
499+
toolCallId: `read-${i}`,
500+
toolName: 'read_files',
501+
input: { paths: [`file${i}.ts`] },
502+
},
503+
],
504+
},
505+
{
506+
role: 'tool',
507+
toolName: 'read_files',
508+
toolCallId: `read-${i}`,
509+
content: [
510+
{
511+
type: 'json',
512+
value: [
513+
{ path: `file${i}.ts`, content: `READ ${i} BODY `.repeat(400) },
514+
],
515+
},
516+
],
517+
},
518+
)
519+
}
520+
return history
521+
}
522+
523+
it('keeps working past the threshold instead of compacting futilely', async () => {
524+
// ~15k fixed + ~6k of reads: over the 20,643 threshold, under the budget.
525+
const result = await runLoop(smallWindow, withReads(4))
526+
expect(runtimeImpl.promptAiSdkStream).toHaveBeenCalledTimes(1)
527+
const sent = JSON.stringify(seenMessages[0])
528+
expect(sent.includes('<conversation_summary>')).toBe(false)
529+
expect(sent.includes('READ 0 BODY')).toBe(true)
530+
expect(result.output.type).not.toBe('error')
531+
})
532+
533+
it('still compacts at the hard budget', async () => {
534+
const result = await runLoop(smallWindow, withReads(8))
535+
const sent = JSON.stringify(seenMessages[0])
536+
expect(sent.includes('<conversation_summary>')).toBe(true)
537+
expect(sent.includes('review the files')).toBe(true)
538+
expect(result.output.type).not.toBe('error')
539+
})
540+
})
541+
478542
it('a null TTL opts out of the opportunistic trigger', async () => {
479543
await runLoop(
480544
{ ...baseTemplate, compactContext: { cacheExpiryMs: null } },

‎packages/agent-runtime/src/__tests__/model-compaction.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { expect, test } from 'bun:test'
22
import {
3+
automaticCompactionIsWorthwhile,
4+
compactedContextCeiling,
35
compactWithModel,
46
compactWithModelOrFallback,
57
COMPACTION_TAG,
@@ -422,3 +424,57 @@ test('compactWithModelOrFallback still propagates cancellation', async () => {
422424
}),
423425
).rejects.toThrow()
424426
})
427+
428+
// Desktop BYOK, 2026-09-24..27: a connection on the untouched 32k default
429+
// compacts at 80% of 90% of (32,768 - 4,096) = 20,643 tokens, and the Desktop
430+
// agent's prompt and tool catalog alone are ~15-20k. The best any compaction
431+
// can do there lands back near the threshold, so firing at it compacted every
432+
// few tool calls, each pass summarizing the previous summary.
433+
test('an automatic compaction that cannot clear its own threshold is not worthwhile', () => {
434+
const byokDefault = { maxContextLength: 25_804, thresholdTokens: 20_643 }
435+
expect(
436+
automaticCompactionIsWorthwhile({ messages, ...byokDefault, fixedTokenCount: 16_000 }),
437+
).toBe(false)
438+
// The same window with a small fixed prompt has room to work after a pass.
439+
expect(
440+
automaticCompactionIsWorthwhile({ messages, ...byokDefault, fixedTokenCount: 3_000 }),
441+
).toBe(true)
442+
// Hosted budgets are unaffected: 400k budget, 320k threshold.
443+
expect(
444+
automaticCompactionIsWorthwhile({
445+
messages,
446+
maxContextLength: 400_000,
447+
thresholdTokens: 320_000,
448+
fixedTokenCount: 20_000,
449+
}),
450+
).toBe(true)
451+
// The ceiling is the fixed prefix, the verbatim live request and the summary budget.
452+
const ceiling = compactedContextCeiling({ messages, maxContextLength: 400_000, fixedTokenCount: 20_000 })
453+
expect(ceiling).toBe(20_000 + countTokensMessages([messages[0]]) + 6_000)
454+
})
455+
456+
test('an automatic fallback aims below the trigger instead of refilling the whole budget', async () => {
457+
const failing = async function* (): ReturnType<PromptAiSdkStreamFn> {
458+
throw new Error('connection reset')
459+
}
460+
const params = {
461+
messages,
462+
system: 'You are a coding agent.',
463+
maxContextLength: 16_384,
464+
fixedTokenCount: 500,
465+
signal: new AbortController().signal,
466+
stream: failing,
467+
logger: noopLogger,
468+
}
469+
const whole = await compactWithModelOrFallback(params)
470+
const aimed = await compactWithModelOrFallback({ ...params, fallbackTargetTokens: 1_500 })
471+
expect(whole?.fallback).toBe(true)
472+
expect(aimed?.fallback).toBe(true)
473+
expect(aimed!.postTokens).toBeLessThanOrEqual(1_500)
474+
expect(aimed!.postTokens).toBeLessThan(whole!.postTokens)
475+
expect(JSON.stringify(aimed!.messages)).toContain('Compare the time units in a.ts and b.ts.')
476+
// A target that cannot hold the live request falls back to the whole budget
477+
// rather than leaving the history uncompacted.
478+
const tooSmall = await compactWithModelOrFallback({ ...params, fallbackTargetTokens: 400 })
479+
expect(tooSmall?.postTokens).toBe(whole!.postTokens)
480+
})

‎packages/agent-runtime/src/model-compaction.ts‎

Lines changed: 118 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,87 @@ export function hasCompactableHistory(messages: Message[]): boolean {
9898
)
9999
}
100100

101+
/** The messages a model compaction keeps verbatim after its summary: the
102+
* latest instructions and the live user request (with its steering). */
103+
function compactionSuffix(messages: Message[]): Message[] {
104+
const lastPrompt = messages.findLastIndex((m) =>
105+
m.tags?.includes('USER_PROMPT'),
106+
)
107+
let promptStart = lastPrompt
108+
while (
109+
promptStart > 0 &&
110+
messages[promptStart - 1].tags?.includes('USER_PROMPT')
111+
)
112+
promptStart--
113+
const live =
114+
promptStart < 0
115+
? []
116+
: messages
117+
.slice(promptStart)
118+
.filter((m) => m.tags?.includes('USER_PROMPT'))
119+
const instructions = messages.findLast((m) =>
120+
m.tags?.includes('INSTRUCTIONS_PROMPT'),
121+
)
122+
return [...(instructions ? [instructions] : []), ...live]
123+
}
124+
125+
function compactionSummaryBudget(params: {
126+
maxContextLength: number
127+
fixedTokenCount: number
128+
suffixTokens: number
129+
}): number {
130+
return Math.min(
131+
SUMMARY_LIMIT,
132+
Math.floor(
133+
(params.maxContextLength - params.fixedTokenCount - params.suffixTokens) /
134+
3,
135+
),
136+
)
137+
}
138+
139+
/**
140+
* The share of the trigger threshold a compaction's result may occupy for the
141+
* automatic trigger to be worth firing. Above it, the next tool result or two
142+
* crosses the threshold again and the run compacts its own summary.
143+
*/
144+
export const COMPACTION_LOW_WATER = 0.85
145+
146+
/**
147+
* The largest context a model compaction can leave behind: the fixed prefix
148+
* (system prompt, tool schemas), the live request it keeps verbatim, and the
149+
* summary budget it asks for. None of it is compactable, so when this is not
150+
* comfortably under the threshold, compacting at the threshold only buys a
151+
* few thousand tokens before the next one: a 32k BYOK window with ~16k of
152+
* Desktop tool schemas compacted every few tool calls, each pass summarizing
153+
* the last summary.
154+
*/
155+
export function compactedContextCeiling(params: {
156+
messages: Message[]
157+
maxContextLength: number
158+
fixedTokenCount: number
159+
}): number {
160+
const suffixTokens = countTokensMessages(compactionSuffix(params.messages))
161+
const summaryBudget = compactionSummaryBudget({ ...params, suffixTokens })
162+
return params.fixedTokenCount + suffixTokens + Math.max(0, summaryBudget)
163+
}
164+
165+
/**
166+
* Whether an automatic compaction at `thresholdTokens` leaves real room to
167+
* work in. When it cannot, the run keeps its history until the hard budget,
168+
* where compaction is no longer optional.
169+
*/
170+
export function automaticCompactionIsWorthwhile(params: {
171+
messages: Message[]
172+
maxContextLength: number
173+
thresholdTokens: number
174+
fixedTokenCount: number
175+
}): boolean {
176+
return (
177+
compactedContextCeiling(params) <=
178+
Math.floor(params.thresholdTokens * COMPACTION_LOW_WATER)
179+
)
180+
}
181+
101182
/** A model handoff, not a mechanical reduction of tool results. Nothing mutates
102183
* the source history until every section has a valid, bounded result. */
103184
export async function compactWithModel(params: {
@@ -121,34 +202,12 @@ export async function compactWithModel(params: {
121202
countTokensMessages(params.messages) + params.fixedTokenCount
122203
// A compact-only request never enters the history. Keep the actual current
123204
// user request verbatim, including steering and attachments.
124-
const lastPrompt = params.messages.findLastIndex((m) =>
125-
m.tags?.includes('USER_PROMPT'),
126-
)
127-
let promptStart = lastPrompt
128-
while (
129-
promptStart > 0 &&
130-
params.messages[promptStart - 1].tags?.includes('USER_PROMPT')
131-
)
132-
promptStart--
133-
const live =
134-
promptStart < 0
135-
? []
136-
: params.messages
137-
.slice(promptStart)
138-
.filter((m) => m.tags?.includes('USER_PROMPT'))
139-
const instructions = params.messages.findLast((m) =>
140-
m.tags?.includes('INSTRUCTIONS_PROMPT'),
141-
)
142-
const suffix = [...(instructions ? [instructions] : []), ...live]
143-
const summaryBudget = Math.min(
144-
SUMMARY_LIMIT,
145-
Math.floor(
146-
(params.maxContextLength -
147-
params.fixedTokenCount -
148-
countTokensMessages(suffix)) /
149-
3,
150-
),
151-
)
205+
const suffix = compactionSuffix(params.messages)
206+
const summaryBudget = compactionSummaryBudget({
207+
maxContextLength: params.maxContextLength,
208+
fixedTokenCount: params.fixedTokenCount,
209+
suffixTokens: countTokensMessages(suffix),
210+
})
152211
if (summaryBudget < 256)
153212
throw new Error(
154213
'The current request and instructions leave too little room to compact. Shorten the request or configure a larger context window.',
@@ -319,6 +378,14 @@ export async function compactWithModelOrFallback(
319378
runId?: string
320379
model?: string
321380
trigger?: string
381+
/**
382+
* Where the mechanical fallback should aim, below `maxContextLength`. That
383+
* pass fills whatever budget it is given, so aimed at the hard budget it
384+
* lands above an automatic trigger's threshold and the very next step
385+
* compacts again. Falls back to `maxContextLength` when the target is too
386+
* small to hold the live request.
387+
*/
388+
fallbackTargetTokens?: number
322389
},
323390
): Promise<{
324391
messages: Message[]
@@ -327,22 +394,41 @@ export async function compactWithModelOrFallback(
327394
postTokens: number
328395
fallback?: true
329396
} | null> {
330-
const { logger, runId, model, trigger, ...modelParams } = params
397+
const {
398+
logger,
399+
runId,
400+
model,
401+
trigger,
402+
fallbackTargetTokens,
403+
...modelParams
404+
} = params
331405
try {
332406
return await compactWithModel(modelParams)
333407
} catch (error) {
334408
if (params.signal.aborted || isAbortError(error)) throw error
335409
const errorMessage = error instanceof Error ? error.message : String(error)
336410
let fallback: ReturnType<typeof compactHistoryNow> = null
337411
let fallbackError: string | undefined
338-
try {
339-
fallback = compactHistoryNow({
412+
const mechanical = (maxContextLength: number) =>
413+
compactHistoryNow({
340414
messages: params.messages,
341-
maxContextLength: params.maxContextLength,
415+
maxContextLength,
342416
fixedTokenCount: params.fixedTokenCount,
343417
logger,
344418
runId,
345419
})
420+
try {
421+
if (
422+
fallbackTargetTokens !== undefined &&
423+
fallbackTargetTokens < params.maxContextLength
424+
) {
425+
try {
426+
fallback = mechanical(fallbackTargetTokens)
427+
} catch {
428+
// The live request does not fit the target; use the whole budget.
429+
}
430+
}
431+
fallback ??= mechanical(params.maxContextLength)
346432
} catch (mechanicalError) {
347433
fallbackError =
348434
mechanicalError instanceof Error

0 commit comments

Comments
 (0)