Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
409f701
feat(context)!: redesign offload conditions in typescript
lizradway Oct 21, 2026
7db98fc
test(context): fold offload condition tests into existing strategy tests
lizradway Oct 21, 2026
fddda55
refactor(context): return a plain object from the offload builders
lizradway Oct 21, 2026
c92baa1
fix(context): clarify offload builder error and percent threshold guard
lizradway Oct 21, 2026
9ea9fa1
fix(context): skip a failed summarize run and continue with the rest
lizradway Oct 22, 2026
1f9e95e
fix(context): leave the newest message alone in gated block offloads
lizradway Oct 22, 2026
f0833f4
fix(context): stop message truncation from re-eliding its own marker
lizradway Oct 22, 2026
63e4f8a
refactor(context): rename blockThreshold back to threshold
lizradway Oct 23, 2026
4ed6fcb
feat(context): reject a utilization token count larger than the conte…
lizradway Oct 23, 2026
a58a30d
Merge remote-tracking branch 'origin/main' into lizradway/offload-con…
lizradway Oct 23, 2026
cee79cc
docs(context): say utilization throws above the context window and cl…
lizradway Oct 26, 2026
7790c77
feat(context)!: act on whole messages for target "*" and add a media …
lizradway Oct 26, 2026
14eb3a9
feat(context): shorten each large message in place for truncate("*") …
lizradway Oct 26, 2026
1cb19b9
fix(context): act on blocks of any size when no threshold is set
lizradway Oct 26, 2026
2f794e5
feat(context): say a dropped media block was viewed before it was rem…
lizradway Oct 26, 2026
844d157
fix(context): leave the task prompt to gated block strategies and rej…
lizradway Oct 26, 2026
36adcd0
fix(context): require a context window for a token-count utilization
lizradway Oct 26, 2026
6670fd5
fix(context): warn instead of throwing when a token-count utilization…
lizradway Oct 26, 2026
0617f1a
fix(context): reject condition names that only match Object.prototype…
lizradway Oct 26, 2026
41efdea
fix(context): reject a token-count utilization below 1,000
lizradway Oct 26, 2026
3231184
docs(context): note that truncation detection matches on the text prefix
lizradway Oct 26, 2026
15fd467
fix(context): reject eager media strategies and tighten offload checks
lizradway Oct 26, 2026
8d0694f
fix(context): treat unmeasured media as over the threshold and guard …
lizradway Oct 26, 2026
901c1c5
fix(context): subtract excluded tools from included ones in a mixed t…
lizradway Oct 26, 2026
8b9108b
feat(context)!: accept only "N%" strings and whole numbers as offload…
lizradway Oct 26, 2026
181260c
refactor(context): infer the condition rules shape instead of declari…
lizradway Oct 27, 2026
7aeb480
fix(context-py): never stash or serve offload previews as originals
lizradway Oct 28, 2026
a264eab
fix(context-ts): never stash or serve offload previews as originals
lizradway Oct 28, 2026
17c5c23
feat(context-ts): add OffloadedBlock, a typed placeholder for offload…
lizradway Oct 28, 2026
ec8ab18
refactor(context-ts): treat OffloadedBlock as a text turn across the …
lizradway Oct 28, 2026
b90933f
feat(context-ts): make removed messages retrievable from their marker
lizradway Oct 28, 2026
ec0d5eb
feat(context-ts): send OffloadedBlock as text wherever a provider or …
lizradway Oct 28, 2026
856ef22
refactor(context-ts): every offload placeholder is an OffloadedBlock
lizradway Oct 28, 2026
cb04c8e
refactor(context-ts): read stash entries through the typed deserializers
lizradway Oct 28, 2026
92be138
refactor(context-ts): one message-removal path, one text-turn predicate
lizradway Oct 28, 2026
6e1c537
refactor(context-ts): drop the text-marker placeholder guard
lizradway Oct 28, 2026
33789ae
fix(context-ts): keep an offload reference resolvable after history e…
lizradway Oct 28, 2026
7794a26
refactor(context-ts): one way to copy a message or tool result with o…
lizradway Oct 28, 2026
69df69a
refactor(context-ts): stash retrieve_context results like any other t…
lizradway Oct 28, 2026
c9951aa
test(context-ts): drop tests the typed placeholder made redundant
lizradway Oct 28, 2026
09d43d8
refactor(context-ts): flatten the retrieval tool callback
lizradway Oct 28, 2026
494c661
refactor(context-ts): split the offload base into target matching, hi…
lizradway Oct 29, 2026
64dda15
refactor(context-ts): one table for the context manager presets
lizradway Oct 29, 2026
6a73346
fix(context-ts): address the review of the OffloadedBlock branch
lizradway Oct 29, 2026
7a1fdc5
refactor(context-ts): make OffloadedBlock a TextBlock
lizradway Oct 29, 2026
df6711f
refactor(context-ts): record an offload placeholder as an optional fi…
lizradway Oct 29, 2026
8d29011
refactor(context-ts): name the message-level removal module message-t…
lizradway Oct 29, 2026
7c2e46b
fix(context-ts): never store a placeholder under its original's key, …
lizradway Oct 29, 2026
02737e9
refactor(context-ts): nest the offload placeholder under TextBlock.me…
lizradway Oct 29, 2026
27f7227
feat(context)!: rename offload conditions and make .when() optional
lizradway Oct 29, 2026
b48a5bd
refactor(context-ts): fold typed offload placeholders into the offloa…
lizradway Oct 29, 2026
6b07652
chore: merge main into offload conditions (typescript)
lizradway Oct 29, 2026
3cb0d77
refactor(context-ts): move offload placeholder wording into one modul…
lizradway Oct 29, 2026
0bf25bf
refactor(context-ts): keep the Percent type out of the public exports
lizradway Oct 29, 2026
5bd28c3
refactor(context-ts): collapse the drop and summarize block paths and…
lizradway Oct 29, 2026
d4f13c6
test(context-ts): remove offload tests that other cases already cover
lizradway Oct 29, 2026
3bcb01a
refactor(context-ts): replace RemovalPreview with a private async pla…
lizradway Oct 29, 2026
fbf703c
refactor(context-ts): name the truncated tool result type and call it…
lizradway Oct 29, 2026
4bf5407
refactor(context-ts): store the offload target and eager flag instead…
lizradway Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions strands-ts/src/context-manager/__tests__/presets.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, it, expect } from 'vitest'
import { resolvePreset, resolveStrategies, STRATEGY_PRESET_NAMES } from '../presets.js'
import { ContextManager } from '../context-manager.js'
import { Offload } from '../strategies/offload/index.js'
import type { ContextStrategy } from '../types.js'

describe('presets', () => {
describe('resolvePreset', () => {
Expand All @@ -25,10 +27,10 @@ describe('presets', () => {
expect(strategies[0]!.name).toBe('offload:truncate')
})

it('overflowProtection resolves to a truncate strategy', () => {
it('overflowProtection resolves to a drop strategy', () => {
const strategies = resolvePreset('overflowProtection')
expect(strategies).toHaveLength(1)
expect(strategies[0]!.name).toBe('offload:truncate')
expect(strategies[0]!.name).toBe('offload:drop')
})

it('staleToolCleanup resolves to a drop strategy', () => {
Expand Down Expand Up @@ -57,7 +59,7 @@ describe('presets', () => {
expect(result).toHaveLength(3)
expect(result[0]!.name).toBe('offload:truncate')
expect(result[1]!.name).toBe('custom')
expect(result[2]!.name).toBe('offload:truncate')
expect(result[2]!.name).toBe('offload:drop')
})

it('preserves order of preset expansion', () => {
Expand All @@ -72,6 +74,22 @@ describe('presets', () => {
'Unknown strategy preset: "proactiveSummarisation"'
)
})

it.each([
['drop', Offload.drop('toolResults')],
['truncate', Offload.truncate('toolResults')],
['summarize', Offload.summarize('*')],
])('rejects an Offload.%s() builder without .when()', (_method, builder) => {
expect(() => resolveStrategies([builder as unknown as ContextStrategy])).toThrow(
'Offload strategies need .when() before use; call .when() with no conditions to run on every new message'
)
})

it('accepts an Offload builder once .when() is called', () => {
const result = resolveStrategies([Offload.drop('toolResults').when({})])
expect(result).toHaveLength(1)
expect(result[0]!.name).toBe('offload:drop')
})
})

describe('ContextManager.from', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('Offload strategies with stash', () => {
const largeText = 'important data '.repeat(1000)
const messages = [makeToolResultMessage(largeText)]
await stashAll(stash, messages)
const strategy = Offload.truncate('toolResults')
const strategy = Offload.truncate('toolResults').when({})
const context = makeContext(messages, stash)

const acted = await strategy.apply(context)
Expand All @@ -133,7 +133,7 @@ describe('Offload strategies with stash', () => {
const largeText = 'x'.repeat(20000)
const messages = [makeToolResultMessage(largeText)]
await stashAll(stash, messages)
const strategy = Offload.truncate('toolResults')
const strategy = Offload.truncate('toolResults').when({})
const context = makeContext(messages, stash)

await strategy.apply(context)
Expand All @@ -146,7 +146,7 @@ describe('Offload strategies with stash', () => {
it('does not stash when no stash is configured', async () => {
const largeText = 'x'.repeat(20000)
const messages = [makeToolResultMessage(largeText)]
const strategy = Offload.truncate('toolResults')
const strategy = Offload.truncate('toolResults').when({})
const context = makeContext(messages)

await strategy.apply(context)
Expand All @@ -163,7 +163,7 @@ describe('Offload strategies with stash', () => {
const largeText = 'critical information '.repeat(500)
const messages = [makeToolResultMessage(largeText)]
await stashAll(stash, messages)
const strategy = Offload.drop('toolResults')
const strategy = Offload.drop('toolResults').when({})
const context = makeContext(messages, stash)

const acted = await strategy.apply(context)
Expand All @@ -182,7 +182,7 @@ describe('Offload strategies with stash', () => {
const largeText = 'x'.repeat(20000)
const messages = [makeToolResultMessage(largeText)]
await stashAll(stash, messages)
const strategy = Offload.drop('toolResults')
const strategy = Offload.drop('toolResults').when({})
const context = makeContext(messages, stash)

await strategy.apply(context)
Expand Down Expand Up @@ -210,7 +210,7 @@ describe('Offload strategies with stash', () => {
}),
]
await stashAll(stash, messages)
const strategy = Offload.drop('toolResults')
const strategy = Offload.drop('toolResults').when({})
const context = makeContext(messages, stash)

const acted = await strategy.apply(context)
Expand Down Expand Up @@ -245,7 +245,7 @@ describe('Offload strategies with stash', () => {
}),
]
await stashAll(stash, messages)
const strategy = Offload.drop('toolResults')
const strategy = Offload.drop('toolResults').when({})
const context = makeContext(messages, stash)

await strategy.apply(context)
Expand Down Expand Up @@ -371,7 +371,7 @@ describe('Offload strategies with stash', () => {
],
}),
]
const strategy = Offload.truncate('toolResults')
const strategy = Offload.truncate('toolResults').when({})
const context = makeContext(messages, stash)

const acted = await strategy.apply(context)
Expand Down Expand Up @@ -401,7 +401,7 @@ describe('Offload strategies with stash', () => {
],
}),
]
const strategy = Offload.truncate('toolResults')
const strategy = Offload.truncate('toolResults').when({})
const context = makeContext(messages)

const acted = await strategy.apply(context)
Expand Down
4 changes: 2 additions & 2 deletions strands-ts/src/context-manager/context-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class ContextManager implements Plugin {
? resolveStrategies(config.strategies)
: [
Offload.truncate('toolResults', { previewTokens: TRUNCATE_PREVIEW_TOKENS }).when({
threshold: AUTO_TRUNCATE_THRESHOLD,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

old name faild loud right, it is expected.

For target '*', it doesn't measure blocks, The name is confusing in one of its two modes

blockThreshold: AUTO_TRUNCATE_THRESHOLD,
}),
Offload.summarize('*').when({ utilization: AUTO_SUMMARIZE_UTILIZATION, preserveRecent: 4 }),
]
Expand All @@ -100,7 +100,7 @@ export class ContextManager implements Plugin {
return new ContextManager({
strategies: [
Offload.truncate('toolResults', { previewTokens: TRUNCATE_PREVIEW_TOKENS }).when({
threshold: AGENTIC_TRUNCATE_THRESHOLD,
blockThreshold: AGENTIC_TRUNCATE_THRESHOLD,
}),
Offload.summarize('*').when({ utilization: 1, preserveRecent: 4 }),
],
Expand Down
28 changes: 22 additions & 6 deletions strands-ts/src/context-manager/methods/truncate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,41 @@ export interface TruncateConfig {
preview?: 'head' | 'tail' | 'headTail'
Comment thread
lizradway marked this conversation as resolved.
}

/**
* Share of the preview budget kept from the head; the rest is kept from the tail.
*
* @internal
*/
export const PREVIEW_HEAD_SHARE = { head: 1, tail: 0, headTail: 0.6 } as const

/**
* Resolves the preview token budget, defaulting to 1,000.
*
* @param config - Truncate configuration
* @returns The preview token budget
* @internal
*/
export function resolvePreviewTokens(config?: TruncateConfig): number {
return typeof config?.previewTokens === 'number' && Number.isFinite(config.previewTokens)
? Math.max(0, config.previewTokens)
: DEFAULT_PREVIEW_TOKENS
}

/**
* Builds a preview of the given text (head, tail, or head-tail depending on config).
*
* @returns The preview string with truncation metadata header, or the original text if already within budget.
*/
export function buildPreview(fullText: string, blockCount: number, config?: TruncateConfig): string {
const previewTokens =
typeof config?.previewTokens === 'number' && Number.isFinite(config.previewTokens)
? Math.max(0, config.previewTokens)
: DEFAULT_PREVIEW_TOKENS
const previewChars = previewTokens * CHARS_PER_TOKEN
const previewChars = resolvePreviewTokens(config) * CHARS_PER_TOKEN
const previewMode = config?.preview ?? 'headTail'
const totalChars = fullText.length

if (totalChars <= previewChars) {
return fullText
}

const headShare = { head: 1, tail: 0, headTail: 0.6 }[previewMode]
const headShare = PREVIEW_HEAD_SHARE[previewMode]
const headChars = Math.floor(previewChars * headShare)
const tailChars = previewChars - headChars
const head = fullText.slice(0, headChars)
Expand Down
11 changes: 8 additions & 3 deletions strands-ts/src/context-manager/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Offload } from './strategies/offload/index.js'
*
* - `'proactiveSummarization'` — batch summarize oldest messages at 70% utilization
* - `'largeToolOffloading'` — truncate tool results over 2500 tokens to a 1000-token preview
* - `'overflowProtection'` — truncate oldest messages when the context window is full
* - `'overflowProtection'` — drop the oldest messages when the context window is full
* - `'staleToolCleanup'` — drop tool results older than 5 messages
*/
export const STRATEGY_PRESET_NAMES = [
Expand All @@ -46,9 +46,9 @@ export function resolvePreset(name: StrategyPresetName): ContextStrategy[] {
case 'proactiveSummarization':
return [Offload.summarize('*').when({ utilization: 0.7, preserveRecent: 0.7 })]
case 'largeToolOffloading':
return [Offload.truncate('toolResults', { previewTokens: 1000 }).when({ threshold: 2500 })]
return [Offload.truncate('toolResults', { previewTokens: 1000 }).when({ blockThreshold: 2500 })]
case 'overflowProtection':
return [Offload.truncate('*').when({ utilization: 1.0, preserveRecent: 4 })]
return [Offload.drop('*').when({ utilization: 1.0, preserveRecent: 4 })]
case 'staleToolCleanup':
return [Offload.drop('toolResults').when({ preserveRecent: 5 })]
}
Expand All @@ -59,6 +59,7 @@ export function resolvePreset(name: StrategyPresetName): ContextStrategy[] {
*
* @param entries - Array of raw strategies and/or preset name strings
* @returns Flattened array of concrete strategies
* @throws Error when an entry is an unknown preset name or an `Offload` builder without `.when()`
* @internal
*/
export function resolveStrategies(entries: (ContextStrategy | StrategyPresetName)[]): ContextStrategy[] {
Expand All @@ -69,6 +70,10 @@ export function resolveStrategies(entries: (ContextStrategy | StrategyPresetName
throw new Error(`Unknown strategy preset: "${entry}". Valid presets: ${STRATEGY_PRESET_NAMES.join(', ')}`)
}
strategies.push(...resolvePreset(entry as StrategyPresetName))
} else if (typeof entry.apply !== 'function') {
throw new Error(
'Offload strategies need .when() before use; call .when() with no conditions to run on every new message'
)
} else {
strategies.push(entry)
}
Expand Down
Loading
Loading