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

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

# Building a delivery-rescheduling agent, prompt-first

Moving a delivery sounds like the easiest job a support agent can have: find a new day, book it, say thanks. The brief for it hides a clock the agent cannot read, a fee that depends on that clock, and a van that may already be on the road.

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

The van knows more than the prompt.

**TL;DR** A delivery-rescheduling agent should never work out a fee or a deadline itself: a slot tool returns each slot with its fee, the agent reads the chosen slot back before a booking tool moves it, and the host passes in whether the van has left so the build ships only the rules that fit.

Moorfield sells sofas, beds and dining tables online and delivers them with two-person crews. Rebooking a delivery is the biggest single reason customers open the chat, and the support lead wants the assistant to handle it end to end. Operations sent a brief of seven lines, which is the delivery terms page boiled down. Here it is as a prompt, then as a spec.

## §1 The brief, checked as written

brief.tg - the seven lines as sent ✗ C (76/100)

```typeglish
# Role
You are the delivery assistant for Moorfield, an online furniture shop.

# Constraints
- Customers can rebook their delivery for free up to 48 hours before the delivery date.
- Rebooking within 48 hours costs 30 pounds.
- Offer the next available slot.
- Don't promise an exact time, only morning or afternoon.
- MUST confirm the new date.
- NEVER rebook a delivery more than 2 times.
- If the van has already left, the delivery can't be changed.
```

tg check and score (excerpt) - brief.tg 3 of 7 read

```
$ typeglish check brief.tg
brief.tg:5:1  info   prompt/unparsed-rule  Reads like a rule, but the rule reader cannot parse it as one, so it earns no Enforceability credit and no measurability check: no modal (MUST / NEVER / SHOULD) and "Customers" is not a verb the checker knows, so it reads as prose. Fix: lead with a modal, or start with a verb from the catalog (Escalate …, Ask …).
brief.tg:5:1  info   prompt/unregistered-doer  A bare generic doer never enters the world model - instruction to the agent, or background about users? Use the imperative if the agent acts, or a definite party ("The user should ...") to register the doer.
brief.tg:6:1  info   prompt/unparsed-rule  Reads like a rule, but the rule reader cannot parse it as one, so it earns no Enforceability credit and no measurability check: no modal (MUST / NEVER / SHOULD) and "Rebooking" is not a verb the checker knows, so it reads as prose. Fix: lead with a modal, or start with a verb from the catalog (Escalate …, Ask …).
brief.tg:8:1  info   prompt/unparsed-rule  Reads like a rule, but the rule reader cannot parse it as one, so it earns no Enforceability credit and no measurability check: no modal (MUST / NEVER / SHOULD) and "Don't" is not a verb the checker knows, so it reads as prose. Fix: lead with a modal, or start with a verb from the catalog (Escalate …, Ask …).
brief.tg:11:1  info   typeglish/if-then  IF needs a THEN — write IF <condition> THEN <action>.
brief.tg:11:1  info   prompt/unparsed-rule  Reads like a rule, but the rule reader cannot parse it as one, so it earns no Enforceability credit and no measurability check: the condition has no THEN branch, so nothing is required when it holds. Fix: add THEN MUST / NEVER / SHOULD <verb> ….
brief.tg:11:1  info   prompt/unintroduced-definite  "the van" retrieves something this document never introduces - a model must guess which van is meant. Mention a van on an earlier line, or anchor it here ("their van", "the van you ...").
brief.tg:11:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."

✓ 1 file — 0 error, 0 warning, 8 info

$ typeglish score brief.tg
brief.tg — C (76/100)  proven errors: none  tiers: base+z3
  planes  runtime 87 (what the model reads) · hygiene 46 (source only)
  facets  enforceability 71 x.21 · hardness 100 x.12 · directness 80 x.08 · consistency 100 x.17 · structure 91 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 72 x.08 · security 100 x.08
  lever   annotation 0/100 (up to +12 overall) — 8 statements without a @@ note (L2 L5 L6 L7 L8 L9 L10 L11) — put a "@@ why" line directly above each
  rules   3 of 7 rule-shaped lines read
    L5   ✗ invisible     Customers can rebook their delivery for free up to 48 hours before the delivery date.
                         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 …)
    L6   ✗ invisible     Rebooking within 48 hours costs 30 pounds.
                         no modal (MUST / NEVER / SHOULD) and "Rebooking" 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 …)
    L8   ✗ invisible     Don't promise an exact time, only morning or afternoon.
                         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 …)
    L11  ✗ invisible     If the van has already left, the delivery can't be changed.
                         the condition has no THEN branch, so nothing is required when it holds → add THEN MUST / NEVER / SHOULD <verb> …
```

Four of seven lines are invisible to the checker: the free window, the fee, the time rule and the van. Those four are the whole policy. The three that read are the ones that sounded simplest.

Nothing here is an error, and that is the trap. The four invisible lines still reach the model as prose, so the agent will do something with them. What it will not do is have them checked against each other, or against anything you add next. The rest of this post turns each of them into something a checker can read and a test can pin.

## §2 Free and 30 pounds is one rule with a clock in it

The obvious repair is to put a modal on lines 5 and 6. Free becomes a ban on the fee; the 48-hour line becomes the case where the fee applies. That is a faithful translation of what operations wrote, and it does not build:

fee.tg - the fee lines, given modals ✗ logic/contradiction

```typeglish
# Role
You are the delivery assistant for Moorfield, an online furniture shop.

# Constraints
- NEVER charge the customer a rebooking fee.
- WHEN the delivery is less than 48 hours away THEN MUST charge the customer a rebooking fee.
- WHEN the customer asks to change their delivery THEN MUST offer the next available slot.
- MUST confirm the new date.
```

tg check - fee.tg ✗ 2 error

```
$ typeglish check fee.tg
fee.tg:5:1  error  logic/contradiction  Conflicts with line 6. Logical conflict — "charge the customer a rebooking fee" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
        ↳ line 6: - WHEN the delivery is less than 48 hours away THEN MUST charge the customer a rebooking fee.
fee.tg:6:1  error  logic/contradiction  Conflicts with line 5. Logical conflict — "charge the customer a rebooking fee" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
        ↳ line 5: - NEVER charge the customer a rebooking fee.
fee.tg:8:1  info   prompt/unintroduced-definite  "the new date" retrieves something this document never introduces - a model must guess which date is meant. Mention a date on an earlier line, or anchor it here ("their date", "the date you ...").

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

The WHEN on line 6 does not shelter it. Line 5 has no condition, so it applies in every case, including the one line 6 describes.

The checker is right, and it is pointing at something the prose hid. *Free* was never unconditional. It meant free when the slot is more than 48 hours away, and the condition lived in the sentence, where only a person could see it. You could fix that by guarding line 5 too. But both guards ask the agent to count hours to a slot, and the agent does not know what time it is, when the original slot is, or how the warehouse counts a weekend.

So take the arithmetic away. The slot tool already knows the date of every slot it offers; let it return the fee for each one, and 0 when the slot is free. The agent’s job shrinks to two things it can actually do: say the fee when the slot it is booking has one, and not bargain it away. [Building a warranty-claim agent, prompt-first](https://typeglish.dev/blog/build-warranty-claim-agent) made the same move with the warranty clock.

> If a rule needs the agent to do date arithmetic, the rule belongs in the tool that already has the dates.

## §3 Morning, afternoon, and the read-back

Three of the remaining lines become smaller once a tool returns the slots.

- **Exact time.** Crews are booked by half day, and the hour is set when the route is planned the evening before. If the slot tool only returns *morning* or *afternoon*, the agent has nothing more exact to give. The rule stays as a ban, *NEVER state an exact delivery time*, because models like to be helpful with a guess such as *around 10am*, and a test can catch a clock time with a regex.

- **The next available slot.** This was a sentence about the tool, not the agent. It becomes a call: when the customer asks to move the delivery, call `find_slots`.

- **Rebooked more than twice.** The agent cannot see how many times an order has moved. The booking tool can. The limit goes into the tool’s description, where the tool enforces it by refusing, and out of the rules.

The brief’s *confirm the new date* needs one more step. Rebooking moves a two-person crew, so it is a write action, and the confirmation has to come before the booking, not after it. Write it as two rules with two triggers: when the customer picks a slot, read the slot back; when they confirm it, call `rebook_delivery`. A trailing *before booking* on one rule is a clause the checker cannot prove, and [Your agent can now do something it cannot undo](https://typeglish.dev/blog/agent-first-write-action) covers why a read-back is the rail worth having.

## §4 The van has left

Line 11 is the interesting one. Whether the van has left is not something the customer knows reliably, and not something the agent can see. The depot system knows exactly, and the chat host can pass it in when the session opens. So it becomes a typed input with three states: *booked*, *loaded* when the van is out, and *missed* when the crew came and nobody was home. A `$SWITCH` gives each state its own rules, and the build ships only one arm.

Here is the first draft of that, with the booking rule written once at the top, where it seemed to belong:

delivery-v2.tg - the switch, with the booking rule outside it ✗ logic/contradiction

```typeglish
$REQUIRE variable delivery_state: one of booked, loaded, missed

$TOOL find_slots
  - description:: Return the next available delivery slots for one order. Each slot has a date, morning or afternoon, and the rebooking fee for that slot, which is 0 when the slot is free.
  - input::
    - order_number:: string: the customer's order number.

$TOOL rebook_delivery
  - description:: Move one order to a slot the customer chose. Refuses an order that has already been rebooked twice.
  - input::
    - order_number:: string: the customer's order number.
    - slot_id:: string: the slot from find_slots.

$TOOL message_driver
  - description:: Send a short note to the driver of a van that has already left, such as a safe place or a gate code.
  - input::
    - order_number:: string: the customer's order number.
    - note:: string: the customer's message for the driver.

# Role
@@ role: one sentence names the speaker, so every later "you" is the assistant
You are the delivery assistant for Moorfield, an online furniture shop.

# Constraints
@@ slot_times: the warehouse books two-person crews by half day, so nobody knows the hour until the route is planned
- NEVER state an exact delivery time.
@@ fee_from_tool: the fee depends on hours until the slot, and only the slot tool knows both
- WHEN the customer picks a slot with a rebooking fee THEN MUST state the fee.
@@ fee_final: the fee pays the crew booking, and the support team owns refunds
- NEVER offer to waive a rebooking fee.
@@ read_back: rebooking moves a two-person crew, so the customer hears the slot first
- WHEN the customer picks a slot THEN MUST read back the slot.
@@ write_action: the read-back is the confirmation, and the tool is the only way a slot changes
- WHEN the customer confirms the slot THEN MUST call @[rebook_delivery].

@@ delivery_state: the depot system knows where the order is, so the host passes it in and the build picks one arm
$SWITCH ON @{delivery_state}
  - booked::
    @@ booked: the order is in the plan and can still move
    - WHEN the customer asks to change their delivery THEN MUST call @[find_slots].
  - loaded::
    @@ loaded: the van has left the depot, so the slot is fixed and only the driver can be reached
    - NEVER call @[rebook_delivery].
    - WHEN the customer asks to change their delivery THEN MUST call @[message_driver].
  - missed::
    @@ missed: the crew came and nobody was in, so the redelivery is a new booking
    - WHEN the customer asks about their delivery THEN MUST call @[find_slots].
```

tg check and build - delivery-v2.tg ✗ build refused

```
$ typeglish check delivery-v2.tg
delivery-v2.tg:34:1  error  logic/contradiction  Conflicts with line 43. Logical conflict — "call @[rebook_delivery]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). A single serve can hold both - e.g. when delivery_state = loaded.
                ↳ line 43: - NEVER call @[rebook_delivery].
delivery-v2.tg:43:5  error  logic/contradiction  Conflicts with line 34. Logical conflict — "call @[rebook_delivery]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). A single serve can hold both - e.g. when delivery_state = loaded.
                ↳ line 34: - WHEN the customer confirms the slot THEN MUST call @[rebook_delivery].

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

$ typeglish build delivery-v2.tg --vars '{"delivery_state":"booked"}'
typeglish build: delivery-v2.tg refused — nothing written
```

The message names the exact case: *when delivery_state = loaded*, the model would be told to call the booking tool on confirmation and never to call it. The build refuses even for *booked*, where the two lines never meet, because the source has to be right for every state it can be built for.

That is a real bug, and a common one. A customer whose van has left says “yes, Thursday is fine” about a slot the agent should never have offered, and the global rule tells the agent to book it. The fix is to put the booking rule only in the arms where booking is possible: *booked* and *missed*. In *loaded*, the only thing the agent can do is pass a note to the driver.

## §5 The spec, checked and tested

delivery.tg - three tools, one typed input, five cases ✓ A (97/100)

```typeglish
$REQUIRE variable delivery_state: one of booked, loaded, missed

$TOOL find_slots
  - description:: Return the next available delivery slots for one order. Each slot has a date, morning or afternoon, and the rebooking fee for that slot, which is 0 when the slot is free.
  - input::
    - order_number:: string: the customer's order number.

$TOOL rebook_delivery
  - description:: Move one order to a slot the customer chose. Refuses an order that has already been rebooked twice.
  - input::
    - order_number:: string: the customer's order number.
    - slot_id:: string: the slot from find_slots.

$TOOL message_driver
  - description:: Send a short note to the driver of a van that has already left, such as a safe place or a gate code.
  - input::
    - order_number:: string: the customer's order number.
    - note:: string: the customer's message for the driver.

# Role
@@ role: one sentence names the speaker, so every later "you" is the assistant
You are the delivery assistant for Moorfield, an online furniture shop.

# Constraints
@@ slot_times: the warehouse books two-person crews by half day, so nobody knows the hour until the route is planned
- NEVER state an exact delivery time.
@@ fee_from_tool: the fee depends on hours until the slot, and only the slot tool knows both
- WHEN the customer picks a slot with a rebooking fee THEN MUST state the fee.
@@ fee_final: the fee pays the crew booking, and the support team owns refunds
- NEVER offer to waive a rebooking fee.
@@ read_back: rebooking moves a two-person crew, so the customer hears the slot first
- WHEN the customer picks a slot THEN MUST read back the slot.

@@ delivery_state: the depot system knows where the order is, so the host passes it in and the build picks one arm
$SWITCH ON @{delivery_state}
  - booked::
    @@ booked: the order is in the plan and can still move
    - WHEN the customer asks to change their delivery THEN MUST call @[find_slots].
    @@ write_action: the read-back is the confirmation, and the tool is the only way a slot changes
    - WHEN the customer confirms the slot THEN MUST call @[rebook_delivery].
  - loaded::
    @@ loaded: the van has left the depot, so the slot is fixed and only the driver can be reached
    - NEVER call @[rebook_delivery].
    - WHEN the customer asks to change their delivery THEN MUST call @[message_driver].
  - missed::
    @@ missed: the crew came and nobody was in, so the redelivery is a new booking
    - WHEN the customer asks about their delivery THEN MUST call @[find_slots].
    - WHEN the customer confirms the slot THEN MUST call @[rebook_delivery].

$TEST move_delivery
  - input:: Can I move my sofa delivery to next week? Order MF-30418.
  - expect::
    - calls find_slots for MF-30418
    - does not state an exact delivery time
    - matches /^(?![\s\S]*\b\d{1,2}(:\d{2})?\s?(am|pm)\b)/i

$TEST picks_a_slot
  - input:: Thursday morning works. Is there a charge for that one?
  - expect::
    - reads back the slot
    - states the fee
    - contains "Thursday"

$TEST waive_the_fee
  - input:: 30 pounds just to move it? Take the fee off, I have been a customer for years.
  - expect::
    - does not offer to waive a rebooking fee
    - matches /^(?![\s\S]*\b(waive|remove the fee|take the fee off)\b)/i

$TEST confirms
  - input:: Yes, Thursday morning is right, book it.
  - expect::
    - calls rebook_delivery

$TEST van_left
  - input:: The driver is on the way but I have had to go out. Can we do another day?
  - expect::
    - calls message_driver
    - does not call rebook_delivery
```

The regex in `move_delivery` fails any reply with a clock time such as *10am* or *2:30 pm*. The one in `waive_the_fee` fails a reply that offers to take the fee off in so many words.

tg check, score, test --dry and build - delivery.tg ✓ A (97/100), one artifact per state

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

$ typeglish score delivery.tg
delivery.tg — A (97/100)  proven errors: none  tiers: base+z3
  planes  runtime 97 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 92 x.21 · hardness 100 x.12 · directness 90 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   11 of 13 rule-shaped lines read · 10/11 covered by $TEST
    L36  · arm           booked::
                         a $SWITCH / $IF arm body — selected at compile time; the checker does not read arm bodies as rules
    L45  · arm           missed::
                         a $SWITCH / $IF arm body — selected at compile time; the checker does not read arm bodies as rules

$ typeglish test delivery.tg --dry
✓ delivery.tg  coverage: 10/11 rules exercised
  uncovered (no case shares the rule's wording):
    L41  loaded::
  · move_delivery — "Can I move my sofa delivery to next week? Order MF-30418." (not run)
      ✓ matches /^(?![\s\S]*\b\d{1,2}(:\d{2})?\s?(am|pm)\b)/i
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · picks_a_slot — "Thursday morning works. Is there a charge for that one?" (not run)
      ✓ contains "Thursday"
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · waive_the_fee — "30 pounds just to move it? Take the fee off, I have been a c" (not run)
      ✓ matches /^(?![\s\S]*\b(waive|remove the fee|take the fee off)\b)/i
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · confirms — "Yes, Thursday morning is right, book it." (not run)
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · van_left — "The driver is on the way but I have had to go out. Can we do" (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed

$ typeglish build delivery.tg --vars '{"delivery_state":"booked"}'
✓ built .typeglish/dist/delivery.txt ← delivery.tg (11e241cef545, full)

$ cat .typeglish/dist/delivery.txt
# Role
You are the delivery assistant for Moorfield, an online furniture shop.

# Constraints
- NEVER state an exact delivery time.
- WHEN the customer picks a slot with a rebooking fee THEN MUST state the fee.
- NEVER offer to waive a rebooking fee.
- WHEN the customer picks a slot THEN MUST read back the slot.

- WHEN the customer asks to change their delivery THEN MUST call find_slots.
- WHEN the customer confirms the slot THEN MUST call rebook_delivery.

$ typeglish build delivery.tg --vars '{"delivery_state":"loaded"}'
✓ built .typeglish/dist/delivery.txt ← delivery.tg (eaf9ee04786c, full)

$ cat .typeglish/dist/delivery.txt
# Role
You are the delivery assistant for Moorfield, an online furniture shop.

# Constraints
- NEVER state an exact delivery time.
- WHEN the customer picks a slot with a rebooking fee THEN MUST state the fee.
- NEVER offer to waive a rebooking fee.
- WHEN the customer picks a slot THEN MUST read back the slot.

- NEVER call rebook_delivery.
- WHEN the customer asks to change their delivery THEN MUST call message_driver.
```

Two builds, two hashes, two different prompts. The *loaded* artifact never mentions `find_slots`, so a customer whose van is on the road is never offered a slot. Coverage is 10 of 11: the report lists the *loaded* arm’s key, L41, as uncovered, because coverage is matched on wording. `van_left` is the case written for that arm, and it runs live.

Eleven of thirteen rule-shaped lines read; the other two are the arm keys, which the score lists as compile-time selections. Nothing in the spec asks the agent to count hours, guess the time, or remember how many times an order has moved. Each of those went to the system that already knew.

## §6 Common questions

**How do I let my AI support agent reschedule deliveries?**

Give it two tools and a typed input. A slot tool returns the next available slots with the fee for each, a booking tool moves the order once the customer confirms, and the host passes in whether the delivery is booked, out on the van, or missed, so the build ships only the rules for that state. Then test each state with its own case.

**Why does my chatbot get the rebooking fee wrong?**

Usually because the prompt asks it to work out the fee from a deadline, such as free up to 48 hours before delivery. The agent does not know the current time, the slot time, or how your warehouse counts weekends. Have the slot tool return the fee for each slot and write the rule as stating that fee, never computing it.

**Should an AI agent promise a delivery time?**

Only as precisely as your systems know it. If crews are booked by half day, the agent should say morning or afternoon and never an exact time; write that as a NEVER rule and assert in a test that no clock time appears in the reply.

**Does TypeGlish check rules inside a $SWITCH against the rest of the prompt?**

Yes. In typeglish 0.11.0, a rule outside the switch that conflicts with a rule inside one arm is reported as logic/contradiction, with the state where they meet, for example when delivery_state = loaded, and the build is refused for every state until it is fixed.

Field note

A `$SWITCH ON` over a typed input is checked as one source: `typeglish check` compares rules outside the switch against every arm, and when they conflict it names the state where they meet. `build --vars` then writes one artifact per state, and losing arms never reach the model. For another case of the host knowing what the prompt was guessing, see [Why your agent asks for the order number it already has](https://typeglish.dev/blog/agent-asks-for-order-number).

∿ washed up Oct 28, 2026 ∿
