Comis ships 15+ specific cache optimizations across the executor, context
engine, and provider adapters. They are listed below.
The 15+ optimizations at a glance
Each row is one named mechanism in code. The “Why” column explains what it prevents.
The numbered list lives across the executor and context engine:
packages/agent/src/executor/adaptive-cache-retention.ts (rows 1–3, 10),
packages/agent/src/context-engine/context-engine.ts (row 4),
packages/agent/src/spawn/lifecycle-hooks.ts (rows 5, 8–9),
packages/agent/src/executor/cache-break-detection.ts and cache-break-diff-writer.ts (rows 6–7),
packages/agent/src/executor/gemini-cache-manager.ts + gemini-cache-injector.ts (row 12),
packages/agent/src/executor/stream-wrappers/request-body/ — breakpoint-orchestration.ts (rows 11, 17 Anthropic markers), store-flag.ts (row 13 + the usesResponsesInputApi gate), tool-result-clearing.ts (rows 17–18 stabilizers), all wired by factory.ts,
and packages/skills/src/builtin/... (rows 14–16 wire into the executor).
The rest of this page explains the most user-visible of these optimizations
in detail.
Provider cache support
Comis caches against three different provider mechanisms, verified end-to-end across Anthropic (Claude), OpenAI (nativeopenai, openai-codex, and Azure), and Google (Gemini). Each provider caches differently, so Comis adapts its strategy per provider — and, critically, stabilizes the prefix the same way for all three so the cache actually hits.
Anthropic — cache_control breakpoints + prefix stabilizers
Claude caches a prompt prefix only when the bytes up to the cache_control marker are byte-for-byte identical to the previous turn. Comis places the markers (zones, ≤4 breakpoints, 20-block lookback — rows 4, 10, 11) and actively keeps the prefix byte-stable (row 17) so the markers register cache reads instead of re-writing:
- Strip replayed thinking. Extended-thinking blocks are stripped from every replayed assistant message so the cached history matches the durable, no-thinking form and never mutates when a turn transitions active → historical.
- Defer transient recall to the uncached tail. The per-turn memory-recall block (
[Relevant context from memory: …]) is moved off the cache-marked query block onto a trailing uncached block — still visible to the model, never cached — so the prefix does not change when that recall rotates out next turn. - Pinned cache anchor. The semi-stable anchor breakpoint is pinned to a fixed position (the compaction summary, else the second user message) so it never re-anchors and re-writes the
[0..anchor]span.
cache_read_input_tokens / cache_creation_input_tokens. A 5-minute write costs ~1.25× the input rate, a 1-hour write ~2×, and a read ~0.1×. The minimum cacheable prefix is model-dependent (~4096 tokens on Opus / Haiku 4.5, ~1024–2048 on Sonnet and older); below it the marker is silently ignored.
OpenAI — automatic prefix caching
The OpenAI Responses and Completions APIs cache automatically — there are nocache_control markers and no TTL knob. The API caches the longest prefix it has already seen, keyed by prompt_cache_key, which Comis derives from the session key so each agent and each sub-agent caches in its own isolated namespace. Running Anthropic’s cache_control machinery on an OpenAI request body would strip the type:"function" tool shape and 400 the request, so Comis disables breakpoints for OpenAI and instead protects the auto-cached prefix the same way it protects the Anthropic one (row 18):
- Defer recall to a trailing item. The current turn’s recall block is re-attached as a separate trailing
inputitem (transient, never persisted) so each user turn is byte-identical to its future historical clean form — the auto-cached prefix no longer collapses to the instructions+tools floor when recall rotates. - Strip replayed reasoning (native
openaiand Azure only). Withstore: falsethe SDK keeps reasoning for recent turns but drops it from aging ones — an early-index prefix mutation. Comis removes replayed reasoning consistently so the prefix stays byte-stable.openai-codexkeeps its reasoning stable and was already optimal, so it is excluded.
cached_tokens in usage. With the stabilizers on, reads grow monotonically; an occasional single cached_tokens: 0 between otherwise-monotonic reads is OpenAI’s own best-effort eviction, not a Comis cache break — the request body is byte-stable by construction.
storeCompletions (→ store: true) is unrelated to caching — it asks OpenAI to retain the generated output for later replay/debugging. Leaving it false does not affect prefix-cache hit rates.Google — explicit CachedContent
Gemini caching is on by default for Gemini agents (geminiCache.enabled: true). Comis creates a server-side CachedContent object holding the system instruction + tool schemas, injects its name into each request, and strips those fields from the body to avoid double-billing. Staleness is detected by comparing a SHA-256 content hash — not field presence. (A field-presence check would keep the cache from ever hitting: pi-ai leaves toolConfig undefined when no tool choice is set, which such a guard mistakes for a change, creating and evicting the cache on every call.) TTL is fixed at 3600s with a 50%-interval refresh on active sessions.
The explicit cache is shallow by design — it covers the system instruction + tools (a guaranteed, stable discount), while Gemini’s implicit prefix caching covers the conversation tail. Below the per-model minimum (~4096 tokens for gemini-3-pro / gemini-3.1-pro) the manager passes through uncached and implicit caching still applies. Reported as cachedContentTokenCount (constant across turns once the cache is live). Only Google AI Studio providers use the explicit path (not Vertex AI). Set geminiCache.enabled: false to opt out — e.g. a system prompt that churns every turn, which would thrash the per-object cache.
The cache fence, adaptive TTL escalation, and
cacheRetention apply only to the Anthropic cache_control path. OpenAI prefix caching and Gemini CachedContent manage their own lifecycles, so those knobs have no effect there.How context grows (and why it costs money)
Each conversation turn adds content to the context window your agent sends to the LLM. Here is what fills it:- Conversation turns. Every user message and assistant reply is stored in a JSONL session file and replayed on the next call.
- Tool results. Every tool invocation appends a result block. A
bashtool returning 50K characters of log output becomes a permanent fixture unless Comis intervenes. - System prompt. Identity files (AGENTS.md), security rules, skill manifests, and workspace instructions are assembled once and sent every turn.
- Memory retrieval. Before each call, the RAG layer fetches up to five memory entries and injects them as additional context.
- JIT skill guides. Verbose operational guides for specific tools are withheld from the system prompt and injected on first use.
Comis never silently deletes conversation messages. The full history is always persisted to disk. What changes between turns is how much of that history the context engine chooses to forward to the model.
Adaptive retention
When Comis places acache_control marker on a prompt block, it signals the provider to cache that block. But there are two TTLs to choose from: short (5 minutes) and long (1 hour). Writing at 1h TTL on a cold session wastes money if the cache entry expires before it is ever read. Adaptive retention solves this.
Here is what is actually happening under the hood:
- Cold start. The first call writes at
"short"(5m) TTL. This minimizes write cost on sessions that may not continue. - Escalation check. After each turn, Comis records the
cacheReadTokensreturned by the provider. Once confirmed reads arrive, the session is “warm.” - Promotion. After 3 turns of confirmed cache reads (or immediately on turn 2 if the first turn wrote more than 20K cache tokens — a signal of a large system prompt), retention escalates to
"long"(1h). - Prefix instability detection. If cache reads stay at or below the system prompt baseline for 5 consecutive turns, the prefix is unstable and further 1h writes would go to waste. Comis forces retention back to
"short"until the prefix stabilizes.
adaptiveCacheRetention (default true) controls this behavior. Set it to false to use the static value from cacheRetention instead.
Cache fence
The context engine pipeline modifies conversation history before each LLM call. Without coordination, those modifications would land inside the cached prefix and invalidate it — defeating the purpose of caching. The cache fence prevents this. After each call, Comis records the message index of the highestcache_control breakpoint as the fence index. Any context-engine layer that would normally modify content at or below that index skips those messages instead.
Concretely:
- The observation masker (which replaces old tool results with placeholders) skips messages below the fence.
- Schema stripping (which strips verbose discovery schemas from session history) skips messages below the fence.
- Microcompaction (which offloads oversized tool results to disk) skips messages in the first third of the session.
Microcompaction
Microcompaction runs at write time — the moment a tool result is appended to the session file, not at the start of the next turn. This is different from the LLM compaction layer, which runs during context assembly. When a tool result exceeds the per-tool inline threshold, Comis:- Saves the clean payload to
tool-results/<toolCallId>.jsonin the session directory. External-origin results (MCP tools, web fetches — anything that arrived wrapped as untrusted content) are unwrapped before the write, so the file holds parseable payload bytes — an offloaded MCP JSON result is valid JSON on disk. The external origin is recorded in a<toolCallId>.origin.jsonsidecar. - Writes a compact reference into the session in its place:
[Tool result offloaded to disk: ...]with a head/tail preview (1,500 and 500 characters respectively) cut from the clean payload, plus a recovery example that is verified against the written bytes (ajson.loadexample only when the file actually parses as JSON; text-tool guidance otherwise). For external-origin results the preview is re-wrapped with the untrusted-content boundary — the security envelope is presentation-layer, not storage-layer. - The agent can recover the full content at any time: the
readtool works for any offloaded file (recovery reads oftool-results/files are exempt from re-offload up to the hard cap), andexecwith python/jq keeps only the extracted data in context. When a recovery read targets an external-origin file, the content re-enters context re-wrapped as untrusted (keyed by the origin sidecar).
Results below the threshold are never offloaded. Above the hard cap, the clean payload is truncated to 100,000 chars on disk and the inline reference says so. Results that are already-offloaded are skipped on subsequent passes. The observation masker recognizes the
[Tool result offloaded to disk: prefix and also skips those entries.
JIT guide injection
Verbose operational guides for specific tools — like the workspace customization guide foragents_manage, or the task delegation section — would consume significant tokens if included in the system prompt for every turn. Most turns never use these tools.
JIT guide injection defers them. The guides sit in memory, indexed by tool name. When a tool produces its first successful result in a session, Comis appends the corresponding guide as a text block at the end of that result:
deliveredGuides set tracks which guides have been delivered in the current session. Once delivered, the guide is never injected again. On session reset, the set is cleared.
If a tool call errors on its first invocation, the guide is not consumed — the delivery slot stays open so a successful retry can fire it. Consuming the slot on a failed call would make the guide silently disappear for the rest of the session.
- Tool guides — per-tool operational instructions (keyed by exact tool name).
- System prompt guides — deferred system prompt sections triggered by a tool name, such as the privileged tools section which fires once on the first use of any of 10 privileged tools.
Learned-skill listing stability
When the Verified Learning reflection layer is enabled (default-off), the agent’s promoted, read-only learned procedures (<source>learned</source>) are listed inside the SAME <available_skills> block as the platform skills — but always appended AFTER the platform skills. The platform byte-prefix therefore never shifts when a learned skill is added or removed, so adding a learned procedure does not invalidate the cached prefix of the skills block.
The listing is captured by a per-session prompt-skills freeze (snapshotted once when the session starts). A newly-promoted procedure is therefore picked up on the next session, not mid-session — the in-session listing stays byte-stable, and a demoted/archived procedure likewise leaves the listing only at the next freeze. With the loop off (the default) the block is byte-identical to a platform-only install.
Tool deferral and parallelism
Deferral
With 50+ tools available, sending every schema to the LLM on every call wastes ~81% of the tokens spent on tool definitions. Comis defers non-essential tools using a BM25-backed discovery tool (discover_tools).
Deferred tools are removed from the tools parameter entirely. Their schemas are stored in a DiscoveryTracker. When the agent needs a deferred tool, it calls discover_tools with a natural-language query. Comis runs BM25 search over the deferred set and re-injects matching tool schemas mid-turn.
Tool deferral is configurable per-agent:
Use
deferredTools.neverDefer and deferredTools.alwaysDefer to pin specific tools regardless of mode.
Parallelism
The SDK can execute multiple tool calls concurrently within a single turn. This is safe for read-only tools but unsafe for tools that modify files, databases, or external services — ordering conflicts can corrupt state. Comis classifies each tool as read-only or mutating. Read-only tools (file reads, web fetches, memory searches) run concurrently. Mutating tools share an async mutex and run one at a time, even when the SDK fires them in parallel. The classification is a static set; MCP tools default to the serialized path.Schema stripping
Whendiscover_tools loads a set of tools into the active context, it returns a <functions> block containing their full JSON schemas. Those schemas can be 1,720 tokens per discovery result. Once the schemas are loaded into the tools parameter for subsequent turns, the verbose block in the conversation history is pure redundancy.
After each turn, Comis scans the session history for discover_tools tool results containing <functions> blocks and replaces them with compact summaries:
[Discovery loaded:) are detected and skipped. Results without a <functions> block pass through unchanged. Stripping respects the cache fence: entries at or below the fence index are not touched.
MCP disconnect cleanup
MCP servers can connect and disconnect at runtime. When a server disconnects, any tools it provided are no longer callable. If those tools remain registered in theDiscoveryTracker, the agent may attempt to call them and receive confusing errors.
Comis subscribes to two MCP lifecycle events:
mcp:server:disconnected— removes all tools from the named server from every active session’s tracker.mcp:server:tools_changed— removes only the specific tools that were removed (not the entire server).
Configuration
These are the config keys most relevant to cache behavior. All live under the per-agent config block in yourconfig.yaml.
Cache retention
Gemini explicit cache
For Gemini agents,
cacheRetention has no effect. Gemini uses the CachedContent API exclusively, managed by GeminiCacheManager. TTL is fixed at 3600 seconds with a 50%-interval refresh on active sessions.Context engine
Tool lifecycle
Compaction (session)
Tips for long-running agents
If your agent drifts after many turns, the observation masker may be aggressively replacing tool results with placeholders before the model has used them. Try raisingcontextEngine.observationKeepWindow from 25 to 35 or 40.
If compaction runs too frequently, raise contextEngine.compactionCooldownTurns. The default of 5 turns is conservative. Sessions with slow-moving conversations (e.g., a Telegram channel where messages arrive every 10 minutes) can tolerate 10–15 turns between compactions.
If you see high cache-write costs on Haiku or Sonnet with small prompts, set adaptiveCacheRetention: false and cacheRetention: "short". This prevents the system from writing expensive 1h TTL entries on sessions that complete in under 5 minutes.
If your agent has many MCP tools, leave deferredTools.mode at "auto" and use deferredTools.neverDefer to pin the tools your agent uses on almost every turn. This avoids a discover_tools round-trip for high-frequency tools.
If a tool produces very large results (build logs, full file contents, API responses), the default 8K microcompaction threshold will offload them automatically. The agent can still read the full content on demand. If the offloaded path is causing confusion, raise maxToolResultChars per-agent to keep more content inline.
For Gemini agents, geminiCache is on by default — the CachedContent API caches the system instruction + tools server-side (guaranteed discount on cached input tokens) and is more predictable than relying on implicit prefix caching alone. Below the per-model minimum (~4096 tokens for gemini-3-pro / gemini-3.1-pro) the manager passes through uncached and Gemini’s implicit cache still covers the conversation tail. Set geminiCache.enabled: false to opt out (e.g. if your system prompt churns every turn, which would thrash the cache).