jsfe 1.10.9 → 1.11.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.
package/README.md CHANGED
@@ -357,6 +357,23 @@ and the decoders (`decodeFlowLlm`, `decodeFlowSystemOne`, `decodeParametersLlm`,
357
357
  a stage-2 failure starts the flow without its parameters (it asks for them). The default, `'single-call'`,
358
358
  is the original single call.
359
359
 
360
+ **The reply ranker (contextual DISPATCH, `onResponse: "continue"`):** when a flow's question gets a reply its
361
+ deterministic reader cannot read, jsfe asks what the reply does with the step's `responseContext` (the question):
362
+ `answers`, `abort` or `new_request`. ONE aiCallback call whose 4th argument is `task: 'rank_reply'` with both
363
+ executors' instructions (`llm`, `systemOne`); the host replies `{ "option": … }`, plus `"fallback": true` when the
364
+ option is `answers` only because nothing cleared the cut (System One). `jsfe/host` exports
365
+ `buildReplyRank`, `decodeReplyRankLlm` and `decodeReplyRankSystemOne` (the highest option at or above 0.5, else
366
+ `answers`). `answers` continues the flow, after extracting the step's `responseParameters` with a stage-2 request
367
+ when it declares them. A confident `answers` (no `fallback`) is never lost: when the declaration is one parameter marked
368
+ `verbatim: true` (an open string whose words are the value, such as a reason) and extraction finds nothing or fails,
369
+ the field is the reply as said. Unmarked fields (a product, a city) always need a real extraction. After a
370
+ `fallback` answer extraction is the judge, and ReadFormResponse asks the question again when it finds nothing; `abort` is a RETURN: every flow ends and the reply is system message `cmd_request_aborted`
371
+ ("The process has been cancelled at your request - how can I help you?"),
372
+ outcome `user_aborted`, no recovery stamp;
373
+ `new_request` ends the flows and routes the input through intent detection. Only a new request is routed: after
374
+ `answers` a later ordinary DISPATCH in the same turn hands the host the turn without detection. A host that does not know the task answers the `llm`
375
+ instructions with its text model.
376
+
360
377
  **Host start (`ContextEntry.start`):** `{ role: 'user', content, start: { flow, arguments } }` starts that
361
378
  flow (any flow, by id or name) with those arguments on this turn, without asking the AI. Only the host sets it;
362
379
  it is never derived from what the user typed.
@@ -373,7 +390,7 @@ const engine = new WorkflowEngine(logger, aiCallback, flows, tools,
373
390
  { ...STANDARD_FUNCTIONS, sendSMSOTP, sendEmail /* the host's own, credentialed functions */ });
374
391
  ```
375
392
 
376
- - Reading answers: `matchesChoice`, `pickChoice`, `isAnswer`, `isAgentRequest`, `isOtherRequest`,
393
+ - Reading answers: `matchesChoice`, `pickChoice`, `isAnswer`, `isAgentRequest`, `isExitRequest`, `isOtherRequest`,
377
394
  `isSpelledFragment`, `isAmountAnswer`, `amountInAnswer`, `readEmail`, `readPhone`,
378
395
  `readInternationalPhone`, `readIdentifier`, `normalizeAndFindCapture`, and `matchInputs` (which of a
379
396
  form's inputs an answer fills; ties between inputs of the same form are returned for the flow to ask).
@@ -1142,7 +1159,7 @@ their original workflow seamlessly.
1142
1159
  ### Flow Execution Modes
1143
1160
  - ✅ **Linear Flow Execution** - Sequential step processing
1144
1161
  - ✅ **Sub-Flow Calls** - Nested workflow execution
1145
- - ✅ **Flow Interruption** - Suspend current flow for new task
1162
+ - ✅ **Flow Interruption** - Suspend current flow for new task: in a flow with `interruptable: true`, each reply goes through the same intent detection as a new request (two-stage when configured); a detected primary starts on a new stack with its extracted arguments
1146
1163
  - ✅ **Flow Resumption** - Return to previously suspended flows
1147
1164
  - ✅ **Flow Replacement** - Replace current flow with new flow
1148
1165
  - ✅ **Flow Reboot** - Nuclear option: clear all flows and restart (SAY output accumulated before the reboot is kept and delivered with the new flow's output; `discardSay: true` drops it)
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA4HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAgZD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA3kBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkwB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA4HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAmbD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA9mBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsyB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -241,7 +241,7 @@ const collectInput = {
241
241
  id: 'collect-input', name: 'CollectInput', version: '1.0.0', scope: 'isolated',
242
242
  description: "Worker: ask the form's current input once and file the answer wherever it belongs in the form chain (matchInputs, decided by scope; a declared confirm_against pair is asked). One reading → collect-input-accept; ambiguous → disambiguate-input; a question about the form's data → provide-data; a short miss → counted, the loop re-asks; 3+ words that fit nothing → DISPATCH. Params: form, current, questions, provides, exit_context(_es).",
243
243
  parameters: ['form', 'current', 'questions', 'provides', 'exit_context', 'exit_context_es'].map((n) => ({ name: n, type: ['questions'].includes(n) ? 'array' : n === 'provides' ? 'object' : 'string', description: n })),
244
- variables: vars({ ci_partial: '', ci_exhaustion: null, ci_d: {}, ci_pre: '', ci_val: '', ci_routed: '', ci_names: [], ci_chain: [], ci_spec: null, ci_choices: [], ci_q: '', ci_q_es: '', ci_missed: 0, ci_max: 3, ci_misses: 0, ci_miss_limit: 2, ci_must: false, ci_m: null, ci_qkey: '', ci_skip_gate: '', ci_words: 0, ci_digits: null, ci_amount: false, ci_as_spoken: false, ci_reader: {}, ci_pii: false, ci_ctx: '', ci_ctx_es: '' }),
244
+ variables: vars({ ci_partial: '', ci_reply_reader: '', ci_exhaustion: null, ci_d: {}, ci_pre: '', ci_val: '', ci_routed: '', ci_names: [], ci_chain: [], ci_spec: null, ci_choices: [], ci_q: '', ci_q_es: '', ci_missed: 0, ci_max: 3, ci_misses: 0, ci_miss_limit: 2, ci_must: false, ci_m: null, ci_qkey: '', ci_skip_gate: '', ci_words: 0, ci_digits: null, ci_amount: false, ci_as_spoken: false, ci_reader: {}, ci_pii: false, ci_ctx: '', ci_ctx_es: '' }),
245
245
  steps: [
246
246
  SET('ci_find_spec', 'ci_spec', "(function () { var s = ((cargo.forms[form] || {}).specs || []).filter(function (s) { return s.name === current; })[0]; return s ? (Array.isArray(s.any_of) ? Object.assign({}, s, { profile: { kind: 'group' } }) : s) : null; })()"),
247
247
  SET('ci_current_names', 'ci_names', "Array.isArray(ci_spec && ci_spec.any_of) ? ci_spec.any_of.map(function (m) { return m.name; }) : [current]"),
@@ -302,12 +302,13 @@ const collectInput = {
302
302
  SET('ci_skip_gate', 'ci_skip_gate', "!provides || !provides.gate || [].concat(provides.gate.verified || []).every(function(p){return !!formPath({cargo:cargo},p);}) ? 'true' : ''"),
303
303
  WHEN('ci_explicit_question', 'ci_m.questions.length > 0', FLOW('ci_explicit_provide', 'provide-data', 'call', { form: '{{form}}', provides: '{{provides}}', asked: '{{ci_qkey}}', asked_keys: '{{ci_m.questions}}', no_menu: 'true', no_gate: '{{ci_skip_gate}}' })),
304
304
  WHEN('ci_explicit_question_done', 'ci_m.questions.length > 0', END('ci_explicit_question_end')),
305
+ SET('ci_reply_reader', 'ci_reply_reader', "ci_choices.length ? 'read-form-choice' : 'read-form-reply'"),
305
306
  { id: 'ci_route', type: 'CASE', branches: {
306
307
  'condition: ci_m.ambiguous.length > 0': FLOW('ci_ambiguous', 'disambiguate-input', 'call', { readings: '{{ci_m.ambiguous}}', hits: '{{ci_m.hits}}', remaining: '{{ci_m.remaining}}' }),
307
308
  'condition: ci_m.hits.length > 0': FLOW('ci_accept', 'collect-input-accept', 'call', { hits: '{{ci_m.hits}}' }),
308
309
  "condition: ci_m.questions.length > 0": END('ci_question_handled'),
309
310
  "condition: ci_qkey && ci_qkey.indexOf('__') !== 0": FLOW('ci_question_asked', 'provide-data', 'call', { form: '{{form}}', provides: '{{provides}}', asked: '{{ci_qkey}}', no_menu: 'true', no_gate: '{{ci_skip_gate}}' }),
310
- "condition: !ci_must && (ci_words >= 3 || ci_spec.profile.kind === 'amount' && ci_words > 0)": yieldDispatch('ci_elsewhere', 'form', "'input_not_matched'"),
311
+ "condition: !ci_must && (ci_words >= 3 || ci_spec.profile.kind === 'amount' && ci_words > 0)": FLOW('ci_elsewhere', '{{ci_reply_reader}}', 'call', { form: '{{form}}', input: '{{current}}', question: '{{ci_q}}', question_es: '{{ci_q_es}}', choices: '{{ci_choices}}' }),
311
312
  default: SIDE('ci_miss', "(missFormInput(cargo,form,current),true)")
312
313
  } },
313
314
  ],
@@ -344,9 +345,9 @@ const validateBatch = {
344
345
  SIDE('vb_reset', "(cargo.inputCheck = vb_h.invalid ? { ok: false } : (vb_h.validator && !vb_known ? null : { ok: true, value: vb_h.value }), true)"),
345
346
  SET('vb_response', 'vb_response', "(function(){var state=cargo.forms[vb_h.form],spec=state && state.specs.flatMap(function(s){return s.any_of || [s];}).find(function(s){return s.name===vb_h.name;});return spec && spec.response || null;})()"),
346
347
  WHEN('vb_contextual_needed', "vb_ready === true && vb_h.current && vb_response && !vb_known && !vb_h.invalid && vb_h.origin !== 'activation' && !vb_h.fromPending", FLOW('vb_read_response', 'read-form-response', 'call', { form: '{{vb_h.form}}', input: '{{vb_h.name}}' })),
347
- WHEN('vb_validator', 'vb_ready === true && vb_h.validator && !vb_known && !vb_h.invalid', FLOW('vb_validate', '{{vb_h.validator}}', 'call', { form: '{{vb_h.form}}', input: '{{vb_h.name}}', value: '{{vb_h.value}}', values: '{{vb_values}}', reading: '{{vb_h}}' })),
348
+ WHEN('vb_validator', 'vb_ready === true && vb_h.validator && !vb_known && !vb_h.invalid && !(cargo.inputCheck && cargo.inputCheck.ok === false)', FLOW('vb_validate', '{{vb_h.validator}}', 'call', { form: '{{vb_h.form}}', input: '{{vb_h.name}}', value: '{{vb_h.value}}', values: '{{vb_values}}', reading: '{{vb_h}}' })),
348
349
  WHEN('vb_refused', 'vb_h.refused === true || vb_h.incompleteMoney === true', SIDE('vb_withhold_refusal', "(cargo.inputCheck=Object.assign({},cargo.inputCheck || {},{ok:false}),true)")),
349
- SET('vb_candidate', 'vb_candidate', "Object.assign({},vb_h,{ok:!!(cargo.inputCheck && cargo.inputCheck.ok === true),value:cargo.inputCheck && cargo.inputCheck.value !== undefined && cargo.inputCheck.value !== null ? cargo.inputCheck.value : vb_h.value,message:cargo.inputCheck && cargo.inputCheck.message,message_es:cargo.inputCheck && cargo.inputCheck.message_es})"),
350
+ SET('vb_candidate', 'vb_candidate', "Object.assign({},vb_h,{ok:!!(cargo.inputCheck && cargo.inputCheck.ok === true),value:cargo.inputCheck && cargo.inputCheck.value !== undefined && cargo.inputCheck.value !== null ? cargo.inputCheck.value : vb_h.value,message:cargo.inputCheck && cargo.inputCheck.message,message_es:cargo.inputCheck && cargo.inputCheck.message_es,quiet:!!(cargo.inputCheck && cargo.inputCheck.quiet)})"),
350
351
  SET('vb_proposal_policy', 'vb_proposal', 'vb_relevant === true && vb_ready === true && vb_candidate.ok && !vb_known ? formProposalPolicy(cargo,vb_candidate,cargo.inputCheck,accepted) : null'),
351
352
  SET('vb_proposal_values', 'vb_proposal_values', 'proposedFormValues(cargo,vb_h.form,accepted.concat([vb_candidate]))'),
352
353
  SIDE('vb_proposal_reset', '(cargo.proposalCheck=undefined,true)'),
@@ -365,7 +366,7 @@ const requestCancelled = {
365
366
  };
366
367
  // Open-ended answers use the same contextual intent/extraction path as other DISPATCH replies.
367
368
  const readFormResponse = {
368
- id: 'read-form-response', name: 'ReadFormResponse', version: '1.0.0', scope: 'isolated', description: 'Read the requested field with contextual DISPATCH; uncertainty returns to the host with saved form progress.',
369
+ id: 'read-form-response', name: 'ReadFormResponse', version: '1.0.0', scope: 'isolated', description: 'Read the requested field with contextual DISPATCH: an answer continues, an answer with nothing usable is asked again, a withdrawal or a new request leaves with saved form progress.',
369
370
  parameters: [{ name: 'form', type: 'string' }, { name: 'input', type: 'string' }],
370
371
  variables: vars({ group: null, spec: null, question: '', reply: null }),
371
372
  steps: [SET('response_group', 'group', "cargo.forms[form].specs.find(function(s){return s.name===input || s.any_of && s.any_of.some(function(member){return member.name===input;});})"),
@@ -373,8 +374,42 @@ const readFormResponse = {
373
374
  { ...SET('response_declared', 'noop', '!!spec && !!validateFormResponse(spec.response)'), requireTrue: true },
374
375
  SET('response_question', 'question', "formInputPrompt(cargo,form,input,Object.assign({},group,spec,{question:spec.question || group.question,question_es:spec.question_es || group.question_es,question_from:spec.question_from || group.question_from}),cargo.forms[form].data || {},language)"),
375
376
  SET('response_clear', 'reply', 'null'),
376
- { ...yieldDispatch('response_dispatch', 'form', "'input_not_matched'"), onResponse: 'continue', responseContext: 'Current question: {{question}}. Requested answer: {{spec.response.description}}. Continue only for an answer to this question. A changed request belongs to its workflow; an unrelated question or uncertain response belongs to the host.', responseParameters: [{ name: 'answer', type: 'string', description: '{{spec.response.description}}' }], responseVariable: 'reply' },
377
- WHEN('response_missing', "!reply || typeof reply.answer !== 'string' || !reply.answer.trim()", yieldDispatch('response_handoff', 'form', "'input_not_matched'"))]
377
+ { ...yieldDispatch('response_dispatch', 'form', "'input_not_matched'"), onResponse: 'continue', responseContext: 'Current question: {{question}}. Requested answer: {{spec.response.description}}.', responseParameters: [{ name: 'answer', type: 'string', description: '{{spec.response.description}}', verbatim: true }], responseVariable: 'reply' },
378
+ // Nothing usable (only after a default `answers`: a confident one carries the reply as said): the reading is
379
+ // rejected, so the form asks the question again (the question alone: it was not a wrong answer). The answer never
380
+ // goes to the host.
381
+ WHEN('response_missing', "!reply || typeof reply.answer !== 'string' || !reply.answer.trim()", SIDE('response_rejected', '(cargo.inputCheck={ok:false,quiet:true},true)'))]
382
+ };
383
+ // An answer the field's reader could not read goes to the reply ranker, never straight to the host (owner 2026-10-26: a form
384
+ // gives up only for a real new intent). `answers`: a choice field takes the offered key the reply picks, else the
385
+ // question is asked again ("Sorry, I didn't catch that."), counted as an attempt so max_attempts still bounds it;
386
+ // `abort` ends every flow; `new_request` goes to intent detection.
387
+ const rankedMiss = (id) => SIDE(id, "(cargo.forms[form].attempts=cargo.forms[form].attempts || {},cargo.forms[form].attempts[input]=Number(cargo.forms[form].attempts[input] || 0)+1,cargo.forms[form].feedback=cargo.forms[form].feedback || {},cargo.forms[form].feedback[input]='miss',true)");
388
+ const readFormReply = {
389
+ id: 'read-form-reply', name: 'ReadFormReply', version: '1.0.0', scope: 'isolated', description: 'Rank an unreadable answer against its question: an answer is asked again, a withdrawal or a new request leaves with saved form progress.',
390
+ parameters: [{ name: 'form', type: 'string' }, { name: 'input', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }],
391
+ variables: vars({ question: '', question_es: '', asked: '' }),
392
+ steps: [SET('reply_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
393
+ { ...yieldDispatch('reply_dispatch', 'form', "'input_not_matched'"), onResponse: 'continue', responseContext: 'Current question: {{asked}}' },
394
+ rankedMiss('reply_miss')]
395
+ };
396
+ const readFormChoice = {
397
+ id: 'read-form-choice', name: 'ReadFormChoice', version: '1.0.0', scope: 'isolated', description: 'Rank an unreadable answer to a choice question and take the offered option it picks; otherwise ask again. A withdrawal or a new request leaves with saved form progress.',
398
+ parameters: [{ name: 'form', type: 'string' }, { name: 'input', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }, { name: 'choices', type: 'array' }],
399
+ variables: vars({ question: '', question_es: '', choices: [], asked: '', options: '', reply: null, picked: '', chain: null, reading: null }),
400
+ steps: [SET('choice_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
401
+ SET('choice_options', 'options', "(choices || []).map(function(c){return String(c.key)+' ('+String(c.label || (Array.isArray(c.phrases) && c.phrases.filter(function(p){return String(p)!==String(c.key);})[0]) || c.key)+')';}).join(', ')"),
402
+ { ...yieldDispatch('choice_dispatch', 'form', "'input_not_matched'"), onResponse: 'continue', responseContext: 'Current question: {{asked}}', responseParameters: [{ name: 'key', type: 'string', description: 'The key of the offered option the reply picks or says it already chose, one of: {{options}}. Leave it empty when the reply picks none of them.' }], responseVariable: 'reply' },
403
+ SET('choice_picked', 'picked', "(function(){var k=String(reply && reply.key || '').trim().toLowerCase();var c=(choices || []).filter(function(c){return String(c.key).toLowerCase()===k;})[0];return c ? String(c.key) : '';})()"),
404
+ WHEN('choice_none', '!picked', rankedMiss('choice_miss')),
405
+ WHEN('choice_none_end', '!picked', END('choice_none_done')),
406
+ SIDE('choice_take', "(cargo.choice=picked,cargo.choiceText=picked,true)"),
407
+ SET('choice_chain', 'chain', 'formInputChain(cargo.forms, form, cargo)'),
408
+ SET('choice_read', 'reading', "enrichFormMatches(readFormAnswer(picked, chain, { form: form, name: input }, picked, []), chain, form, [input], input)"),
409
+ WHEN('choice_unread', '!reading.hits.length', rankedMiss('choice_unread_miss')),
410
+ WHEN('choice_unread_end', '!reading.hits.length', END('choice_unread_done')),
411
+ SIDE('choice_clear', 'clearFormMisses(cargo,form,input)'),
412
+ FLOW('choice_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })]
378
413
  };
379
414
  const collectContextualAnswer = formFlow({ id: 'collect-contextual-answer', name: 'CollectContextualAnswer', description: 'Collect one open-ended answer using contextual DISPATCH and preserve the caller wording.',
380
415
  parameters: [...['question', 'question_es', 'description', 'context', 'context_es'].map(name => ({ name, type: 'string' })), { name: 'choices', type: 'array' }, { name: 'digits', type: 'object' }],
@@ -415,8 +450,8 @@ const acceptedEffects = {
415
450
  WHEN('ie_none', '!ca_h', END('ie_end')),
416
451
  SET('ie_rest', 'ca_rest', 'readings.slice(1)'),
417
452
  WHEN('ie_reject', "ca_h.outcome === 'rejected'", SIDE('ie_attempt', "(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] = Number(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] || 0) + 1, cargo.forms[ca_h.form].feedback=cargo.forms[ca_h.form].feedback || {}, cargo.forms[ca_h.form].feedback[ca_h.asked || ca_h.name]='invalid', true)")),
418
- SET('ie_say_en', 'ca_ack', "ca_h.outcome === 'rejected' ? (ca_h.message || ('That ' + (ca_h.label || ca_h.name) + \" doesn't look right.\")) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Thank you, I already have your ' + ca_h.label + '.') : ca_h.outcome === 'changed' ? ('Okay, I updated your ' + ca_h.label + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Got it, your ' + ca_h.label + '.') : ''"),
419
- SET('ie_say_es', 'ca_ack_es', "ca_h.outcome === 'rejected' ? (ca_h.message_es || ca_h.message || ('Ese ' + (ca_h.label_es || ca_h.label || ca_h.name) + ' no parece correcto.')) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Gracias, ya tengo su ' + ca_h.label_es + '.') : ca_h.outcome === 'changed' ? ('Listo, actualicé su ' + ca_h.label_es + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Entendido, su ' + ca_h.label_es + '.') : ''"),
453
+ SET('ie_say_en', 'ca_ack', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message || ('That ' + (ca_h.label || ca_h.name) + \" doesn't look right.\")) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Thank you, I already have your ' + ca_h.label + '.') : ca_h.outcome === 'changed' ? ('Okay, I updated your ' + ca_h.label + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Got it, your ' + ca_h.label + '.') : ''"),
454
+ SET('ie_say_es', 'ca_ack_es', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message_es || ca_h.message || ('Ese ' + (ca_h.label_es || ca_h.label || ca_h.name) + ' no parece correcto.')) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Gracias, ya tengo su ' + ca_h.label_es + '.') : ca_h.outcome === 'changed' ? ('Listo, actualicé su ' + ca_h.label_es + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Entendido, su ' + ca_h.label_es + '.') : ''"),
420
455
  WHEN('ie_say', 'ca_ack', SAY('ie_ack', '{{ca_ack}}', '{{ca_ack_es}}')),
421
456
  WHEN('ie_callback', "ca_h.after && ca_h.outcome !== 'known' && ca_h.outcome !== 'rejected' && ca_h.outcome !== 'pending'", FLOW('ie_after', '{{ca_h.after}}', 'call', { form: '{{ca_h.form}}', input: '{{ca_h.name}}', value: '{{ca_h.value}}' })),
422
457
  WHEN('ie_more', 'ca_rest.length > 0', FLOW('ie_next', 'input-accepted-effects', 'replace', { readings: '{{ca_rest}}' })),
@@ -791,7 +826,7 @@ const resumeForm = { id: 'resume-form', name: 'ResumeForm', version: '1.0.0', sc
791
826
  SET('resume_definition', 'target', 'cargo.forms[form].definition'), SET('resume_parent', 'parent', "cargo.forms[form].parent || ''"), SET('resume_slot', 'slot', "cargo.forms[form].result_input || ''"),
792
827
  FLOW('resume_owned_form', '{{target}}', 'replace', { form_instance: '{{form}}', parent_form: '{{parent}}', result_input: '{{slot}}' })
793
828
  ] };
794
- const LIBRARY_FLOWS = [confirmInputProposal, proposalDeclined, resumeForm, performAction, sendTextAction, prepareInput, runForm, collectInput, validateBatch, requestCancelled, readFormResponse, collectContextualAnswer, accept, acceptedEffects, disambiguate, confirmForm, formDone, providerMenu, ...validators, ...fieldCollectors, ...subForms, provideData, provideQueuedData, loadSource, answerFacts];
829
+ const LIBRARY_FLOWS = [readFormReply, readFormChoice, confirmInputProposal, proposalDeclined, resumeForm, performAction, sendTextAction, prepareInput, runForm, collectInput, validateBatch, requestCancelled, readFormResponse, collectContextualAnswer, accept, acceptedEffects, disambiguate, confirmForm, formDone, providerMenu, ...validators, ...fieldCollectors, ...subForms, provideData, provideQueuedData, loadSource, answerFacts];
795
830
  const LIBRARY_CONTRACTS = {
796
831
  'confirm-input-proposal': { role: 'collector', requires: [INPUT_PROPOSALS_REQUIREMENT] }, 'confirm-input-proposal-declined': { role: 'worker', requires: [INPUT_PROPOSALS_REQUIREMENT] },
797
832
  'resume-form': { role: 'worker' },
@@ -800,6 +835,7 @@ const LIBRARY_CONTRACTS = {
800
835
  'prepare-input': { role: 'worker' }, 'run-form': { role: 'worker' }, 'collect-input': { role: 'worker' },
801
836
  'request-cancelled': { role: 'worker' },
802
837
  'read-form-response': { role: 'worker', requires: [CONTEXTUAL_RESPONSES_REQUIREMENT] },
838
+ 'read-form-reply': { role: 'worker' }, 'read-form-choice': { role: 'worker', requires: [CONTEXTUAL_RESPONSES_REQUIREMENT] },
803
839
  'collect-contextual-answer': { role: 'collector', requires: [CONTEXTUAL_RESPONSES_REQUIREMENT] },
804
840
  'validate-input-batch': { role: 'worker', result: { path: 'cargo.inputBatch', type: 'object' } },
805
841
  'collect-input-accept': { role: 'worker' }, 'input-accepted-effects': { role: 'worker' },
@@ -111,7 +111,7 @@ const contact = worker('get-cell-or-email', 'GetCellOrEmail', 'Identify a suppli
111
111
  when('identity_contact_split_overshoot', "contact_partial && contact_digits.length > (contact_options.account_shape || []).reduce(function(max,s){return Math.max(max,Math.max.apply(null,s.lengths || [0]));},0)", call('identity_contact_restart_split', 'contact-split-failed', { contact_options: '{{contact_options}}' }, 'replace')),
112
112
  set('identity_contact_identifier', 'contact_id', "contact_options.account_pattern ? readIdentifier(contact_text,contact_options.account_pattern) : null"),
113
113
  set('identity_contact_phone', 'contact_phone', "readPhone(contact_text) || (validatePhone(contact_digits) ? contact_digits : '')"), set('identity_contact_email', 'contact_mail', 'readEmail(contact_text)'),
114
- set('identity_contact_classify', 'contact_kind', "contact_id && contact_id.kind === 'account' ? 'account' : contact_phone ? 'phone' : contact_mail ? 'email' : contact_options.account_pattern && !contact_digits && matchesChoice(contact_text,contact_names_account) ? 'names_account' : readInternationalPhone(contact_text) ? 'international' : isAgentRequest(contact_text) ? 'agent' : isAnswer(contact_text,'exit') && contact_text.trim().split(/\\s+/).length <= 2 ? 'exit' : contact_allowed && /^(?:no|nope|nah)\\b/i.test(contact_text.trim()) && !/[0-9@]/.test(contact_text) ? 'decline' : contact_allowed && cargo.serviceAnswer && cargo.serviceAnswer.values.caller_decision === 'yes' ? 'caller' : contact_digits ? 'invalid_phone' : contact_text.trim().split(/\\s+/).length>=3 && !/[0-9@]/.test(contact_text) ? 'unmapped' : 'invalid'"),
114
+ set('identity_contact_classify', 'contact_kind', "contact_id && contact_id.kind === 'account' ? 'account' : contact_phone ? 'phone' : contact_mail ? 'email' : contact_options.account_pattern && !contact_digits && matchesChoice(contact_text,contact_names_account) ? 'names_account' : readInternationalPhone(contact_text) ? 'international' : isAgentRequest(contact_text) ? 'agent' : isExitRequest(contact_text) ? 'exit' : contact_allowed && /^(?:no|nope|nah)\\b/i.test(contact_text.trim()) && !/[0-9@]/.test(contact_text) ? 'decline' : contact_allowed && cargo.serviceAnswer && cargo.serviceAnswer.values.caller_decision === 'yes' ? 'caller' : contact_digits ? 'invalid_phone' : contact_text.trim().split(/\\s+/).length>=3 && !/[0-9@]/.test(contact_text) ? 'unmapped' : 'invalid'"),
115
115
  set('identity_contact_named', 'contact_named_options', "Object.assign({},contact_options,{fromMenu:false,intro:'Sure, your account number.',intro_es:'Claro, su número de cuenta.'})"),
116
116
  set('identity_contact_error', 'contact_error', expression(contactError, 'contact_kind', 'contact_options')), set('identity_contact_error_es', 'contact_error_es', expression(contactError, 'contact_kind', 'contact_options', 'true')),
117
117
  cases('identity_contact_route', {
@@ -212,7 +212,7 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
212
212
  cases('otp_response', {
213
213
  'condition: normalized_otp_code.length === 6': { id: 'otp_check_tool', type: 'CALL-TOOL', tool: 'validate-otp', variable: 'otp_validation_result', args: { otp: '{{normalized_otp_code}}', container: '{{cargo}}' }, onFail: call('otp_check_failed', 'get-and-validate-otp-code', undefined, 'replace') },
214
214
  'condition: wants_other_account': call('otp_change_account', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
215
- "condition: isAnswer(user_choice,'exit')": call('otp_exit', '{{otp_cancel}}', undefined, 'reboot'),
215
+ "condition: isExitRequest(user_choice)": call('otp_exit', '{{otp_cancel}}', undefined, 'reboot'),
216
216
  "condition: isAnswer(user_choice,'agent')": call('otp_agent', 'live-agent-requested', undefined, 'reboot'),
217
217
  "condition: isAnswer(user_choice,'text')": call('otp_text', 'switch-to-text', undefined, 'reboot'),
218
218
  'condition: matchesChoice(user_choice,otp_phrases.resend)': call('otp_resend', 'resend-otp-code', { resend_reason: 'requested' }),
@@ -261,7 +261,7 @@ const retry = worker('generic-retry-with-options', 'GenericRetryWithOptions', 'C
261
261
  set('retry_latest_answer', 'user_choice', 'String(cargo.serviceAnswer.raw || "")'), set('retry_capture', 'smart_capture_result', 'capture_patterns && capture_patterns.length ? normalizeAndFindCapture(user_choice,capture_patterns) : null'),
262
262
  cases('retry_capture_route', { 'condition: smart_capture_result && retry_call_type === "replace"': call('retry_captured_replace', '{{retry_flow}}', { cell_number: '', email: '', '{{smart_capture_result.variable}}': '{{smart_capture_result.value}}', retry_flow: '{{retry_flow}}', cancel_flow: '{{cancel_flow}}', retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'replace'), 'condition: smart_capture_result': call('retry_captured_reboot', '{{retry_flow}}', { '{{smart_capture_result.variable}}': '{{smart_capture_result.value}}', retry_flow: '{{retry_flow}}', cancel_flow: '{{cancel_flow}}', retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'reboot'), default: effect('retry_no_capture', 'true') }),
263
263
  set('retry_typed_code', 'normalized_otp_code', "allow_code_entry && user_choice.replace(/\\D/g,'').length === 6 ? user_choice.replace(/\\D/g,'') : ''"), when('retry_validate_code', '!!normalized_otp_code', call('retry_code_validate', 'validate-otp-code')),
264
- cases('retry_decision', { 'condition: normalized_otp_code && cargo.otpVerified === true': end('retry_verified'), "condition: (user_choice.trim() === '1' || isAnswer(user_choice,'yes',['phone','email','cell','mobile','teléfono','telefono','celular','móvil','movil','correo electrónico','correo'])) && retry_call_type === 'replace'": call('retry_yes_replace', '{{retry_flow}}', { retrying: true, cell_number: '', email: '', contact_options: null, parent_form: '{{parent_form}}' }, 'replace'), "condition: user_choice.trim() === '1' || isAnswer(user_choice,'yes',['phone','email','cell','mobile','teléfono','telefono','celular','móvil','movil','correo electrónico','correo'])": call('retry_yes_reboot', '{{retry_flow}}', { retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'reboot'), "condition: user_choice.trim() === '9' || isAnswer(user_choice,'text')": call('retry_text', 'switch-to-text', undefined, 'reboot'), "condition: isAgentRequest(user_choice)": call('retry_agent', 'live-agent-requested', undefined, 'reboot'), "condition: !user_choice.trim() || /^\\d$/.test(user_choice.trim()) || isAnswer(user_choice,'exit')": call('retry_cancel', '{{cancel_flow}}', undefined, 'reboot'), default: { id: 'retry_dispatch', type: 'DISPATCH', outcome: '{{caller_outcome}}', reason: '{{caller_reason}}' } })
264
+ cases('retry_decision', { 'condition: normalized_otp_code && cargo.otpVerified === true': end('retry_verified'), "condition: (user_choice.trim() === '1' || isAnswer(user_choice,'yes',['phone','email','cell','mobile','teléfono','telefono','celular','móvil','movil','correo electrónico','correo'])) && retry_call_type === 'replace'": call('retry_yes_replace', '{{retry_flow}}', { retrying: true, cell_number: '', email: '', contact_options: null, parent_form: '{{parent_form}}' }, 'replace'), "condition: user_choice.trim() === '1' || isAnswer(user_choice,'yes',['phone','email','cell','mobile','teléfono','telefono','celular','móvil','movil','correo electrónico','correo'])": call('retry_yes_reboot', '{{retry_flow}}', { retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'reboot'), "condition: user_choice.trim() === '9' || isAnswer(user_choice,'text')": call('retry_text', 'switch-to-text', undefined, 'reboot'), "condition: isAgentRequest(user_choice)": call('retry_agent', 'live-agent-requested', undefined, 'reboot'), "condition: !user_choice.trim() || /^\\d$/.test(user_choice.trim()) || isExitRequest(user_choice)": call('retry_cancel', '{{cancel_flow}}', undefined, 'reboot'), default: { id: 'retry_dispatch', type: 'DISPATCH', outcome: '{{caller_outcome}}', reason: '{{caller_reason}}' } })
265
265
  ] });
266
266
  const EMAIL_CHOICES = { ok: ['1', 'ok', 'okay', 'yes', 'send', 'send it', 'si', 'sí', 'enviar', 'envíalo', 'correcto'], another: ['otro correo', 'otro correo electrónico', 'otro correo electronico', 'otro email', 'otra dirección', 'otra direccion', 'a otro correo', 'another email', 'a different email', 'different email', 'other email', 'another address', 'a different address'] };
267
267
  export function emailReadback(address, voice, es = false) {
@@ -277,7 +277,7 @@ const email = worker('ask-email', 'AskEmail', 'Collect an email through shared F
277
277
  set('email_question_es', 'email_question_es', "email_state.why === 'more' ? 'Entendido. Continúe.' : (email_state.attempt>0 ? email_state.why === 'retry' ? 'De acuerdo, intentemos de nuevo. ' : 'Eso no parece un correo electrónico. ' : '')+email_state.question_es"),
278
278
  call('email_collect', 'collect-service-answer', { profiles: '{{email_profiles}}', question: '{{email_question}}', question_es: '{{email_question_es}}', keypad: { min: 1, max: 1 }, parent_form: '{{email_state.parent_form}}', context: 'email', context_es: 'correo' }),
279
279
  set('email_raw', 'email_raw', 'String(cargo.serviceAnswer.raw || "")'),
280
- cases('email_command', { "condition: isAgentRequest(email_raw)": call('email_agent', 'live-agent-requested', undefined, 'reboot'), "condition: isAnswer(email_raw,'exit')": call('email_exit', 'contact-support', { support_context: 'email', support_context_es: 'correo' }, 'reboot'), default: effect('email_is_answer', 'true') }),
280
+ cases('email_command', { "condition: isAgentRequest(email_raw)": call('email_agent', 'live-agent-requested', undefined, 'reboot'), "condition: isExitRequest(email_raw)": call('email_exit', 'contact-support', { support_context: 'email', support_context_es: 'correo' }, 'reboot'), default: effect('email_is_answer', 'true') }),
281
281
  set('email_choice', 'email_choice', "pickChoice(email_raw,[{key:'ok',phrases:email_choices.ok},{key:'another',phrases:email_choices.another}],false)"),
282
282
  when('email_other_request_clear', "!email_choice && !readEmail(email_raw) && isOtherRequest(email_raw,'email')", effect('email_other_request_draft', "(email_state.draft='',cargo.emailDraft='',cargo.email='',true)")),
283
283
  when('email_other_request', "!email_choice && !readEmail(email_raw) && isOtherRequest(email_raw,'email')", { id: 'email_dispatch_request', type: 'DISPATCH' }),
@@ -3,6 +3,11 @@
3
3
  * YES, AGENT and TEXT never match a negated answer ("not sure", "I don't want an agent"); 0 is AGENT, * is EXIT.
4
4
  */
5
5
  export function isAnswer(input: any, kind: any, extra?: any[]): boolean;
6
+ /**
7
+ * Whether the WHOLE answer is an exit word ("exit", "Cancelar.", "salir por favor", "*"), never a sentence that contains
8
+ * one: "Cancelar el pedido" asks for something; it does not withdraw (prod 2026-10-26, at a verification-code prompt).
9
+ */
10
+ export function isExitRequest(input: any): boolean;
6
11
  /** Whether the WHOLE answer asks for a person ("agent", "I want to talk to a representative", "0") — for free-text prompts. */
7
12
  export function isAgentRequest(input: any): boolean;
8
13
  /** Whether the answer is (part of) a spelled address — letters said one at a time, or a spelled symbol. */
@@ -1 +1 @@
1
- {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AA6MA;;;GAGG;AACH,wEAkBC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAmCC;AAUD;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AA/aD,qCAKwE"}
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED;;;GAGG;AACH,mDAGC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAmCC;AAUD;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AAjcD,qCAKwE"}
@@ -31,7 +31,7 @@ export const ANSWER_WORDS = {
31
31
  agent: ['live agent', 'agent', 'agents', 'representative', 'rep', 'human', 'operator', 'manager', 'supervisor', 'someone',
32
32
  'somebody', 'real person', 'a person', 'customer service', 'support', 'agente', 'agentes', 'gente', 'representante',
33
33
  'humano', 'operador', 'operadora', 'gerente', 'alguien', 'persona', 'servicio al cliente', 'al cliente', 'servicio',
34
- 'recepcionista', 'recepcion', 'recepción'],
34
+ 'recepcionista', 'recepcion', 'recepción', 'asesor', 'asesora', 'asesores'],
35
35
  exit: ['exit', 'quit', 'abort', 'salir', 'cancel', 'cancelar'],
36
36
  text: ['text', 'texto', 'sms', 'message', 'mensaje', 'text me', 'by text', 'por texto', 'mensaje de texto', 'whatsapp',
37
37
  'escríbeme', 'escribeme'],
@@ -60,7 +60,7 @@ const NEGATIONS = ['no', 'not', 'nope', 'nah', 'never', 'dont', 'doesnt', 'didnt
60
60
  // Phrases that hold a negation word but agree ("no problem", "why not").
61
61
  const AGREEING = /\b(no problem|no worries|no hay problema|no te preocupes|why not|por que no|por qué no)\b/g;
62
62
  const DEFERRAL = /\b(later|tomorrow|next week|not now|not today|another time|mañana|manana|después|despues|luego|otro día|otro dia|ahora no|hoy no)\b/;
63
- const AGENT_ONLY = /^(?:(?:i want|i need|i would like|id like|can i|could i|let me|please|quiero|necesito|puedo|por favor)\s+)*(?:(?:to\s+)?(?:talk|speak|hablar|comunicarme)\s+(?:to|with|con)\s+)?(?:a|an|un|una|el|la)?\s*(?:live agent|agent|representative|rep|human|operator|real person|a person|person|someone|somebody|customer service|agente|representante|humano|operador|operadora|persona|alguien|servicio al cliente|agente en vivo|gerente|manager|supervisor)(?:\s+(?:please|por favor))?$/;
63
+ const AGENT_ONLY = /^(?:(?:i want|i need|i would like|id like|can i|could i|let me|please|quiero|necesito|puedo|por favor)\s+)*(?:(?:to\s+)?(?:talk|speak|hablar|comunicarme)\s+(?:to|with|con)\s+)?(?:a|an|un|una|el|la)?\s*(?:live agent|agent|representative|rep|human|operator|real person|a person|person|someone|somebody|customer service|agente|representante|humano|operador|operadora|persona|alguien|servicio al cliente|agente en vivo|gerente|manager|supervisor|asesor|asesora)(?:\s+(?:please|por favor))?$/;
64
64
  // Accents are not meaning here (owner 2026-10-16): text callers type "direccion" for "dirección", "si" for "sí". Both
65
65
  // sides of every comparison pass through normalize, so a word list matches with or without them; ñ is kept.
66
66
  const plainVowels = (s) => s.replace(/[áàâä]/g, 'a').replace(/[éèêë]/g, 'e').replace(/[íìîï]/g, 'i').replace(/[óòôö]/g, 'o').replace(/[úùûü]/g, 'u');
@@ -196,6 +196,11 @@ function firmKey(b, key) {
196
196
  // Words that name the key itself when said right before it: "el número 4", "opción 2", "number 4", "the 4" (prod
197
197
  // 2026-10-25 18:25: "Número 4" and "El número 4" at a numbered product list were not understood, twice).
198
198
  const KEY_NAMES = new Set(['number', 'numero', 'option', 'opcion', 'item', 'choice', 'the', 'el', 'la'].map(normalize));
199
+ // Saying the key was pressed, before it: "ya oprimí el uno", "I already pressed one", "marqué el 2" (prod 2026-10-26:
200
+ // a pressed-key sentence at a sub-account choice went to the guru).
201
+ const PRESS_WORDS = new Set(['ya', 'already', 'i', 'yo', 'ive', 'just', 'pressed', 'press', 'pushed', 'push', 'hit', 'dialed',
202
+ 'entered', 'chose', 'picked', 'selected', 'presione', 'presiono', 'presiona', 'oprimi', 'oprime', 'oprimo', 'marque', 'marco',
203
+ 'marca', 'pulse', 'pulso', 'toque', 'elegi', 'escogi', 'seleccione', 'le', 'di', 'al', 'a'].map(normalize));
199
204
  function keyWithOnlyCourtesy(b, key, yesOk) {
200
205
  const tk = String(b).replace(/\./g, ' ').split(' ').filter(Boolean);
201
206
  const at = tk.reduce((acc, t, i) => (t === key ? [...acc, i] : acc), []);
@@ -206,6 +211,12 @@ function keyWithOnlyCourtesy(b, key, yesOk) {
206
211
  let from = at[0];
207
212
  while (from > 0 && at[0] - from < 2 && KEY_NAMES.has(tk[from - 1]))
208
213
  from--;
214
+ // a press verb is required for the press words to count ("ya" alone, "a 1", are not a press)
215
+ let press = from;
216
+ while (press > 0 && from - press < 4 && PRESS_WORDS.has(tk[press - 1]))
217
+ press--;
218
+ if (tk.slice(press, from).some((t) => !['ya', 'already', 'i', 'yo', 'ive', 'just', 'le', 'al', 'a'].includes(t)))
219
+ from = press;
209
220
  const rest = tk.filter((t, i) => i < from || i > at[0]).join(' ');
210
221
  return onlyAffirmative(rest, yesOk ? ANSWER_WORDS.yes : [], false);
211
222
  }
@@ -239,6 +250,14 @@ export function isAnswer(input, kind, extra = []) {
239
250
  return matchesChoice(b, words);
240
251
  }
241
252
  }
253
+ /**
254
+ * Whether the WHOLE answer is an exit word ("exit", "Cancelar.", "salir por favor", "*"), never a sentence that contains
255
+ * one: "Cancelar el pedido" asks for something; it does not withdraw (prod 2026-10-26, at a verification-code prompt).
256
+ */
257
+ export function isExitRequest(input) {
258
+ const b = normalize(input).replace(/^[.\s]+|[.\s]+$/g, '');
259
+ return b === '*' || /^(exit|quit|salir|abort|cancel|cancelar)( please| por favor)?$/.test(b);
260
+ }
242
261
  /** Whether the WHOLE answer asks for a person ("agent", "I want to talk to a representative", "0") — for free-text prompts. */
243
262
  export function isAgentRequest(input) {
244
263
  // the period normalize() keeps for emails is not part of the answer at its ends ("Operator." on a call, 2026-10-17)
@@ -1 +1 @@
1
- {"version":3,"file":"contextual-response.d.ts","sourceRoot":"","sources":["../../src/helpers/contextual-response.js"],"names":[],"mappings":"AAMA,2GAA2G;AAC3G;;;qBAiBC;AACD,+FAIC;AAED,gGAAgG;AAChG,yDAGC;AAnCD,4FAA4F;AAC5F,+CAAgD,8BAA8B,CAAC;AACxE,qDAA0C;AAkC1C,kDAA8J"}
1
+ {"version":3,"file":"contextual-response.d.ts","sourceRoot":"","sources":["../../src/helpers/contextual-response.js"],"names":[],"mappings":"AAMA,2GAA2G;AAC3G;;;qBAkBC;AACD,+FAIC;AAED,gGAAgG;AAChG,yDAGC;AApCD,4FAA4F;AAC5F,+CAAgD,8BAA8B,CAAC;AACxE,qDAA0C;AAmC1C,kDAA8J"}
@@ -30,7 +30,9 @@ export function validateContextualResponseStep(step, flow) {
30
30
  fail('response parameters must be public extraction parameters');
31
31
  if (own(p, 'jsonSchema'))
32
32
  fail('response parameters support primitive types only; jsonSchema is not supported');
33
- if (Object.keys(p).some(k => !['name', 'description', 'type', 'enum', 'internal'].includes(k)))
33
+ if (p.verbatim !== undefined && (p.verbatim !== true || p.type !== undefined && p.type !== 'string' || p.enum !== undefined))
34
+ fail('verbatim is true on an open string parameter only');
35
+ if (Object.keys(p).some(k => !['name', 'description', 'type', 'enum', 'internal', 'verbatim'].includes(k)))
34
36
  fail('responseParameters accepts only canonical parameter descriptors');
35
37
  }
36
38
  try {
@@ -33,6 +33,7 @@ export namespace STANDARD_FUNCTIONS {
33
33
  export { matchesChoice };
34
34
  export { isAnswer };
35
35
  export { isAgentRequest };
36
+ export { isExitRequest };
36
37
  export { isOtherRequest };
37
38
  export { isSpelledFragment };
38
39
  export { isAmountAnswer };
@@ -152,6 +153,7 @@ import { ANSWER_WORDS } from './answer.js';
152
153
  import { CONFIRM_WORDS } from './answer.js';
153
154
  import { isAnswer } from './answer.js';
154
155
  import { isAgentRequest } from './answer.js';
156
+ import { isExitRequest } from './answer.js';
155
157
  import { isOtherRequest } from './answer.js';
156
158
  import { isSpelledFragment } from './answer.js';
157
159
  import { isAmountAnswer } from './answer.js';
@@ -238,5 +240,5 @@ import { validatePhone } from './validators.js';
238
240
  import { validateEmail } from './validators.js';
239
241
  import { urlToSpeech } from './url-speech.js';
240
242
  import { textWithUrlToSpeech } from './url-speech.js';
241
- export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
243
+ export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
242
244
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI8D,wBAAwB;2CAHlF,0BAA0B;qCAA1B,0BAA0B;gCAA1B,0BAA0B;mCACsE,qBAAqB;kCAArB,qBAAqB;iCAArB,qBAAqB;qCAArB,qBAAqB;iCAArB,qBAAqB;mCAArB,qBAAqB;oCAArB,qBAAqB;6CAArB,qBAAqB;qCAC/E,wBAAwB;2CAAxB,wBAAwB;+CAAxB,wBAAwB;uCAAxB,wBAAwB;gCACJ,wBAAwB;8BAAxB,wBAAwB;iCAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmBjC,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;8BAiBrB,qBAAqB;6BAEkG,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACT,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUjC,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCACkoB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;wCAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;iCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACnrB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI8D,wBAAwB;2CAHlF,0BAA0B;qCAA1B,0BAA0B;gCAA1B,0BAA0B;mCACsE,qBAAqB;kCAArB,qBAAqB;iCAArB,qBAAqB;qCAArB,qBAAqB;iCAArB,qBAAqB;mCAArB,qBAAqB;oCAArB,qBAAqB;6CAArB,qBAAqB;qCAC/E,wBAAwB;2CAAxB,wBAAwB;+CAAxB,wBAAwB;uCAAxB,wBAAwB;gCACJ,wBAAwB;8BAAxB,wBAAwB;iCAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmBjC,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;8BAiBrB,qBAAqB;6BAEiH,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;8BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACxB,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUjC,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCACkoB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;wCAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;iCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACnrB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
@@ -23,7 +23,7 @@ import { consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFo
23
23
  import { sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion } from './form-sources.js';
24
24
  import { matchesChoice } from './matches-choice.js';
25
25
  import { readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions } from './money.js';
26
- import { ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice } from './answer.js';
26
+ import { ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice } from './answer.js';
27
27
  import { readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational } from './contact.js';
28
28
  import { normalizeAndFindCapture } from './capture.js';
29
29
  import { anythingElse, goodbye, sayName } from './phrases.js';
@@ -35,13 +35,13 @@ import { resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, fo
35
35
  import { checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
36
36
  import { validateDigits, validatePhone, validateEmail } from './validators.js';
37
37
  import { urlToSpeech, textWithUrlToSpeech } from './url-speech.js';
38
- export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
38
+ export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
39
39
  /** The functions flows call by name, ready to spread into a host's approved functions. */
40
40
  export const STANDARD_FUNCTIONS = {
41
41
  continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse,
42
42
  formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor,
43
43
  clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome,
44
- readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
44
+ readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
45
45
  readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
46
46
  normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance,
47
47
  validateOTP, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech,
@@ -10,6 +10,7 @@ export { validateFlowRecovery, flowHandoffInfo, renderFlowRecovery } from './flo
10
10
  export type { FlowRecovery } from './flow-recovery.js';
11
11
  export * from './systemone-intent.js';
12
12
  export * from './intent-stages.js';
13
+ export * from './reply-rank.js';
13
14
  export { createActionJournal, createMemoryActionStorage, ActionJournalError } from './action-journal.js';
14
15
  export { actionSessionSnapshot, checkpointActionSession } from './action-session.js';
15
16
  export { retainRecoveryState, applyRecoveryStatePolicy } from './recovery-policy.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/host/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAC,oBAAoB,EAAC,eAAe,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAC3F,YAAY,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrD,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AAEnC,OAAO,EAAC,mBAAmB,EAAC,yBAAyB,EAAC,kBAAkB,EAAC,MAAM,qBAAqB,CAAC;AACrG,OAAO,EAAC,qBAAqB,EAAC,uBAAuB,EAAC,MAAM,qBAAqB,CAAC;AAElF,OAAO,EAAC,mBAAmB,EAAC,wBAAwB,EAAC,MAAM,sBAAsB,CAAC;AAElF,OAAO,EAAC,oBAAoB,EAAC,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAC,4BAA4B,EAAC,4BAA4B,EAAC,6BAA6B,EAAC,MAAM,wBAAwB,CAAC;AAE/H,OAAO,EAAC,0BAA0B,EAAC,uBAAuB,EAAC,qBAAqB,EAAC,MAAM,mBAAmB,CAAC;AAE3G,OAAO,EAAC,iBAAiB,EAAC,4BAA4B,EAAC,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/host/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAC,oBAAoB,EAAC,eAAe,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAC3F,YAAY,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrD,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAEhC,OAAO,EAAC,mBAAmB,EAAC,yBAAyB,EAAC,kBAAkB,EAAC,MAAM,qBAAqB,CAAC;AACrG,OAAO,EAAC,qBAAqB,EAAC,uBAAuB,EAAC,MAAM,qBAAqB,CAAC;AAElF,OAAO,EAAC,mBAAmB,EAAC,wBAAwB,EAAC,MAAM,sBAAsB,CAAC;AAElF,OAAO,EAAC,oBAAoB,EAAC,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAC,4BAA4B,EAAC,4BAA4B,EAAC,6BAA6B,EAAC,MAAM,wBAAwB,CAAC;AAE/H,OAAO,EAAC,0BAA0B,EAAC,uBAAuB,EAAC,qBAAqB,EAAC,MAAM,mBAAmB,CAAC;AAE3G,OAAO,EAAC,iBAAiB,EAAC,4BAA4B,EAAC,MAAM,2BAA2B,CAAC"}
@@ -9,6 +9,7 @@ export * from './request.js';
9
9
  export { validateFlowRecovery, flowHandoffInfo, renderFlowRecovery } from './flow-recovery.js';
10
10
  export * from './systemone-intent.js';
11
11
  export * from './intent-stages.js';
12
+ export * from './reply-rank.js';
12
13
  export { createActionJournal, createMemoryActionStorage, ActionJournalError } from './action-journal.js';
13
14
  export { actionSessionSnapshot, checkpointActionSession } from './action-session.js';
14
15
  export { retainRecoveryState, applyRecoveryStatePolicy } from './recovery-policy.js';
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The reply ranker: what the caller's latest message does with the question a flow asked — the instructions and the
3
+ * decoders, zero runtime imports. jsfe asks it at a contextual DISPATCH (`onResponse: "continue"`), passing the request
4
+ * below to aiCallback as its 4th argument (task 'rank_reply'). The host is the executor: System One (Jev) answers, a
5
+ * text model covers it (the host's aiCallbackRank), and the CANONICAL reply is a JSON string: { "option": "<name>" }.
6
+ *
7
+ * answers the reply answers the question (the default: an open question's reply that clears no option is its answer)
8
+ * abort the caller withdraws the request and asks for nothing else
9
+ * new_request the caller asks for something else: a different service or an unrelated question
10
+ *
11
+ * Both executors' instructions come from the same criteria. A host that does not know this task answers the `llm`
12
+ * instructions with its text model, which is a complete answer.
13
+ */
14
+ export declare const REPLY_OPTIONS: readonly ["answers", "abort", "new_request"];
15
+ export type ReplyOption = typeof REPLY_OPTIONS[number];
16
+ /** An open question's reply that clears no option is its answer (owner decision 2026-10-26). */
17
+ export declare const REPLY_DEFAULT: ReplyOption;
18
+ /** A noul at or above this is a yes. */
19
+ export declare const REPLY_YES_THRESHOLD = 0.5;
20
+ export interface ReplyTurn {
21
+ role: 'user' | 'assistant';
22
+ content: string;
23
+ }
24
+ export interface ReplyRankRequest {
25
+ task: 'rank_reply';
26
+ input: string;
27
+ conversation: ReplyTurn[];
28
+ /** the flow's question and what answers it (the step's responseContext, interpolated) */
29
+ question: string;
30
+ options: ReplyOption[];
31
+ llm: {
32
+ system: string;
33
+ user: string;
34
+ schema: Record<string, unknown>;
35
+ };
36
+ systemOne: {
37
+ state: Record<string, unknown>;
38
+ questions: Record<string, unknown>;
39
+ index: {
40
+ id: string;
41
+ option: ReplyOption;
42
+ }[];
43
+ };
44
+ }
45
+ /** The reply ranker's instructions for both executors. */
46
+ export declare function buildReplyRank(input: string, conversation: ReplyTurn[], question: string): ReplyRankRequest;
47
+ /** A text model's answer as the canonical reply; throws when it names no offered option. */
48
+ export declare function decodeReplyRankLlm(text: string, request: ReplyRankRequest): {
49
+ option: ReplyOption;
50
+ };
51
+ /**
52
+ * System One's answers as the canonical reply: the highest-rated option when it clears the cut, else the default
53
+ * (`answers`), with every option's probability (for logs). Throws if any question is unanswered.
54
+ */
55
+ export declare function decodeReplyRankSystemOne(answers: Record<string, {
56
+ noul?: unknown;
57
+ } | undefined> | undefined, request: ReplyRankRequest, { threshold }?: {
58
+ threshold?: number | undefined;
59
+ }): {
60
+ option: ReplyOption;
61
+ scores: Record<string, number>;
62
+ fallback: boolean;
63
+ };
64
+ /** Is this jsfe's reply-rank request (a host's aiCallback receives it as the 4th argument)? */
65
+ export declare function isReplyRankRequest(request: unknown): request is ReplyRankRequest;
66
+ //# sourceMappingURL=reply-rank.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply-rank.d.ts","sourceRoot":"","sources":["../../src/host/reply-rank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,aAAa,8CAA+C,CAAC;AAC1E,MAAM,MAAM,WAAW,GAAG,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;AACvD,gGAAgG;AAChG,eAAO,MAAM,aAAa,EAAE,WAAuB,CAAC;AACpD,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,MAAM,WAAW,SAAS;IAAG,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,SAAS,EAAE,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IACvE,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,WAAW,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;CACjI;AAsBD,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CA0C3G;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,CAMnG;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAAC,GAAG,SAAS,EACnE,OAAO,EAAE,gBAAgB,EACzB,EAAE,SAA+B,EAAE;;CAAK,GACvC;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAa5E;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,gBAAgB,CAGhF"}