Long session context
ContextStrategy eviction factories (createLruContextStrategy, createImportanceContextStrategy) for file-context budgets in long-running coding sessions. Strategy factories shipped; the runtime-side CodingContextManager is roadmap.
Status: strategy factories shipped; runtime manager roadmap. Both
strategy factories — createLruContextStrategy and
createImportanceContextStrategy from @pleach/coding-agent/context —
ship today, alongside the locked ContextStrategy contract on
CodingAgentRuntimeConfig (packages/coding-agent/src/types.ts). What
is NOT shipped yet is the runtime-side CodingContextManager that
tracks entries and calls strategy.evict() after each tool turn —
that remains conditional on an open scoping decision (whether the
budget consumer lives in @pleach/coding-agent or in
@pleach/core/runtime). Consumers who bring their own context manager
can inject a shipped strategy today.
Long coding sessions exhaust the model's context window long before they exhaust their workspace. A 200K-token context window holds ~50 typical source files; a non-trivial coding task touches more than that. The model needs a policy for which files to evict from its working set when the budget is tight — not at the runtime layer (which would defeat replay determinism) but at a layer the runtime delegates to.
ContextStrategy is that contract. CodingContextManager is the
runtime-side consumer that calls the strategy with the current
state and a budget.
The contract
import type {
ContextStrategy,
ContextEntry,
ContextBudget,
} from "@pleach/coding-agent";
export interface ContextStrategy {
evict(
state: readonly ContextEntry[],
budget: ContextBudget,
): readonly string[];
}
export interface ContextEntry {
readonly id: string; // stable id, typically `${path}:${revisionHash}`
readonly path: string; // file path or other resource identifier
readonly lastAccessedAt: number; // epoch ms — LRU-load-bearing
readonly sizeBytes: number;
readonly accessCount?: number; // optional frequency — importance scoring; absent → recency×size proxy
}
export interface ContextBudget {
readonly maxBytes: number;
readonly maxEntries?: number; // strategies MAY ignore
}The strategy returns the array of entry ids to evict, in eviction
order. Strategies MUST be pure: same state + budget returns the
same eviction list. Side-effects belong outside the strategy.
This is the same shape pattern as the EvictionPolicy contract in
@pleach/core/cache — (state, budget) → string[] —
which keeps the strategy substitutable across the runtime and the
cache layers.
Strategies
Two pure ContextStrategy factories ship today from
@pleach/coding-agent/context.
lru — recency-only
import { createLruContextStrategy } from "@pleach/coding-agent/context";
const strategy = createLruContextStrategy({ maxEntries: 100 });Sorts state by lastAccessedAt ascending; evicts the oldest entries
until the remaining set's total sizeBytes fits under budget.maxBytes.
The default strategy. Coding agents that work file-by-file fit this shape well — a file the agent hasn't touched in N tool calls is probably not needed for the next N tool calls either.
importance — weighted by access frequency × recency
import { createImportanceContextStrategy } from "@pleach/coding-agent/context";
const strategy = createImportanceContextStrategy({ maxEntries: 100 });Scores each entry as frequency × recency ÷ size, where frequency is
the optional ContextEntry.accessCount, recency is lastAccessedAt
normalized across the current state, and size is sizeBytes.
Lowest-importance entries evict first. When accessCount is absent the
score degrades to a recency × size proxy, so the strategy stays usable
against the un-augmented ContextEntry shape.
Pairs with project-shaped workflows where the agent re-visits a few "hub" files (a router, a config, a schema) repeatedly across the session. Pure recency would evict them between visits; importance keeps them warm.
Wiring (planned)
import { createCodingAgentRuntime } from "@pleach/coding-agent/runtime";
import { createLruContextStrategy } from "@pleach/coding-agent/context";
const runtime = createCodingAgentRuntime({
sandboxProvider,
contextStrategy: createLruContextStrategy({ maxEntries: 100 }),
// ...rest of config
});Omit contextStrategy and the runtime ships createLruContextStrategy({ maxEntries: 100 })
by default (the literal "v1.0 default" in the
CodingAgentRuntimeConfig contract).
Honest scope-limit
What IS shipped today: the ContextStrategy, ContextEntry, and
ContextBudget types AND both strategy factories
(createLruContextStrategy, createImportanceContextStrategy from
@pleach/coding-agent/context) — the contract is locked, the field is
on CodingAgentRuntimeConfig, and the factories are pure functions
over the (state, budget) → string[] contract.
What is NOT shipped today: the runtime-side CodingContextManager
that tracks entries and calls strategy.evict(state, budget) after
each tool turn. Until it lands, the factories are injectable into a
context manager you bring yourself; the runtime only smoke-invokes
evict once at boot. Consumers can author against the full contract
today and expect it to work without breaking changes when the runtime
ships the manager.
Why this is conditional
The open question is layer placement. Two candidate homes:
@pleach/coding-agent/context— keeps the strategy with the surface that needs it. Adds aContextStrategyfield to the coding-agent runtime config and nowhere else.@pleach/core/runtimebudget primitive — promotes the strategy contract one layer down, so observability / agents that are NOT coding-shaped (chat with a large file-attach surface, research-agent with a fanout corpus) can share the same eviction primitive.
The (2) shape is more general but adds a contract surface to
@pleach/core that hosts who don't want budget management still pay
type-check cost for. The (1) shape ships the value where the demand
is.
Resolution is tracked in an open scoping decision.
What strategies CAN'T do
The strategy is a pure function of (state, budget) → string[]. It
cannot:
- Re-fetch evicted content. When the agent asks for a file it
previously evicted, the runtime re-reads from the sandbox via
read_file. The strategy doesn't cache; the workspace IS the cache. - Mutate the entry shape. The strategy returns ids to evict — not modifications to the remaining entries.
- Block on async work. Strategies are synchronous. They run on the post-tool-turn boundary; making them async would mean blocking the next LLM turn behind their resolution.
These constraints are load-bearing for replay determinism — a strategy that re-fetched on the side would make the recorded event log not re-derivable from the recorded inputs.
Where to go next
Multi-synthesize per turn
The per-runtime maxSynthesizePerTurn knob — contract widened in @pleach/core, default policy shipped in @pleach/coding-agent. Host support depends on the counter implementation.
SWE-Bench Lite recipe
Load the SWE-Bench Lite starter sample, compose it with evalLab and DivergenceReporter, and run it across a model matrix. Honest about the Docker harness gap.