@alexkroman1/aai-cli 5.14.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
  2. package/dist/_agent.d.ts +18 -0
  3. package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
  4. package/dist/_api-client.d.ts +39 -0
  5. package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
  6. package/dist/_bundler.d.ts +43 -5
  7. package/dist/_cli-common.d.ts +67 -3
  8. package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
  9. package/dist/_deploy.d.ts +2 -5
  10. package/dist/_dev-restart.d.ts +6 -1
  11. package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
  12. package/dist/_dev-server.d.ts +30 -2
  13. package/dist/_e2e-test-utils.d.ts +80 -0
  14. package/dist/_fault-mode.d.ts +175 -0
  15. package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
  16. package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
  17. package/dist/_output.d.ts +9 -2
  18. package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
  19. package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
  20. package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
  21. package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
  22. package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
  23. package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
  24. package/dist/_utils.d.ts +18 -2
  25. package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
  26. package/dist/_vite-env.d.ts +30 -0
  27. package/dist/build-BO1Ni6oJ.mjs +62 -0
  28. package/dist/build.d.ts +19 -0
  29. package/dist/cli-workflow.d.ts +10 -0
  30. package/dist/cli.d.ts +1 -2
  31. package/dist/cli.mjs +406 -224
  32. package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
  33. package/dist/client-bundler.mjs +1 -1
  34. package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
  35. package/dist/delete.d.ts +2 -8
  36. package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
  37. package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
  38. package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
  39. package/dist/eject.d.ts +30 -3
  40. package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
  41. package/dist/init.d.ts +11 -0
  42. package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
  43. package/dist/project-config.mjs +1 -1
  44. package/dist/scaffold/CLAUDE.md +498 -91
  45. package/dist/scaffold/package.json +11 -8
  46. package/dist/scaffold/pnpm-workspace.yaml +8 -0
  47. package/dist/scaffold/server.mjs +53 -57
  48. package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
  49. package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
  50. package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
  51. package/dist/templates/code-interpreter/agent.ts +0 -2
  52. package/dist/templates/dispatch-center/agent.test.ts +17 -19
  53. package/dist/templates/dispatch-center/agent.ts +2 -29
  54. package/dist/templates/dispatch-center/client.tsx +33 -16
  55. package/dist/templates/dispatch-center/shared.ts +53 -19
  56. package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
  57. package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
  58. package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
  59. package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
  60. package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
  61. package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
  62. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
  63. package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
  66. package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
  67. package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
  68. package/dist/templates/embedded-assets/agent.test.ts +83 -0
  69. package/dist/templates/embedded-assets/agent.ts +1 -48
  70. package/dist/templates/embedded-assets/shared.ts +17 -0
  71. package/dist/templates/embedded-assets/system-prompt.md +13 -0
  72. package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
  73. package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
  74. package/dist/templates/health-assistant/agent.test.ts +169 -0
  75. package/dist/templates/health-assistant/agent.ts +1 -166
  76. package/dist/templates/health-assistant/fda.ts +100 -0
  77. package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
  78. package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
  79. package/dist/templates/infocom-adventure/agent.test.ts +205 -0
  80. package/dist/templates/infocom-adventure/agent.ts +3 -106
  81. package/dist/templates/infocom-adventure/client.tsx +28 -12
  82. package/dist/templates/infocom-adventure/shared.ts +28 -11
  83. package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
  84. package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
  85. package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
  86. package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
  87. package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
  88. package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
  89. package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
  90. package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
  91. package/dist/templates/link-digest/agent.test.ts +223 -0
  92. package/dist/templates/link-digest/agent.ts +79 -0
  93. package/dist/templates/link-digest/client.tsx +146 -0
  94. package/dist/templates/link-digest/workflows/digest.ts +248 -0
  95. package/dist/templates/math-buddy/agent.ts +0 -2
  96. package/dist/templates/night-owl/agent.test.ts +80 -0
  97. package/dist/templates/night-owl/agent.ts +1 -68
  98. package/dist/templates/night-owl/system-prompt.md +10 -0
  99. package/dist/templates/night-owl/tools/recommend.ts +64 -0
  100. package/dist/templates/personal-finance/agent.ts +0 -2
  101. package/dist/templates/pizza-ordering/agent.test.ts +52 -47
  102. package/dist/templates/pizza-ordering/agent.ts +9 -158
  103. package/dist/templates/pizza-ordering/shared.ts +35 -12
  104. package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
  105. package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
  106. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
  107. package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
  108. package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
  109. package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
  110. package/dist/templates/plan-and-execute/agent.test.ts +411 -0
  111. package/dist/templates/plan-and-execute/agent.ts +23 -0
  112. package/dist/templates/plan-and-execute/client.tsx +115 -0
  113. package/dist/templates/plan-and-execute/graph.ts +178 -0
  114. package/dist/templates/plan-and-execute/prompts.ts +110 -0
  115. package/dist/templates/plan-and-execute/shared.ts +161 -0
  116. package/dist/templates/plan-and-execute/system-prompt.md +18 -0
  117. package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
  118. package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
  119. package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
  120. package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
  121. package/dist/templates/recap-workflow/agent.test.ts +762 -0
  122. package/dist/templates/recap-workflow/agent.ts +95 -0
  123. package/dist/templates/recap-workflow/shared.ts +43 -0
  124. package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
  125. package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
  126. package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
  127. package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
  128. package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
  129. package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
  130. package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
  131. package/dist/templates/redline/agent.test.ts +222 -0
  132. package/dist/templates/redline/agent.ts +89 -0
  133. package/dist/templates/redline/client.tsx +184 -0
  134. package/dist/templates/redline/workflows/prompts.ts +68 -0
  135. package/dist/templates/redline/workflows/redline.ts +258 -0
  136. package/dist/templates/research-workflow/agent.test.ts +525 -0
  137. package/dist/templates/research-workflow/agent.ts +94 -0
  138. package/dist/templates/research-workflow/shared.ts +35 -0
  139. package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
  140. package/dist/templates/research-workflow/tools/request_research.ts +33 -0
  141. package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
  142. package/dist/templates/research-workflow/tools/research_status.ts +40 -0
  143. package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
  144. package/dist/templates/research-workflow/workflows/research.ts +558 -0
  145. package/dist/templates/retail/agent.test.ts +236 -172
  146. package/dist/templates/retail/agent.ts +5 -42
  147. package/dist/templates/retail/client.tsx +33 -11
  148. package/dist/templates/retail/registry.test.ts +31 -16
  149. package/dist/templates/retail/resolve.ts +28 -64
  150. package/dist/templates/retail/shared.test.ts +4 -3
  151. package/dist/templates/retail/shared.ts +24 -7
  152. package/dist/templates/retail/store.test.ts +12 -10
  153. package/dist/templates/retail/store.ts +69 -30
  154. package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
  155. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
  156. package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
  157. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
  158. package/dist/templates/retail/tools/get_item_details.ts +5 -7
  159. package/dist/templates/retail/tools/get_order_details.ts +5 -7
  160. package/dist/templates/retail/tools/get_product_details.ts +4 -7
  161. package/dist/templates/retail/tools/get_user_details.ts +4 -7
  162. package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
  163. package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
  164. package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
  165. package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
  166. package/dist/templates/retail/tools/modify_user_address.ts +4 -9
  167. package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
  168. package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
  169. package/dist/templates/solo-rpg/agent.test.ts +152 -29
  170. package/dist/templates/solo-rpg/agent.ts +3 -21
  171. package/dist/templates/solo-rpg/client.tsx +19 -6
  172. package/dist/templates/solo-rpg/shared.ts +19 -11
  173. package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
  174. package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
  175. package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
  176. package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
  177. package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
  178. package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
  179. package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
  180. package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
  181. package/dist/templates/support-line/agent.test.ts +343 -0
  182. package/dist/templates/support-line/agent.ts +27 -0
  183. package/dist/templates/support-line/client.tsx +122 -0
  184. package/dist/templates/support-line/graph.ts +224 -0
  185. package/dist/templates/support-line/knowledge.json +65 -0
  186. package/dist/templates/support-line/prompts.ts +95 -0
  187. package/dist/templates/support-line/shared.ts +240 -0
  188. package/dist/templates/support-line/system-prompt.md +17 -0
  189. package/dist/templates/support-line/tools/answer_question.ts +72 -0
  190. package/dist/templates/support-line/tools/list_topics.ts +22 -0
  191. package/dist/templates/support-line/tools/log_ticket.ts +30 -0
  192. package/dist/templates/transcription-workflow/agent.test.ts +563 -0
  193. package/dist/templates/transcription-workflow/agent.ts +92 -0
  194. package/dist/templates/transcription-workflow/client.tsx +234 -0
  195. package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
  196. package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
  197. package/dist/templates/travel-concierge/agent.test.ts +338 -0
  198. package/dist/templates/travel-concierge/agent.ts +29 -0
  199. package/dist/templates/travel-concierge/client.tsx +161 -0
  200. package/dist/templates/travel-concierge/routing.ts +138 -0
  201. package/dist/templates/travel-concierge/shared.ts +547 -0
  202. package/dist/templates/travel-concierge/system-prompt.md +24 -0
  203. package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
  204. package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
  205. package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
  206. package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
  207. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
  208. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
  209. package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
  210. package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
  211. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
  212. package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
  213. package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
  214. package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
  215. package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
  216. package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
  217. package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
  218. package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
  219. package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
  220. package/dist/templates/web-researcher/agent.ts +0 -2
  221. package/dist/templates/web-researcher/system-prompt.md +10 -0
  222. package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
  223. package/dist/typecheck.mjs +17 -15
  224. package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
  225. package/dist/worker-bundler.d.ts +14 -0
  226. package/dist/worker-bundler.mjs +1 -108
  227. package/dist/workflow-B-O97AqI.mjs +125 -0
  228. package/dist/workflow-bundler.d.ts +133 -0
  229. package/dist/workflow.d.ts +67 -0
  230. package/package.json +13 -9
  231. package/dist/build-DQJ1ZSm9.mjs +0 -39
@@ -0,0 +1,547 @@
1
+ /**
2
+ * The concierge's booking world, its dialog stack, and its confirmation gate.
3
+ *
4
+ * **Adapted from LangGraph's customer-support tutorial** (MIT,
5
+ * <https://github.com/langchain-ai/langgraph>,
6
+ * `docs/docs/tutorials/customer-support/customer-support.ipynb`) — the Swiss
7
+ * Airlines support bot. Two things in it are the reason to take its shape
8
+ * rather than invent one, and both land differently over a phone than in a
9
+ * chat window:
10
+ *
11
+ * | customer-support | here |
12
+ * | --- | --- |
13
+ * | `State.dialog_state` (an annotated push/pop stack) | {@link TripState.dialogState} |
14
+ * | `ToFlightBookingAssistant` & friends (delegation tools) | the four tools {@link SPECIALISTS} generates in `routing.ts` |
15
+ * | `CompleteOrEscalate` | `complete_or_escalate`, which pops the same stack |
16
+ * | `interrupt_before=["…_sensitive_tools"]` | {@link stageAction} + `confirm_action` |
17
+ * | `fetch_user_flight_information` (sqlite) | `lookup_booking` over {@link seedTrip} |
18
+ *
19
+ * **The dialog stack is what keeps a long call on the rails.** Their insight is
20
+ * that one prompt holding every tool degrades as the tool list grows, so the
21
+ * primary assistant delegates to a specialist and the specialist works with a
22
+ * narrow brief. A voice agent cannot swap its system prompt mid-session — the
23
+ * session's prompt is fixed at connect — so the specialist's brief arrives as
24
+ * the DELEGATION TOOL'S RESULT, which is the last thing the model reads before
25
+ * it answers. The stack itself is real state either way, which is what
26
+ * `complete_or_escalate` pops and what the sidebar renders.
27
+ *
28
+ * **`interrupt_before` becomes a spoken confirmation, and that is not a
29
+ * downgrade.** Their graph halts before a sensitive tool and waits for a human
30
+ * to approve in the notebook; a caller cannot type "y", but asking out loud and
31
+ * hearing "yes" is the same gate with a better interface. So a sensitive tool
32
+ * never mutates: it STAGES a {@link PendingAction} and returns the sentence the
33
+ * model should read back, and `confirm_action` is the only thing that applies
34
+ * one. The mechanism is worth copying whole — it is the difference between an
35
+ * agent that can rebook a flight and one that can rebook a flight *by mistake*.
36
+ */
37
+
38
+ import { type DeepReadonly, pushCapped, sessionSlot, type ToolFailure } from "@alexkroman1/aai";
39
+
40
+ // ─── The booking world ───────────────────────────────────────────────────────
41
+ // Their notebook downloads a sqlite database of a real airline's schedule and
42
+ // rewrites its timestamps to "now". A template ships no database, so the world
43
+ // is a handful of frozen rows — enough for every tool to return something a
44
+ // caller could act on, and small enough to read.
45
+
46
+ export interface Flight {
47
+ id: string;
48
+ route: string;
49
+ departs: string;
50
+ arrives: string;
51
+ fare: number;
52
+ seatsLeft: number;
53
+ }
54
+
55
+ export interface Hotel {
56
+ id: string;
57
+ name: string;
58
+ city: string;
59
+ area: string;
60
+ pricePerNight: number;
61
+ stars: number;
62
+ }
63
+
64
+ export interface CarRental {
65
+ id: string;
66
+ vendor: string;
67
+ city: string;
68
+ tier: string;
69
+ pricePerDay: number;
70
+ }
71
+
72
+ export interface Excursion {
73
+ id: string;
74
+ name: string;
75
+ city: string;
76
+ kind: string;
77
+ price: number;
78
+ }
79
+
80
+ export const FLIGHTS: Flight[] = [
81
+ {
82
+ id: "LX40",
83
+ route: "Zurich to Boston",
84
+ departs: "Tue 13:05",
85
+ arrives: "Tue 15:45",
86
+ fare: 640,
87
+ seatsLeft: 9,
88
+ },
89
+ {
90
+ id: "LX52",
91
+ route: "Zurich to Boston",
92
+ departs: "Wed 09:20",
93
+ arrives: "Wed 12:05",
94
+ fare: 590,
95
+ seatsLeft: 2,
96
+ },
97
+ {
98
+ id: "LX54",
99
+ route: "Zurich to Boston",
100
+ departs: "Thu 17:40",
101
+ arrives: "Thu 20:25",
102
+ fare: 505,
103
+ seatsLeft: 21,
104
+ },
105
+ {
106
+ id: "LX15",
107
+ route: "Boston to Zurich",
108
+ departs: "Sun 21:15",
109
+ arrives: "Mon 10:40",
110
+ fare: 615,
111
+ seatsLeft: 6,
112
+ },
113
+ {
114
+ id: "LX17",
115
+ route: "Boston to Zurich",
116
+ departs: "Mon 19:55",
117
+ arrives: "Tue 09:30",
118
+ fare: 700,
119
+ seatsLeft: 14,
120
+ },
121
+ ];
122
+
123
+ export const HOTELS: Hotel[] = [
124
+ {
125
+ id: "H1",
126
+ name: "Harborview Suites",
127
+ city: "Boston",
128
+ area: "Seaport",
129
+ pricePerNight: 265,
130
+ stars: 4,
131
+ },
132
+ {
133
+ id: "H2",
134
+ name: "The Back Bay",
135
+ city: "Boston",
136
+ area: "Back Bay",
137
+ pricePerNight: 340,
138
+ stars: 5,
139
+ },
140
+ {
141
+ id: "H3",
142
+ name: "Cambridge Rooms",
143
+ city: "Boston",
144
+ area: "Cambridge",
145
+ pricePerNight: 180,
146
+ stars: 3,
147
+ },
148
+ {
149
+ id: "H4",
150
+ name: "Limmat Riverside",
151
+ city: "Zurich",
152
+ area: "Old Town",
153
+ pricePerNight: 295,
154
+ stars: 4,
155
+ },
156
+ ];
157
+
158
+ export const CAR_RENTALS: CarRental[] = [
159
+ { id: "C1", vendor: "Alpine Rentals", city: "Boston", tier: "compact", pricePerDay: 48 },
160
+ { id: "C2", vendor: "Alpine Rentals", city: "Boston", tier: "midsize", pricePerDay: 62 },
161
+ { id: "C3", vendor: "Harbor Auto", city: "Boston", tier: "suv", pricePerDay: 95 },
162
+ { id: "C4", vendor: "Limmat Cars", city: "Zurich", tier: "compact", pricePerDay: 54 },
163
+ ];
164
+
165
+ export const EXCURSIONS: Excursion[] = [
166
+ { id: "E1", name: "Freedom Trail walk", city: "Boston", kind: "walking tour", price: 35 },
167
+ { id: "E2", name: "Harbor sunset sail", city: "Boston", kind: "boat", price: 78 },
168
+ { id: "E3", name: "North End food crawl", city: "Boston", kind: "food", price: 95 },
169
+ { id: "E4", name: "Lake Zurich cruise", city: "Zurich", kind: "boat", price: 52 },
170
+ ];
171
+
172
+ /** `$1,234` — one money format, so a spoken price and a rendered one agree. */
173
+ export function formatPrice(amount: number): string {
174
+ return `$${amount.toLocaleString("en-US")}`;
175
+ }
176
+
177
+ // ─── The dialog stack ────────────────────────────────────────────────────────
178
+
179
+ export const SPECIALIST_IDS = ["flight", "hotel", "car_rental", "excursion"] as const;
180
+ export type SpecialistId = (typeof SPECIALIST_IDS)[number];
181
+
182
+ /** Who is holding the call. `"primary"` is the host assistant — their
183
+ * `dialog_state` bottom, reached by popping everything else off. */
184
+ export type DialogState = "primary" | SpecialistId;
185
+
186
+ /**
187
+ * One specialist's brief, handed back as the delegation tool's RESULT.
188
+ *
189
+ * The instructions are adapted from the four `…_prompt` templates in the
190
+ * notebook, compressed for a phone: theirs end in a paragraph about not making
191
+ * up tools, which a chat model needs and a voice model has no room for. What is
192
+ * kept verbatim in spirit is the part that does the work — search persistently,
193
+ * confirm the details back, and escalate rather than improvise.
194
+ */
195
+ export interface Specialist {
196
+ /** What the caller hears this desk called. */
197
+ title: string;
198
+ /** The brief the model is handed on delegation. */
199
+ instructions: string;
200
+ }
201
+
202
+ export const SPECIALISTS: Record<SpecialistId, Specialist> = {
203
+ flight: {
204
+ title: "flight desk",
205
+ instructions: [
206
+ "You are now the flight desk. Handle searching and changing flights only.",
207
+ "Search before you quote anything, and be persistent: if the first search",
208
+ "finds nothing, widen it rather than telling the caller there is nothing.",
209
+ "Read the flight number, the day, the departure time and the fare change",
210
+ "back to the caller before you change a ticket. A ticket is only changed",
211
+ "once confirm_action has run.",
212
+ "If the caller wants a hotel, a car or something to do, or changes their",
213
+ "mind, call complete_or_escalate instead of improvising.",
214
+ ].join(" "),
215
+ },
216
+ hotel: {
217
+ title: "hotel desk",
218
+ instructions: [
219
+ "You are now the hotel desk. Handle searching and booking hotels only.",
220
+ "Ask which city and roughly what budget, then search. Offer at most three",
221
+ "options out loud, cheapest first, with the neighbourhood and nightly rate.",
222
+ "Confirm the hotel, the number of nights and the total before booking.",
223
+ "A room is only held once confirm_action has run.",
224
+ "If the caller wants flights, a car or an excursion, call",
225
+ "complete_or_escalate.",
226
+ ].join(" "),
227
+ },
228
+ car_rental: {
229
+ title: "car rental desk",
230
+ instructions: [
231
+ "You are now the car rental desk. Handle searching and booking cars only.",
232
+ "Ask the city and how many days, then search. Name the vendor, the tier",
233
+ "and the daily rate, and confirm the total before booking.",
234
+ "A car is only reserved once confirm_action has run.",
235
+ "If the caller wants flights, a hotel or an excursion, call",
236
+ "complete_or_escalate.",
237
+ ].join(" "),
238
+ },
239
+ excursion: {
240
+ title: "excursions desk",
241
+ instructions: [
242
+ "You are now the excursions desk. Handle recommending and booking things",
243
+ "to do only. Ask the city and what kind of thing they enjoy, then search.",
244
+ "Describe two or three options in a sentence each — this is the part of",
245
+ "the call a caller actually wants to hear about.",
246
+ "A booking is only made once confirm_action has run.",
247
+ "If the caller wants flights, a hotel or a car, call complete_or_escalate.",
248
+ ].join(" "),
249
+ },
250
+ };
251
+
252
+ // ─── Session state ───────────────────────────────────────────────────────────
253
+
254
+ /** A confirmed booking, as the itinerary carries it. */
255
+ export interface BookingRecord {
256
+ kind: "flight" | "hotel" | "car" | "excursion";
257
+ reference: string;
258
+ summary: string;
259
+ price: number;
260
+ }
261
+
262
+ /**
263
+ * A sensitive action that has been described to the caller and not yet applied.
264
+ *
265
+ * A discriminated union rather than a stored closure: the whole point is that
266
+ * the staged action is INSPECTABLE — `confirm_action` re-derives the effect
267
+ * from it, the projection renders it in the sidebar, and a spec can assert on
268
+ * it. A closure would be none of those things.
269
+ */
270
+ export type PendingAction =
271
+ | { kind: "update_ticket"; flightId: string }
272
+ | { kind: "cancel_ticket" }
273
+ | { kind: "book_hotel"; hotelId: string; nights: number }
274
+ | { kind: "book_car"; carId: string; days: number }
275
+ | { kind: "book_excursion"; excursionId: string };
276
+
277
+ export interface TripState {
278
+ passenger: string;
279
+ /** The ticket the caller is holding, or `null` once they cancel it. */
280
+ ticket: { reference: string; flightId: string } | null;
281
+ /** Their `dialog_state` stack: `primary` is always the bottom. */
282
+ dialogState: DialogState[];
283
+ /** Staged-but-unconfirmed sensitive action — at most one at a time, which
284
+ * {@link stageAction} ENFORCES by refusing a second rather than by saying so
285
+ * here. A concurrent step emitting two sensitive tools is ordinary. */
286
+ pending: PendingAction | null;
287
+ bookings: BookingRecord[];
288
+ /** What has happened on this call, for the sidebar. Capped on append. */
289
+ log: string[];
290
+ bookingCounter: number;
291
+ }
292
+
293
+ /** Growth cap on the call log — it rides in every `syncState` frame. */
294
+ export const MAX_LOG_ENTRIES = 40;
295
+
296
+ /** The caller as the airline already knows them — their
297
+ * `fetch_user_flight_information`, minus the sqlite. */
298
+ export function seedTrip(): TripState {
299
+ return {
300
+ passenger: "Nadia Rossi",
301
+ ticket: { reference: "QZ7F2K", flightId: "LX40" },
302
+ dialogState: ["primary"],
303
+ pending: null,
304
+ bookings: [],
305
+ log: [],
306
+ bookingCounter: 0,
307
+ };
308
+ }
309
+
310
+ /**
311
+ * The call's state, as one typed slot.
312
+ *
313
+ * `after` holds the one invariant nothing else should have to remember: the
314
+ * stack always has `primary` at the bottom, so a `complete_or_escalate` that
315
+ * pops one too many cannot leave the session with no assistant at all.
316
+ */
317
+ export const tripSlot = sessionSlot("trip", seedTrip, {
318
+ after: (state) => {
319
+ if (state.dialogState.length === 0) state.dialogState.push("primary");
320
+ },
321
+ });
322
+
323
+ /**
324
+ * The call as a READ hands it out: deep-frozen, and typed to say so.
325
+ *
326
+ * The pure helpers below take this rather than {@link TripState}, which is the
327
+ * widening a deep-readonly slot forces and the reason it is worth doing: a
328
+ * mutable state still satisfies it, so every `updateTool` draft passes
329
+ * unchanged, while a helper that WOULD have mutated stops compiling instead of
330
+ * throwing at its first call.
331
+ */
332
+ export type FrozenTripState = DeepReadonly<TripState>;
333
+
334
+ export function activeAssistant(state: FrozenTripState): DialogState {
335
+ return state.dialogState.at(-1) ?? "primary";
336
+ }
337
+
338
+ export function note(state: TripState, entry: string): void {
339
+ pushCapped(state.log, entry, MAX_LOG_ENTRIES);
340
+ }
341
+
342
+ // ─── The confirmation gate ───────────────────────────────────────────────────
343
+
344
+ /**
345
+ * Describe a staged action in one sentence, in the second person — this is
346
+ * read aloud, so it is a question's worth of text and not a receipt.
347
+ */
348
+ export function describeAction(action: DeepReadonly<PendingAction>): string | ToolFailure {
349
+ switch (action.kind) {
350
+ case "update_ticket": {
351
+ const flight = FLIGHTS.find((f) => f.id === action.flightId);
352
+ if (!flight) return { error: `No flight ${action.flightId} in the schedule.` };
353
+ return `move your ticket to ${flight.id}, ${flight.route}, departing ${flight.departs}, at ${formatPrice(flight.fare)}`;
354
+ }
355
+ case "cancel_ticket":
356
+ return "cancel your ticket entirely";
357
+ case "book_hotel": {
358
+ const hotel = HOTELS.find((h) => h.id === action.hotelId);
359
+ if (!hotel) return { error: `No hotel ${action.hotelId}.` };
360
+ return `book ${hotel.name} in ${hotel.area} for ${action.nights} night${action.nights === 1 ? "" : "s"}, ${formatPrice(hotel.pricePerNight * action.nights)} total`;
361
+ }
362
+ case "book_car": {
363
+ const car = CAR_RENTALS.find((c) => c.id === action.carId);
364
+ if (!car) return { error: `No car ${action.carId}.` };
365
+ return `reserve the ${car.tier} from ${car.vendor} for ${action.days} day${action.days === 1 ? "" : "s"}, ${formatPrice(car.pricePerDay * action.days)} total`;
366
+ }
367
+ case "book_excursion": {
368
+ const excursion = EXCURSIONS.find((e) => e.id === action.excursionId);
369
+ if (!excursion) return { error: `No excursion ${action.excursionId}.` };
370
+ return `book ${excursion.name} at ${formatPrice(excursion.price)}`;
371
+ }
372
+ // Unreachable while `PendingAction` is exhausted above — and the arm a new
373
+ // member of the union lands in until it has one of its own, which is a
374
+ // refusal rather than an unexplained sentence read down a phone.
375
+ default:
376
+ return { error: "That change cannot be described, so it will not be staged." };
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Stage a sensitive action and return what a sensitive tool answers with.
382
+ *
383
+ * Every sensitive tool ends in this call and none of them mutate anything —
384
+ * that is the whole gate, and keeping it to one helper is what stops the next
385
+ * sensitive tool from being the one that forgets it.
386
+ *
387
+ * **It REFUSES a second staging, and that is the enforcement half of
388
+ * {@link TripState.pending}'s "at most one at a time".** The LLM loop runs a
389
+ * step's tool calls CONCURRENTLY, so two sensitive tools in one step is an
390
+ * ordinary outcome — the model says "change my flight and book the hotel" and
391
+ * emits both. An unconditional assignment made the second win: both tools
392
+ * answered `awaitingConfirmation`, the model read both back, the caller said
393
+ * yes, and `confirm_action` applied ONE of them with nothing anywhere saying
394
+ * the other had been dropped. A gate whose failure is a silent no-op is not a
395
+ * gate. Refusing names the sentence already waiting, which is what lets the
396
+ * model ask about that one first and restage the rest afterwards.
397
+ */
398
+ export function stageAction(
399
+ state: TripState,
400
+ action: PendingAction,
401
+ ):
402
+ | { awaitingConfirmation: true; readBack: string; expires: "on the caller's next answer" }
403
+ | ToolFailure {
404
+ const described = describeAction(action);
405
+ if (typeof described !== "string") return described;
406
+ const waiting = state.pending ? describeAction(state.pending) : null;
407
+ if (waiting !== null) {
408
+ return {
409
+ error:
410
+ `Something is already waiting for the caller's yes: ${typeof waiting === "string" ? waiting : state.pending?.kind}. ` +
411
+ "Settle it with confirm_action or cancel_action, then ask for this one. " +
412
+ "Only one change can be waiting at a time — nothing about this request has been staged.",
413
+ };
414
+ }
415
+ state.pending = action;
416
+ note(state, `Awaiting confirmation: ${described}`);
417
+ return {
418
+ awaitingConfirmation: true,
419
+ readBack: `Ask the caller to confirm, out loud, that they want to ${described}. Nothing has changed yet.`,
420
+ expires: "on the caller's next answer",
421
+ };
422
+ }
423
+
424
+ /** Apply the staged action. The ONE place any of them takes effect. */
425
+ export function applyPending(
426
+ state: TripState,
427
+ ): { applied: string; reference?: string } | ToolFailure {
428
+ const action = state.pending;
429
+ if (!action) {
430
+ return { error: "Nothing is waiting for confirmation. Use the booking tool first." };
431
+ }
432
+ const described = describeAction(action);
433
+ if (typeof described !== "string") return described;
434
+ state.pending = null;
435
+ state.bookingCounter++;
436
+ const reference = `BK${String(1000 + state.bookingCounter)}`;
437
+
438
+ switch (action.kind) {
439
+ case "update_ticket": {
440
+ const flight = FLIGHTS.find((f) => f.id === action.flightId);
441
+ if (!flight) return { error: `No flight ${action.flightId}.` };
442
+ if (flight.seatsLeft <= 0) return { error: `${flight.id} has no seats left.` };
443
+ if (!state.ticket) return { error: "There is no ticket to move — it was cancelled." };
444
+ state.ticket = { reference: state.ticket.reference, flightId: flight.id };
445
+ note(state, `Ticket ${state.ticket.reference} moved to ${flight.id}`);
446
+ return { applied: `Ticket moved to ${flight.id}, ${flight.departs}.` };
447
+ }
448
+ case "cancel_ticket": {
449
+ if (!state.ticket) return { error: "There is no ticket to cancel." };
450
+ const cancelled = state.ticket.reference;
451
+ state.ticket = null;
452
+ note(state, `Ticket ${cancelled} cancelled`);
453
+ return { applied: `Ticket ${cancelled} cancelled.` };
454
+ }
455
+ case "book_hotel": {
456
+ const hotel = HOTELS.find((h) => h.id === action.hotelId);
457
+ if (!hotel) return { error: `No hotel ${action.hotelId}.` };
458
+ const price = hotel.pricePerNight * action.nights;
459
+ state.bookings.push({
460
+ kind: "hotel",
461
+ reference,
462
+ summary: `${hotel.name} (${hotel.area}), ${action.nights} night${action.nights === 1 ? "" : "s"}`,
463
+ price,
464
+ });
465
+ note(state, `Hotel booked: ${hotel.name} — ${reference}`);
466
+ return { applied: `${hotel.name} booked, ${formatPrice(price)}.`, reference };
467
+ }
468
+ case "book_car": {
469
+ const car = CAR_RENTALS.find((c) => c.id === action.carId);
470
+ if (!car) return { error: `No car ${action.carId}.` };
471
+ const price = car.pricePerDay * action.days;
472
+ state.bookings.push({
473
+ kind: "car",
474
+ reference,
475
+ summary: `${car.vendor} ${car.tier}, ${action.days} day${action.days === 1 ? "" : "s"}`,
476
+ price,
477
+ });
478
+ note(state, `Car reserved: ${car.vendor} ${car.tier} — ${reference}`);
479
+ return {
480
+ applied: `${car.tier} from ${car.vendor} reserved, ${formatPrice(price)}.`,
481
+ reference,
482
+ };
483
+ }
484
+ case "book_excursion": {
485
+ const excursion = EXCURSIONS.find((e) => e.id === action.excursionId);
486
+ if (!excursion) return { error: `No excursion ${action.excursionId}.` };
487
+ state.bookings.push({
488
+ kind: "excursion",
489
+ reference,
490
+ summary: `${excursion.name} (${excursion.city})`,
491
+ price: excursion.price,
492
+ });
493
+ note(state, `Excursion booked: ${excursion.name} — ${reference}`);
494
+ return { applied: `${excursion.name} booked, ${formatPrice(excursion.price)}.`, reference };
495
+ }
496
+ // Same as `describeAction`: unreachable today, and a refusal rather than a
497
+ // silent no-op for whatever the union grows next.
498
+ default:
499
+ return { error: "That change has no handler, so nothing was applied." };
500
+ }
501
+ }
502
+
503
+ // ─── The projection ──────────────────────────────────────────────────────────
504
+
505
+ export interface TripView {
506
+ passenger: string;
507
+ /** Which desk is holding the call — the stack's top, rendered as a badge. */
508
+ assistant: DialogState;
509
+ assistantTitle: string;
510
+ ticket: { reference: string; flightId: string; route: string; departs: string } | null;
511
+ bookings: readonly DeepReadonly<BookingRecord>[];
512
+ total: number;
513
+ /** The staged action's sentence, so the browser shows what voice just asked. */
514
+ pending: string | null;
515
+ log: readonly string[];
516
+ }
517
+
518
+ /**
519
+ * What the browser sees. A projection rather than the raw state, for the reason
520
+ * `syncState` takes one: the caller's ticket reference is theirs, the staged
521
+ * action has to be rendered as PROSE rather than as a union the client would
522
+ * have to re-switch on, and the stack top is the one field the sidebar is
523
+ * actually built around.
524
+ */
525
+ export function tripView(state: FrozenTripState): TripView {
526
+ const assistant = activeAssistant(state);
527
+ const flight = state.ticket ? FLIGHTS.find((f) => f.id === state.ticket?.flightId) : undefined;
528
+ const described = state.pending ? describeAction(state.pending) : null;
529
+ return {
530
+ passenger: state.passenger,
531
+ assistant,
532
+ assistantTitle: assistant === "primary" ? "concierge" : SPECIALISTS[assistant].title,
533
+ ticket:
534
+ state.ticket && flight
535
+ ? {
536
+ reference: state.ticket.reference,
537
+ flightId: flight.id,
538
+ route: flight.route,
539
+ departs: flight.departs,
540
+ }
541
+ : null,
542
+ bookings: state.bookings,
543
+ total: state.bookings.reduce((sum, b) => sum + b.price, 0),
544
+ pending: typeof described === "string" ? described : null,
545
+ log: state.log,
546
+ };
547
+ }
@@ -0,0 +1,24 @@
1
+ You are the main concierge for Swiss Air Travel, taking calls from ticketed passengers. Keep every answer short and spoken — one or two sentences, no lists, no markdown.
2
+
3
+ You do not do the specialist work yourself. There are four desks, and each one has its own tools:
4
+
5
+ - flights — searching and changing tickets → `to_flight_assistant`
6
+ - hotels — searching and booking rooms → `to_hotel_assistant`
7
+ - car rental — searching and reserving cars → `to_car_rental_assistant`
8
+ - excursions — things to do at the destination → `to_excursion_assistant`
9
+
10
+ How to run a call:
11
+
12
+ - Start with `lookup_booking` so you know who you are speaking to and what they are holding.
13
+ - The moment the caller raises something a desk owns, hand it over with that desk's tool and pass along what they asked for. Do not mention the transfer — the caller should hear one continuous conversation.
14
+ - The desk's reply tells you how it works. Follow it until it is done, then use `complete_or_escalate` to come back.
15
+ - Answer small talk, general questions and anything about the existing booking yourself.
16
+
17
+ Confirming changes — this part matters:
18
+
19
+ - Every tool that would change a booking stages the change instead of making it. Nothing has happened until `confirm_action` runs.
20
+ - When a tool answers with `awaitingConfirmation`, read the change back in your own words and ask a yes-or-no question. Say the price.
21
+ - If the caller says yes, call `confirm_action`. If they say no, or want different details, call `cancel_action`.
22
+ - Never call `confirm_action` on your own initiative, and never claim something is booked before it has run.
23
+
24
+ Voice manners: numbers spoken out ("six forty" not "$640.00"), flight numbers letter by letter when reading them back, and one question at a time.
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { stageAction, tripSlot } from "../shared.ts";
3
+
4
+ /** SENSITIVE — their `book_car_rental`, staged rather than applied. */
5
+ export default tripSlot.updateTool({
6
+ description:
7
+ "Reserve a rental car. This does NOT reserve anything yet — it stages the " +
8
+ "reservation so you can read the total back and hear a yes.",
9
+ inputSchema: z.object({
10
+ carId: z.string().max(20).describe("The car id from search_car_rentals, e.g. 'C2'"),
11
+ days: z.number().int().min(1).max(60).describe("How many days"),
12
+ }),
13
+ execute(args, trip) {
14
+ return stageAction(trip, {
15
+ kind: "book_car",
16
+ carId: args.carId.toUpperCase(),
17
+ days: args.days,
18
+ });
19
+ },
20
+ });
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { stageAction, tripSlot } from "../shared.ts";
3
+
4
+ /** SENSITIVE — their `book_excursion`, staged rather than applied. */
5
+ export default tripSlot.updateTool({
6
+ description:
7
+ "Book an excursion. This does NOT book anything yet — it stages the " +
8
+ "booking so you can read it back and hear a yes.",
9
+ inputSchema: z.object({
10
+ excursionId: z.string().max(20).describe("The excursion id from search_excursions, e.g. 'E2'"),
11
+ }),
12
+ execute(args, trip) {
13
+ return stageAction(trip, {
14
+ kind: "book_excursion",
15
+ excursionId: args.excursionId.toUpperCase(),
16
+ });
17
+ },
18
+ });
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { stageAction, tripSlot } from "../shared.ts";
3
+
4
+ /** SENSITIVE — their `book_hotel`, staged rather than applied. */
5
+ export default tripSlot.updateTool({
6
+ description:
7
+ "Hold a hotel room. This does NOT book anything yet — it stages the " +
8
+ "booking so you can read the total back and hear a yes.",
9
+ inputSchema: z.object({
10
+ hotelId: z.string().max(20).describe("The hotel id from search_hotels, e.g. 'H1'"),
11
+ nights: z.number().int().min(1).max(30).describe("How many nights"),
12
+ }),
13
+ execute(args, trip) {
14
+ return stageAction(trip, {
15
+ kind: "book_hotel",
16
+ hotelId: args.hotelId.toUpperCase(),
17
+ nights: args.nights,
18
+ });
19
+ },
20
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `cancel_action` — part of the dialog-stack control plane, which lives together in
3
+ * `../routing.ts` because the four tools share its helpers. This file is what
4
+ * gives it its name.
5
+ */
6
+
7
+ export { cancelAction as default } from "../routing.ts";
@@ -0,0 +1,16 @@
1
+ import { stageAction, tripSlot } from "../shared.ts";
2
+
3
+ /**
4
+ * SENSITIVE — their `cancel_ticket`. The most destructive thing on the call and
5
+ * the clearest argument for the gate: staged, read back, and only applied by
6
+ * `confirm_action`.
7
+ */
8
+ export default tripSlot.updateTool({
9
+ description:
10
+ "Cancel the caller's ticket outright. This does NOT cancel anything yet — " +
11
+ "it stages the cancellation so you can read it back and hear a yes.",
12
+ execute(_args, trip) {
13
+ if (!trip.ticket) return { error: "There is no ticket to cancel." };
14
+ return stageAction(trip, { kind: "cancel_ticket" });
15
+ },
16
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `complete_or_escalate` — part of the dialog-stack control plane, which lives together in
3
+ * `../routing.ts` because the four tools share its helpers. This file is what
4
+ * gives it its name.
5
+ */
6
+
7
+ export { completeOrEscalate as default } from "../routing.ts";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `confirm_action` — part of the dialog-stack control plane, which lives together in
3
+ * `../routing.ts` because the four tools share its helpers. This file is what
4
+ * gives it its name.
5
+ */
6
+
7
+ export { confirmAction as default } from "../routing.ts";