Message Context Volatile vs Stable Split Architecture in Craft Agents

Craft Agents separates prompt context into volatile data that changes every turn and stable data that remains constant for the entire session, allowing the system prompt to be cached while dynamic information travels in the user message tail.

The craft-ai-agents/craft-agents-oss repository implements this message context volatile vs stable split architecture to optimize LLM prompt caching and reduce token costs. By dividing the prompt context into two distinct logical groups, the system prevents cache invalidation on every turn while maintaining byte-identical output compatibility with existing Claude-agent paths.

What Is the Volatile vs Stable Split?

The architecture partitions the agent's prompt context into two categories based on mutability:

Volatile context contains data that changes on every turn. This includes the current date-time, the session-state block, and optional source-state information. Because these values fluctuate constantly, they must travel in the user-message tail to avoid invalidating the cached system prompt.

Stable context contains information that remains static for the entire session. This includes workspace capabilities and the working-directory block. Since these values never change, they can safely reside in the system-prompt prefix and be cached across multiple turns.

How the Split Is Implemented

The PromptBuilder class in packages/shared/src/agent/core/prompt-builder.ts exposes two distinct methods to construct these contexts separately.

Building Volatile Context Parts

The buildVolatileContextParts method constructs the three dynamic blocks that change each turn. According to the source code, this method assembles the date/time block, session state block, and source state block.

// From packages/shared/src/agent/core/prompt-builder.ts (lines 86-102)
const volatile = builder.buildVolatileContextParts(
  { plansFolderPath: '/tmp/plans' },
  '<sources>\nActive: none\n</sources>'
);
// Returns: ['<date_time>…', '<session_state>…', '<source_state>…']

This method also consumes the one-shot mode-change signal, ensuring that permission-mode flags are emitted exactly once and not duplicated in cached content.

Building Stable Context Parts

The buildStableContextParts method creates the two invariant blocks that define the workspace environment. As implemented in packages/shared/src/agent/core/prompt-builder.ts (lines 136-148), this method returns the workspace capabilities and working-directory blocks.

// Stable part – goes into the system prompt
const stable = builder.buildStableContextParts();   
// Returns: ['<workspace_capabilities>…', '<working_dir>…']

These blocks are pure and idempotent, meaning they can be called multiple times without side effects and will always produce identical output for a given session.

Assembling the Final Prompt

The PiAgent.chat method in packages/shared/src/agent/pi-agent.ts demonstrates how the split is applied to construct the final message payload. The method routes stable parts to the system prompt and volatile parts to the user message.

// Inside PiAgent.chat() (lines 14-22)
const stableParts   = this.promptBuilder.buildStableContextParts();
const volatileParts = this.promptBuilder.buildVolatileContextParts(
  { plansFolderPath },
  sourceContext
);

// System prompt (cached)
const systemPrompt = [systemPromptHeader, ...stableParts].join('\n\n');

// User message (tail)
const userMessage = [...volatileParts, ...attachmentParts, message].join('\n\n');

This approach mirrors the Claude-agent path, preserving the byte-identical prompt layout while ensuring that dynamic context is appended only to the user message.

Why This Architecture Matters

The volatile vs stable split delivers three critical performance and reliability benefits:

Prompt caching efficiency. By keeping the stable context in the system prompt, the prefix remains byte-identical across turns. This allows the LLM provider to cache the system prompt and avoid re-processing it on every request, dramatically reducing latency and token costs.

One-shot signal isolation. When a permission-mode change is requested, the system emits a one-shot flag. Only the volatile builder consumes this signal (as seen in lines 58-67 of the prompt-builder), ensuring the flag is not duplicated in the stable cache and is consumed exactly once per session.

Byte-identical compatibility. The split builds the same five blocks in the same order as the original monolithic buildContextParts method. This guarantees that downstream logic, including the Claude-agent path, sees no difference in output format while gaining the performance benefits of the split.

Testing the Context Split Contract

The unit tests in packages/shared/src/agent/__tests__/prompt-builder-context-split.test.ts validate the architectural invariants. The test suite verifies three specific behaviors:

  • The combined context equals the concatenation of volatile plus stable parts.
  • Blocks are routed correctly to their respective message sections.
  • The one-shot mode-change signal is consumed exactly once on the volatile path.

These tests (lines 32-80) ensure that the volatile vs stable split maintains its contract as the codebase evolves, preventing regression in caching behavior or message assembly.

Summary

  • Craft Agents implements a message context split that separates volatile per-turn data from stable session data.
  • Volatile context (date-time, session state, source state) travels in the user message tail via buildVolatileContextParts to prevent cache invalidation.
  • Stable context (workspace capabilities, working directory) lives in the cached system prompt via buildStableContextParts.
  • PiAgent.chat assembles the final prompt by combining stable parts with the system header and appending volatile parts to the user message.
  • The architecture preserves byte-identical output with existing paths while enabling efficient LLM prompt caching and proper handling of one-shot signals.

Frequently Asked Questions

What happens if volatile data is placed in the system prompt?

Placing volatile data in the system prompt would cause a cache miss on every turn because the prompt content would change with each request. This would eliminate the performance benefits of prompt caching and increase token costs significantly, as the LLM provider would need to re-process the entire system prefix for every interaction.

How does the one-shot mode-change signal work?

The one-shot mode-change signal is a flag emitted when the agent needs to change permission modes. Only the volatile builder consumes this signal, ensuring it appears exactly once in the conversation. If the stable builder processed this signal, it would persist in the cached system prompt indefinitely, causing the mode change to be signaled repeatedly rather than consumed once.

Is the split architecture specific to the Pi-agent implementation?

No, the split architecture is implemented in the shared PromptBuilder class and is used by multiple agent types. The Pi-agent demonstrates the pattern in packages/shared/src/agent/pi-agent.ts, but the same byte-identical output guarantees allow any agent path to utilize the volatile vs stable split without modifying downstream logic.

Why are the stable blocks considered idempotent?

The stable blocks are considered idempotent because they derive entirely from the workspace configuration and session initialization, which remain constant throughout the conversation. Unlike volatile blocks that depend on the current timestamp or mutable session state, stable blocks will always return identical results for a given session, making them safe to cache and reuse indefinitely.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →