pleach
Coding agent

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:

  1. @pleach/coding-agent/context — keeps the strategy with the surface that needs it. Adds a ContextStrategy field to the coding-agent runtime config and nowhere else.
  2. @pleach/core/runtime budget 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

On this page