<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>Pranay Varma</title>
  <link>https://prnay.site/</link>
  <atom:link href="https://prnay.site/feed.xml" rel="self" type="application/rss+xml" />
  <description>Notes on AI agent harnesses and the infrastructure underneath.</description>
  <language>en</language>
  <lastBuildDate>Fri, 12 Jun 2026 00:00:00 GMT</lastBuildDate>
  <item>
    <title>The system prompt is compiler output, not a string</title>
    <link>https://prnay.site/writing/system-prompt-is-compiled/</link>
    <guid isPermaLink="true">https://prnay.site/writing/system-prompt-is-compiled/</guid>
    <pubDate>Fri, 12 Jun 2026 00:00:00 GMT</pubDate>
    <description>Six harnesses, one pattern. The system prompt is a build artifact, not a hand-written string.</description>
    <category>harness</category>
    <category>prompts</category>
    <content:encoded><![CDATA[<p>I spent a few weekends reading the source of every open-source agent harness I could find. Codex. Pi. OpenClaw. Symphony. Claude Code. Hermes.</p>
<p>I went in expecting an architectural shouting match. Six teams, six harnesses, surely they fight about the basics.</p>
<p>They mostly do. There is one thing they do not fight about.</p>
<h2>The thing they all agree on</h2>
<p>The system prompt is not a string you write. It is the output of a build step that runs every turn.</p>
<p>Said differently. The naive way is to think of the system prompt as a fixed block of text you author once, paste into the SDK call, and forget. Every mature harness in the corpus rejects that view. They treat the system prompt as the product of a pipeline that runs before each model call, assembled from fragments owned by different parts of the codebase.</p>
<p>Pi calls it out by name. Their docs read: <em>&quot;The prompt is synthesized, not just stored.&quot;</em> Codex puts it in a section titled <em>Prompt lesson for builders</em>:</p>
<blockquote>
<p>A good harness should feel like it has a prompt compiler, not just a system prompt. Separate stable behavior, policy fragments, local instructions, environment state, and current turn input so each can evolve independently.</p>
</blockquote>
<p>Once you start looking at the code, it is everywhere.</p>
<h2>The shape</h2>
<p>The fragments do not vary much across harnesses. The order, mostly, does not vary either. The most stable layers sit at the bottom and the most dynamic at the top, with a cache boundary somewhere in the middle.</p>
<pre><code class="language-mermaid">flowchart TB
  subgraph stable[Stable / cached prefix]
    direction TB
    L1[Base instructions]
    L2[Tool inventory + policy fragments]
    L3[AGENTS.md walked root to cwd]
    L4[Skill inventory: names + descriptions in XML]
    L1 --&gt; L2 --&gt; L3 --&gt; L4
  end

  SENT[__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ sentinel]

  subgraph dynamic[Dynamic / per-turn suffix]
    direction TB
    D1[Environment context as diffable XML]
    D2[Session history + tool outputs]
    D3[Queued steer / current user turn]
    D1 --&gt; D2 --&gt; D3
  end

  L4 --&gt; SENT --&gt; D1
</code></pre>
<p>That picture is Codex&#39;s Figure 2 with the labels lightly rephrased. It is also Pi&#39;s Figure 4. It is OpenClaw&#39;s system-prompt builder enumerating roughly fourteen sections. It is Claude Code&#39;s chain of <code>getSimpleIntroSection</code>, <code>getSimpleSystemSection</code>, <code>getActionsSection</code>, <code>getUsingYourToolsSection</code>, <code>getSimpleToneAndStyleSection</code>, <code>getOutputEfficiencySection</code>, followed by a literal token <code>__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__</code>, followed by <code>getSessionSpecificGuidanceSection</code> and <code>computeSimpleEnvInfo</code>.</p>
<p>The point of the order is not aesthetics. The point is the prompt cache.</p>
<h2>Why the order is load-bearing</h2>
<p>Every layer above the cache boundary is content the provider can cache for a five-minute (Anthropic) or longer window. Every layer below is content that genuinely varies turn to turn. If you put a timestamp at the bottom of the stable section, every turn busts the cache. If you put your tool registry in a non-deterministic order, every turn busts the cache.</p>
<p>Claude Code has the funniest version of this. Their tool-registry file (<code>src/tools.ts:190</code>) ships with this comment:</p>
<blockquote>
<p>NOTE: This MUST stay in sync with <code>https://console.statsig.com/.../claude_code_global_system_caching</code>, in order to cache the system prompt across users.</p>
</blockquote>
<p>Their tool ordering is pinned to a remote feature-flag config because if the order drifts, the prompt-prefix cache silently breaks across users. The system prompt has become so much of a build output that it has a CI-style invariant attached to it. That is not a hand-written string. That is compiler output.</p>
<p>Hermes goes one step further. The system prompt is built once per session and stored as a column in SQLite (<code>sessions.system_prompt</code>). Every continuation reads that column back verbatim. Mid-session mutations require an explicit <code>--now</code> flag. The whole design exists to keep the byte-for-byte prefix stable across continuations.</p>
<h2>What gets compiled in</h2>
<p>Six things, every time, in roughly this order.</p>
<p><strong>Base instructions.</strong> Codex splits this into two files: <code>core/prompt.md</code> (stable contract) plus <code>core/gpt-5.2-codex_prompt.md</code> (model-family overlay). Pi exposes the split as <code>SYSTEM.md</code> (replaces the base) versus <code>APPEND_SYSTEM.md</code> (appends to it). Different verbs, same surface area.</p>
<p><strong>Tool inventory and policy fragments.</strong> Codex treats sandbox mode and approval policy as orthogonal fragment families, then assembles them as a Cartesian product:</p>
<pre><code class="language-text">permissions/sandbox_mode/{read_only,workspace_write,danger_full_access}.md
                              x
permissions/approval_policy/{on_request,on_failure,never,unless_trusted}.md
</code></pre>
<p>Approval rides on tool-schema fields like <code>sandbox_permissions</code>, <code>justification</code>, <code>prefix_rule</code>. It is part of the typed protocol. It is not a chat popup.</p>
<p><strong>Project context.</strong> <code>AGENTS.md</code> or <code>CLAUDE.md</code> walked root-to-cwd, deeper files later so they override broader ones. Loaded fresh at session boot, often cached for the lifetime of the session.</p>
<p><strong>Skill inventory.</strong> Names and descriptions only, usually wrapped in XML. Skill bodies (<code>SKILL.md</code>) load on demand via the Read tool when the model decides one is relevant. Inlining the bodies blows the cache and the context budget for capabilities the turn may never use. Pi&#39;s docs call this <em>progressive disclosure</em>.</p>
<p><strong>Environment context.</strong> Date, cwd, shell, network, OS. Codex serializes it as <code>&lt;environment_context&gt;</code> XML and diffs it turn to turn, so only the delta gets re-emitted. Most harnesses re-render the whole block every turn. Codex&#39;s approach is the better one.</p>
<p><strong>Session history and queued input.</strong> Whatever the model needs to continue.</p>
<p>Claude Code adds one more sentence to the memory layer that I think about a lot. When the harness loads <code>CLAUDE.md</code>, it wraps the contents with this:</p>
<blockquote>
<p>Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.</p>
</blockquote>
<p>Without that wrapper, project memory is just more prose. With it, project memory genuinely outranks the system prompt. A single sentence reorders the precedence stack. It is the kind of thing you only write after watching an LLM ignore a <code>CLAUDE.md</code> rule three times.</p>
<h2>The compile step, drawn</h2>
<p>A turn does not start by calling the model. It starts by building the thing you are going to send.</p>
<pre><code class="language-mermaid">sequenceDiagram
  autonumber
  participant U as User
  participant H as Harness
  participant C as Prompt compiler
  participant FS as Files
  participant M as Model

  U-&gt;&gt;H: input
  H-&gt;&gt;C: build_prompt(turn, history)
  C-&gt;&gt;FS: walk AGENTS.md root to cwd
  C-&gt;&gt;FS: scan skill inventory
  C-&gt;&gt;C: assemble stable layers
  C-&gt;&gt;C: insert __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__
  C-&gt;&gt;C: assemble dynamic suffix
  C--&gt;&gt;H: compiled prompt
  H-&gt;&gt;M: complete(compiled prompt)
  M--&gt;&gt;H: stream tool_use blocks + text
  H-&gt;&gt;FS: append entry to transcript

  Note over C,M: Stable prefix hits the cache. Suffix is fresh every turn.
</code></pre>
<p>The compile step has a few properties worth naming.</p>
<p>It is <strong>strict</strong>. Symphony renders its <code>WORKFLOW.md</code> body with a Liquid-like template engine that hard-fails on unknown variables or filters. Their stated reason: <em>&quot;it prevents accidental prompt drift or silent template bugs.&quot;</em> A naive renderer that swaps unknown variables for empty strings silently drops prompt content for a week before anyone notices. Strict beats lenient at this scale.</p>
<p>It supports <strong>hot reload, with a last-known-good fallback</strong>. Symphony&#39;s <code>workflow_store.ex</code> polls the workflow file every one second and reloads on mtime change. If the new file fails parse, the last valid copy stays mounted. Lenient renderers go silently blank. Strict renderers without a fallback brick the service during every typo. You want both.</p>
<p>It is <strong>scoped to one turn</strong>. Codex rebuilds its tool registry on every sampling request, then builds the prompt around it. Tools are not a global runtime object. They are an artifact of <em>this</em> turn&#39;s compile pass. That is what makes feature-flagged tools, per-turn allowlists, and sub-agent role gates work at all.</p>
<h2>The cache boundary is the contract</h2>
<p>Here is the part I missed for the first six months I worked on harnesses. The boundary between stable and dynamic is not just where caching kicks in. It is the contract between two halves of the codebase.</p>
<p>Above the line is the part the harness team owns. The base prompt. The tool registry. The skill index. The memory wrapper. It changes when you ship the harness.</p>
<p>Below the line is the part that changes every turn. Environment context. Recent history. The user&#39;s current message.</p>
<p>Once you pick that line, every interesting design decision falls into one of two questions. <em>Is this stable enough to cache?</em> and <em>Does this need to be re-checked every turn?</em> You can argue about whether <code>AGENTS.md</code> belongs above the line. (Most harnesses cache it. They re-read on session boot, not per turn.) You can argue about whether environment context belongs above or below. (Codex puts it just below, and diffs it.) The question is the same question.</p>
<p>If you do not have that line in your head, your prompt grows like a junk drawer.</p>
<h2>Where it breaks</h2>
<p>Three failure modes, all of them visible in someone&#39;s code.</p>
<p>The <strong>mega-string</strong>. One file. One prompt. Edited by hand. No layers, no order, no cache discipline. Works fine on day one. By month six the prompt has forty sections that contradict each other and nobody knows which one is load-bearing. The fix is not to write a better string. It is to break the string into fragments and write a compiler that joins them in a known order.</p>
<p>The <strong>lenient renderer</strong>. Templates that render unknown variables as empty strings. You rename a config key. The prompt silently goes blank for that section. The model degrades. Your evals move two points and you spend a week blaming the model upgrade.</p>
<p>The <strong>hot-reload with no last-known-good</strong>. Symphony&#39;s solution is the right one. If hot reload is allowed, the harness has to keep the last valid copy mounted, or every typo in production becomes an outage.</p>
<p>OpenClaw has a fourth one I keep thinking about. They have a regex-based reminder-honesty guard that watches the model&#39;s prose for claims like <em>&quot;I&#39;ll remind you&quot;</em> and <em>&quot;I&#39;ll follow up&quot;</em>. If no cron job was added on this turn and none exists on the session key, the harness appends a literal note to the transcript:</p>
<blockquote>
<p>Note: I did not schedule a reminder in this turn, so this will not trigger automatically.</p>
</blockquote>
<p>They do not trust the model&#39;s text to reflect durable system state, because compaction will eat the prose and leave the user holding nothing.</p>
<p>That is the same shape of problem the prompt compiler exists to solve. The model&#39;s words are not the runtime. The compiled prompt is.</p>
<h2>What I took away</h2>
<p>I no longer think of &quot;the system prompt&quot; as a file you edit. I think of it as a stream of fragments owned by different parts of the codebase, joined by a build step that runs every turn, split by a cache boundary that is itself the design.</p>
<p>If your harness has one big prompt string and you edit it by hand, that is the first thing I would refactor. Not because the string is bad. Because the absence of structure makes every other improvement harder.</p>
<p>A prompt compiler is the same idea as any compiler. Source artifacts go in. A canonical output comes out. The interesting work is the layer stack in the middle: the sentinels, the cache discipline, the strict template engine, the last-known-good fallback, the wrapper sentences that re-rank precedence.</p>
<p>Six harnesses, one pattern. The prompt is not the source. The fragments are the source. The prompt is what you ship.</p>
<hr>
<p>Related reading: <a href="/writing/six-rules-every-harness-agrees-on/">six rules every harness gets right</a>, more of what these same six codebases agree on.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Six rules every harness gets right</title>
    <link>https://prnay.site/writing/six-rules-every-harness-agrees-on/</link>
    <guid isPermaLink="true">https://prnay.site/writing/six-rules-every-harness-agrees-on/</guid>
    <pubDate>Fri, 12 Jun 2026 00:00:00 GMT</pubDate>
    <description>What I took from reading Codex, Pi, OpenClaw, Symphony, Claude Code, and Hermes. One rule, one example, one reason.</description>
    <category>harness</category>
    <category>design</category>
    <content:encoded><![CDATA[<p>After reading six agent harnesses end to end, the rules they share end up being more interesting than the things they fight about.</p>
<p>These six show up in every one of them. One sentence each. One concrete example each. One reason it matters.</p>
<h2>1. <code>stop_reason</code> is unreliable. Parse the stream.</h2>
<p>The naive way to dispatch a tool call is to wait for the response&#39;s <code>stop_reason</code> to come back as <code>tool_use</code>, then dispatch. That signal lies, and both Codex and Claude Code ship code comments saying so.</p>
<p>The right contract: as soon as a <code>tool_use</code> block arrives in the streaming response, schedule the tool future onto an ordered in-flight queue. <code>Completed</code> is for flushing pending text, updating token counts, emitting diffs, and deciding follow-up. Tool dispatch is not one of those.</p>
<pre><code class="language-mermaid">sequenceDiagram
  autonumber
  participant H as Harness
  participant M as Model
  participant T1 as Tool A
  participant T2 as Tool B

  H-&gt;&gt;M: complete(prompt)
  M--&gt;&gt;H: text chunk
  M--&gt;&gt;H: tool_use block A
  H-&gt;&gt;T1: dispatch A
  M--&gt;&gt;H: text chunk
  M--&gt;&gt;H: tool_use block B
  H-&gt;&gt;T2: dispatch B
  M--&gt;&gt;H: Completed
  T1--&gt;&gt;H: result A
  T2--&gt;&gt;H: result B
  Note over H,M: A and B run while M is still writing
</code></pre>
<p><strong>Why it matters:</strong> if you wait for <code>stop_reason</code>, you serialize every tool call behind generation length. Stream-parse and a single turn fires three tools in parallel while the model is still finishing prose.</p>
<h2>2. Approval lives in the tool schema. Not in chat.</h2>
<p>A popup that says <em>may I run <code>rm -rf</code>?</em> is unloggable, unauditable, and un-automatable. Every mature harness moves the question into the typed protocol.</p>
<p>Codex&#39;s <code>on_request.md</code> approval fragment teaches the model to request approval <strong>through tool parameters</strong>, not free-form text. The schema carries <code>sandbox_permissions</code>, <code>justification</code>, <code>prefix_rule</code>, <code>additional_permissions</code>. The model emits a structured request, the harness routes it. Sandbox mode and approval policy are kept orthogonal and assembled per turn as a Cartesian product:</p>
<pre><code class="language-text">permissions/sandbox_mode/{read_only,workspace_write,danger_full_access}.md
                              x
permissions/approval_policy/{on_request,on_failure,never,unless_trusted}.md
</code></pre>
<p>Pi makes the same point with one synchronous <code>tool_call</code> hook that can allow, block, or modify any call. Claude Code layers an auto-allowlist, then an LLM classifier, then a popup. Three shapes, one underlying rule. The policy is a contract, not a conversation.</p>
<p><strong>Why it matters:</strong> anything you negotiate in chat is something you cannot replay, audit, or feature-flag.</p>
<h2>3. Compaction is not cleanup. It is compression plus a recovery scaffold.</h2>
<p>Pi triggers compaction when the estimated token count crosses <code>contextWindow - reserveTokens</code> (default <code>reserveTokens = 16384</code>). The cut walks backward, snaps to turn boundaries so no tool result is orphaned from its assistant message, and produces a <code>CompactionEntry { summary, firstKeptEntryId }</code>. The <code>firstKeptEntryId</code> is a <strong>pointer</strong>, not a truncation. The raw JSONL stays on disk.</p>
<p>OpenClaw goes further. Before compaction, the harness runs a silent turn that appends durable facts to <code>memory/YYYY-MM-DD.md</code>. Append only, never overwrite. After compaction, it re-injects the <code>Session Startup</code> and <code>Red Lines</code> sections from <code>AGENTS.md</code>, wrapped in a <em>Post-compaction context refresh</em> envelope that explicitly tells the agent to re-run its startup sequence.</p>
<pre><code class="language-mermaid">flowchart LR
  A[Over budget] --&gt; B[Silent memory flush turn]
  B --&gt; C[Append memory/2026-06-12.md]
  C --&gt; D[Compact transcript]
  D --&gt; E[Re-inject AGENTS.md sections]
  E --&gt; F[Continue turn]
</code></pre>
<p><strong>Why it matters:</strong> if compaction is just <em>summarize and move on</em>, every long-running agent quietly forgets why it was working on the task. With a recovery scaffold, identity and constraints survive the compress.</p>
<h2>4. Skills live as an inventory. Bodies load on demand.</h2>
<p>Pi advertises its skill catalog as an XML block of names and descriptions only. The full <code>SKILL.md</code> body never enters the system prompt. When the model decides a skill is relevant, it calls the <code>read</code> tool to fetch the body.</p>
<pre><code class="language-xml">&lt;skills&gt;
  &lt;skill name=&quot;commit&quot; description=&quot;Stage, commit, push following repo conventions.&quot; /&gt;
  &lt;skill name=&quot;debug&quot;  description=&quot;Triage a failing test or unexpected behavior.&quot; /&gt;
  &lt;skill name=&quot;land&quot;   description=&quot;Open a PR, watch CI, merge when green.&quot;  /&gt;
&lt;/skills&gt;
</code></pre>
<p>That is the entire surface. Every harness calls this <em>progressive disclosure</em>. The inventory is short, the cache stays warm, and the body is fetched only on turns that actually use it. The same pattern applies to memory (<code>memory/YYYY-MM-DD.md</code> loaded by date), to project rules (<code>paths:</code> glob in YAML frontmatter), to documentation. The model is not told everything. The model is told <em>where to look</em>.</p>
<p><strong>Why it matters:</strong> the naive move is to inline every capability into the system prompt &quot;just in case&quot;. Six months later your prompt is forty thousand tokens, the cache is busted, and most of the bytes were never relevant to the turn at hand.</p>
<h2>5. The model&#39;s prose is not durable state.</h2>
<p>OpenClaw ships a file at <code>src/auto-reply/reply/agent-runner-reminder-guard.ts</code> that regex-matches the model&#39;s output for claims like <em>&quot;I&#39;ll remind you&quot;</em>, <em>&quot;I&#39;ll follow up&quot;</em>, <em>&quot;I&#39;ll check back&quot;</em>.</p>
<p>If a match fires AND no cron job was added this turn AND no existing cron exists on the session key, the harness appends this literal sentence to the transcript:</p>
<blockquote>
<p>Note: I did not schedule a reminder in this turn, so this will not trigger automatically.</p>
</blockquote>
<p>That is not natural-language understanding. It is a state reconciliation check between the model&#39;s prose and the durable scheduler. The model said it would do a thing. The harness asks the question the user actually cares about: <em>did the durable system record the commitment?</em> If not, the user gets told.</p>
<p>The same shape shows up elsewhere. ESAA forbids agents from writing files directly; agents emit <code>agent.result</code> intentions and the orchestrator validates and applies. Codex&#39;s <code>update_plan</code> is a UX tool, not a reasoning tool. Claude Code workers return structured <code>&lt;task-notification&gt;</code> payloads instead of free prose.</p>
<p><strong>Why it matters:</strong> compaction will eventually eat the model&#39;s prose. Whatever was only said, and never recorded, is gone.</p>
<h2>6. The session is a tree. Not a flat log.</h2>
<p>Pi&#39;s session is a JSONL tree of typed entries shaped like <code>{ id, parentId, timestamp, type }</code>. Types include <code>header</code>, <code>user</code>, <code>assistant</code>, <code>toolResult</code>, <code>bashExecution</code>, <code>custom</code>, <code>compaction</code>, <code>branchSummary</code>.</p>
<p>Branching is first class. An abandoned exploration gets a <code>branchSummary</code> entry so the work survives navigation away. Compaction inserts a summary pointer without destroying raw history. Plugins write their own state into the session file as <code>custom</code> entries, no separate database needed.</p>
<p>A flat transcript represents none of this. You either lose abandoned work, lose the ability to fork from a past turn, or invent a second source of truth to track what the transcript cannot.</p>
<p><strong>Why it matters:</strong> the moment you want to fork from turn 14 to try a different path, a flat log forces you to copy-edit a file. A tree handles it as one new <code>parentId</code>.</p>
<h2>The pattern underneath</h2>
<p>Every one of these rules has the same shape. <em>The model is not the runtime.</em></p>
<p>The runtime parses the stream. Owns the policy. Transforms the state. Indexes the capabilities. Reconciles the prose against durable systems. Structures the history. The model is invited in for one turn at a time, on a compiled prompt, under a typed contract, against a state it does not directly own.</p>
<p>That is the only sentence I would underline twice. Every harness in the corpus, on every axis I looked at, is built around it. The mistakes I see in production code are almost always one team or another quietly trusting the model to be the runtime.</p>
<hr>
<p>Related reading: <a href="/writing/system-prompt-is-compiled/">the system prompt is compiler output, not a string</a>, a seventh thing those six harnesses agree on, up close.</p>
]]></content:encoded>
  </item>
</channel>
</rss>
