โ† Tidelines/Best practices

The chat agent is getting a phone number

The chat assistant works, so the plan is to put the same prompt behind the phone line and add a short section for voice. Most of what the chat prompt says is about a screen, and the phone does not have one.

by TypeGlish team7 min read#best-practices
The phone has no screen.

TL;DR When a chat agent's prompt moves to a voice channel, do not append a voice section: make the channel a typed input, give each channel its own arm for length, format and link rules, build and read the voice artifact, and test only what holds on both channels.

Orla Broadband's chat assistant, Pip, has been answering broadband questions for a year. Next month the same assistant starts taking calls, and the ticket says reuse the chat prompt, add voice rules. That is a reasonable plan with a predictable first draft. Here are the eight checks for the pull request that moves Pip onto the phone line.

§1The voice section somebody appended

pip-patched.tg - the chat prompt, plus a voice sectionโœ— logic/quantifier-strength
# Role
You are Pip, the support assistant for Orla Broadband. ๐Ÿ˜Š

# Constraints
- MUST keep every reply to at most 4 sentences.
- MUST format steps as a bulleted list.
- WHEN a help article answers the question THEN MUST include a link to the article.

# Voice
- MUST keep every reply to at most 2 sentences.
- NEVER say a URL.
The first seven lines are the chat prompt as it shipped. The last section is the voice patch. Each half makes sense on its own.
tg check - pip-patched.tgโœ— build refused
$ typeglish check pip-patched.tg
pip-patched.tg:2:56  warn   structure/special-character  Emoji is outside the language: the instruction plane is checkable English, and models mirror prompt decoration into output. Remove it, or demonstrate it inside <examples>.
pip-patched.tg:5:1  error  logic/quantifier-strength  Conflicts with line 10. One rule, two counts โ€” "keep every reply to at most 2 sentences." already sits inside "keep every reply to at most 4 sentences." (per reply); the looser bound is dead weight. Keep the tighter line.
                โ†ณ line 10: - MUST keep every reply to at most 2 sentences.
pip-patched.tg:7:1  info   prompt/unintroduced-definite  "the question" retrieves something this document never introduces - a model must guess which question is meant. Mention a question on an earlier line, or anchor it here ("their question", "the question you ...").
pip-patched.tg:7:1  info   prompt/unintroduced-definite  "the article" retrieves something this document never introduces - a model must guess which article is meant. Mention an article on an earlier line, or anchor it here ("their article", "the article you ...").
pip-patched.tg:10:1  error  logic/quantifier-strength  Conflicts with line 5. One rule, two counts โ€” "keep every reply to at most 2 sentences." already sits inside "keep every reply to at most 4 sentences." (per reply); the looser bound is dead weight. Keep the tighter line.
                โ†ณ line 5: - MUST keep every reply to at most 4 sentences.

โœ— 1 file โ€” 2 error, 1 warning, 2 info

Check 1: strip the decoration. The smiley in the Role line has been there since launch, and in chat it was harmless at worst. The checker warns about it on any channel, because models mirror prompt decoration into their output. On the phone, a mirrored emoji either gets read aloud by the speech engine or silently dropped mid-sentence, and neither is a sound you want. Delete it. If the brand wants warmth, write a rule about tone, not a glyph.

Check 2: do not append a voice section. Under a # Voice heading, at most 2 sentences is not a voice rule. It is a second rule about every reply, and it sits beside the chat rule that allows 4. The checker refuses the build: one rule, two counts. Without the checker, the model gets both lines on every call and every chat, and picks.

A heading that says Voice is a label for the reader. The model reads every line under it on every channel.

§2One prompt, one arm per channel

Check 3: make the channel an input. Declare it with $REQUIRE variable channel: one of chat, voice and put the channel-specific rules in a $SWITCH ON @{channel} block. A $SWITCH needs an arm for every member of the domain, so the day somebody adds sms to the list, every switch without an sms arm fails the check instead of quietly defaulting to chat. How to turn parts of your system prompt on and off covers the mechanics in more depth.

tg check - pip.tg with sms added to the channel listโœ— structure/non-exhaustive-switch
$ typeglish check sms.tg
sms.tg:14:1  error  structure/non-exhaustive-switch  @{channel} can be sms, but no arm handles it โ€” add a "- <member>::" row for each (or a deliberate "- otherwise::" fallback).

โœ— 1 file โ€” 1 error, 0 warning, 0 info
The same file as below with one word added to the $REQUIRE line. The new channel cannot ship until somebody decides what it says.

Check 4: move every format rule into the chat arm. Bulleted lists, bold, numbered steps, Markdown of any kind: on the phone, they are either read out as symbols or flattened into a run-on sentence. Search the chat prompt for every rule about how a reply looks, and move each one under - chat::. If the chat side already fights Markdown, How to stop your support agent replying in Markdown has the rules for that arm.

Check 5: turn every link into an action. A caller cannot click. A rule that says include a link to the article becomes, on voice, a ban on saying the URL plus a tool that texts the link to the caller. Declare the tool, point the rule at it with @[text_link], and the checker holds the pointer to a real declaration.

pip.tg - one prompt, two channelsโœ“ A (97/100)
$REQUIRE variable channel: one of chat, voice

$TOOL text_link
  - description:: Send the caller a help article link by SMS.
  - input::
    - url:: string: the article link.

# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Pip, the support assistant for Orla Broadband.

# Constraints
@@ channel_rules: each channel keeps its own length and format rules, so the bounds never meet
$SWITCH ON @{channel}
  - chat::
    - MUST keep every reply to at most 4 sentences.
    - MUST format steps as a bulleted list.
    - WHEN a help article answers the question THEN MUST include a link to the article.
  - voice::
    - MUST keep every reply to at most 2 sentences.
    - NEVER say a URL.
    - WHEN a help article answers the question THEN MUST call @[text_link].

$TEST article
  - input:: How do I restart my router?
  - expect::
    - matches /^(?!.*(scroll|click|see below))/
    - at most 4 sentences
    - includes a link to the article or calls text_link
    - does not say a URL out loud
The two length bounds now live in different arms, so no single build carries both. The emoji is gone, and the link rule has a voice twin that uses a tool.

§3Read what the phone gets

Check 6: build the voice artifact and read it. The source holds both channels. Each deployment gets one. Bind the variable at build time and the losing arm never reaches the model:

tg check and build - pip.tg for voiceโœ“ no chat rules in the voice build
$ typeglish check pip.tg
โœ“ 1 file โ€” 0 error, 0 warning, 0 info

$ typeglish build pip.tg --vars '{"channel":"voice"}'
โœ“ built .typeglish/dist/pip.txt โ† pip.tg (92455fdda317, full)

$ cat .typeglish/dist/pip.txt
# Role
You are Pip, the support assistant for Orla Broadband.

# Constraints
- MUST keep every reply to at most 2 sentences.
- NEVER say a URL.
- WHEN a help article answers the question THEN MUST call text_link.
tg build - pip.tg for chatโœ“ the chat build is unchanged
$ typeglish build pip.tg --vars '{"channel":"chat"}'
โœ“ built .typeglish/dist/pip.txt โ† pip.tg (fc43610e37f4, full)

$ cat .typeglish/dist/pip.txt
# Role
You are Pip, the support assistant for Orla Broadband.

# Constraints
- MUST keep every reply to at most 4 sentences.
- MUST format steps as a bulleted list.
- WHEN a help article answers the question THEN MUST include a link to the article.
Two artifacts, two hashes, one source. The chat build reads like the chat prompt did before the move, minus the emoji, which is the regression check for the channel that already works.

Read the voice artifact out loud once. It is the fastest review there is for a voice prompt: anything that sounds wrong spoken by you will sound wrong spoken by the agent, and anything that only makes sense on a screen will stand out. If a rule about reading back account numbers or handling silence is missing, that is the next pull request; How to write read-back rules for a voice agent and Rewrite: nobody is on the other end cover both.

§4Test what both channels share

Check 7: search for screen words. Chat prompts and chat examples are full of scroll up, click and see below. Grep the source for them, then pin the result with a deterministic assert, so a later edit cannot bring them back: matches /^(?!.*(scroll|click|see below))/ passes only when none of those phrases appears in the reply.

Check 8: let the dry run name the rule one suite cannot cover. A $TEST in this file runs against whichever channel you build, so every expectation in it has to be true on both. A reply of at most 4 sentences is: the voice limit is 2. An expectation about bulleted lists is not, because it would fail on every call. So the format rule stays uncovered, and the dry run says so.

tg score and test --dry - pip.tgโœ“ 5/6 rules exercised
$ typeglish score pip.tg
pip.tg โ€” A (97/100)  proven errors: none  tiers: base+z3
  planes  runtime 96 (what the model reads) ยท hygiene 100 (source only)
  facets  enforceability 88 x.21 ยท hardness 100 x.12 ยท directness 97 x.08 ยท consistency 100 x.17 ยท structure 100 x.12 (hygiene) ยท annotation 100 x.12 (hygiene) ยท style 100 x.08 ยท security 100 x.08
  lever   enforceability 88/100 (up to +3 overall) โ€” Lead every rule with a modal โ€” MUST / NEVER / SHOULD โ€” or put it after THEN; one action and one object per line, a verb the checker knows or a bound ("at most 3 sentences"). Descriptive detail with no bound does not move this facet. `typeglish score <file>` prints the rules table with the edit per line; `typeglish --explain enforceability <file>` prints just that table.
  rules   6 of 8 rule-shaped lines read ยท 5/6 covered by $TEST
    L15  ยท arm           chat::
                         a $SWITCH / $IF arm body โ€” selected at compile time; the checker does not read arm bodies as rules
    L19  ยท arm           voice::
                         a $SWITCH / $IF arm body โ€” selected at compile time; the checker does not read arm bodies as rules

$ typeglish test pip.tg --dry
โœ“ pip.tg  coverage: 5/6 rules exercised
  uncovered (no case shares the rule's wording):
    L17  format steps as a bulleted list.
  ยท article โ€” "How do I restart my router?" (not run)
      โœ“ matches /^(?!.*(scroll|click|see below))/
      โœ“ at most 4 sentences
      ยท rubric โ€” 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
โœ“ 1 prompt โ€” 0 failed
The two arm headers appear in the rules table as lines the checker does not read as rules, which is where the enforceability points go. The one uncovered rule is the chat-only format rule, named by line.

That gap is the honest answer, not a defect to paper over. An expectation that is only true on one channel would turn the suite red on the other, and the usual reaction to a test that always fails is to delete it. Keep the uncovered line visible, and check it against the chat build on review.

§5Common questions

Can I use my chat agent's system prompt for a voice agent?
You can reuse most of it, but not unchanged. Rules about formatting, links, emoji and screen words like scroll up or click only make sense in chat. Make the channel a typed input, put the channel-specific rules in one arm per channel, and build a separate artifact for the voice deployment so the chat rules never reach the phone line.
Why does my voice agent read out asterisks, emoji or URLs?
Because the prompt still tells it to format replies, or decorates its own text, and the model mirrors that into its output. On a phone line the speech engine either reads the symbols aloud or drops them mid-sentence. Remove the decoration from the prompt, move every format rule into a chat-only arm, and replace link rules on voice with a ban on saying URLs plus a tool that texts the link.
Should a voice agent have shorter replies than a chat agent?
Usually, because a caller cannot scroll back. But write the shorter limit as the voice arm of a channel switch, not as an extra rule under a Voice heading. Two length bounds that both apply to every reply are one rule with two counts, and TypeGlish refuses to build it with logic/quantifier-strength.
How do I test a prompt that serves chat and voice?
Keep the shared test suite to expectations that are true on both channels, such as a sentence limit the longer channel allows and a deterministic assert that bans screen words like scroll or click. typeglish test --dry then names any channel-only rule the suite cannot cover, so the gap stays visible instead of being hidden behind a test that fails on one channel.
Field note

The diff that matters for this pull request is between the two artifacts, not between the old and new source. Build with --vars once per channel and put the two .txt files side by side. Every line that appears in both should be true on a screen and on a call; every line in only one should be there because of the channel. A line in the voice artifact that mentions how something looks is the bug the source review missed.

∿ washed up Oct 12, 2026 ∿