Skip to main content

Context strategies

A long agent run fills its context window with material it will never read again: a 40,000-character command output, twelve tool schemas it never called, thirty turns of a conversation whose conclusion is one sentence. A context strategy decides what the runtime does about that — what gets moved out of the prompt and left retrievable, and what gets withheld until asked for. There are three values: static, hybrid and dynamic. hybrid is the default, and each one is a superset of the last.

The problem

Context windows are large, but they are not the binding constraint people expect. Three separate pressures push against them, and they want different answers. The first is a single enormous tool result. One find over a big tree can spend a third of a window on output the agent needed two lines of. Truncating loses the two lines as often as not. The second is accumulated history. Compaction — summarizing old turns to reclaim room — is the standard answer, and it is lossy by construction. Once a turn has been summarized, a question that depends on its detail has nowhere to go. The third is tool schemas. An agent with eight MCP servers attached carries every tool definition from all eight on every request, whether or not this task touches any of them, and schemas are not small. Solving all three unconditionally is the wrong call, because each solution costs something. Offloading adds a round trip and a read-back tool the model has to understand. Lazy tool disclosure adds an activation turn before the first real tool call, and a model that handles it badly stalls. So the platform makes it a setting rather than a default behaviour.

How AgentArea approaches it

What each strategy turns on

Note what is not in the table: compaction itself. Compaction runs under all three strategies. What static gives up is the preservation of what compaction discards, not the discarding.

Resolution

The strategy is resolved once, early in the run, before tool discovery — it has to be, because it decides which discovery activity runs. Resolution order is the agent’s override, then the model spec’s default_context_strategy, then "hybrid". The resolved value is stored on workflow state and carried across continue-as-new so a long run does not change strategy mid-flight. An unrecognized value does not raise. It resolves to hybrid. See Limits — this matters more than it sounds.

Output offloading

Under hybrid and dynamic, a tool result longer than 8,000 characters (about 2,000 tokens) is written to object storage under tasks/{workspace_id}/{task_id} and replaced in the conversation by a summary. The summary is not a truncation. It carries a header naming the stored id with the exact character and line counts, then the first 500 characters, then — if the content is longer than head plus tail — the last 200 characters, and it ends by telling the model how to get the rest:
The grep parameter is the point. The common case is not “read all 41,000 characters back” but “find the three lines that matter”, and offloading turns an unaffordable read into an affordable search. A read-back is itself capped at 16,000 characters. Offloading is best-effort in both directions. If the store fails inside the workflow, the full content stays in the conversation — the agent is never blocked by a storage outage. The same offload applied inside an activity, where the payload would otherwise become Temporal history, degrades differently: it truncates at 8,000 characters and says so explicitly in the text.

History preservation

Under hybrid and dynamic, the messages about to be compacted are written to object storage as a numbered chunk first, and a counter increments. Compaction then proceeds as usual. This makes recall_history meaningfully better. When the model calls it with a grep or tool_name filter and at least one chunk has been stored, the workflow searches the preserved chunks in object storage and returns up to 20 matches. Only if that search finds nothing or fails does it fall back to querying the database event log. Under static there are no chunks, so recall_history has only the event-log fallback. Preservation is also best-effort — a failed store logs a warning and compaction continues, because refusing to compact would leave the run wedged against its context limit.

When compaction happens

Usage is tracked against an effective limit of the model’s context window less a 15% reserve for output. A warning fires at 60% of that limit; compaction triggers at 75%. Compaction cannot cut anywhere. find_compaction_boundary searches from the largest possible split downwards for one that keeps the last 4 messages, does not orphan a tool result whose originating tool call would be removed, and does not drop activated skill content. If no such boundary exists it returns 0 and compaction is skipped with a warning.

Lazy tool disclosure

Only dynamic changes what tools the model is shown. Instead of discovering and injecting every tool definition, the workflow discovers tool providers — MCP servers, code toolsets, agents, built-ins — and injects a catalog block:
Each entry names the provider, its type, its tool count and up to five tool names as a preview. Built-in providers are loaded eagerly regardless — they are small and always relevant. An activate_tool_source tool is added whose source_name enum contains exactly the not-yet-activated sources, so the model cannot request one that does not exist. Activated sources are recorded on workflow state and replayed across continue-as-new, so a long run does not have to re-activate. A separate, finer mechanism exists for OpenAPI connections and is not governed by the context strategy at all: a tool config with load_mode: "searchable" keeps its operation schemas out of the prompt behind a load_tools meta-tool that reveals them by exact name. That is per-tool configuration, not a strategy-wide switch.

Why not always use dynamic

Dynamic looks strictly better — it is the superset — and it is not the default, for two reasons. It costs a turn. Under dynamic the model cannot call a tool until it has first called activate_tool_source, which means at minimum one extra LLM round trip before any work begins, and more when the model activates sources one at a time. For a task that was always going to use the one MCP server the agent has, that is pure overhead. And it depends on a model behaviour that is not uniform. Choosing a source from a catalog, activating it, and then calling a tool from it is a three-step plan held across turns. Weaker models activate the wrong source, forget to activate at all and hallucinate a tool call, or activate everything immediately — which reproduces static while having paid for the extra turns. The eager path has no such failure mode: the tool is either in the list or it is not. hybrid is the default because its two features have no behavioural cost. The model is not asked to do anything differently; large outputs arrive summarized with a documented way to read more. The one thing it does ask is that the model understands read_tool_output, and that is a normal tool call rather than a multi-turn plan. The cost of making this a setting at all is that the same agent behaves differently on two models, and a bug that only appears under dynamic will not reproduce on a model whose spec leaves the default unset.

Limits

  • The per-agent override is not wired. Resolution reads agent_config["context_strategy"] first, but the agent config the activity builds has no such field — AgentConfigResult carries only default_context_strategy. There is no context_strategy column on the agent and no API field for it. The agent-level override is always absent, so the model spec is the only thing that selects a strategy.
  • There is no auto-inference from the model name. The model-spec DTO comments default_context_strategy as “Auto-inferred from model_name if None”, and no inference exists. A null value resolves to hybrid.
  • An invalid value fails silently to hybrid. Resolution catches the ValueError and returns hybrid with no log line, and the enum values are lowercase — so "STATIC" or "Dynamic" does not select what it looks like it selects, it selects hybrid.
  • Offloading is measured in characters, not tokens. The 8,000-character threshold is documented as roughly 2,000 tokens, which holds for English prose and not for dense JSON, minified code, or non-Latin scripts.
  • The preview may not contain the useful part. 500 characters of head and 200 of tail is a small window on a 40,000-character output. The model has to decide from that whether a read_tool_output call is warranted.
  • Activating an unknown tool source is silent. ToolCatalog.activate returns an empty list for a name it does not know rather than an error, deliberately, on the assumption the model hallucinated it.
  • Offload and preservation failures are invisible to the agent. Both log a warning and continue. Under hybrid, a storage outage means history is compacted without being preserved, and recall_history silently has less to find.
  • static still compacts. Choosing static does not preserve the full conversation; it only means nothing is written to object storage before compaction discards it.
  • Skills — activated skill content is exempt from compaction, which interacts directly with the boundary search.
  • What is an agent — where model selection lives, and therefore where the strategy is chosen today.
  • Durable execution — continue-as-new, which the resolved strategy and activated sources are carried across.
  • Artifacts — the other thing a task writes to object storage, on a different path and with different durability rules.