The request was small. The prompt was not

At Lindy, I could inspect the system prompts behind several agents. Each prompt mixed rules about safety, privacy, tools, formatting, and behavior.

The mismatch was obvious on ordinary requests. An email accepting lunch did not need every sensitive-data rule in the system. Those rules were still present on requests that could never activate them.

During the launch of Lindy Teammate, I watched a trivial greeting pull in an enormous amount of input context. Some of that may have been an internal accounting problem, but the prompts themselves contained plenty that the request could never activate.

This was my first time working this close to an agent's instructions. It led to the question that became PolicyC:

Given a policy prompt P and request x, can I construct a much smaller prompt Px without losing any critical obligations?

I made the policy explicit before I tried to compile it

Starting from an arbitrary natural-language prompt would have combined three research problems: extracting policies, selecting the relevant ones, and emitting them without changing their meaning. When an answer failed, I would not know which layer caused it.

I isolated the compiler with a synthetic enterprise-agent policy system and synthetic requests. No private production data entered the studies. Compiler 0.5 began with 39 manually structured policy nodes. Compiler 0.9 has 44 across six domains. Each node has a stable ID, activation triggers, dependencies, severity, instructions, obligations, and prohibitions.

Compilers 0.5 through 0.7 detected intents, selected nodes, closed every declared dependency, and emitted a stable list of rules. Compiler 0.8 inserted one request predicate before emission. That fixed the shape of one bug, but it still left policy semantics split between regular expressions and prose.

PolicyC compiler architecture by version
PolicyC compilers 0.5 through 0.8 match and retain policy nodes, close their dependencies, then specialize and emit a compact prompt. Compiler 0.8 adds one regular-expression predicate, while most state, limits, and precedence remain in matching code and emitted prose.
Figure 1A. Compilers 0.5–0.8 use a filter-first pipeline. Version 0.8 adds one specialization predicate without changing the larger architecture.
PolicyC compiler 0.9 reads the request once into a typed RequestState while selecting and closing policy nodes. For the held-out study, its model-assisted extractor makes one compile-time call per request and persists that state before either execution condition runs. Partial evaluation chooses declared branches, applies obligation precedence, lowers unavailable tools, and passes a resolved program to a printer that makes no policy decisions.
Figure 1B. Compiler 0.9 separates reading, selection, semantic evaluation, and printing; 0.10 extends the state and the field contracts without changing the shape. The held-out frontend made one model call per request and persisted the resulting state before either execution condition ran.
Polaris keeps the policy's original 61 clauses, deduplicates repeated boilerplate, and prunes only on a structural fact the case declares, giving a verbatim slice of about three thousand tokens. A separate model call reads that slice and the request and writes request-specific directives; under contract 2 it answers each condition listed from a frozen source-first index exactly once. Only the directives are appended for the answer call, and an empty reading renders the bare slice.
Figure 1C. Polaris, the second approach. The policy stays in its own words; a model reads the slice and the request and writes directives. Nothing deterministic sits between the request and the directive.

Compiler 0.9 is the architectural break. A frontend reads the request once into a typed RequestState. Policy nodes declare conditional branches. A precedence table decides which obligations yield to user limits or pending confirmation. Partial evaluation resolves those decisions before the printer emits Px.

The manual graph is still a deliberate boundary and a real limitation. PolicyC does not yet parse a production prompt into policy nodes. That let me study selection and semantic preservation without confusing them with errors from policy extraction.

Five frozen studies, five new held-out sets

Once I inspected a frozen study, its cases became development evidence and were never reused as fresh held-out evidence. Compilers 0.5 through 0.9 therefore faced different case sets. Their sequence shows what each version exposed, not a controlled comparison between versions.

Every case declared its request, context, obligations, prohibitions, tool expectations, and semantic rubric independently of the candidate prompt. The same request ran three times under the full policy and three times under the compiled slice using one pinned GPT-5 mini snapshot.

Five frozen studies used 280 cases and 1,680 planned trial slots. The table compares observed full-policy billed and compiler-slice billed cost per execution: 0.002592 dollars versus 0.001995 dollars for compiler 0.5; 0.003934 versus 0.002970 for 0.6; 0.002760 versus 0.002425 for 0.7; 0.002885 versus 0.002365 for 0.8; and 0.003427 versus 0.002837 for 0.9. Version 0.9's normalized columns use the same 177 complete pairs, while its run total includes 179 issued full-policy and 180 issued compiler-slice calls. Run cost across all issued paired-study calls was 4.9013 dollars. Compiler 0.9 also used 60 compile-time extractor calls costing 0.2678 dollars, reported separately.
Figure 2. Full-policy billed / execution and Compiler-slice billed / execution are observed costs on equal denominators within each version. Versions 0.5–0.7 use all planned executions; 0.8 and 0.9 use 163 and 177 complete pairs. The All issued / run total column includes every issued study call, including 0.9's unmatched 180th compiler-slice call. The five studies cost $4.9013; compiler 0.9's 60 extractor calls cost $0.2678 separately.

The execution system evolved with the compiler. A separate Python runtime bounded paid calls, tokens, tool use, and cost; persisted raw responses before parsing; resumed completed trials without paying twice; and tied artifacts together with hashes. Truncation, provider-schema rejection, accounting, and resume failures stayed in the audit trail instead of disappearing from the final numbers.

Starting with 0.7, I built anonymous packets that removed strategy, prompt size, token count, latency, cost, and the private answer map. Three isolated Codex reviewers graded each study against independently authored obligations. I hash-locked the merged grades before restoring strategy identities.

I designed, enforced, and audited the strategy-blind evaluation; isolated Codex reviewers performed the grading.

Compiler 0.5 proved that the active slice could be tiny

The first frozen study used 50 held-out cases, three samples per condition, and 300 model executions. It had no provider tools. Compiler 0.5 introduced a compact universal kernel for rules that every request still needed.

Mean actual input tokens fell by 98.23%. Uncached-equivalent cost fell by 67.80%, actual billed cost fell by 23.01%, and latency was 19.95% lower than under the full policy.

The behavioral result was weaker. Conditional critical-obligation preservation reached 85.98%, below the 95% target. Fifteen paired outputs passed under the full policy and failed under the compiled slice.

Eleven of those regressions shared one defect: a machine-oriented directive resembling report_unavailable_tool:web leaked into model-visible text. The model copied or acted on an internal control token that should never have become an instruction. The run also produced 26 incomplete responses at its 2,048-token output cap, leaving 129 of 150 complete pairs.

Compiler 0.5 established both sides of the project. Most policy text was inactive for a given request, but a very small slice could still preserve the wrong thing.

Compiler 0.6 showed that equal averages could hide losses

Compiler 0.6 removed the leaked directive, added policies derived from confirmed 0.5 regressions, and expanded the study to web and synthetic function-tool cases. Its fresh 50-case run again used 300 model executions.

Input reduction reached 89.69%, and conditional preservation reached 86.49%. The full and compiled conditions both had marginal pass rates of about 53.6%. Three complete pairs were semantically ungradable, so the strategy pass-rate denominators differ. Read alone, the averages still looked like parity.

Among 136 determinate pairs, 64 passed in both conditions, ten passed only under the full policy, nine passed only under the compiler, and 53 failed in both. A compiler-only success on one request could not cancel the loss of a critical behavior on another.

The regressions moved into tool and scope handling: calendar clarification and confirmation, image-tool availability, external-forwarding scope and privacy consequences, and a missing visible rationale. Two current-information failures looked primarily stochastic. Compiler 0.6 changed what I considered evidence. Similar averages were not preservation.

Compiler 0.7 found the semantic gap

Compiler 0.7 grew the graph to 43 nodes, hardened tool-availability handling, and incorporated selected regressions from 0.6. I froze a new 60-case set and ran three samples through the full and compiled conditions, producing 360 model executions and all 180 complete pairs.

Mean input fell by 93.75%, but the compiled condition preserved only 130 of 163 full-policy successes, or 79.75%. Across 180 pairs, 130 passed in both conditions, 33 passed only under the full policy, 11 passed only under the compiler, and six failed in both. The version passed only two of its six preregistered gates.

Post-lock attribution made the 0.7 failure concrete: 21 regressions came from emitter wording or semantic loss, seven from selector errors, two from a context-interface asymmetry, and three primarily from model variance.

The largest cluster involved confirmation. A selected policy could mean, "Ask before deleting something unless the user has already confirmed." The emitter flattened it into an unconditional command:

policy
ask before deleting unless already confirmed
state
confirmation already supplied
emitted
Required actions: ask_confirmation
result
asks again instead of executing

I was treating the prompt like a document that could be filtered. In reality, it behaved more like a program.

Words such as "if," "unless," "only when," and "already confirmed" are control flow. Selecting the right node was not enough if the emitter dropped its predicate, state, negation, or precedence.

It changed the project from a demo I was trying to prove successful into an experiment that was allowed to tell me I was wrong.

Compiler 0.8 proved that a safe predicate could still be too narrow

Compiler 0.8 added a specialization stage after dependency closure. Seven confirmation policies gained an alternative branch for requests that had already authorized the exact action. Any unknown or ambiguous state kept the conservative ask, so the new path failed closed.

I built the explicit_confirmation predicate from six spent 0.7 regressions, all phrased as "I confirm," and fifteen negative controls. It fixed every development case. In the 36-execution Phase G development run, all 18 compiler-slice executions called the confirmed tool with the confirmed arguments instead of asking again.

The fresh held-out set exposed the fit immediately. Its authors wrote "no need to check back," "everyone's confirmed," "I've been through the list," and "go ahead." The predicate matched none of the four act-side cases. Compiler 0.8 preserved 100 of 132 full-policy successes, or 75.76%, while reducing mean input by 94.76% and uncached-equivalent cost by 66.00%.

The fail-closed default kept the narrow predicate from authorizing the wrong action. It did not make the predicate useful on new phrasing. The larger failure class was tool restraint: fourteen full-only pairs called a tool that the user had limited or forbidden. Version 0.8 fixed one known symptom without giving the compiler a general representation for state, limits, or precedence.

Compiler 0.9 changed the architecture, not the verdict

Version 0.9 made PolicyC a compiler in the ordinary sense. A frontend reads each request once into a typed state. Policy nodes declare branches over that state. One obligation table encodes precedence. Partial evaluation chooses branches, applies masks, lowers unavailable tools, and records every decision. The emitter became a printer.

The study frontend used one schema-bound extractor call per request and persisted its output as part of the run identity. On 23 paraphrase fixtures, it read 21 correctly, compared with 12 for the deterministic reader. It recognized all six fresh authorization phrasings; the deterministic reader recognized one.

Reading the 60 held-out requests cost $0.2678. That compile-time extraction spend is reported beside the execution study, not netted into its cost reductions: the 17.21% billed and 58.69% uncached-equivalent figures compare only the paired full-policy and compiled execution conditions.

The architecture changed, but the fifth held-out study still failed. Among 177 complete pairs, compiler 0.9 preserved 104 of 137 full-policy successes, or 75.91%. Mean input fell by 92.99%, uncached-equivalent cost by 58.69%, and billed cost by 17.21%. Sixteen cases contained at least one full-only regression.

Primary strategy-blind semantic paired outcomes. Compiler 0.5 had 92 both-pass, 15 full-only, 9 compiler-only, and 13 both-fail pairs. Compiler 0.6 had 64 both-pass, 10 full-only, 9 compiler-only, and 53 both-fail pairs, plus 3 ungradable pairs. Compiler 0.7 had 130, 33, 11, and 6. Compiler 0.8 had 100, 32, 11, and 20. Compiler 0.9 had 104, 33, 21, and 19.
Figure 3. Primary strategy-blind semantic outcomes. Different datasets and denominators make the bars descriptive study results, not a controlled version comparison.

The diagnosis was different. Authorization was read correctly on every regressed case, so the failure that sank 0.8 did not recur. All 33 full-only pairs mapped to a compiler bin: 15 selector gaps, nine undeclared conditions, seven frontend misreads, and two cases of model variance.

The selector missed current information phrased as "as it stands today" or "in effect right now." A branch learned from one spent case asked again on fully specified spreadsheet and slide edits because those artifacts had no field contract. The extractor mistook scope or output-shape language for a tool limit. One unsafe case exposed a fact the state did not represent: approval from legal did not settle the consequence of forwarding privileged material outside the company.

The preservation number barely moved. The quality of the failure changed. Every regression now had an address in selection, the frontend, the intermediate representation, or model variance.

Compression was stable. Economics were not

Input reduction was the most stable result across the project. Every compiler removed between 89.69% and 98.23% of mean actual input. Uncached-equivalent cost fell between 55.36% and 67.80%.

The observed bill moved less. Actual billed-cost reduction ranged from 12.14% to 24.50%. Version 0.8 saved 18.03%, and 0.9's paired execution saved 17.21%; the latter excludes the separately reported $0.2678 extractor pass. Repeated full prompts benefited from prompt caching, while request-specific slices changed. Tool fees also mattered.

Latency moved in every direction. Compiler 0.8 was 7.07% faster than its full-policy condition, while 0.9 was 13.36% slower. Smaller context was useful, but it was not a direct proxy for the bill or wall-clock speed.

Compiler 0.10 failed its gate before it cost anything

Version 0.10 extended the state where v5 found gaps: current-information language, field contracts for artifact edits, the consequence of external disclosure, effect-aware masking so a pending confirmation suppresses acting tools but keeps reads. It passed every visible test.

Before I paid for a sixth study, I scored the deterministic reader once against five hidden slices someone else had written: 180 rows, 440 assertions. It matched 333 of 440, and 93 of 180 rows fully. That is frontend label agreement, not preservation, and it is a failed gate. The slices were spent on the score and never diagnosed. No paid run was made from 0.10.

Three regularities were now visible across 0.7, 0.8, and 0.9. Each version fixed the classes its development set named and lost to the classes the next fresh set named. Each fix fitted to a spent case generalized worst. And the compiled prompt's size and its behavior were governed by different things: a prompt at 3% of the original could still ask for a confirmation the user had just given.

I stopped building architectures and started isolating mechanisms.

The reference only agrees with itself 90% of the time

The preservation metric conditions on one sample of the full policy passing. Nobody had asked how often a second sample of the full policy passes. The v5 grades already had three full-policy samples per case, so the answer cost nothing: over 177 ordered pairs, a second sample reproduced the first sample's pass 90.4% of the time. Thirteen of 60 cases split.

A byte-identical copy of the full prompt would have failed my 95% gate. Compiler 0.9's 75.91% is 14.5 points below the reference's self-consistency, not 19 below the gate. That is the honest comparison, and it is why the preservation chart at the top of this article now carries a second line.

One correction I had to make to my own language: 90.4% is a baseline, not a ceiling. A different representation can be more stable, or more correct, than the reference and exceed it. From here every result I report has three numbers side by side: did the arm do what the policy says, did it do what the full prompt did, and how often does the full prompt agree with itself.

Polaris: keep the policy's words and let a model read them

Every compiler so far rewrote the policy before any branch ran. Tracing five cases stage by stage showed that authoring was where conditions died: "confirmation when appropriate" became always ask; "notes only when they help" became a mandatory format; "prefer drafts" became "do not draft." Only 18 of 97 visible cases ever took a branch. The other 79 shipped those defaults.

So I built an emission path that keeps the source's own bytes. A hand-audited map of 61 clauses, one per paragraph, line-anchored and hash-checked, with dependencies that travel and a rule that a clause can be removed only by a structural fact the case declares. I called the second approach Polaris.

The first thing it measured was embarrassing. The clause slice is 3,259 tokens, 80% below the full policy, and it is the same prompt for every visible case. Nothing was pruned; the whole reduction is deduplication. The synthetic policy repeats nine boilerplate lines in twenty sections: 13,371 of its 16,123 tokens. Real policy prose is about 2,450 tokens. A real prompt that does not repeat itself would gain almost nothing from this, and some of my 90%+ reductions had been this corpus's redundancy all along.

Then two small paid canaries, each with an isolated author writing fresh cases and a rubric before any output existed, and kill conditions fixed in advance. Canary 1: given the verbatim confirmation rule and a send the user had reviewed word for word, the clause slice re-asked in both samples. So did the compiler. Preserving a condition did not make the model apply it. Canary 2: quoting the request's own evidence beside the rule made it worse. The evidence arms re-asked on four of six satisfied samples against the full policy's two, and they re-asked about the exact items the quote had just established.

request
attendees confirmed, same link, no recurrence
emitted
rule + the user's sentence, quoted
result
"keep the same attendees, same link, no recurrence — please confirm"

The model re-verifies facts. It follows directives. Everything I built after this emits directives or nothing.

Five canaries. Canary 1: 4 cases, full, clause, and compiler arms, 29 calls, 0.0616 dollars, asked whether the model applies a preserved condition; no, it re-asked on the confirmed send. Canary 2: 8 cases, adding two evidence-binding arms, 80 calls, 0.1639 dollars; quoted evidence made it worse. Canary 3: 8 cases, adding the model reader, 92 calls, 0.411 dollars; the reader read 3 of 4 fresh confirmations with 0 unsafe actions. Canary 4: 8 cases, 3 cheaper readers, 24 calls, 0.026 dollars; all killed at the ledger gate. Canary 5: 8 cases, 4 readers and the condition list, 96 calls, 0.245 dollars; cheap readers still did not read. 321 paid calls and 0.907 dollars in total.
Figure 4. The canaries. Each one had a question, an isolated author, a rubric locked before any output, and, from canary 2 on, a separate isolated blind grader. Eight cases is direction, not rate, and every set is spent after one use.

Canary 3: a model read what the regex could not

The third canary held the clause slice fixed and added one thing: a gpt-5-mini call that reads the verbatim clauses and the request, resolves the policy's own conditions, and writes directives. Only the directives are appended. No RequestState, no regex, no enumerated authorization vocabulary anywhere on the path. The reader prompt was written before any case existed and never revised.

On the four fresh confirmation phrasings the author wrote ("word for word the text is…", "read it back and it is exactly…", "carry on…"), the deterministic frontend read authorization: absent on every one, so the compiler asked on every sample. The model reader read three as confirmed, wrote "proceed, do not ask for further confirmation," and the answering model acted on all six corresponding trials. Its one miss was in the safe direction.

Canary 3, sixteen trials per arm: the compiler slice passed 2 with 0 unsafe actions and 8 redundant re-asks and kept 1 of 6 full-policy passes; the clause slice passed 5 with 2 unsafe and 3 re-asks and kept 2 of 6; the full policy passed 6 with 3 unsafe and 2 re-asks and reproduced 2 of 6 of its own; the model reader passed 6 with 0 unsafe and 2 re-asks and kept 4 of 6. Canary 5 without a reader: compiler 4 passes, 8 re-asks, kept 4 of 5; clause slice 4 passes, 7 re-asks, kept 4 of 5; full policy 5 passes with 2 unsafe actions, reproduced 4 of 5 of its own; condition list 3 passes, 4 re-asks, kept 3 of 5.
Figure 5. Top: the model reader matched the full policy's passes, took no unsafe action where the full policy took three, and kept 4 of the full policy's 6 passes where the compiler kept 1. Bottom: canary 5's answer run, where the conditions were listed in the answer prompt with no reader.

The reader arm did not merely mimic the full policy. It was safer. The full policy forwarded privileged counsel material once and overwrote spreadsheet formulas twice; the reader's directive said to inspect the range first, and the model left it alone. Six versions had spent their effort reconstructing natural-language policy state with the wrong reader. What generalized was a model reading the policy's own sentences, because the ways a person says "I approve" are not enumerable.

Then the catch. The reader call cost $0.0059 and 24 seconds per request, two thousand of its tokens reasoning. The full-policy answer costs $0.0046 and 8 seconds, because 71% of that 17k-token prompt is served from cache at a tenth of the fresh price. The pipeline was twice the cost and four times the latency of doing nothing, with a prompt 80% smaller.

The problem inverted. Old Polaris was cheap and brittle. Model-reader Polaris was behaviorally right and twice as expensive as the thing it replaced.

Cheaper readers kept the rule and lost the request

The break-even budget followed from the arithmetic: the reader arm's own answer already ran at $0.0035 and 7.6 seconds, so the reader could spend about $0.001 and under a second for the pipeline to beat the full policy on both. Canary 4 held the reader contract fixed and varied only the model and reasoning effort, with a gate before any answer run: a reader that told the assistant to obtain confirmation on more than one of the five confirmed actions was killed.

All three were. Mini at minimal effort, with zero reasoning tokens, wrote seven contradictory resolutions on one confirmed forward and settled on "only after the user confirms." Nano at minimal returned an empty reading on every confirmed action. Nano at low effort read every confirmed action as "ask" and the privileged external send as "proceed." Every cheap reader was right on the unconfirmed cases and wrong on the confirmed ones. They kept the rule and lost the request, which is exactly what the regex had done.

I thought the failure was search: finding the request-dependent rules in three thousand tokens of clauses. So I audited the whole policy once, offline, for every sentence whose effect depends on what the request says, froze sixteen conditions over twelve clauses, and wrote a second reader contract. The conditions of the retained clauses are listed for the reader, and it must answer each exactly once with a verdict, a quote, and a directive; the runtime rejects anything else. Alongside, a single-call arm lists the same sentences in the answer prompt with no reader at all, to ask whether a separate reader was needed.

Reader cost per case on a log scale. gpt-5-mini at default effort cost 0.0059 dollars and 24 seconds under contract 1 and 0.0077 dollars and 30.6 seconds under contract 2, and read the confirmations both times, above the full-policy answer's 0.0046 dollars. mini at minimal effort cost 0.0026 and 0.0024 dollars and did not read. nano at low effort cost 0.00047 and 0.0009 dollars and did not read. nano at minimal effort cost 0.00014 and 0.0003 dollars and did not read. The break-even reader budget is about 0.001 dollars.
Figure 6. Every reader I tried, by cost per request. The only two that read the fresh confirmations are the two above the full-policy answer's cost. Everything under the break-even line did not read.

Canary 5 made the failure countable instead of a judgment call, because the author also wrote the expected verdict for every listed condition before any output existed. The reference reader agreed with the labels on 25 of 33, got every confirmed action's specific condition right, and got dearer: listing thirteen conditions made the one reader that reads think for 2,680 tokens. Mini at minimal filled the schema with "yes" on all eight cases and asked anyway in every directive. Nano at low could not even answer the list. Removing the search did not restore the comparison.

The single-call worklist did not substitute either. On a confirmed archive, with "Confirmation should specify the target, scope, operation, and consequence" sitting in its prompt under an instruction to resolve it first, it re-asked on both samples. It cut redundant re-asks against the bare slice and cost a third less than the full policy, and it kept fewer of the full policy's passes than the bare slice did.

A resolution the model performs for itself does not behave like a directive it is given.

One harness lesson I am keeping: my runtime records tool calls and does not execute them, and the author's tool schemas exposed identifier parameters. On three of five confirmed cases, every arm's only call was a lookup, the full policy included. Those cases said nothing about anyone. The next brief requires tools that let the requested action be taken in one call from what the request itself supplies.

Where I stopped

Ten paid studies, about $6.90 of provider spend, one pinned model. What they established: most policy text is inactive for most requests, and a compiled prompt removes it reliably, though on a redundant corpus much of that is deduplication. Deterministic compilation preserves three quarters to six sevenths of the reference's critical passes against a reference that reproduces itself nine times in ten. And the gap is made of reading, not slicing: the model acts on directives, the directive that generalizes comes from a model reading the policy's own sentences, and that reading costs about what the whole full-policy answer costs. Every attempt to buy it cheaper produced a reader that kept the rule and lost the request.

What it does not establish: any preservation rate for the reader architecture, which has eight cases behind it; whether the reference reader's directives change behavior under the second contract, which I designed and did not run; anything about another provider, where caching or reasoning prices could move the arithmetic either way.

I stopped here because the bound has a mechanism attached, and because the next question is not mine to answer with a synthetic policy: whether the price of reading the request falls with better models, cached readings across repeated requests, or a deterministic fast path with a semantic fallback on the ambiguous fraction of traffic. Everything needed to answer it is in the repository: the frozen sets, the locked rubrics and labels, the condition index over the source, a reader contract that makes a reading a checkable verdict, a gate that decides before money is spent, and every raw response.