Lesson 4 had a stage called “context assembly” and another called “streaming replies.” Here we open both. How the gateway builds the exact prompt the model reads — under tight token budgets and a cache boundary — and how it streams the answer back into a chat app that has no concept of token-by-token output. You'll build a prompt assembler, a streaming chunker, and measure the cache win.
Picture the gateway in the middle of a turn. A human just typed something into WhatsApp. Before the model can think, the gateway has to build the exact block of text the model will read — who the agent is, what tools it has, what's in its workspace, what the user said. That's the first pipe: context in. Text flows toward the model.
Then the model produces an answer, and the gateway has to get that answer back onto the screen. But here is the catch that makes this hard: the model emits its reply token by token, a steady drip of word-fragments. A chat app like WhatsApp has no idea what a token is. It knows how to show a message — a finished chunk of text. So the gateway has to repackage a token stream into something a chat surface can actually display. That's the second pipe: tokens out. Text flows away from the model.
Notice these are genuinely different problems. The inbound pipe is about selection and ordering under a size cap — what to include, what to drop, what to put where. The outbound pipe is about buffering and chunking under a display constraint — how much to hold before you send, where it's safe to cut. We'll spend Chapters 1 through 5 on the first pipe and Chapters 6 and 7 on the second, then watch both run together in the showcase.
Why care this much? Because these two pipes are where an agent gateway feels either professional or broken. Get context assembly wrong and the model forgets its own name halfway through a long workspace, or you burn money re-sending the same unchanging text every single turn. Get streaming wrong and your assistant either spams the channel with one-line fragments or freezes for thirty silent seconds and then dumps a wall of text. The whole art is here.
Let's follow the inbound pipe. The single most important piece of text in the whole system is the system prompt — the instructions and context the model reads before the user's message. It tells the model who it is, how to behave, what tools exist, what's in the workspace. Everything downstream depends on getting this block of text right.
You might imagine the gateway building this prompt by stuffing variables into one giant template string. It doesn't — and the reason it doesn't is a clean lesson in software design. OpenClaw splits prompt-building into three layers, each with a strictly narrower job than the last.
Read the layers from the inside out and the logic is: the runtime adapters gather live facts, the config resolver folds in settings, and the pure renderer turns both into text. The renderer is deliberately dumb about where its inputs came from. That separation is what lets you reason about the prompt at all — the only layer that produces words is the one with no side effects.
There's one more flourish. The prompt is built from named sections — an interaction-style section, a tool-call-style section, an execution-bias section, and so on. Because each is named, a provider plugin can swap a small named section for a different model without rewriting the whole prompt. A model that likes terse tool calls gets a different tool_call_style section; the rest of the prompt is untouched. Named, swappable pieces — not one frozen block.
buildAgentSystemPrompt deliberately written as a PURE renderer that reads no files and calls no network?Not every agent run needs the full, lavish system prompt. A small sub-agent spun up to do one narrow job doesn't need the whole personality, the memory-recall instructions, the messaging etiquette, the voice hints. Sending it all that is wasted tokens — and tokens are both money and space. So the renderer takes a mode.
Modes are not just about cost; they are about fit. Every model has a hard ceiling on how much text it can read — its context window, measured in tokens. The prompt plus the conversation plus the room for the answer all have to live inside that ceiling. OpenClaw enforces model-specific token limits: the assembled prompt is checked against the target model's budget, and the prompt must fit. There is no “mostly fits.”
Below is the core of the inbound pipe in miniature: an assemble function that respects the mode and the budget. The numbers are scaled down so you can see the behavior on a few short strings, but the logic is the real logic — omit by mode, truncate per-section, stop at the total cap.
minimal mode. What is true of its system prompt compared to a full one?So far the prompt has been instructions about the agent. But the agent also needs to know about its world — the project it's working in, its own personality, who its owner is. That world lives in a set of workspace files, and the gateway injects them into the prompt when the agent boots. These are the bootstrap context files.
Each file has a job, and the gateway pulls them in by the lifetime they describe:
AGENTS.md — the project's working agreement: how this codebase or workspace expects an agent to behave.SOUL.md — the agent's personality: tone, character, voice.TOOLS.md — notes on the tools available here.IDENTITY.md & USER.md — who the agent is, and who the owner is.HEARTBEAT.md — what to do on a timed beat.BOOTSTRAP.md — first-run instructions, injected only for new workspaces.MEMORY.md — the long-term memory of the workspace.Now the constraint returns, because these files can be huge. OpenClaw enforces two caps. Each individual file is limited by bootstrapMaxChars — twenty thousand characters — and the files together are limited by bootstrapTotalMaxChars, sixty thousand. A workspace with a sprawling MEMORY.md would blow past both. So when a file is over its cap, the gateway truncates it and leaves a marker where the cut happened.
memory_search or memory_get — tools that read the complete file on demand. The prompt holds a bounded preview; the tools hold the rest. Truncation trades a smaller, cheaper prompt for one extra tool call when (and only when) the model actually needs the deeper content.
This is the same pattern you'll see again and again in the inbound pipe: put a small, bounded thing in the prompt; keep the full thing reachable by tool. The prompt is an index, not the library. And it scales down for sub-agents too: a sub-agent doesn't get the whole workspace — it's injected only AGENTS.md and TOOLS.md, the bare minimum to do focused work, keeping its context small on purpose.
MEMORY.md is far larger than the per-file cap, so the prompt only contains a truncated slice of it. Has the agent lost the rest of its memory?Bootstrap files told the agent about its world. Skills tell it about its abilities — reusable procedures it can follow, each documented in a SKILL.md file. A workspace might have dozens of them. If the gateway pasted every full SKILL.md into the prompt, the budget would evaporate before the user even spoke.
So OpenClaw does the same trick as with memory, sharpened. Into the prompt goes a compact list of available skills: for each, just its path and a small content-version marker. That's it — a menu. The full SKILL.md is read on demand, only when the model decides to use that skill.
There is still a budget here — even the menu can grow too long. OpenClaw caps the skills list with skills.limits.maxSkillsPromptChars (plus a per-agent allowance). But because the list is just paths and markers, that cap buys a lot of skills: hundreds of one-line entries fit where a handful of full files would not. This is the whole reason on-demand loading exists — it keeps the base prompt small while leaving an enormous library one tool-call away.
Here is the single most valuable idea in the inbound pipe, and it's about money. Every turn, the gateway sends the model a prompt. Most of that prompt — the identity, the project context, the skill menu — is the same as last turn. Re-processing identical text from scratch, turn after turn, is pure waste. LLM backends know this, and they fix it with prefix caching.
That last sentence is the whole game. Caching reuses the longest unbroken stable prefix. So a single volatile section placed near the top of the prompt poisons everything below it — even content that never changes. The fix is almost embarrassingly simple, and it's the trick: order the prompt by volatility.
Let's make this measurable. In the lab below, sections are tagged stable or volatile, and you'll compute how many characters the cache can actually reuse for two layouts: a GOOD one (all stable first) and a NAIVE one (a big volatile section sitting near the top). The gap between them is real money saved per turn.
And here is the same idea as a picture. Drag the cache boundary up and down a stacked prompt; sections above are reused (cached), sections below are recomputed. Flip on the naive layout to drop a volatile section high and watch the reuse number collapse.
A prompt is a stack of sections; teal = stable, orange = volatile. The dashed line is the cache boundary: everything above it is reused from the cache; everything below is recomputed this turn. Drag the line, and toggle naive layout to slip a volatile section up high — the reuse percent reacts. The real win comes from putting every stable section above the highest volatile one.
Switch pipes. The model is now producing an answer, token by token, and we have to get it onto a chat surface. The obvious dream is true token-by-token streaming — the words appearing one at a time, like a person typing. The problem: most channels can't take that. A WhatsApp or Telegram message is an atomic thing you send once; you don't dribble a single message into existence one character at a time. So OpenClaw streams in coarse blocks instead of token deltas.
What's a graceful place? The chunker prefers to break where text naturally breathes, walking down a break-preference ladder: first try to end at a paragraph break; if none, a newline; if none, a sentence end; if none, any whitespace; and only as a last resort, a hard break mid-word at maxChars. The ladder means blocks almost always end on a clean boundary — a finished thought — rather than slicing a word in half.
The controls expose all of this: blockStreamingDefault (off unless you opt in), blockStreamingBreak (whether to break at text_end or only at message_end), and blockStreamingChunk — the {minChars, maxChars, breakPreference} triple that tunes the bounds and the ladder. Let's build the core of it: the chunker that honors both bounds and the break ladder.
minChars but not yet reached maxChars. How does it decide where to cut the block?Block streaming gets clean chunks out. But raw chunks, fired as fast as they're ready, still feel wrong — a burst of five tiny messages, or a robotic instant reply. The outbound pipe has a second half whose entire job is to make the stream feel human and legible. Three mechanisms do it.
First, preview streaming. Some surfaces — Telegram, Discord, Slack — let you edit a message after sending it. OpenClaw exploits this to show progress in a single message that updates in place, rather than spamming new ones. It has modes:
Second, coalescing. Even with clean blocks, consecutive small chunks can arrive in a flurry. Coalescing merges them: it waits a short idleMs for the next chunk and, if one comes, glues them together, capped by maxChars. On chatty surfaces — Signal, Slack, Discord — the default minChars is bumped up to fifteen hundred so blocks are bigger and the channel doesn't fill with one-liners. Merge the dribble; send fewer, fuller messages.
NO_REPLY, which the gateway filters out entirely — no empty message reaches the surface. Finally, while a tool is running, the gateway can show a tool-progress preview (“searching the web…”) so the user isn't staring at silence during a long tool call.
Stack these and the outbound pipe is complete: blocks are cut cleanly (Chapter 6), merged so they aren't spammy (coalescing), paced so they feel human (humanDelay), shown live where the surface allows (previews), and suppressed entirely when there's nothing to say (NO_REPLY). The model emits a token drip; the user sees a calm, well-timed conversation.
minChars up to ~1500 and waits idleMs to merge consecutive chunks. What problem do coalescing and that higher floor solve?Here is the outbound pipe in motion, with the one rule that's hard to believe until you see it: the chunker never splits a code fence. Below is a long Markdown reply — prose, then a fenced code block, then more prose. Press Stream and watch the chunker emit blocks that honor minChars and maxChars and the break ladder, and that close and reopen the code fence rather than ever cutting it open. Slide the bounds and replay.
The full reply is the faint text; each emitted block is outlined as the chunker cuts it. Teal outlines end on a clean boundary; the orange region is inside a code fence — notice the chunker closes the fence at a block's end and reopens it at the next, so every block is valid Markdown. Tune minChars / maxChars and press Stream.
NO_REPLY when there's nothing to say.