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.
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.
§1The brief, checked as written
# 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.
$ 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> …
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.
§2Free 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:
# 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.
$ 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 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 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.
§3Morning, 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 covers why a read-back is the rail worth having.
§4The 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:
$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].
$ 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
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.
§5The spec, checked and tested
$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
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.$ 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.
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.
§6Common 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.
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.