@curless/hotel-storefront 0.55.1 → 0.55.2

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.
package/dist/tools.js CHANGED
@@ -2312,10 +2312,15 @@ const couponRefusedLine = (q) => q.couponRefused
2312
2312
  ? `The coupon ${q.couponRefused.name ? couponName({ name: q.couponRefused.name, names: q.couponRefused.names }) : q.couponRefused.id} was NOT taken off: ${q.couponRefused.why} The total is without it.`
2313
2313
  : '';
2314
2314
  const tripText = (quote, noCards) => {
2315
+ // A part not priced because its group did not answer just now: said once, and
2316
+ // left to the guest to ask for again (`askLater`; the trip is drawn as a card).
2317
+ const later = quote.dropped?.some((d) => d.code === 'unreachable') ? askLater(!noCards) : '';
2315
2318
  // ⚠️ NOT "EMPTY" WHEN SOMETHING WAS LEFT OUT: a trip whose only group did not
2316
2319
  // answer is not an empty trip (2026-11-03 review, C5).
2317
2320
  if (quote.lines.length === 0 && quote.dropped?.length)
2318
- return quote.dropped.map((d) => `Left out: ${d.title} — ${d.why}`).join('\n');
2321
+ return [...quote.dropped.map((d) => `Left out: ${d.title} — ${d.why}`), later]
2322
+ .filter(Boolean)
2323
+ .join('\n');
2319
2324
  if (quote.lines.length === 0)
2320
2325
  return 'The trip is empty.';
2321
2326
  const lines = quote.lines.map((l) => l.kind === 'room'
@@ -2327,6 +2332,7 @@ const tripText = (quote, noCards) => {
2327
2332
  couponRefusedLine(quote),
2328
2333
  ...memberTripLines(quote),
2329
2334
  ...(quote.dropped ?? []).map((d) => `Left out: ${d.title} — ${d.why}`),
2335
+ later,
2330
2336
  ]
2331
2337
  .filter(Boolean)
2332
2338
  .join('\n');
@@ -2994,6 +3000,20 @@ const remember = (deps, k, amount) => {
2994
3000
  };
2995
3001
  /** "a, b and c" - "a and b" - "a". */
2996
3002
  const listAnd = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}` : (xs[0] ?? '');
3003
+ /**
3004
+ * What the model does when a hotel group did not answer: tells the guest, and
3005
+ * leaves asking again to them - said after the sentence that names the groups,
3006
+ * in the text and in `structuredContent` alike.
3007
+ *
3008
+ * ⚠️ NEVER "ASKING AGAIN IN A MOMENT MAY BRING THEM" (robin 11-04). Read as
3009
+ * "ask again now", it had the model call the list a second time in the same
3010
+ * turn: two of the same card under 「打开 Otaless」 on claude.ai, with every
3011
+ * group of the backend silent. The guest asks again: with the card's 「重试」
3012
+ * where a card was drawn, in words where none was.
3013
+ */
3014
+ const askLater = (card) => card
3015
+ ? 'Do not look again in this turn - no second call for it: tell the guest, who can press Try again (重试) on the card when they want to look again.'
3016
+ : 'Do not look again in this turn - no second call for it: tell the guest, and that they can ask you to look again later.';
2997
3017
  /**
2998
3018
  * The groups of a market that did not answer a search, as the sentence the
2999
3019
  * model is told and the fields the card draws its own line from - the same
@@ -3013,12 +3033,14 @@ noun = deps.vocab.nounPlural,
3013
3033
  * card called it 锦江之星, another group (11-03, text-only eval). A card host
3014
3034
  * is told the English name alone; the card says the group in its own words.
3015
3035
  */
3016
- noCards = false) => {
3036
+ noCards = false,
3037
+ /** This answer draws a card, with its 「重试」 (`askLater`) - not so for `look_up`, which draws none. */
3038
+ card = !noCards) => {
3017
3039
  const down = answer.degraded ?? [];
3018
3040
  if (down.length === 0)
3019
3041
  return { structured: {}, said: '' };
3020
3042
  const one = down.length === 1;
3021
- const said = `${listAnd(down.map((d) => withZh(d.name, d.names, noCards)))} did not answer in time, so none of ${one ? 'its' : 'their'} ${noun} is in this list - this does not mean ${one ? 'it has' : 'they have'} none here; asking again in a moment may bring them.`;
3043
+ const said = `${listAnd(down.map((d) => withZh(d.name, d.names, noCards)))} did not answer in time, so none of ${one ? 'its' : 'their'} ${noun} is in this list - this does not mean ${one ? 'it has' : 'they have'} none here. ${askLater(card)}`;
3022
3044
  return {
3023
3045
  structured: {
3024
3046
  degraded: down.map((d) => ({ supplier: d.supplier, name: d.name })),
@@ -3036,13 +3058,15 @@ noCards = false) => {
3036
3058
  * time"), not the name of whichever was asked first; and in every case it
3037
3059
  * denies the reading a guest would make of silence, that there is nothing.
3038
3060
  */
3039
- const notAnswered = (deps, key, down) => {
3061
+ const notAnswered = (deps, key, down,
3062
+ /** The answer draws a card, with its 「重试」 - see `askLater`. */
3063
+ card) => {
3040
3064
  const groups = deps.backend.suppliers?.().length ?? 0;
3041
3065
  const names = listAnd(down.map((d) => d.name));
3042
3066
  const one = down.length === 1;
3043
3067
  const said = groups > 1 && down.length >= groups
3044
- ? `No hotel group answered this time: ${names} did not answer in time - this does not mean there is nothing here; asking again in a moment may bring them.`
3045
- : `${names} did not answer in time, so nothing from ${one ? 'it' : 'them'} could be read just now - this does not mean ${one ? 'it has' : 'they have'} none here; asking again in a moment may bring ${one ? 'it' : 'them'}.`;
3068
+ ? `No hotel group answered this time: ${names} did not answer in time - this does not mean there is nothing here. ${askLater(card)}`
3069
+ : `${names} did not answer in time, so nothing from ${one ? 'it' : 'them'} could be read just now - this does not mean ${one ? 'it has' : 'they have'} none here. ${askLater(card)}`;
3046
3070
  return {
3047
3071
  structured: {
3048
3072
  view: 'error',
@@ -3107,7 +3131,9 @@ const countProblem = (t, name, args) => {
3107
3131
  * properties of null" - were read back too (2026-10-05 audit); they go to the
3108
3132
  * log now.
3109
3133
  */
3110
- const guestWords = (err, fault = 'That could not be done just now - nothing was changed. Try again in a moment.') => {
3134
+ const guestWords = (err,
3135
+ // Not "try again in a moment" (robin 11-04): the guest asks for it again.
3136
+ fault = 'That could not be done just now - nothing was changed. Tell the guest; they can ask for it again later.') => {
3111
3137
  const e = err;
3112
3138
  // ⚠️ A READ THAT TIMED OUT IS NOT A REFUSAL (2026-11-03 review, C5). After a
3113
3139
  // booking was made, "Atour did not answer within 8000 ms" would be read
@@ -3666,7 +3692,9 @@ export const handleTool = async (deps, name, rawArgs, ctx) => {
3666
3692
  const down = unansweredBy(err);
3667
3693
  if (!down)
3668
3694
  throw err;
3669
- return notAnswered(deps, ctx.tripKey ?? tripKey(ctx.session, deps.vocab.brand), down);
3695
+ // `look_up` and the policies draw no card, so there is no 「重试」 to press.
3696
+ const card = !ctx.noCards && name !== deps.tools.lookUp && name !== deps.tools.policies;
3697
+ return notAnswered(deps, ctx.tripKey ?? tripKey(ctx.session, deps.vocab.brand), down, card);
3670
3698
  });
3671
3699
  const suggestions = await suggestionsFor(deps, answer.structured).catch(() => []);
3672
3700
  return { answer, suggestions };
@@ -5386,7 +5414,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5386
5414
  const description = hotelText('description', h.name,
5387
5415
  // Its 【…】 parts a line each, the words as they came (`aboutLines`).
5388
5416
  aboutLines(h.summary), DESCRIPTION_MAX, deps.vocab.demo);
5389
- const policies = await deps.backend.policies(h.id);
5417
+ // ⚠️ NO POLICIES WHERE THE POLICIES TOOL IS NOT OFFERED (总线程 11-04): Atour's
5418
+ // single store (`policies: false`) still listed its topics on every hotel, and
5419
+ // the card drew six rows of 「入住须知」 that each opened on "could not load
5420
+ // this policy". Not read, not in the answer - the model is not told of a
5421
+ // policy nobody can open either.
5422
+ const offersPolicies = (deps.backend.capabilities ?? FULL_CAPABILITIES).policies !== false;
5423
+ const policies = offersPolicies ? await deps.backend.policies(h.id) : [];
5390
5424
  const extras = await deps.backend.extrasFor(h.id, stay);
5391
5425
  const base = {
5392
5426
  ...common(deps, key),
@@ -5396,7 +5430,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5396
5430
  fromNightly: h.fromNightly ? formatMoney(h.fromNightly) : undefined,
5397
5431
  },
5398
5432
  extras: extras.map((e) => ({ ...e, price: formatMoney(e.price) })),
5399
- policies: policies.map((p) => ({ topic: p.topic, title: p.title })),
5433
+ ...(offersPolicies
5434
+ ? { policies: policies.map((p) => ({ topic: p.topic, title: p.title })) }
5435
+ : {}),
5400
5436
  };
5401
5437
  if (!stay) {
5402
5438
  return {
@@ -5437,7 +5473,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5437
5473
  // (it says it in the guest's language from `noRoomsCode`).
5438
5474
  const said = detail.noRooms && detail.noRoomsCode === 'none-on-sale'
5439
5475
  ? notOnSaleWords(groupNameOf(deps, h.id), detail.noRooms)
5440
- : detail.noRooms;
5476
+ : // Rooms that could not be read just now: asked for again by the guest.
5477
+ detail.noRooms && detail.noRoomsCode === 'unreachable'
5478
+ ? `${detail.noRooms} ${askLater(!ctx.noCards)}`
5479
+ : detail.noRooms;
5441
5480
  return {
5442
5481
  structured: {
5443
5482
  view: 'no-rooms',
@@ -5588,7 +5627,8 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5588
5627
  total = found.total;
5589
5628
  degradedFound = found;
5590
5629
  }
5591
- const degraded = degradedOf(deps, degradedFound, undefined, ctx?.noCards);
5630
+ // `look_up` draws no card: the guest asks again in words (`askLater`).
5631
+ const degraded = degradedOf(deps, degradedFound, undefined, ctx?.noCards, false);
5592
5632
  const unique = [...new Set(ids)];
5593
5633
  // A name that fits more than one is said with the ones it fits; the rest are not found.
5594
5634
  const notFound = dropped.filter((d) => !candidates.some((c) => c.for === d));
@@ -6398,7 +6438,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
6398
6438
  : []),
6399
6439
  ...fitsMany,
6400
6440
  ...silent.map((d) => `Left out: ${d.name} — ${d.why}`),
6401
- ].join('\n'),
6441
+ silent.length ? askLater(!ctx.noCards) : '',
6442
+ ]
6443
+ .filter(Boolean)
6444
+ .join('\n'),
6402
6445
  };
6403
6446
  }
6404
6447
  const stayWords = comparison.stay
@@ -6440,6 +6483,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
6440
6483
  : '',
6441
6484
  ...comparison.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
6442
6485
  ...fitsManyOf(deps, comparison.dropped),
6486
+ comparison.dropped.some((d) => d.code === 'supplier-timeout') ? askLater(!ctx.noCards) : '',
6443
6487
  ]
6444
6488
  .filter(Boolean)
6445
6489
  .join('\n'),
@@ -6464,7 +6508,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
6464
6508
  unchecked = down;
6465
6509
  }
6466
6510
  const uncheckedSaid = unchecked.length
6467
- ? `${listAnd(unchecked.map((d) => d.name))} did not answer in time, so ${unchecked.length === 1 ? 'its' : 'their'} bookings were not checked against this plan - a stay there may already be booked without the plan saying so; asking again in a moment may check them.`
6511
+ ? `${listAnd(unchecked.map((d) => d.name))} did not answer in time, so ${unchecked.length === 1 ? 'its' : 'their'} bookings were not checked against this plan - a stay there may already be booked without the plan saying so. ${askLater(!ctx.noCards)}`
6468
6512
  : '';
6469
6513
  const plan = await buildItinerary(deps.backend, days.map((d) => ({
6470
6514
  label: str(d.label),
@@ -6565,6 +6609,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
6565
6609
  : []),
6566
6610
  ...plan.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
6567
6611
  ...fitsManyOf(deps, plan.dropped),
6612
+ // Once, for every group left silent - the bookings' sentence has its own.
6613
+ !uncheckedSaid && plan.dropped.some((d) => d.code === 'supplier-timeout')
6614
+ ? askLater(!ctx.noCards)
6615
+ : '',
6568
6616
  uncheckedSaid,
6569
6617
  ctx.noCards ? PLAN_NOT_DRAWN : PLAN_DRAWN_ONCE,
6570
6618
  ]