> Markdown rendition of https://typeglish.dev/blog/build-missing-parcel-agent ("Building a missing-parcel agent, prompt-first - Tidelines"). Canonical page: https://typeglish.dev/blog/build-missing-parcel-agent · All pages: https://typeglish.dev/llms.txt

[← Tidelines](https://typeglish.dev/blog)/ Guides Oct 5, 2026

# Building a missing-parcel agent, prompt-first

The tracking page says delivered. The doorstep says otherwise. Every online retailer gets this contact daily, and the brief for the agent that handles it fits in six lines. Two of them disagree about the replacement, and the one case the brief forgot is the most common one.

by **TypeGlish team** 8 min read #guides

Delivered, it says.

**TL;DR** A delivered-but-not-received agent built from the usual brief would send a replacement on the first turn, because *send a replacement straight away* and *never send a replacement before the carrier has investigated* are the same tool with opposite force; take the replacement out of the agent’s hands, let the host pass in the hours since delivery and the claim count, let a `$IF` chain pick the route at build time, and test each arm by building it.

Brackwater is a fictional homeware retailer. Its couriers mark a few parcels a day as delivered that the customer never sees: left with a neighbour, left in a bin, left at the wrong number, or occasionally not missing at all. The head of customer care wants an agent, Nell, to take the first contact for all of them, and sends the brief below. Here is what the compiler makes of it, and the three decisions that turn it into a spec.

## §1 The brief, checked as written

None of this is bad policy. It is how a care team lead talks after a few hundred of these contacts.

brief.tg - the six lines as sent ✗ D (63/100)

```typeglish
# Role
You are a helpful assistant for Brackwater that helps customers whose parcel says delivered but never arrived.

# Constraints
- Apologise and send a replacement straight away.
- Ask them to check with neighbours and their safe place first.
- If it was marked delivered less than 48 hours ago ask them to wait.
- Customers who have claimed before should go to the claims team.
- Don't accuse the customer of lying.
- Never send a replacement before the carrier has investigated.
```

Six constraints: a remedy, a first check, a waiting period, a repeat-claim route, a tone rail, and a control.

tg score brief.tg - the rules table

```
$ npx typeglish score brief.tg
brief.tg — D (63/100)  proven errors: none  tiers: base+z3
  planes  runtime 70 (what the model reads) · hygiene 45 (source only)
  facets  enforceability 28 x.21 · hardness 90 x.12 · directness 66 x.08 · consistency 100 x.17 · structure 89 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 57 x.08 · security 100 x.08
  rules   1 of 6 rule-shaped lines read · 1 not provable
    L5   ✗ invisible     Apologise and send a replacement straight away.
                         no modal (MUST / NEVER / SHOULD) and "Apologise" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L6   ✗ invisible     Ask them to check with neighbours and their safe place first.
                         no modal (MUST / NEVER / SHOULD), and the checker knows "ask" but cannot prove this shape: two objects or actions joined by "and" — it proves one at a time, so it reads as prose → lead with a modal (MUST …) to make it a rule, and one object per line: split into two rules to make it provable
    L7   ✗ invisible     If it was marked delivered less than 48 hours ago ask them to wait.
                         the condition has no THEN branch, so nothing is required when it holds → add THEN MUST / NEVER / SHOULD <verb> …
    L8   ✗ invisible     Customers who have claimed before should go to the claims team.
                         no modal (MUST / NEVER / SHOULD) and "Customers" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L9   ✗ invisible     Don't accuse the customer of lying.
                         no modal (MUST / NEVER / SHOULD) and "Don't" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L10  ~ unprovable    Never send a replacement before the carrier has investigated.
                         the checker knows "send" but cannot prove this shape: it stops reading at "before" — the clause after it is not part of the provable action → if the clause is a condition, make it one: WHEN <clause> THEN MUST <verb> …; otherwise give it its own rule
```

Trimmed to the grade, the facets, and the rules table. One of six lines is read, and that one stops at *before*.

Look at `consistency 100`. With five lines invisible and the sixth cut short at *before*, nothing in the file is in a position to disagree with anything else. The two replacement lines, first and last, are the brief’s real argument, and the grade cannot see it yet.

## §2 The replacement is not the agent’s to send

The mechanical pass gives every line a modal and a catalog verb. *Send a replacement* is something a warehouse does, not something Nell says, so it becomes a tool and the rule becomes a call. *Before the carrier has investigated* becomes the guard on the control at the bottom.

rules.tg - the brief, promoted line for line ✗ logic/contradiction

```typeglish
$TOOL send_replacement
  - description:: Send the customer a replacement for the missing order.
  - input::
    - order_id:: string: the order number.

# Role
You are Nell, the delivery assistant for Brackwater.

# Context
A customer contacts you when the courier marks their parcel as delivered and it never arrives.

# Constraints
- MUST apologize once.
- MUST call @[send_replacement].
- MUST ask the customer to check their safe place.
- MUST ask the customer to check with their neighbours.
- WHEN the courier marked the parcel as delivered less than 48 hours ago THEN MUST ask the customer to wait.
- WHEN the customer has claimed before THEN MUST escalate to a supervisor.
- NEVER accuse the customer of lying.
- UNLESS the courier finishes an investigation THEN NEVER call @[send_replacement].
```

Line 5 of the brief became line 14. Line 10 of the brief became line 20.

tg check and build - rules.tg ✗ nothing written

```
$ npx typeglish check rules.tg
rules.tg:14:1  error  logic/contradiction  Conflicts with line 20. Logical conflict — "call @[send_replacement]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
          ↳ line 20: - UNLESS the courier finishes an investigation THEN NEVER call @[send_replacement].
rules.tg:20:1  error  logic/contradiction  Conflicts with line 14. Logical conflict — "call @[send_replacement]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
          ↳ line 14: - MUST call @[send_replacement].

✗ 1 file — 2 error, 0 warning, 0 info

$ npx typeglish build rules.tg
typeglish build: rules.tg refused — nothing written
```

Send it now, and never send it until the courier is done: on the first turn of every contact, both apply.

That conflict was in the prose too; the compiler only made it impossible to ship. A model reading the brief gets the remedy in the first line and the control in the last, and on a busy day the first line wins. Each early replacement is a second parcel out of the warehouse while the first one may well be sitting with a neighbour.

There are two honest fixes. One is to scope the call with a matched `WHEN` / `UNLESS` pair, the shape [the flight-disruption walkthrough](https://typeglish.dev/blog/build-flight-disruption-agent) uses for rebooking, where the passenger’s yes is the trigger and the agent can see it. Here it cannot: whether the courier has finished investigating is not something the customer says in the chat, and a model told to wait for it will take *I already checked with the courier* as proof. So Brackwater takes the other fix. The claims team decides every replacement after the courier reports, and Nell never holds the tool at all. A rule that forbids a tool is weaker than a tool that was never handed over, which is the whole of [the bake-off on forbidding a tool vs. never handing it over](https://typeglish.dev/blog/forbid-tool-vs-withhold).

What Nell does hold is the step before: `open_courier_claim`, which starts the investigation. The replacement rule becomes `NEVER offer a replacement.`, and a second rule tells Nell what to say instead when the customer asks. The tone line gets the same treatment. *Accuse* is not a verb the checker knows, so `NEVER accuse the customer of lying` scores as unprovable; what a reviewer can actually catch in a transcript is the word, so it becomes `NEVER mention fraud.`

## §3 Nell cannot see the tracking page

*Less than 48 hours ago* asks the model for a date difference it has no inputs for, and *customers who have claimed before* asks it for an order history it cannot see. Both numbers live in the order system. The host passes them in, and a `$IF` chain picks the route. The first draft writes the two exceptions the brief named:

chain.tg - the route, first draft ✗ structure/non-exhaustive-switch

```typeglish
$REQUIRE variable hours_since_delivery: number
$REQUIRE variable claims_this_year: number

# Role
You are Nell, the delivery assistant for Brackwater.

# Context
Brackwater has a claims team for repeat claims.

# Constraints
$IF @{hours_since_delivery} is less than 48:
  - MUST ask the customer to check their safe place.
$ELSE IF @{claims_this_year} is at least 2:
  - MUST escalate to the claims team.
```

tg check chain.tg - output ✗ 1 error

```
$ npx typeglish check chain.tg
chain.tg:11:1  error  structure/non-exhaustive-switch  This chain isn't exhaustive — no arm handles @{hours_since_delivery}=48.0, @{claims_this_year}=0.0. Add an arm or an ELSE.

✗ 1 file — 1 error, 0 warning, 0 info
```

The witness is a customer whose parcel was marked delivered two days ago and who has never claimed before.

Read the witness again. It is not an edge case. It is the ordinary customer, the one most of these contacts come from: past the waiting period, first claim, parcel genuinely missing. The brief spent its lines on the exceptions and never said what happens in the normal case, because to the care lead it was obvious. To the model it was a gap, and in a gap a model improvises, which in this contact usually means a replacement.

The `$ELSE` arm is the line the brief left out: open the claim. With it, every pair of numbers lands on exactly one arm, and the order of the arms is a decision on the page: a repeat claimant whose parcel was marked delivered an hour ago is asked to check the safe place first, like anyone else.

> The brief listed the exceptions. The compiler asked about everyone else.

## §4 The spec, and what one customer’s model reads

spec.tg - the missing-parcel desk, finished ✓ A (100/100)

```typeglish
$REQUIRE variable hours_since_delivery: number
$REQUIRE variable claims_this_year: number

$TOOL open_courier_claim
  - description:: Open an investigation with the courier for a parcel marked as delivered.
  - input::
    - order_id:: string: the order number.

# Role
@@ role: one name, so every later "you" is the agent
You are Nell, the delivery assistant for Brackwater.

# Context
@@ customer: introduces the one party every rule below is about
A customer contacts you when the courier marks their parcel as delivered and it never arrives.
@@ investigation: the courier looks first, so a replacement is never the agent's call
Brackwater opens a courier investigation for every missing parcel.
@@ claims_team: repeat claims go to people who can see the whole history
Brackwater has a claims team for repeat claims.

# Constraints
@@ brevity: the customer is reading on a phone on the doorstep
- MUST keep every reply to at most 3 sentences.
@@ apology: one apology, not one per reply
- MUST apologize once.
@@ replacement: the claims team decides, after the courier investigates
- NEVER offer a replacement.
@@ next_step: the honest answer to "when do I get my stuff"
- WHEN the customer asks about a replacement THEN MUST explain the courier investigation.
@@ no_accusation: the claim count decides the route, never the tone
- NEVER mention fraud.
@@ route: the host passes both numbers in, and the compiler picks one arm
$IF @{hours_since_delivery} is less than 48:
  - MUST ask the customer to check their safe place.
$ELSE IF @{claims_this_year} is at least 2:
  - MUST escalate to the claims team.
$ELSE:
  - MUST call @[open_courier_claim].

$TEST replacement
  - input:: It says delivered but there's nothing here. Just send me another one.
  - expect::
    - explains the courier investigation
    - does not offer a replacement
    - at most 3 sentences

$TEST accusation
  - input:: Are you saying I'm lying? I want my parcel.
  - expect::
    - does not mention fraud
    - apologizes once
```

Eight rules, one tool, two tests. A `$IF` chain counts as one statement, so its `@@` note sits above the `$IF`.

tg check + score - spec.tg ✓ 0 error

```
$ npx typeglish check spec.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score spec.tg
spec.tg — A (100/100)  proven errors: none  tiers: base+z3
  planes  runtime 99 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 95 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   8 of 8 rule-shaped lines read · 5/8 covered by $TEST
```

Now build it the way the host will, once per conversation with that customer’s numbers. Here is a repeat claimant, 60 hours after the courier marked the parcel delivered, with three claims this year:

tg build --vars - one customer, one arm ✓ built

```
$ npx typeglish build spec.tg --vars '{"hours_since_delivery":60,"claims_this_year":3}'
✓ built .typeglish/dist/spec.txt ← spec.tg (ec40868f30d7, full)

$ cat .typeglish/dist/spec.txt
# Role
You are Nell, the delivery assistant for Brackwater.

# Context
A customer contacts you when the courier marks their parcel as delivered and it never arrives. Brackwater opens a courier investigation for every missing parcel. Brackwater has a claims team for repeat claims.

# Constraints
- MUST keep every reply to at most 3 sentences.
- MUST apologize once.
- NEVER offer a replacement.
- WHEN the customer asks about a replacement THEN MUST explain the courier investigation.
- NEVER mention fraud.
- MUST escalate to the claims team.
```

One arm survives. The number 3 is nowhere in the artifact, and neither are the two arms that lost.

That last cnote is the quiet win. The claim count decided the route and then stayed behind: the model is told to escalate, not why, so there is no count in its context for a customer to talk it into reciting. A `# Context` line like *The customer has made @{claims_this_year} claims this year* would have shipped the number as a slot for the platform to fill, and anything in the model’s context can come back out of it, the argument of [Your prompt has no private half](https://typeglish.dev/blog/prompt-has-no-private-half).

Two cautions, both from the CLI. First, build without `--vars` and the chain does not disappear: it ships as prose, *If hours_since_delivery is less than 48*, then *If claims_this_year is at least 2*, then *Otherwise*. The second arm loses its *else* on the way, so a repeat claimant inside 48 hours matches two arms, and the model has no numbers to test either one against. The chain only does its job when the host builds per conversation. Second, the test coverage:

tg test --dry - spec.tg 5/8 rules exercised

```
$ npx typeglish test spec.tg --dry
✓ spec.tg  coverage: 5/8 rules exercised
  uncovered (no case shares the rule's wording):
    L34  ask the customer to check their safe place.
    L36  escalate to the claims team.
    L38  call @[open_courier_claim].
  · replacement — "It says delivered but there's nothing here. Just send me ano" (not run)
      ✓ at most 3 sentences
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · accusation — "Are you saying I'm lying? I want my parcel." (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed
```

The three uncovered rules are the three arms.

That gap is honest, and adding three cases with matching wording would turn it green without proving anything. A `$TEST` has an `input::` and an `expect::` and nothing else, so a case cannot choose which customer it is. The arms are tested a different way: build once per arm, the way the host will, and keep the three artifacts in CI. Twenty hours and three claims selects the safe-place arm; sixty hours and none selects the claim. Three builds gave three different hashes (`96398ca76371`, `ec40868f30d7`, `c3d0e08ad8db`), so a diff on the artifacts catches the day somebody reorders the arms.

## §5 Common questions

**How do I write a system prompt for an agent that handles missing parcels?**

Start from what the agent can actually see. It cannot see the tracking page or the order history, so have the host pass in the hours since the courier marked the parcel delivered and the customer’s claim count, and let a $IF chain pick one route: wait and check the safe place, escalate a repeat claim, or open a courier claim. Write the ordinary case as the $ELSE arm, because a brief usually lists only the exceptions.

**Should my AI agent send a replacement when a parcel says delivered but never arrived?**

Usually not on its own. A replacement before the courier has investigated sends a second parcel while the first may be with a neighbour. Give the agent a tool that opens the courier claim, keep the replacement tool with the team that decides after the investigation, and add NEVER offer a replacement. with a rule that explains the courier investigation when the customer asks.

**How do I stop my support agent accusing customers of fraud?**

Write the rule around something observable. NEVER accuse the customer of lying is unprovable because accuse is not a verb the checker knows; NEVER mention fraud. is read and proven. Keep the claim count out of the model’s context as well: use it in a $IF condition at build time, so the count picks the route and never reaches the reply.

**Can a TypeGlish $TEST check each arm of a $IF chain?**

Not by choosing the inputs: a $TEST has only input:: and expect::, so a case cannot set the variables that pick an arm, and test --dry lists the arm rules as uncovered. Build the prompt once per arm with --vars and keep those artifacts under review; each arm produces a different build hash.

Field note

Every walkthrough in this series ends up removing something from the model’s hands: a date it could not compute, a flight it should not book, and here a parcel it should not send. The pattern is the same each time. The brief is written by someone who knows the system, so it reads like instructions to a colleague who can see the order screen. The agent cannot, and the fastest way to find out what it cannot see is to make every line a rule and let the checker ask. The exhaustiveness check is the part that pays for itself here: it found the one customer the brief never mentioned, and that customer is most of the queue. For the same move on a number that changes per customer, see [Prices go up on the first. Your prompt has both.](https://typeglish.dev/blog/price-rise-checklist)

∿ washed up Oct 5, 2026 ∿
