← Tidelines/Deep dives

Half your prompt arrives at runtime

Your platform prepends the help article, the customer record and this month’s promotion. The file you review is a fragment, and the seam between the two halves is the one place your gate has nothing to say.

by TypeGlish team8 min read#deep-dives
The file is not the prompt.

TL;DR The prompt your model reads is your file plus whatever the platform injects at runtime, and a checker can only prove things about the half it can see: declare the seam with $REQUIRE section so citations are tracked rather than assumed, and move any text you actually own onto the compile plane with $IMPORT file, where the proofs come back.

Open the prompt for any contact-center agent shipped in the last two years and you are looking at a fragment. Above it, at runtime, your platform is pasting in the retrieved help article, the customer record, the last four turns of the conversation, and a promotion block somebody in marketing edits from a CMS. You reviewed one of those. You checked one of those. The model reads all of them as one document.

§1The document has a co-author

This is not a criticism of your platform. Runtime composition is the correct architecture: the help article has to be the one that matches this question, the customer record has to be this customer’s, and neither can live in a file. The problem is narrower and it is entirely about where your gate sits. Your gate sits on the file. The defects live on the join.

Here is the shape it takes in practice. A broadband support prompt with three rules, all three written well, all three pointing at a block of text the file has never heard of.

undeclared.tg - three rules about a place that does not exist✗ blocked
# Role
@@ role: name the domain so "helpful" has a scope
You are a support agent for Northwind Broadband.

# Constraints
@@ grounding: the article is the only source of fact in this conversation
- You MUST answer only from <kb_article>.
@@ no_invented_price: a price the article does not carry is a price nobody approved
- You NEVER state a price that <kb_article> does not carry.
@@ handoff: an uncovered question is a handoff, not a guess
- WHEN <kb_article> does not cover a question THEN you MUST hand off to a human agent.
Annotated, measurable, second person, no hedges. Every rule refers to a section this document never mentions again.
tg check undeclared.tg - output
undeclared.tg:7:1   error  structure/dangling-section-ref  Reference to section <kb_article>, which is
  not defined.

undeclared.tg:9:1   error  structure/dangling-section-ref  Reference to section <kb_article>, which is
  not defined.

undeclared.tg:11:1  error  structure/dangling-section-ref  Reference to section <kb_article>, which is
  not defined.

 1 file - 3 error, 0 warning, 0 info
Three errors, one per citation, and the score comes back F (85/100): proven errors, grade capped at F, with structure 0 and every other facet untouched. The rules are fine. The pointer is not.

What the compiler is objecting to is not the sentence, it is the reference. A <tag> in TypeGlish is a place, the same way @{name} is a value and @[name] is a tool, and a pointer whose target does not exist is a dangling pointer whether or not the sentence around it reads well. Your file said answer only from over there and never said where over there is.

§2Name the hole

The fix is one line, and the reason it is worth a section is that the line is a declaration about your runtime, not about your prompt.

declared.tg - the seam, written down✓ compiles
<$CONFIG>
  $REQUIRE section kb_article
</$CONFIG>

# Role
@@ role: name the domain so "helpful" has a scope
You are a support agent for Northwind Broadband.

# Constraints
@@ grounding: the article is the only source of fact in this conversation
- You MUST answer only from <kb_article>.
@@ no_invented_price: a price the article does not carry is a price nobody approved
- You NEVER state a price that <kb_article> does not carry.
@@ handoff: an uncovered question is a handoff, not a guess
- WHEN <kb_article> does not cover a question THEN you MUST hand off to a human agent.
Not one rule changed. 0 error, 0 warning, 0 info, and F (85/100) becomes A (98/100) on the strength of structure 0 going to 100.

Now build it, because the artifact is where the mental model becomes concrete.

tg build declared.tg - the artifact
 built .typeglish/dist/declared.txt ← declared.tg (c30e46abb9aa, full)

# Role
You are a support agent for Northwind Broadband.

# Constraints
- You MUST answer only from <kb_article>.
- You NEVER state a price that <kb_article> does not carry.
- WHEN <kb_article> does not cover a question THEN you MUST hand off to a human agent.
The $REQUIRE line is gone, stripped like every $-command. The <kb_article> tags survive verbatim. What the compiler shipped is a hole with a name on it, and the name is the contract your runtime has to honor.

Two smaller behaviours make the declaration worth more than a comment. Declare a section and never cite it and you get structure/unused-import: Imported section "customer_record" is never used, which under check --strict is an error and exit 1. And the declaration is scoped to the name you wrote, so a citation of <kb_articles> beside a declaration of kb_article is still a blocking structure/dangling-section-ref plus an unused-import warning for the one you declared and never used. Both halves of the typo get reported.

A declared seam is a symbol. An undeclared seam is a hope with a bracket around it.

§3What crosses the seam is not checked

Here is the part that changes how you review a prompt, and it needs a controlled pair to see properly. Two files, same agent, same policy, differing in exactly one thing: whether the promotion text lives in the file or arrives at runtime.

The scenario is the ordinary one. Marketing runs a promotion this month, it is edited in a CMS, and your platform prepends it to the system prompt. Finance, separately, has a rule about fee waivers that has been in the agent prompt since launch. Nobody has read both sentences in the same sitting for a while.

pasted.tg - the promotion pasted into the file✗ blocked
<$CONFIG>
  $REQUIRE section kb_article
</$CONFIG>

# Role
@@ role: name the domain so "helpful" has a scope
You are a support agent for Northwind Broadband.

# Context
@@ fee_def: the charge is ours, so the model cannot know which one is meant
The fee IS a one-off charge for connecting a new line.

# Current promotion
@@ promo: pasted out of the CMS block the platform injects this month
- You MUST waive the fee.

# Constraints
@@ grounding: the article is the only source of fact in this conversation
- You MUST answer only from <kb_article>.
@@ no_waivers: a fee waiver is a retention decision and it costs margin
- You NEVER waive the fee.
Two rules written eighteen months apart by two departments, in one file for the first time.
tg check pasted.tg - output
pasted.tg:15:1  error  logic/contradiction  Logical conflict - "waive the fee" is both required and
  forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
  (conflicts with line 12)

pasted.tg:21:1  error  logic/contradiction  Logical conflict - "waive the fee" is both required and
  forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
  (conflicts with line 9)

 1 file - 2 error, 0 warning, 0 info
F (79/100), runtime 72, consistency 0, and every other facet at 100. Exactly what you want a gate to do.

Now move that promotion back where it actually lives, which is at runtime, behind a section your platform composes in. Same policy. Same words. Same two departments disagreeing.

injected.tg - the promotion delivered by the platform✓ compiles
<$CONFIG>
  $REQUIRE section kb_article, current_promotion
</$CONFIG>

# Role
@@ role: name the domain so "helpful" has a scope
You are a support agent for Northwind Broadband.

# Context
@@ fee_def: the charge is ours, so the model cannot know which one is meant
The fee IS a one-off charge for connecting a new line.

# Constraints
@@ promo: the CMS block the platform injects this month
- You MUST follow <current_promotion>.
@@ grounding: the article is the only source of fact in this conversation
- You MUST answer only from <kb_article>.
@@ no_waivers: a fee waiver is a retention decision and it costs margin
- You NEVER waive the fee.
0 error, 0 warning, 0 info. check --strict returns the same line and exit 0. A (98/100) with consistency 100, which is the facet that was 0 a moment ago.

The two files ship the same contradiction to the same model. One of them is F (79/100) with two blocking errors and no build; the other is the best-scoring file in this post. Nothing was fixed in between. The text simply moved to the other side of the seam, where the prover cannot reach it.

Note what is not happening here. This is not a lenient default that --strict would tighten, and it is not an advisory finding somebody has to notice. There is no finding. There is no severity to escalate, because there is no text to analyse. consistency 100 is an honest report about the document the compiler was handed, and the document the compiler was handed is half the prompt.

§4Move the seam when you own the text

Which raises the question the rest of the post is about: does that promotion actually have to arrive at runtime? It changes monthly, not per conversation. It is written by people who work here. It is text somebody could put in a file.

Put it in one, and compose it with $IMPORT file instead.

prompts/promotion.tg - the CMS block, now a file in the repo✓ compiles
# current_promotion
- You MUST waive the fee.
A fragment, and it checks like one: alone it is 0 error, 0 warning, 1 info, the info being prompt/unintroduced-definite on the fee, a noun this file does not introduce because the file that imports it does.
prompts/agent.tg - one seam runtime, one seam compile-time
<$CONFIG>
  $REQUIRE section kb_article
  $IMPORT file "promotion.tg" as promo
</$CONFIG>

# Role
@@ role: name the domain so "helpful" has a scope
You are a support agent for Northwind Broadband.

# Context
@@ fee_def: the charge is ours, so the model cannot know which one is meant
The fee IS a one-off charge for connecting a new line.

# Constraints
@@ promo: the promotion is a file in this repo now, not a runtime injection
@<promo.current_promotion>
@@ grounding: the article is the only source of fact in this conversation
- You MUST answer only from <kb_article>.
@@ no_waivers: a fee waiver is a retention decision and it costs margin
- You NEVER waive the fee.
The article still arrives at runtime, because it has to. The promotion is transcluded at compile time by @<promo.current_promotion>, which drops the section’s content in where the pointer sits.
tg check prompts - the program pass
prompts/agent.tg:2:3   warn   import/unmet-section-requirement  $REQUIRE section kb_article is unmet -
  no file in this composition declares <kb_article>, so citations of it stay promises. Declare
  <kb_article> in a file this one composes with, or keep the $REQUIRE if the platform supplies
  the section at deploy time.

prompts/agent.tg:16:1  error  logic/contradiction  Logical conflict - "waive the fee" is both required
  and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN
  ...). (conflicts with line 12)

prompts/agent.tg:20:1  error  logic/contradiction  Logical conflict - "waive the fee" is both required
  and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN
  ...). (conflicts with line 10)

prompts/promotion.tg:2:1  info   prompt/unintroduced-definite  "the fee" retrieves something this document
  never introduces - a model must guess which fee is meant. Introduce it on another line ("You
  manage a fee.") or name it outright.

 2 files - 2 error, 1 warning, 1 info
program: 1 root - prompts/agent.tg (2 files)
The contradiction is back, and it is back because the text is back. Two errors, one per participating rule, exit 1, no build.

Read the warning at the top, because it is the compiler stating this post’s thesis in its own words: no file in this composition declares <kb_article>, so citations of it stay promises. That is the correct state for the help article, which genuinely does arrive at deploy time. It would be a bug report for anything else.

Two wrinkles worth knowing before they cost you ten minutes. The program pass runs on directory targets only, so tg check prompts is two errors and exit 1 while tg check prompts/agent.tg on the same tree is 0 error, 0 warning, 0 info and exit 0; a per-file CI loop is this gate with the proofs switched off, which the paragraph that lives in three prompts works through in full. And a transcluded rule is reported at the line of the pointer that pulled it in, not at its line in the imported file, so line 16 above is the @<promo.current_promotion> row.

§5Which seam for which content

The decision is not stylistic and it has one test: does this text change per conversation?

  • Yes, per conversation. The retrieved article, the customer record, the order list, the transcript so far. These have to arrive at runtime, so declare them with $REQUIRE section and cite them by name. You get a checked pointer and nothing more, which is the honest ceiling for content that does not exist yet when you check.
  • No, but it changes monthly. The promotion, the current SLA, the seasonal escalation path. This is the category that quietly moved to runtime because a CMS was convenient, and it is the category where you lose the most. Put it in a .tg file, compose it with $IMPORT file, and let the person editing it hit the same gate you do.
  • No, it changes when policy changes. Brand voice, refund caps, verification rules. If any of this is being injected by your platform rather than living beside the agent, that is not composition, that is a second prompt nobody is reviewing.

And then the rule that survives both seams: keep the rules about the injected text in the file. You cannot check the article, but You MUST answer only from <kb_article> is a rule in your file, on your plane, graded on enforceability and provable against everything else you wrote. The seam limits what the compiler can read. It does not limit what you can say about it.

Check the half you own. Declare the half you do not. Move the half that never needed to be over there.

§6Common questions

Why does my agent ignore the knowledge base article my platform injects?
Usually because nothing in the prompt tells it that the article outranks everything else it knows, and nothing in the prompt names the article as a place. A rule that says answer only from the help article is addressed to a reader who cannot see which text you mean. Cite the injected block as a section, You MUST answer only from <kb_article>, and declare it with $REQUIRE section kb_article so the citation is checked rather than hoped for. If the agent still drifts, read the built artifact: the tag is passed through verbatim, so if your runtime prepends the article under a different tag name, the rule is pointing at a place that never arrives.
How do I check a system prompt that my platform assembles at runtime?
Check the half you own and declare the half you do not. Every block your runtime prepends gets a $REQUIRE section line and a named citation in the rules, which turns a silent assumption into a symbol the compiler tracks: a citation with no declaration is a blocking structure/dangling-section-ref, a declaration nothing cites is structure/unused-import, and a misspelled citation still errors even when the correct name is declared. What the compiler cannot do is read text it never receives, so no diagnostic will ever fire on the content of an injected block.
What is $REQUIRE section for?
It declares that a host composes a named section into the prompt at deploy time, so citations of that section resolve instead of dangling. It is a promise rather than a proof, and the compiler is explicit about that: run a directory check on a composition where nothing supplies the section and you get import/unmet-section-requirement, whose message says citations of it stay promises. That warning is the correct state for a genuinely host-supplied block, and it is a bug report for a block you thought lived in a file.
Should our policy text live in the prompt file or be injected at runtime?
Inject only what varies per conversation, and keep everything else in a file. The help article and the customer record change on every contact, so they have to arrive at runtime. This month’s promotion, the escalation matrix and the brand voice do not change per conversation, and every one of them that arrives at runtime instead is a rule your gate cannot see. Move those into a .tg file and compose them with $IMPORT file, and the proofs come back: the same two sentences that were invisible across a runtime seam are two blocking logic/contradiction errors across an import seam.
Field note

The compile-time and runtime planes inside a single file are laid out in your system prompt has a compile time, and this post is the same split seen from one level up: the file has a compile time, and the prompt has an assembly step that happens after your build. The habit that catches both is the same one. Build the file, read the artifact, and count what is missing from it, which is also how the policy doc somebody pasted into your agent turns out to be addressed to a reader who already had the context.

∿ washed up Aug 12, 2026 ∿