@curless/hotel-storefront 0.48.1 → 0.49.1

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 (52) hide show
  1. package/dist/backend.d.ts.map +1 -1
  2. package/dist/backend.js.map +1 -1
  3. package/dist/card.generated.d.ts +1 -1
  4. package/dist/card.generated.js +2 -2
  5. package/dist/card.generated.js.map +1 -1
  6. package/dist/demo-hotels.d.ts +7 -0
  7. package/dist/demo-hotels.d.ts.map +1 -1
  8. package/dist/demo-hotels.js +167 -79
  9. package/dist/demo-hotels.js.map +1 -1
  10. package/dist/fence.d.ts +10 -0
  11. package/dist/fence.d.ts.map +1 -1
  12. package/dist/fence.js +15 -0
  13. package/dist/fence.js.map +1 -1
  14. package/dist/huazhu/backend.d.ts.map +1 -1
  15. package/dist/huazhu/backend.js +21 -51
  16. package/dist/huazhu/backend.js.map +1 -1
  17. package/dist/huazhu/bookings.d.ts.map +1 -1
  18. package/dist/huazhu/bookings.js +1 -1
  19. package/dist/huazhu/bookings.js.map +1 -1
  20. package/dist/huazhu/catalog.d.ts +0 -1
  21. package/dist/huazhu/catalog.d.ts.map +1 -1
  22. package/dist/huazhu/catalog.js +7 -28
  23. package/dist/huazhu/catalog.js.map +1 -1
  24. package/dist/listing.d.ts +5 -1
  25. package/dist/listing.d.ts.map +1 -1
  26. package/dist/listing.js +6 -2
  27. package/dist/listing.js.map +1 -1
  28. package/dist/present.d.ts +32 -0
  29. package/dist/present.d.ts.map +1 -1
  30. package/dist/present.js +48 -2
  31. package/dist/present.js.map +1 -1
  32. package/dist/same-booking.d.ts +33 -0
  33. package/dist/same-booking.d.ts.map +1 -0
  34. package/dist/same-booking.js +74 -0
  35. package/dist/same-booking.js.map +1 -0
  36. package/dist/server.d.ts +2 -2
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +10 -4
  39. package/dist/server.js.map +1 -1
  40. package/dist/sessions.d.ts +26 -1
  41. package/dist/sessions.d.ts.map +1 -1
  42. package/dist/sessions.js +35 -2
  43. package/dist/sessions.js.map +1 -1
  44. package/dist/tools.d.ts +21 -3
  45. package/dist/tools.d.ts.map +1 -1
  46. package/dist/tools.js +155 -42
  47. package/dist/tools.js.map +1 -1
  48. package/dist/trip.d.ts +0 -11
  49. package/dist/trip.d.ts.map +1 -1
  50. package/dist/trip.js +21 -6
  51. package/dist/trip.js.map +1 -1
  52. package/package.json +1 -1
package/dist/tools.d.ts CHANGED
@@ -15,7 +15,7 @@ export declare const LOOK_UP_MAX = 6;
15
15
  */
16
16
  export declare const buildTools: (n: ToolNames, v: Vocabulary, caps?: BackendCapabilities) => ({
17
17
  readonly name: `list_${string}${string}`;
18
- readonly description: `${string}, shown as a card for a guest to look through and pick one ${string} to book. A trip of several days that wants its days planned (\u884C\u7A0B, \u5B89\u6392, an itinerary) is planned the other way round - the days, then for each part of the trip a ${string} near where those days are spent, read up as text and set on the plan - and this card is no part of it, also when the guest was looking through it a moment before. Asked with no place - or a country of many cities - it answers with the places to choose from: regions, countries and cities, each with how many ${string} it has; \`all\` lists the ${string} themselves. \`city\` (a city or a country) or \`query\` answers with the ${string}: a picture, the city and area, a rating where the ${string} publishes one, and a from-price, and a city usually holds several. With \`checkIn\` and \`checkOut\` every ${string} is priced for those nights - its cheapest room that takes the party, \`rooms\` of it, for the whole stay with taxes - or marked as having no room for them. \`sort\` and \`maxNightly\` order and cap by that price, or by the from-price without dates, where the matches share one currency. \`brand\`, \`facilities\`, \`freeCancellation\`, \`breakfast\` and \`bed\` narrow the list, and the answer counts what each would leave. A long list comes a page at a time: the answer says how many more there are and the \`offset\` of the next page. Words or a place that match nothing are widened once, and the answer names what it widened to.${string}`;
18
+ readonly description: `${string}, shown as a card for a guest to look through and pick one ${string} to book. It holds no plan of days: a day-by-day plan (\u884C\u7A0B, \u5B89\u6392, an itinerary) is a card of its own, laid out by the days, with a ${string} for each part of the trip read up as text. Asked with no place - or a country of many cities - it answers with the places to choose from: regions, countries and cities, each with how many ${string} it has; \`all\` lists the ${string} themselves. \`city\` (a city or a country) or \`query\` answers with the ${string}: a picture, the city and area, a rating where the ${string} publishes one, and a from-price, and a city usually holds several. With \`checkIn\` and \`checkOut\` every ${string} is priced for those nights - its cheapest room that takes the party, \`rooms\` of it, for the whole stay with taxes - or marked as having no room for them. \`sort\` and \`maxNightly\` order and cap by that price, or by the from-price without dates, where the matches share one currency. \`brand\`, \`facilities\`, \`freeCancellation\`, \`breakfast\` and \`bed\` narrow the list, and the answer counts what each would leave. A long list comes a page at a time: the answer says how many more there are and the \`offset\` of the next page. Words or a place that match nothing are widened once, and the answer names what it widened to.${string}`;
19
19
  readonly annotations: {
20
20
  readonly title: string;
21
21
  readonly readOnlyHint: true;
@@ -178,7 +178,7 @@ export declare const buildTools: (n: ToolNames, v: Vocabulary, caps?: BackendCap
178
178
  };
179
179
  } | {
180
180
  readonly name: `look_up_${string}${string}`;
181
- readonly description: `Reads up to 6 ${string} - a \`city\`'s (or a country's), or the ones named in \`${string}\` - and returns them as text, with nothing drawn and nothing shown to the guest: each one's id, area, rating, description and amenities, its add-ons with their ids and prices, and, given \`checkIn\` and \`checkOut\`, its rooms priced for those nights with taxes in. It is how the ${string} of a day-by-day plan are chosen - for each part of the trip, those near where its days are spent (a week in a big city has two or three parts, its far-out days side by side with a ${string} near them); a city with a span of dates and \u884C\u7A0B is such a plan, also right after a list of ${string} - and the ids are the ones a comparison and a plan take.`;
181
+ readonly description: `Reads up to 6 ${string} - a \`city\`'s (or a country's), or the ones named in \`${string}\` - and returns them as text, with nothing drawn and nothing shown to the guest: each one's id, area, rating, description and amenities, its add-ons with their ids and prices, and, given \`checkIn\` and \`checkOut\`, its rooms priced for those nights with taxes in. Nothing it reads is shown, so the ${string} of a day-by-day plan - for each part of the trip, those near where its days are spent - can be read up before the plan is drawn; its ids are the ones a comparison and a plan take.`;
182
182
  readonly annotations: {
183
183
  readonly title: string;
184
184
  readonly readOnlyHint: true;
@@ -663,7 +663,7 @@ export declare const buildTools: (n: ToolNames, v: Vocabulary, caps?: BackendCap
663
663
  };
664
664
  } | {
665
665
  readonly name: `show_${string}itinerary`;
666
- readonly description: `A day-by-day plan (\u884C\u7A0B) as one card - the card for a trip of several days, or for a guest who asks to have their days planned (a city with a span of dates and \u884C\u7A0B is one, also right after a list of ${string}): the days are laid out by where they are spent, and each part of the trip has a ${string} near those days, chosen from the ${string} read up as text. A week in a big city is usually two or three stays: its far-out sights fall on neighbouring days, and those nights have a ${string} near them, with the reason in that day's \`why\` (Beijing's Summer Palace, Yuanmingyuan, Fragrant Hills and the Peking and Tsinghua campuses - \u9890\u548C\u56ED, \u5706\u660E\u56ED, \u9999\u5C71, \u5317\u5927\u6E05\u534E - are days side by side in the north-west); a day out of town, such as the Great Wall, is a day of its own, there and back. One ${string} for the whole week is the plan when every day is within about 45 minutes of it, with the stay in two parts as the other way the reply names. Nothing here knows a sight's opening days: each day is drawn as it is given, with its date (many museums close on Mondays, the Palace Museum - \u6545\u5BAB - among them). Each day carries its own label, an optional note, the moments of the day in order, and the ${string} and add-ons attached to it, with their details filled in from their own records - a ${string} with its photograph and, given dates, the price of its nights. Each stay is marked with whether it is booked, in the trip or only in the plan, and the card says how many of each. Nothing is added to the trip.`;
666
+ readonly description: `A day-by-day plan (\u884C\u7A0B) as one card - the card for a trip of several days, or for days a guest asked to have planned (a city with a span of dates and \u884C\u7A0B is one): the days are laid out by where they are spent, and each part of the trip has a ${string} near those days, chosen from the ${string} read up as text. A week in a big city is usually two or three stays: its far-out sights fall on neighbouring days, and those nights have a ${string} near them, with the reason in that day's \`why\` (Beijing's Summer Palace, Yuanmingyuan, Fragrant Hills and the Peking and Tsinghua campuses - \u9890\u548C\u56ED, \u5706\u660E\u56ED, \u9999\u5C71, \u5317\u5927\u6E05\u534E - are days side by side in the north-west); a day out of town, such as the Great Wall, is a day of its own, there and back; one ${string} for the whole week suits days that are all within about 45 minutes of it. Nothing here knows a sight's opening days: each day is drawn as it is given, with its date (many museums close on Mondays, the Palace Museum - \u6545\u5BAB - among them). Each day carries its own label, an optional note, the moments of the day in order, and the ${string} and add-ons attached to it, with their details filled in from their own records - a ${string} with its photograph and, given dates, the price of its nights. Each stay is marked with whether it is booked, in the trip or only in the plan, and the card says how many of each. Nothing is added to the trip.`;
667
667
  readonly annotations: {
668
668
  readonly title: string;
669
669
  readonly readOnlyHint: true;
@@ -1079,6 +1079,14 @@ export type ToolContext = {
1079
1079
  tripKey?: string;
1080
1080
  /** The client's network address, as the edge proxy saw it - for the booking limits only. */
1081
1081
  ip?: string;
1082
+ /**
1083
+ * The client said at `initialize` that it shows no cards (`cardSupport` in
1084
+ * sessions.ts). An answer whose card carries what the model would say - the
1085
+ * plan, the comparison - then says that nothing was drawn and carries it in
1086
+ * words. Absent - a card host, or a session from before a restart - every
1087
+ * answer is as it always was.
1088
+ */
1089
+ noCards?: boolean;
1082
1090
  };
1083
1091
  export type ToolAnswer = {
1084
1092
  structured: Record<string, unknown>;
@@ -1155,4 +1163,14 @@ export declare const handleTool: (deps: ToolDeps, name: string, rawArgs: unknown
1155
1163
  * of a long stay at one hotel. It is in every plan's answer now.
1156
1164
  */
1157
1165
  export declare const PLAN_DRAWN_ONCE = "The plan on the card is this one. Something to change that you notice now - a sight closed on its day, a day too far from its hotel - goes in your reply, not in another plan: the plan is drawn again only when the guest asks for the change.";
1166
+ /**
1167
+ * The same place in the answer, for a client that shows no cards.
1168
+ *
1169
+ * ⚠️ NO CARD, SO THE REPLY IS THE PLAN (总线程, 2026-10-28, a client whose
1170
+ * `initialize` declared nothing): it was told "The plan on the card is this
1171
+ * one", and a model told that leaves the plan to a card the guest never sees.
1172
+ */
1173
+ export declare const PLAN_NOT_DRAWN = "Nothing was drawn: this app shows no cards, so the guest has seen none of this plan. Tell it to them in your reply, day by day - where each day is spent, and each stay with its hotel, dates, price and terms as above. Something to change that you notice now - a sight closed on its day, a day too far from its hotel - is said in that reply, not planned again: the plan is made again only when the guest asks for the change.";
1174
+ /** A comparison's answer, for a client that shows no cards - see `PLAN_NOT_DRAWN`. */
1175
+ export declare const COMPARISON_NOT_DRAWN = "Nothing was drawn: this app shows no cards, so the guest has seen none of this comparison. Compare the hotels in your reply, with each one's price and terms as given here - each is its cheapest room that sleeps the party.";
1158
1176
  //# sourceMappingURL=tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/dist/tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_PAGE, FULL_CAPABILITIES, LASTING_REQUESTS, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, guestTooLong, unitWords, } from './backend.js';
2
2
  import { MAX_GUESTS, isDay, nights as nightsBetween } from './dates.js';
3
3
  import { FACILITIES } from './facilities.js';
4
- import { DESCRIPTION_MAX, POLICY_MAX, hotelText } from './fence.js';
4
+ import { DESCRIPTION_MAX, POLICY_MAX, hotelName, hotelText } from './fence.js';
5
5
  import { clockOf, minutesOf, startProblem, stayWords, windowWords } from './hourly.js';
6
6
  import { amountsIn, formatMoney, keepingAmounts, money, multiplyMoney, toMinor } from './money.js';
7
7
  import { namedExactly, roomNamesOf, roomTypeNamed, withTraditional } from './names.js';
@@ -298,7 +298,7 @@ const allTools = (n, v) => {
298
298
  // description says it too, as what the card is, never as an order: the
299
299
  // directory rule (tool-descriptions.test.ts) holds, and "first" is one of
300
300
  // its words, hence "the other way round".
301
- description: `${upFirst(catalogue(v))}, shown as a card for a guest to look through and pick one ${v.noun} to book. A trip of several days that wants its days planned (行程, 安排, an itinerary) is planned the other way round - the days, then for each part of the trip a ${v.noun} near where those days are spent, read up as text and set on the plan - and this card is no part of it, also when the guest was looking through it a moment before. Asked with no place - or a country of many cities - it answers with the places to choose from: regions, countries and cities, each with how many ${v.nounPlural} it has; \`all\` lists the ${v.nounPlural} themselves. \`city\` (a city or a country) or \`query\` answers with the ${v.nounPlural}: a picture, the city and area, a rating where the ${v.noun} publishes one, and a from-price, and a city usually holds several. With \`checkIn\` and \`checkOut\` every ${v.noun} is priced for those nights - its cheapest room that takes the party, \`rooms\` of it, for the whole stay with taxes - or marked as having no room for them. \`sort\` and \`maxNightly\` order and cap by that price, or by the from-price without dates, where the matches share one currency. \`brand\`, \`facilities\`, \`freeCancellation\`, \`breakfast\` and \`bed\` narrow the list, and the answer counts what each would leave. A long list comes a page at a time: the answer says how many more there are and the \`offset\` of the next page. Words or a place that match nothing are widened once, and the answer names what it widened to.${v.hints.search ? ` ${v.hints.search}` : ''}`,
301
+ description: `${upFirst(catalogue(v))}, shown as a card for a guest to look through and pick one ${v.noun} to book. It holds no plan of days: a day-by-day plan (行程, 安排, an itinerary) is a card of its own, laid out by the days, with a ${v.noun} for each part of the trip read up as text. Asked with no place - or a country of many cities - it answers with the places to choose from: regions, countries and cities, each with how many ${v.nounPlural} it has; \`all\` lists the ${v.nounPlural} themselves. \`city\` (a city or a country) or \`query\` answers with the ${v.nounPlural}: a picture, the city and area, a rating where the ${v.noun} publishes one, and a from-price, and a city usually holds several. With \`checkIn\` and \`checkOut\` every ${v.noun} is priced for those nights - its cheapest room that takes the party, \`rooms\` of it, for the whole stay with taxes - or marked as having no room for them. \`sort\` and \`maxNightly\` order and cap by that price, or by the from-price without dates, where the matches share one currency. \`brand\`, \`facilities\`, \`freeCancellation\`, \`breakfast\` and \`bed\` narrow the list, and the answer counts what each would leave. A long list comes a page at a time: the answer says how many more there are and the \`offset\` of the next page. Words or a place that match nothing are widened once, and the answer names what it widened to.${v.hints.search ? ` ${v.hints.search}` : ''}`,
302
302
  annotations: reads(`Browse ${catalogue(v)}`, open),
303
303
  inputSchema: {
304
304
  type: 'object',
@@ -431,7 +431,7 @@ const allTools = (n, v) => {
431
431
  // the guest for one question (robin, 2026-09-29, Opus in Claude.ai). The
432
432
  // reference apps do the same split: search without a card, present with
433
433
  // one.
434
- description: `Reads up to ${LOOK_UP_MAX} ${v.nounPlural} - a \`city\`'s (or a country's), or the ones named in \`${v.nounPlural}\` - and returns them as text, with nothing drawn and nothing shown to the guest: each one's id, area, rating, description and amenities, its add-ons with their ids and prices, and, given \`checkIn\` and \`checkOut\`, its rooms priced for those nights with taxes in. It is how the ${v.nounPlural} of a day-by-day plan are chosen - for each part of the trip, those near where its days are spent (a week in a big city has two or three parts, its far-out days side by side with a ${v.noun} near them); a city with a span of dates and 行程 is such a plan, also right after a list of ${v.nounPlural} - and the ids are the ones a comparison and a plan take.`,
434
+ description: `Reads up to ${LOOK_UP_MAX} ${v.nounPlural} - a \`city\`'s (or a country's), or the ones named in \`${v.nounPlural}\` - and returns them as text, with nothing drawn and nothing shown to the guest: each one's id, area, rating, description and amenities, its add-ons with their ids and prices, and, given \`checkIn\` and \`checkOut\`, its rooms priced for those nights with taxes in. Nothing it reads is shown, so the ${v.nounPlural} of a day-by-day plan - for each part of the trip, those near where its days are spent - can be read up before the plan is drawn; its ids are the ones a comparison and a plan take.`,
435
435
  annotations: reads(`Look up ${catalogue(v)}`, open),
436
436
  inputSchema: {
437
437
  type: 'object',
@@ -740,7 +740,7 @@ const allTools = (n, v) => {
740
740
  },
741
741
  {
742
742
  name: n.itinerary,
743
- description: `A day-by-day plan (行程) as one card - the card for a trip of several days, or for a guest who asks to have their days planned (a city with a span of dates and 行程 is one, also right after a list of ${v.nounPlural}): the days are laid out by where they are spent, and each part of the trip has a ${v.noun} near those days, chosen from the ${v.nounPlural} read up as text. A week in a big city is usually two or three stays: its far-out sights fall on neighbouring days, and those nights have a ${v.noun} near them, with the reason in that day's \`why\` (Beijing's Summer Palace, Yuanmingyuan, Fragrant Hills and the Peking and Tsinghua campuses - 颐和园, 圆明园, 香山, 北大清华 - are days side by side in the north-west); a day out of town, such as the Great Wall, is a day of its own, there and back. One ${v.noun} for the whole week is the plan when every day is within about 45 minutes of it, with the stay in two parts as the other way the reply names. Nothing here knows a sight's opening days: each day is drawn as it is given, with its date (many museums close on Mondays, the Palace Museum - 故宫 - among them). Each day carries its own label, an optional note, the moments of the day in order, and the ${v.nounPlural} and add-ons attached to it, with their details filled in from their own records - a ${v.noun} with its photograph and, given dates, the price of its nights. Each stay is marked with whether it is booked, in the trip or only in the plan, and the card says how many of each. Nothing is added to the trip.`,
743
+ description: `A day-by-day plan (行程) as one card - the card for a trip of several days, or for days a guest asked to have planned (a city with a span of dates and 行程 is one): the days are laid out by where they are spent, and each part of the trip has a ${v.noun} near those days, chosen from the ${v.nounPlural} read up as text. A week in a big city is usually two or three stays: its far-out sights fall on neighbouring days, and those nights have a ${v.noun} near them, with the reason in that day's \`why\` (Beijing's Summer Palace, Yuanmingyuan, Fragrant Hills and the Peking and Tsinghua campuses - 颐和园, 圆明园, 香山, 北大清华 - are days side by side in the north-west); a day out of town, such as the Great Wall, is a day of its own, there and back; one ${v.noun} for the whole week suits days that are all within about 45 minutes of it. Nothing here knows a sight's opening days: each day is drawn as it is given, with its date (many museums close on Mondays, the Palace Museum - 故宫 - among them). Each day carries its own label, an optional note, the moments of the day in order, and the ${v.nounPlural} and add-ons attached to it, with their details filled in from their own records - a ${v.noun} with its photograph and, given dates, the price of its nights. Each stay is marked with whether it is booked, in the trip or only in the plan, and the card says how many of each. Nothing is added to the trip.`,
744
744
  annotations: reads(v.market ? `Lay out an itinerary on ${v.brand}` : `Lay out a ${v.brand} itinerary`, open),
745
745
  inputSchema: {
746
746
  type: 'object',
@@ -983,6 +983,8 @@ const TRIP_SCHEMA = {
983
983
  subtotal: STR,
984
984
  taxesAndFees: STR,
985
985
  total: STR,
986
+ /** Which of a room type's rates a room named by its type was put in as - the text's first line. */
987
+ took: STR,
986
988
  /** Who is signed in, on every payload. */
987
989
  member: OBJ,
988
990
  /** What the tier took off THIS trip, and what it earns. */
@@ -1340,6 +1342,8 @@ export const buildOutputSchemas = (n) => ({
1340
1342
  /** Present only when the columns are priced in more than one currency. */
1341
1343
  currencies: STRS,
1342
1344
  rowLabels: STRS,
1345
+ /** Only for a client that shows no cards: that nothing was drawn - `COMPARISON_NOT_DRAWN`. */
1346
+ notDrawn: STR,
1343
1347
  },
1344
1348
  required: ['view'],
1345
1349
  },
@@ -1373,6 +1377,12 @@ export const buildOutputSchemas = (n) => ({
1373
1377
  inTrip: { type: 'number' },
1374
1378
  planned: { type: 'number' },
1375
1379
  dropped: ARR,
1380
+ /** The stays not booked yet, together - only when all are priced in one currency. */
1381
+ staysTotal: STR,
1382
+ /** What the text says of a long stay at one hotel, in the same words. */
1383
+ nudges: STRS,
1384
+ /** Only for a client that shows no cards: that nothing was drawn - `PLAN_NOT_DRAWN`. */
1385
+ notDrawn: STR,
1376
1386
  },
1377
1387
  required: ['view'],
1378
1388
  },
@@ -2815,8 +2825,8 @@ const suggestionsFor = async (deps, d) => {
2815
2825
  out.push({
2816
2826
  // The label IS what is sent: a chip that sends more than it says is
2817
2827
  // a chip that says something else in the guest's name.
2818
- label: `Compare ${hotel.name} with the other ${hotel.city} hotels`,
2819
- say: `Compare ${hotel.name} with the other ${hotel.city} hotels`,
2828
+ label: `Compare ${hotelName(hotel.name)} with the other ${hotelName(hotel.city)} hotels`,
2829
+ say: `Compare ${hotelName(hotel.name)} with the other ${hotelName(hotel.city)} hotels`,
2820
2830
  });
2821
2831
  }
2822
2832
  // The price order is not a suggestion any more: it is the list's own sort
@@ -2825,12 +2835,12 @@ const suggestionsFor = async (deps, d) => {
2825
2835
  const pick = (d.columns ?? []).find((c) => c.recommended);
2826
2836
  if (pick) {
2827
2837
  out.push({
2828
- label: `Rooms at ${pick.hotel.name}`,
2838
+ label: `Rooms at ${hotelName(pick.hotel.name)}`,
2829
2839
  ask: { tool: 'getHotel', args: { hotel: pick.hotel.id, ...on } },
2830
2840
  });
2831
2841
  out.push({
2832
- label: `Plan the days at ${pick.hotel.name}`,
2833
- say: `Plan the days at ${pick.hotel.name}`,
2842
+ label: `Plan the days at ${hotelName(pick.hotel.name)}`,
2843
+ say: `Plan the days at ${hotelName(pick.hotel.name)}`,
2834
2844
  });
2835
2845
  }
2836
2846
  }
@@ -2994,6 +3004,48 @@ const OTHER_AREAS_MAX = 4;
2994
3004
  * of a long stay at one hotel. It is in every plan's answer now.
2995
3005
  */
2996
3006
  export const PLAN_DRAWN_ONCE = 'The plan on the card is this one. Something to change that you notice now - a sight closed on its day, a day too far from its hotel - goes in your reply, not in another plan: the plan is drawn again only when the guest asks for the change.';
3007
+ /**
3008
+ * The same place in the answer, for a client that shows no cards.
3009
+ *
3010
+ * ⚠️ NO CARD, SO THE REPLY IS THE PLAN (总线程, 2026-10-28, a client whose
3011
+ * `initialize` declared nothing): it was told "The plan on the card is this
3012
+ * one", and a model told that leaves the plan to a card the guest never sees.
3013
+ */
3014
+ export const PLAN_NOT_DRAWN = 'Nothing was drawn: this app shows no cards, so the guest has seen none of this plan. Tell it to them in your reply, day by day - where each day is spent, and each stay with its hotel, dates, price and terms as above. Something to change that you notice now - a sight closed on its day, a day too far from its hotel - is said in that reply, not planned again: the plan is made again only when the guest asks for the change.';
3015
+ /** A comparison's answer, for a client that shows no cards - see `PLAN_NOT_DRAWN`. */
3016
+ export const COMPARISON_NOT_DRAWN = "Nothing was drawn: this app shows no cards, so the guest has seen none of this comparison. Compare the hotels in your reply, with each one's price and terms as given here - each is its cheapest room that sleeps the party.";
3017
+ /**
3018
+ * One column of a comparison as a line, for a client that shows no cards:
3019
+ * what the card's rows say - where, the rating, the room, the price - and the
3020
+ * terms the rows only answer yes or no to.
3021
+ */
3022
+ const comparedInWords = (c) => {
3023
+ const h = c.hotel;
3024
+ const about = [
3025
+ [h.area, h.city].filter(Boolean).join(', '),
3026
+ h.rating ? `${h.rating.value}/${h.rating.scaleMax} from ${h.rating.count} reviews` : '',
3027
+ ]
3028
+ .filter(Boolean)
3029
+ .join(' · ');
3030
+ const p = c.price;
3031
+ const priced = p
3032
+ ? [
3033
+ `${hotelName(p.room)} at ${p.nightly} a night, ${p.stayTotal} for the stay incl. taxes and fees`,
3034
+ p.freeCancellationUntil
3035
+ ? `free to cancel until ${freeUntilText(p)}`
3036
+ : p.refundable
3037
+ ? 'no free cancellation'
3038
+ : 'non-refundable',
3039
+ p.payment === 'prepaid' ? 'paid in advance' : '',
3040
+ p.breakfast ? 'breakfast included' : 'no breakfast',
3041
+ ]
3042
+ .filter(Boolean)
3043
+ .join('; ')
3044
+ : c.from
3045
+ ? `from ${c.from} a night; rooms are priced once there are dates`
3046
+ : 'not priced without dates';
3047
+ return `- ${hotelName(h.name)} (\`${h.id}\`)${about ? ` · ${about}` : ''}: ${priced}`;
3048
+ };
2997
3049
  /** An area's names as a day might say them: "Haidian", "海淀区" and "海淀". */
2998
3050
  const areaWords = (h) => {
2999
3051
  const out = new Set();
@@ -3072,7 +3124,7 @@ const longStayNudges = async (deps, stays, days) => {
3072
3124
  const h = [...byArea.values()].find((o) => dayNames(day, areaWords(o)));
3073
3125
  return h
3074
3126
  ? [
3075
- `Day ${i + 1}${day.date ? ` (${day.date})` : ''} names ${h.area}, where ${h.name} (\`${h.id}\`) is.`,
3127
+ `Day ${i + 1}${day.date ? ` (${day.date})` : ''} names ${hotelName(h.area)}, where ${hotelName(h.name)} (\`${h.id}\`) is.`,
3076
3128
  ]
3077
3129
  : [];
3078
3130
  })
@@ -3081,14 +3133,18 @@ const longStayNudges = async (deps, stays, days) => {
3081
3133
  continue;
3082
3134
  const others = [...byArea.values()]
3083
3135
  .slice(0, OTHER_AREAS_MAX)
3084
- .map((h) => `${h.area} - ${h.name} (\`${h.id}\`)`);
3136
+ .map((h) => `${hotelName(h.area)} - ${hotelName(h.name)} (\`${h.id}\`)`);
3137
+ // The plan's own stay, in the same cleaned words.
3138
+ const at = hotelName(x.name);
3139
+ const area = hotelName(x.area);
3140
+ const city = hotelName(x.city);
3085
3141
  out.push(whole
3086
3142
  ? [
3087
- `All ${x.nights} nights are at ${x.name} (${x.area}). ${x.city} also has: ${others.join('; ')}.`,
3143
+ `All ${x.nights} nights are at ${at} (${area}). ${city} also has: ${others.join('; ')}.`,
3088
3144
  ...elsewhere,
3089
- `One hotel for the whole trip fits only when every day is within about 45 minutes of it. In your reply, name the days spent farther out than that and add one sentence on the other way - "it could also be two stays: ..." - those nights at the hotel here nearest them, and why; or say in one sentence why every day is within reach of ${x.area}.`,
3145
+ `One hotel for the whole trip fits only when every day is within about 45 minutes of it. In your reply, name the days spent farther out than that and add one sentence on the other way - "it could also be two stays: ..." - those nights at the hotel here nearest them, and why; or say in one sentence why every day is within reach of ${area}.`,
3090
3146
  ].join(' ')
3091
- : `${x.nights} nights are at ${x.name} (${x.area}). ${x.city} also has: ${others.join('; ')}. In your reply, name the planned days that are far from ${x.area} and offer moving those nights to the hotel here nearest them, with the reason in one sentence - or say why every day suits ${x.area}.`);
3147
+ : `${x.nights} nights are at ${at} (${area}). ${city} also has: ${others.join('; ')}. In your reply, name the planned days that are far from ${area} and offer moving those nights to the hotel here nearest them, with the reason in one sentence - or say why every day suits ${area}.`);
3092
3148
  }
3093
3149
  return out;
3094
3150
  };
@@ -3191,6 +3247,31 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
3191
3247
  // 2026-10-12).
3192
3248
  const who = identity(deps, ctx, key);
3193
3249
  const startTime = str(args.startTime);
3250
+ // ⚠️ TAKING A ROOM OUT NEEDS NO ROOM FOR SALE. The lookups below are of
3251
+ // what is for sale, so a room that had gone since it went into the trip -
3252
+ // the stay a booking could not make - could not be taken out of it
3253
+ // (2026-10-14). Nor could a room by the hour once its window had passed:
3254
+ // the start was checked against today's window first, and every line of a
3255
+ // trip must be removable (2026-10-27 audit, F6). The line in the trip is the
3256
+ // one asked about - by its nights, or a room by the hour by its start.
3257
+ if (args.rooms !== undefined && clampCount(args.rooms, MAX_ROOMS_PER_LINE) === 0) {
3258
+ const asked = asStay(args);
3259
+ const start = startTime ? minutesOf(startTime) : undefined;
3260
+ const held = deps.trips
3261
+ .read(store)
3262
+ .find((l) => l.kind === 'room' &&
3263
+ l.hotelId === hotelId &&
3264
+ l.roomId === roomRef &&
3265
+ (l.hourly
3266
+ ? start !== undefined &&
3267
+ minutesOf(l.hourly.start) === start &&
3268
+ (!asked || l.stay.checkIn === asked.checkIn)
3269
+ : !!asked && l.stay.checkIn === asked.checkIn && l.stay.checkOut === asked.checkOut));
3270
+ if (held) {
3271
+ deps.trips.set(store, unset(held));
3272
+ return currentTrip(deps, key, who, store);
3273
+ }
3274
+ }
3194
3275
  const today = (startTime || !asStay(args)
3195
3276
  ? deps.backend.hourlyRooms?.(hotelId, guestsOf(args), who)
3196
3277
  : undefined);
@@ -3269,23 +3350,6 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
3269
3350
  : 'Which nights? A room needs a check-in and a check-out date before it can go into the trip.',
3270
3351
  };
3271
3352
  }
3272
- // ⚠️ TAKING A ROOM OUT NEEDS NO ROOM FOR SALE. The lookup below is of
3273
- // what is for sale, so a room that had gone since it went into the trip -
3274
- // the stay a booking could not make - could not be taken out of it
3275
- // (2026-10-14). The line in the trip is the one asked about.
3276
- if (args.rooms !== undefined && clampCount(args.rooms, MAX_ROOMS_PER_LINE) === 0) {
3277
- const held = deps.trips
3278
- .read(store)
3279
- .find((l) => l.kind === 'room' &&
3280
- l.hotelId === hotelId &&
3281
- l.roomId === roomRef &&
3282
- l.stay.checkIn === stay.checkIn &&
3283
- l.stay.checkOut === stay.checkOut);
3284
- if (held) {
3285
- deps.trips.set(store, unset(held));
3286
- return currentTrip(deps, key, identity(deps, ctx, key), store);
3287
- }
3288
- }
3289
3353
  // The backend owns the calendar - it knows each hotel's own timezone, and a
3290
3354
  // second opinion computed from this process's clock would disagree with it
3291
3355
  // for eight hours a day.
@@ -3370,7 +3434,11 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
3370
3434
  rooms: clampCount(args.rooms ?? 1, MAX_ROOMS_PER_LINE),
3371
3435
  });
3372
3436
  const now = await currentTrip(deps, key, identity(deps, ctx, key), store);
3373
- return took ? { ...now, text: `${took}\n${now.text}` } : now;
3437
+ // Which rate was taken, in the structure too and in the same words - see
3438
+ // the itinerary's `nudges` (2026-10-27 audit, F11).
3439
+ return took
3440
+ ? { ...now, structured: { ...now.structured, took }, text: `${took}\n${now.text}` }
3441
+ : now;
3374
3442
  };
3375
3443
  /**
3376
3444
  * Books the rooms in one slot of the trip store for a named guest - THE ONE
@@ -4370,7 +4438,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4370
4438
  const extras = await deps.backend.extrasFor(h.id, stay);
4371
4439
  const rooms = (detail.rooms ?? []).filter((r) => r.roomsLeft >= roomsWanted);
4372
4440
  blocks.push([
4373
- `### ${h.name} · id \`${h.id}\``,
4441
+ `### ${hotelName(h.name)} · id \`${h.id}\``,
4374
4442
  [
4375
4443
  [h.area, h.city, h.country].filter(Boolean).join(', '),
4376
4444
  h.brand,
@@ -4386,7 +4454,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4386
4454
  : rooms.length
4387
4455
  ? [
4388
4456
  `Rooms for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}:`,
4389
- ...rooms.map((r) => `- ${r.name} · sleeps ${r.sleeps}${r.breakfast ? ' · breakfast' : ''} · ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${r.freeCancellationUntil ? `, free to cancel until ${freeUntilText(r)}` : r.refundable ? '' : ', non-refundable'}`),
4457
+ ...rooms.map((r) => `- ${r.name} · sleeps ${r.sleeps}${r.breakfast ? ' · breakfast' : ''} · ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${roomsWanted > 1 ? ` (${formatMoney(multiplyMoney(r.stayTotal, roomsWanted))} for ${roomsWanted} rooms)` : ''}${r.freeCancellationUntil ? `, free to cancel until ${freeUntilText(r)}` : r.refundable ? '' : ', non-refundable'}`),
4390
4458
  ].join('\n')
4391
4459
  : (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
4392
4460
  extras.length
@@ -4721,9 +4789,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4721
4789
  url: step.url,
4722
4790
  ...(step.kind === 'confirm' ? { booking: bookingForCard(step.booking) } : {}),
4723
4791
  },
4792
+ // ⚠️ THE LINK STAYS ON THE CARD (2026-10-27 audit, F9). Only the card
4793
+ // calls this, and the card hands this text to the model as context - so
4794
+ // UnionPay's own page, a link nobody here knows the life of, went into
4795
+ // the conversation. `url` is in the structure, for the card alone.
4724
4796
  text: step.kind === 'enroll'
4725
- ? `No UnionPay card is on file for that email yet. UnionPay's page to add one: ${step.url} - once it is added, paying with UnionPay again opens the confirmation.`
4726
- : `UnionPay's page to confirm ${step.booking.confirmationCode} on: ${step.url} - the hotel is asked once UnionPay has confirmed it.`,
4797
+ ? `No UnionPay card is on file for that email yet. UnionPay's page to add one is on the card - once it is added, paying with UnionPay again opens the confirmation.`
4798
+ : `UnionPay's page to confirm ${step.booking.confirmationCode} on is on the card - the hotel is asked once UnionPay has confirmed it.`,
4727
4799
  };
4728
4800
  }
4729
4801
  if (name === t.previewChange || name === t.changeBooking) {
@@ -5080,6 +5152,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5080
5152
  ].join('\n'),
5081
5153
  };
5082
5154
  }
5155
+ const stayWords = comparison.stay
5156
+ ? ` for ${comparison.stay.checkIn} to ${comparison.stay.checkOut}`
5157
+ : '';
5083
5158
  return {
5084
5159
  structured: {
5085
5160
  view: 'comparison',
@@ -5089,11 +5164,24 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5089
5164
  ...c,
5090
5165
  hotel: { ...c.hotel, fromNightly: undefined },
5091
5166
  })),
5167
+ // In both, like the itinerary's: a host with no card may hand its
5168
+ // model this rather than the text.
5169
+ ...(ctx.noCards ? { notDrawn: COMPARISON_NOT_DRAWN } : {}),
5092
5170
  },
5093
5171
  text: [
5094
5172
  // The card carries the table; the text carries only what the model
5095
5173
  // could not see before it asked - what was dropped, and why.
5096
- `${comparison.columns.length} side by side${comparison.stay ? ` for ${comparison.stay.checkIn} to ${comparison.stay.checkOut}` : ''}.`,
5174
+ //
5175
+ // ⚠️ UNLESS THERE IS NO CARD (总线程, 2026-10-28): a client that shows
5176
+ // none was answered "2 side by side" and nothing else, and its model
5177
+ // could not say what either hotel cost. Then the table is said here,
5178
+ // one line a hotel, from the same records the card's rows are read off.
5179
+ ...(ctx.noCards
5180
+ ? [
5181
+ `${comparison.columns.length} compared${stayWords}. ${COMPARISON_NOT_DRAWN}`,
5182
+ ...comparison.columns.map(comparedInWords),
5183
+ ]
5184
+ : [`${comparison.columns.length} side by side${stayWords}.`]),
5097
5185
  // ⚠️ SAY IT IN THE TEXT TOO. The card carries the warning for whoever is
5098
5186
  // looking at it; this line is for the model, which is about to describe
5099
5187
  // the table in prose and would otherwise read "¥67,500" against
@@ -5146,22 +5234,47 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5146
5234
  const stayItems = plan.days
5147
5235
  .flatMap((day) => day.items)
5148
5236
  .filter((x) => x.kind === 'hotel' && (x.night ?? 1) === 1 && x.checkIn && x.roomId);
5149
- const stays = stayItems.map((x, i) => `Stay ${i + 1}: ${x.name} (\`${x.id}\`), room \`${x.roomId}\`${x.roomName ? ` (${x.roomName})` : ''}, ${x.checkIn} to ${x.checkOut}${x.nights ? ` (${x.nights} ${x.nights === 1 ? 'night' : 'nights'})` : ''}${x.booked ? ` - booked, ${x.booked}` : x.inTrip ? ' - in the trip' : ''}`);
5237
+ // Each stay's rate says its terms where they bind - see `ItineraryItem.refundable`.
5238
+ const termsOf = (x) => [
5239
+ x.refundable === false ? 'non-refundable' : '',
5240
+ x.payment === 'prepaid' ? 'paid in advance' : '',
5241
+ ]
5242
+ .filter(Boolean)
5243
+ .join(', ');
5244
+ // With no card, each stay's own price too: the card shows it beside the
5245
+ // stay, and the reply is now the only place the guest reads it.
5246
+ const stays = stayItems.map((x, i) => `Stay ${i + 1}: ${hotelName(x.name)} (\`${x.id}\`), room \`${x.roomId}\`${x.roomName ? ` (${hotelName(x.roomName)}${termsOf(x) ? `; ${termsOf(x)}` : ''})` : termsOf(x) ? ` (${termsOf(x)})` : ''}, ${x.checkIn} to ${x.checkOut}${x.nights ? ` (${x.nights} ${x.nights === 1 ? 'night' : 'nights'})` : ''}${ctx.noCards && x.stayTotal ? `, ${x.stayTotal} for the stay incl. taxes and fees` : ''}${x.booked ? ` - booked, ${x.booked}` : x.inTrip ? ' - in the trip' : ''}`);
5247
+ // ⚠️ IN BOTH, IN THE SAME WORDS (2026-10-27 audit, F11): a host that hands
5248
+ // the model `structuredContent` rather than the text never saw what the
5249
+ // text said of a long stay. The model's part of the answer is the same on
5250
+ // either side.
5251
+ const nudges = stays.length ? await longStayNudges(deps, stayItems, plan.days) : [];
5150
5252
  return {
5151
- structured: { view: 'itinerary', ...common(deps, key), ...plan, gap },
5253
+ structured: {
5254
+ view: 'itinerary',
5255
+ ...common(deps, key),
5256
+ ...plan,
5257
+ gap,
5258
+ ...(nudges.length ? { nudges } : {}),
5259
+ // In both, like the nudges: a host with no card may hand its model
5260
+ // this rather than the text.
5261
+ ...(ctx.noCards ? { notDrawn: PLAN_NOT_DRAWN } : {}),
5262
+ },
5152
5263
  text: [
5153
5264
  `**${plan.title}** · ${plan.days.length} ${plan.days.length === 1 ? 'day' : 'days'}. ${gap}`,
5154
5265
  ...(stays.length
5155
5266
  ? [
5156
5267
  `Priced stays (${plan.stay?.guests ?? 2} guests; ${t.addToTrip} takes each one's hotel id, room id and dates):`,
5157
5268
  ...stays,
5158
- // ⚠️ IN THE TEXT ONLY - not in `structured`, not in the schema.
5159
- ...(await longStayNudges(deps, stayItems, plan.days)),
5269
+ ...(plan.staysTotal
5270
+ ? [`Together, the stays not booked yet: ${plan.staysTotal}.`]
5271
+ : []),
5272
+ ...nudges,
5160
5273
  ]
5161
5274
  : []),
5162
5275
  ...plan.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
5163
5276
  ...fitsManyOf(deps, plan.dropped),
5164
- PLAN_DRAWN_ONCE,
5277
+ ctx.noCards ? PLAN_NOT_DRAWN : PLAN_DRAWN_ONCE,
5165
5278
  deps.vocab.demo ? noteFor(deps.vocab) : '',
5166
5279
  ]
5167
5280
  .filter(Boolean)