> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duraton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Context management

> Bound what reaches the model each turn. A summary is a durable step, so a replay reads what was written instead of writing it again.

`step.ai.loop`'s history array grows by one entry every turn and nothing trims it. An agent that
runs long enough eventually hits the model's context window and fails - the only failure mode of
its kind, since every other bound the loop enforces (`maxIterations`, `maxApprovals`) is a ceiling
you set on purpose.

A **context trimmer** is that bound, as a port. Duraton ships three adapters - `count`, `token-budget`
and `summarize` - and the port is what lets a later one drop in without touching the loop or the
adapters already there.

## Turning it on

```ts theme={null}
import { getContextTrimmer } from "@duraton/sdk";

const result = await ctx.step.ai.loop("agent", {
  prompt: `Resolve this ticket: ${ticket}`,
  maxIterations: 20,
  context: getContextTrimmer({ strategy: "count", keepLast: 10 }),
  tools: { "search-policy": searchPolicy, "issue-refund": issueRefund },
  turn,
});
```

With `@duraton/agent-kit` it is a compact descriptor on `agent()` instead - see
[Bounding context](/agent-kit/agents-and-tools#bounding-context) for that surface.

Omitted, an agent behaves exactly as it did before this option existed: the whole history reaches
every turn.

## The three strategies

| Strategy       | What it does                                                                                                                    | Calls a model |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `count`        | Drops the oldest iterations beyond `keepLast`                                                                                   | No            |
| `token-budget` | Same drop, sized by an estimated token count (`maxTokens`) instead of a count                                                   | No            |
| `summarize`    | Overwrites the earliest surviving iteration's own result with a summary of everything older than `keepLast`, and drops the rest | Yes           |

`count` and `token-budget` never remove anything the run itself remembers - each is a fresh view
computed before every turn, so the loop's own history (what a replay reads, what a later `stop`
predicate sees) still has every iteration. Recomputing costs nothing, so there is nothing to gain
by remembering the last result.

`summarize` is different: it pays for its own output; the loop's own history is replaced by the
result, so a later turn starts from the already-reduced baseline instead of paying to re-summarize
the same growing prefix on every turn.

```ts theme={null}
context: getContextTrimmer({
  strategy: "summarize",
  maxTokens: 8000,
  keepLast: 4,
  model: "claude-haiku-4-5", // optional - a cheaper model than the agent's own is a common choice
  generate: async (prompt) => (await provider.generate({ model, prompt })).text,
}),
```

`@duraton/agent-kit`'s `agent()` builds `generate` for you from the same provider/model/apiKey every
turn already uses - the raw SDK option above takes it directly because `step.ai.loop` has no
provider of its own to resolve one from.

## How a summary is represented

A summary does not add a new kind of history entry - `LoopIteration` stays exactly
`{ toolCalls, toolResults }`, the same shape it has always been. Instead, the earliest surviving
iteration's own `toolResults[].output` is overwritten with the summary text, reusing that entry's
real `id` and `name` rather than fabricating one:

```json theme={null}
// Before: three real tool calls
[
  { "toolCalls": [{ "id": "c1", "name": "search-policy", "input": { "q": "refund window" } }], "toolResults": [{ "id": "c1", "output": { "days": 30 } }] },
  { "toolCalls": [{ "id": "c2", "name": "search-policy", "input": { "q": "exceptions" } }], "toolResults": [{ "id": "c2", "output": { "none": true } }] },
  { "toolCalls": [{ "id": "c3", "name": "issue-refund", "input": { "amount": 40 } }], "toolResults": [{ "id": "c3", "output": { "refunded": true } }] }
]

// After summarizing with keepLast: 1 - "c1" is reused, "c2" is gone, "c3" (the kept tail) is untouched
[
  { "toolCalls": [{ "id": "c1", "name": "search-policy", "input": { "q": "refund window" } }], "toolResults": [{ "id": "c1", "output": "Checked refund policy (30-day window, no exceptions found)." }] },
  { "toolCalls": [{ "id": "c3", "name": "issue-refund", "input": { "amount": 40 } }], "toolResults": [{ "id": "c3", "output": { "refunded": true } }] }
]
```

This mirrors Anthropic's own [`clear_tool_uses`](https://docs.claude.com/en/docs/build-with-claude/context-editing)
context-editing behaviour: an existing entry's content is cleared or replaced in place, never a new
one invented. A second summarization folds the same way - the reused entry's current content (raw
or already a summary) feeds the next summarization call, so history never carries more than one
summary-bearing entry at once.

## The summary is a durable step

Every context-trim pass - `count` and `token-budget` included, not only `summarize` - writes its own
step, a sibling of the turn step and the guardrail step, never a suffix of either. Two consequences
follow from that and from nothing else:

* **A replay reads the recorded result.** `summarize`'s model call does not run again, so a re-run
  of the same run cannot disagree with the original, and the summary is an auditable fact rather
  than something re-derived on every pass.
* **Every adapter gets the same guarantee**, whether or not it happens to call a model. `count` and
  `token-budget` are pure functions that would be replay-safe either way; wrapping them identically
  means adding a future model-calling adapter never needs new plumbing.

A loop with no `context` option writes no context-trim steps, so turning this on costs nothing
until you do.

## Writing a trimmer

A trimmer takes the accumulated history and the prompt, and returns a (possibly unchanged) result.
It never mutates the caller's history array - it says what the new view should be and the loop
applies it.

```ts theme={null}
import type { ContextTrimmer } from "@duraton/sdk";

const keepRecentSearches: ContextTrimmer = {
  name: "count",
  trim: ({ history }) => {
    const keepLast = 5;
    if (history.length <= keepLast) return { history: [...history], trimmed: false, persist: false };
    return { history: history.slice(-keepLast), trimmed: true, persist: false };
  },
};
```

`persist` is what separates a cheap per-turn view (`false` - recomputed every time, the loop's own
record is untouched) from a result that should become the new baseline (`true` - the loop replaces
its own accumulator, so a later turn's trim pass sees the reduced history rather than the original).
Only set it `true` when the work being saved by not recomputing is worth the loop's history no
longer holding what was replaced.
