jsfe 1.14.1 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -354,7 +354,10 @@ The host runs either, or both as a hedge, and replies with the canonical JSON: `
354
354
  stage 1, `{ "parameters": { … } }` for stage 2. `jsfe/host` exports `buildFlowStage`, `buildParameterStage`
355
355
  and the decoders (`decodeFlowLlm`, `decodeFlowSystemOne`, `decodeParametersLlm`, `decodeParametersSystemOne`,
356
356
  `isNearMiss`), so a host never interprets an answer itself. Only primaries are ever activated by detection;
357
- a stage-2 failure starts the flow without its parameters (it asks for them). The default, `'single-call'`,
357
+ a stage-2 failure starts the flow without its parameters (it asks for them). Stage 1's System One request
358
+ is one `choice` over the primaries (`route`) and one `noul` (`any`): does the message start any of them; the
359
+ routed flow starts when `any` reaches 0.5, else `"None"`. Both executors are told that a question starts a flow
360
+ only when it asks for the flow's action, never because it mentions its subject. The default, `'single-call'`,
358
361
  is the original single call.
359
362
 
360
363
  **The reply ranker (contextual DISPATCH, `onResponse: "continue"`):** when a flow's question gets a reply its
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAqdD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAjpBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA00B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAueD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAnqBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA41B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -269,7 +269,9 @@ const collectInput = {
269
269
  SET('ci_question', 'ci_q', "formInputPrompt(cargo,form,current,ci_spec,ci_d,'en')"),
270
270
  SET('ci_question_es', 'ci_q_es', "formInputPrompt(cargo,form,current,ci_spec,ci_d,'es')"),
271
271
  SIDE('ci_feedback_consumed', 'consumeFormFeedback(cargo,form,current)'),
272
- SET('ci_own_choices', 'ci_choices', "ci_spec.choices_from && Array.isArray(ci_d[ci_spec.choices_from]) ? ci_d[ci_spec.choices_from] : (ci_spec.profile.kind === 'choice' && Array.isArray(ci_spec.profile.choices) ? ci_spec.profile.choices : [])"),
272
+ // A grouped input (any_of: a plan option, an amount...) offers the ranker every choice its members declare (prod 2026-10-28:
273
+ // the pay offer offered none, so a caller's own words for a listed option were not understood).
274
+ SET('ci_own_choices', 'ci_choices', "ci_spec.choices_from && Array.isArray(ci_d[ci_spec.choices_from]) ? ci_d[ci_spec.choices_from] : (ci_spec.profile.kind === 'choice' && Array.isArray(ci_spec.profile.choices) ? ci_spec.profile.choices : (Array.isArray(ci_spec.any_of) ? ci_spec.any_of.reduce(function(all,m){return all.concat(m && m.profile && m.profile.kind === 'choice' && Array.isArray(m.profile.choices) ? m.profile.choices.filter(function(c,i,own){return !all.some(function(a){return String(a.key)===String(c.key);}) && own.findIndex(function(o){return String(o.key)===String(c.key);})===i;}) : []);},[]) : []))"),
273
275
  SET('ci_keypad', 'ci_digits', 'ci_spec.digits || null'),
274
276
  SET('ci_is_amount', 'ci_amount', "ci_spec.amount === 'marked' ? 'marked' : (ci_spec.amount === true || ci_spec.profile.kind === 'amount')"),
275
277
  // FORM profile readers own normalization and compound matching. Never discard other
@@ -389,10 +391,18 @@ const readFormResponse = {
389
391
  // `answers` (picks none): asked again ("Sorry, I didn't catch that."), counted as an attempt so max_attempts still bounds
390
392
  // it; `abort` ends every flow; `new_request` goes to intent detection.
391
393
  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)");
392
- const ESCAPES_JS = "[{key:'__agent',label:'speak with a person (a live agent)'}].concat(cargo.voice && callerTextNumber(cargo) ? [{key:'__text',label:'continue by text message'}] : [])";
394
+ // The ranker is offered only a person. Text is never offered: its label matched "continuar", "texto" and "enlace", and a
395
+ // call moved to text hangs up (prod 2026-10-28: 7 of 7 such picks were wrong). TEXTO and keypad 9 are read before it.
396
+ const ESCAPES_JS = "[{key:'__agent',label:'speak with a person (a live agent)'}]";
393
397
  const rankedChoices = (id, from) => SET(id, 'ranked', `(${from}).concat(${ESCAPES_JS})`);
394
- const escapeRoutes = (id) => [WHEN(`${id}_agent`, "reply && reply.key === '__agent'", FLOW(`${id}_agent_flow`, 'live-agent-requested', 'reboot')),
395
- WHEN(`${id}_text`, "reply && reply.key === '__text'", FLOW(`${id}_text_flow`, 'switch-to-text', 'reboot'))];
398
+ // A choice as the ranker is offered it: its key, and its label or its worded phrases.
399
+ const RANK_LABEL_JS = "function(c){var said=(Array.isArray(c.phrases) ? c.phrases : []).map(String).filter(function(p){return /[a-zà-ÿ]/i.test(p) && p!==String(c.key);}).slice(0,6);return String(c.label || (said.length ? c.key+' ('+said.join(', ')+')' : c.key));}";
400
+ // The choices of this form's OTHER inputs that the answer could also set (the word reader reads them all: 'LO VENCIDO' at
401
+ // the pay confirmation): the ranker is offered every choice the reader would accept (owner 2026-10-28). A confirmation
402
+ // input is answered only when asked, and a key already offered is not offered twice. Each carries its input, so a pick is
403
+ // read as that input's answer.
404
+ const otherChoicesJS = (asked, taken) => `(function(){var seen=(${taken}).map(function(c){return String(c.key);}).concat(['yes','no']);return (chain || []).filter(function(i){return i.form===form && i.name!==${asked} && !i.confirmation && i.profile && i.profile.kind==='choice' && Array.isArray(i.profile.choices);}).reduce(function(all,i){return all.concat(i.profile.choices.filter(function(c){var k=String(c.key);if(seen.indexOf(k)>=0)return false;seen.push(k);return true;}).map(function(c){return {key:String(c.key),label:(${RANK_LABEL_JS})(c),owner:{form:i.form,name:i.name}};}));},[]);})()`;
405
+ const escapeRoutes = (id) => [WHEN(`${id}_agent`, "reply && reply.key === '__agent'", FLOW(`${id}_agent_flow`, 'live-agent-requested', 'reboot'))];
396
406
  const rankedDispatch = (id, reason) => ({ ...yieldDispatch(id, 'form', `'${reason}'`), onResponse: 'continue', responseContext: 'Current question: {{asked}}', responseChoices: 'ranked', responseVariable: 'reply' });
397
407
  const readFormReply = {
398
408
  id: 'read-form-reply', name: 'ReadFormReply', version: '1.1.0', scope: 'isolated', description: 'Rank an unreadable answer against its question: a request for a person or for text is taken; an answer is asked again; a withdrawal or a new request leaves with saved form progress.',
@@ -405,37 +415,45 @@ const readFormReply = {
405
415
  rankedMiss('reply_miss')]
406
416
  };
407
417
  const readFormChoice = {
408
- id: 'read-form-choice', name: 'ReadFormChoice', version: '1.2.0', scope: 'isolated', description: 'Rank an unreadable answer to a choice question and take the offered option it picks, or a request for a person or for text; otherwise ask again. A withdrawal or a new request leaves with saved form progress.',
418
+ id: 'read-form-choice', name: 'ReadFormChoice', version: '1.3.0', scope: 'isolated', description: 'Rank an unreadable answer to a choice question and take the offered option it picks, or a request for a person or for text; otherwise ask again. A withdrawal or a new request leaves with saved form progress.',
409
419
  parameters: [{ name: 'form', type: 'string' }, { name: 'input', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }, { name: 'choices', type: 'array' }, { name: 'firm', type: 'string' }],
410
- variables: vars({ question: '', question_es: '', choices: [], firm: '', asked: '', ranked: [], reply: null, picked: '', chain: null, reading: null }),
420
+ variables: vars({ question: '', question_es: '', choices: [], firm: '', asked: '', ranked: [], reply: null, picked: '', chain: null, reading: null, others: [], owner: null }),
411
421
  steps: [SET('choice_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
412
- rankedChoices('choice_ranked', "(choices || []).filter(function(c){return !(firm==='firm' && String(c.key)==='yes');}).map(function(c){var said=(Array.isArray(c.phrases) ? c.phrases : []).map(String).filter(function(p){return /[a-zà-ÿ]/i.test(p) && p!==String(c.key);}).slice(0,6);return {key:String(c.key),label:String(c.label || (said.length ? c.key+' ('+said.join(', ')+')' : c.key))};})"),
422
+ SET('choice_chain', 'chain', 'formInputChain(cargo.forms, form, cargo)'),
423
+ SET('choice_others', 'others', otherChoicesJS('input', 'choices || []')),
424
+ rankedChoices('choice_ranked', `(choices || []).filter(function(c,i,all){return !(firm==='firm' && String(c.key)==='yes') && all.findIndex(function(o){return String(o.key)===String(c.key);})===i;}).map(function(c){return {key:String(c.key),label:(${RANK_LABEL_JS})(c)};}).concat(others.map(function(c){return {key:c.key,label:c.label};}))`),
413
425
  rankedDispatch('choice_dispatch', 'input_not_matched'),
414
426
  ...escapeRoutes('choice_escape'),
415
- SET('choice_picked', 'picked', "(function(){var k=String(reply && reply.key || '');var c=(choices || []).filter(function(c){return String(c.key)===k;})[0];return c ? String(c.key) : '';})()"),
427
+ SET('choice_picked', 'picked', "(function(){var k=String(reply && reply.key || '');var c=(choices || []).filter(function(c){return String(c.key)===k;})[0] || others.filter(function(c){return c.key===k;})[0];return c ? String(c.key) : '';})()"),
428
+ SET('choice_owner', 'owner', "(others.filter(function(c){return c.key===picked && !(choices || []).some(function(own){return String(own.key)===picked;});})[0] || {}).owner || null"),
416
429
  WHEN('choice_none', '!picked', rankedMiss('choice_miss')),
417
430
  WHEN('choice_none_end', '!picked', END('choice_none_done')),
418
431
  SIDE('choice_take', "(cargo.choice=picked,cargo.choiceText=picked,true)"),
419
- SET('choice_chain', 'chain', 'formInputChain(cargo.forms, form, cargo)'),
420
- SET('choice_read', 'reading', "enrichFormMatches(readFormAnswer(picked, chain, { form: form, name: input }, picked, []), chain, form, [input], input)"),
432
+ SET('choice_read', 'reading', "owner ? enrichFormMatches(readFormAnswer(picked, chain, owner, picked, []), chain, form, [owner.name], owner.name) : enrichFormMatches(readFormAnswer(picked, chain, { form: form, name: input }, picked, []), chain, form, [input], input)"),
421
433
  WHEN('choice_unread', '!reading.hits.length', rankedMiss('choice_unread_miss')),
422
434
  WHEN('choice_unread_end', '!reading.hits.length', END('choice_unread_done')),
423
435
  SIDE('choice_clear', 'clearFormMisses(cargo,form,input)'),
424
436
  FLOW('choice_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })]
425
437
  };
426
438
  // A confirmation's unread answer goes to the reply ranker with the confirmation question (owner 2026-10-27), never
427
- // straight to the host. A form's confirmation is exact (it authorizes the form's action), so the ranker is offered NO
428
- // and the escapes, never YES. Result: cargo.confirmRanked = 'no' | 'none' (ConfirmForm reads it); `abort` ends every
429
- // flow; `new_request` goes to intent detection.
439
+ // straight to the host. A form's confirmation is exact (it authorizes the form's action), so the ranker is offered NO, the
440
+ // form's other choices (a correction) and a person, never YES. Result: cargo.confirmRanked = 'no' | 'changed' | 'none'
441
+ // (ConfirmForm reads it); `abort` ends every flow; `new_request` goes to intent detection.
430
442
  const readConfirmReply = {
431
- id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.1.0', scope: 'isolated', description: 'Rank an unreadable answer to a confirmation: it declines (NO), asks for a person or for text, or is asked again; a withdrawal or a new request leaves with saved form progress. Never confirms.',
443
+ id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.2.0', scope: 'isolated', description: 'Rank an unreadable answer to a confirmation: it declines (NO), picks another of the form\'s choices (a correction, read as that input\'s answer), asks for a person, or is asked again; a withdrawal or a new request leaves with saved form progress. Never confirms.',
432
444
  parameters: [{ name: 'form', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }],
433
- variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null }),
445
+ variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null, chain: null, others: [], picked: null, reading: null }),
434
446
  steps: [SET('confirm_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
435
- rankedChoices('confirm_ranked_choices', "[{key:'no',label:'no: declines or refuses what the question asks to confirm'}]"),
447
+ SET('confirm_chain', 'chain', 'formInputChain(cargo.forms, form, cargo)'),
448
+ SET('confirm_others', 'others', otherChoicesJS("''", '[]')),
449
+ rankedChoices('confirm_ranked_choices', "[{key:'no',label:'no: declines or refuses what the question asks to confirm'}].concat(others.map(function(c){return {key:c.key,label:c.label};}))"),
436
450
  rankedDispatch('confirm_dispatch', 'confirmation_not_matched'),
437
451
  ...escapeRoutes('confirm_escape'),
438
- SIDE('confirm_ranked', "(cargo.confirmRanked=reply && reply.key==='no' ? 'no' : 'none',true)")]
452
+ // a correction picked by the ranker is read as its input's answer: the form changes and is confirmed again, never confirmed
453
+ SET('confirm_picked', 'picked', "others.filter(function(c){return reply && c.key===reply.key;})[0] || null"),
454
+ SET('confirm_read', 'reading', "picked ? enrichFormMatches(readFormAnswer(picked.key, chain, picked.owner, picked.key, []), chain, form, [picked.owner.name], picked.owner.name) : null"),
455
+ WHEN('confirm_change', 'reading && reading.hits.length', FLOW('confirm_change_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })),
456
+ SIDE('confirm_ranked', "(cargo.confirmRanked=reading && reading.hits.length ? 'changed' : reply && reply.key==='no' ? 'no' : 'none',true)")]
439
457
  };
440
458
  const collectContextualAnswer = formFlow({ id: 'collect-contextual-answer', name: 'CollectContextualAnswer', description: 'Collect one open-ended answer using contextual DISPATCH and preserve the caller wording.',
441
459
  parameters: [...['question', 'question_es', 'description', 'context', 'context_es'].map(name => ({ name, type: 'string' })), { name: 'choices', type: 'array' }, { name: 'digits', type: 'object' }],
@@ -23,8 +23,6 @@
23
23
  import type { FlowParameterDefinition } from '../index.js';
24
24
  /** A noul at or above this is a yes. */
25
25
  export declare const STAGE_YES_THRESHOLD = 0.5;
26
- /** Stage 1, System One: two top flows that together reach this pick the higher one (prod 10-17). */
27
- export declare const STAGE_PAIR_SUM = 0.7;
28
26
  export interface StageTurn {
29
27
  role: 'user' | 'assistant';
30
28
  content: string;
@@ -52,9 +50,9 @@ export interface StageSystemOne<I> {
52
50
  questions: Record<string, unknown>;
53
51
  index: I[];
54
52
  }
53
+ /** Stage 1, System One: `route` is the choice among the flows, `any` whether the message starts one at all. */
55
54
  export interface FlowQuestionRef {
56
- id: string;
57
- flow: number;
55
+ id: 'route' | 'any';
58
56
  }
59
57
  export type ParameterQuestionRef = {
60
58
  id: string;
@@ -99,26 +97,34 @@ export type StageSystemOneAnswers = Record<string, {
99
97
  } | undefined> | undefined;
100
98
  /** The flow's own parameters, never asked of the user. */
101
99
  export declare const isInternalParameter: (p: StageParameter | FlowParameterDefinition) => boolean;
102
- /** Stage 1's instructions for both executors. `flows` are the primaries offered. */
100
+ /**
101
+ * Stage 1's instructions for both executors. `flows` are the primaries offered.
102
+ *
103
+ * System One gets ONE request with two questions over the same list: `route`, a `choice` among the flows (each
104
+ * option's criterion is its description, so every flow is weighed against the others), and `any`, a `noul`
105
+ * asking whether the message starts any of them. TypeSafe documents that a "none of the above" option may not
106
+ * work well and advises the companion noul.
107
+ */
103
108
  export declare function buildFlowStage(input: string, conversation: StageTurn[], flows: StageFlow[]): FlowStageRequest;
104
109
  /** A text model's stage-1 answer as the canonical reply; throws when it names no offered flow. */
105
110
  export declare function decodeFlowLlm(text: string, stage: FlowStageRequest): {
106
111
  flowName: string;
107
112
  };
108
113
  /**
109
- * System One's stage-1 answers as the canonical reply, with the top three scores (for logs and a host's hedge
110
- * policy) and `pair` when two close candidates picked the higher. Throws if any question is unanswered.
114
+ * System One's stage-1 answers as the canonical reply. `any` at or above the threshold starts the flow `route`
115
+ * chose; below it, "None". `scores` are the top three flows by the chance that each is the one started,
116
+ * any × its route probability (for logs and the host's near-miss hedge); `any` is logged with them. Throws when
117
+ * either answer is missing or names no offered flow.
111
118
  */
112
- export declare function decodeFlowSystemOne(answers: StageSystemOneAnswers, stage: FlowStageRequest, { threshold, pairSum }?: {
119
+ export declare function decodeFlowSystemOne(answers: StageSystemOneAnswers, stage: FlowStageRequest, { threshold }?: {
113
120
  threshold?: number | undefined;
114
- pairSum?: number | undefined;
115
121
  }): {
116
122
  flowName: string;
117
123
  scores: {
118
124
  name: string;
119
125
  p: number;
120
126
  }[];
121
- pair: boolean;
127
+ any: number;
122
128
  };
123
129
  /**
124
130
  * A System One "None" that is a near miss: the chance that at least one of its two top flows applies,
@@ -1 +1 @@
1
- {"version":3,"file":"intent-stages.d.ts","sourceRoot":"","sources":["../../src/host/intent-stages.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"intent-stages.d.ts","sourceRoot":"","sources":["../../src/host/intent-stages.ts"],"names":[],"mappings":"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"}
@@ -22,11 +22,11 @@
22
22
  */
23
23
  /** A noul at or above this is a yes. */
24
24
  export const STAGE_YES_THRESHOLD = 0.5;
25
- /** Stage 1, System One: two top flows that together reach this pick the higher one (prod 10-17). */
26
- export const STAGE_PAIR_SUM = 0.7;
27
25
  // ── the rules: written once, rendered for both executors ─────────────────────────────────────────────
28
26
  const FLOW_TRUE = "The latest message, read with the conversation, is what the workflow's description says should start it: the user asks for its action or information, or raises the problem it exists to handle. Also when it accepts the assistant's offer in the conversation to start or resume it (assistant: \"Shall I try the payment again?\" → user: \"yes\").";
29
27
  const FLOW_FALSE = "It only shares a topic with the workflow, is not what the workflow's description covers, or is a greeting, an acknowledgement or a fragment that asks for nothing.";
28
+ /** A question usually asks for information the assistant gives itself; it is weighed before it is taken as a trigger. */
29
+ const QUESTION_RULE = "A question usually asks for information the assistant can give itself. Consider it carefully: it starts a workflow only when it asks for that workflow's action or raises the problem it exists to handle, never because it mentions the workflow's subject (\"can I pay with this card at a store?\" asks how the card is used, not to make a payment).";
30
30
  const PARAMETER_RULES = [
31
31
  'Give a value only when the user gave it, in the latest message or earlier in the conversation. Never guess.',
32
32
  "Write it as the parameter's rule says; otherwise the value alone, in the user's words (never the whole sentence around it).",
@@ -49,33 +49,45 @@ const stateOf = (conversation, input, instructions) => ({
49
49
  });
50
50
  const jsonTail = (schema) => `\n\n<json-schema>\n${JSON.stringify(schema)}\n</json-schema>\n\nRespond ONLY with valid JSON matching the schema. No other text.`;
51
51
  // ── stage 1 ───────────────────────────────────────────────────────────────────────────────────────────
52
- /** Stage 1's instructions for both executors. `flows` are the primaries offered. */
52
+ /**
53
+ * Stage 1's instructions for both executors. `flows` are the primaries offered.
54
+ *
55
+ * System One gets ONE request with two questions over the same list: `route`, a `choice` among the flows (each
56
+ * option's criterion is its description, so every flow is weighed against the others), and `any`, a `noul`
57
+ * asking whether the message starts any of them. TypeSafe documents that a "none of the above" option may not
58
+ * work well and advises the companion noul.
59
+ */
53
60
  export function buildFlowStage(input, conversation, flows) {
54
61
  const names = flows.map(f => f.name);
55
62
  const schema = {
56
63
  type: 'object', additionalProperties: false, required: ['flowName'],
57
64
  properties: { flowName: { type: 'string', enum: [...names, 'None'] } },
58
65
  };
66
+ const list = flows.map(f => `- ${f.name}: ${f.description}`);
59
67
  const system = [
60
68
  'Decide which workflow, if any, the latest message starts.',
61
69
  `A workflow is chosen when: ${FLOW_TRUE}`,
62
70
  `"None" when: ${FLOW_FALSE}`,
71
+ QUESTION_RULE,
63
72
  'Answer with the exact workflow name, or "None".',
64
73
  '',
65
74
  '<workflows>',
66
- ...flows.map(f => `- ${f.name}: ${f.description}`),
75
+ ...list,
67
76
  '</workflows>',
68
77
  ].join('\n') + jsonTail(schema);
69
- const questions = {};
70
- const index = [];
71
- flows.forEach((f, i) => {
72
- const id = `f${i}`;
73
- questions[id] = {
74
- type: 'noul', criteria: { true: FLOW_TRUE, false: FLOW_FALSE },
75
- instructions: { flow: { name: f.name, description: f.description }, question: 'Should the latest message start the workflow in `flow`?' },
76
- };
77
- index.push({ id, flow: i });
78
- });
78
+ const workflows = Object.fromEntries(flows.map(f => [f.name, f.description]));
79
+ const questions = {
80
+ any: {
81
+ type: 'noul',
82
+ criteria: { true: `${FLOW_TRUE} The workflow is one of \`workflows\`.`, false: `${FLOW_FALSE} ${QUESTION_RULE}` },
83
+ instructions: { workflows, question: 'Should the latest message start one of the workflows in `workflows`?' },
84
+ },
85
+ };
86
+ questions.route = {
87
+ type: 'choice', criteria: workflows,
88
+ instructions: 'Which workflow does the latest message, read with the conversation, come closest to starting?',
89
+ };
90
+ const index = [{ id: 'any' }, { id: 'route' }];
79
91
  return {
80
92
  task: 'detect_flow', stage: 1, input, conversation, flows,
81
93
  llm: { system, user: conversationText(conversation, input), schema },
@@ -97,23 +109,27 @@ export function decodeFlowLlm(text, stage) {
97
109
  }
98
110
  }
99
111
  /**
100
- * System One's stage-1 answers as the canonical reply, with the top three scores (for logs and a host's hedge
101
- * policy) and `pair` when two close candidates picked the higher. Throws if any question is unanswered.
112
+ * System One's stage-1 answers as the canonical reply. `any` at or above the threshold starts the flow `route`
113
+ * chose; below it, "None". `scores` are the top three flows by the chance that each is the one started,
114
+ * any × its route probability (for logs and the host's near-miss hedge); `any` is logged with them. Throws when
115
+ * either answer is missing or names no offered flow.
102
116
  */
103
- export function decodeFlowSystemOne(answers, stage, { threshold = STAGE_YES_THRESHOLD, pairSum = STAGE_PAIR_SUM } = {}) {
104
- const p = answered(answers, stage.systemOne.index);
105
- const ranked = stage.systemOne.index.map(q => ({ name: stage.flows[q.flow].name, p: p.get(q.id) })).sort((a, b) => b.p - a.p);
117
+ export function decodeFlowSystemOne(answers, stage, { threshold = STAGE_YES_THRESHOLD } = {}) {
118
+ const any = Number(answers?.any?.noul);
119
+ if (!Number.isFinite(any))
120
+ throw new Error('System One: question any unanswered');
121
+ const names = stage.flows.map(f => f.name);
122
+ const route = answers?.route;
123
+ const raw = route?.probabilities;
124
+ if (!raw || typeof raw !== 'object')
125
+ throw new Error('System One: question route unanswered');
126
+ if (!names.includes(String(route?.choice)))
127
+ throw new Error(`System One: route chose "${String(route?.choice).slice(0, 60)}", not an offered workflow`);
128
+ const probabilities = Object.fromEntries(names.map(n => [n, Number(raw[n]) || 0]));
129
+ const ranked = names.map(n => ({ name: n, p: any * probabilities[n] })).sort((a, b) => b.p - a.p);
106
130
  const scores = ranked.slice(0, 3);
107
- const [a, b] = ranked;
108
- if (a && a.p >= threshold) {
109
- return { flowName: a.name, scores, pair: false };
110
- }
111
- else if (a && b && a.p + b.p >= pairSum) {
112
- return { flowName: a.name, scores, pair: true };
113
- }
114
- else {
115
- return { flowName: 'None', scores, pair: false };
116
- }
131
+ const flowName = any >= threshold && ranked[0] ? ranked[0].name : 'None';
132
+ return { flowName, scores, any };
117
133
  }
118
134
  /**
119
135
  * A System One "None" that is a near miss: the chance that at least one of its two top flows applies,
@@ -26,6 +26,11 @@ export declare const CHOICE_PREFIX = "choice:";
26
26
  export declare const REPLY_DEFAULT: ReplyOption;
27
27
  /** A noul at or above this is a yes. */
28
28
  export declare const REPLY_YES_THRESHOLD = 0.5;
29
+ /**
30
+ * The cut an option must clear, by what a wrong pick costs: an option not listed here takes REPLY_YES_THRESHOLD. Set
31
+ * from labelled prod replies (owner 2026-10-28), never guessed.
32
+ */
33
+ export declare const REPLY_CUTS: Partial<Record<ReplyOption, number>>;
29
34
  export interface ReplyTurn {
30
35
  role: 'user' | 'assistant';
31
36
  content: string;
@@ -60,15 +65,15 @@ export declare function decodeReplyRankLlm(text: string, request: ReplyRankReque
60
65
  option: ReplyOption;
61
66
  };
62
67
  /**
63
- * System One's answers as the canonical reply: the highest-rated option when it clears the cut, else the default
64
- * (`answers`), with every option's probability (for logs). A declared choice that clears the cut is taken over `answers`
65
- * ("picks none of them") and `new_request`; only `abort` keeps its rank (owner 2026-10-28: a reply rated its menu
66
- * choice 0.65 and `answers` 0.81, and the menu was asked again). Throws if any question is unanswered.
68
+ * System One's answers as the canonical reply: the highest-rated option that clears its own cut (REPLY_CUTS, else the
69
+ * threshold), a cleared choice before `answers` and `new_request`; when none does, the default (`answers`: asked again at a choice question, the answer at an open one), with
70
+ * every option's probability (for logs). Throws if any question is unanswered.
67
71
  */
68
72
  export declare function decodeReplyRankSystemOne(answers: Record<string, {
69
73
  noul?: unknown;
70
- } | undefined> | undefined, request: ReplyRankRequest, { threshold }?: {
71
- threshold?: number | undefined;
74
+ } | undefined> | undefined, request: ReplyRankRequest, { threshold, cuts }?: {
75
+ threshold?: number;
76
+ cuts?: Partial<Record<ReplyOption, number>>;
72
77
  }): {
73
78
  option: ReplyOption;
74
79
  scores: Record<string, number>;
@@ -1 +1 @@
1
- {"version":3,"file":"reply-rank.d.ts","sourceRoot":"","sources":["../../src/host/reply-rank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,aAAa,8CAA+C,CAAC;AAC1E,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,OAAO,aAAa,CAAC,MAAM,CAAC,GAAG,UAAU,MAAM,EAAE,CAAC;AAC5E,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC3D,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,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,0DAA0D;IAC1D,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,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;AAwCD,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,WAAW,EAAO,GAAG,gBAAgB,CA0CxI;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,CAMnG;AAED;;;;;GAKG;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,CAe5E;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,gBAAgB,CAGhF"}
1
+ {"version":3,"file":"reply-rank.d.ts","sourceRoot":"","sources":["../../src/host/reply-rank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,aAAa,8CAA+C,CAAC;AAC1E,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,OAAO,aAAa,CAAC,MAAM,CAAC,GAAG,UAAU,MAAM,EAAE,CAAC;AAC5E,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC3D,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,gGAAgG;AAChG,eAAO,MAAM,aAAa,EAAE,WAAuB,CAAC;AACpD,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAO3D,CAAC;AAEF,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,0DAA0D;IAC1D,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,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;AAwCD,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,WAAW,EAAO,GAAG,gBAAgB,CA+CxI;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,CAMnG;AAED;;;;GAIG;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,IAAiB,EAAE,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;CAAO,GAC/H;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAiB5E;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,gBAAgB,CAGhF"}
@@ -19,6 +19,18 @@ export const CHOICE_PREFIX = 'choice:';
19
19
  export const REPLY_DEFAULT = 'answers';
20
20
  /** A noul at or above this is a yes. */
21
21
  export const REPLY_YES_THRESHOLD = 0.5;
22
+ /**
23
+ * The cut an option must clear, by what a wrong pick costs: an option not listed here takes REPLY_YES_THRESHOLD. Set
24
+ * from labelled prod replies (owner 2026-10-28), never guessed.
25
+ */
26
+ export const REPLY_CUTS = {
27
+ // a NO cancels what the caller was confirming. Measured 2026-10-28: replies that are not a NO ("imposible", "no es un")
28
+ // rated 0.51-0.63, real NOs ("No me interesa", "No de verdad", "mejor no") 0.67-0.95
29
+ 'choice:no': 0.65,
30
+ // a new request leaves the question unanswered. Unclear replies at a question ("la curazán" at a confirmation, "Es que
31
+ // mi tarjeta se cambió" at the pay menu) rated 0.47-0.58 and vary ±0.04 between calls; they are asked again
32
+ new_request: 0.6,
33
+ };
22
34
  // ── the criteria: written once, rendered for both executors ─────────────────────────────────────────
23
35
  const CRITERIA = {
24
36
  answers: {
@@ -81,7 +93,12 @@ export function buildReplyRank(input, conversation, question, choices = []) {
81
93
  ].join('\n');
82
94
  const questions = {};
83
95
  const index = [];
84
- options.forEach((o, i) => {
96
+ // With declared choices, `answers` ("picks none of them") is not a question System One answers: it competes with the
97
+ // choices it excludes. It is what is left when no option clears its cut (owner 2026-10-28). A text model still has it.
98
+ // An escape (`__agent`) is not the question's own choice: a question that offers only escapes is an open question,
99
+ // where `answers` is the reply (prod 2026-10-28: a city named at "check nearby stores" went to intent detection).
100
+ const ownChoices = choices.some(c => !c.key.startsWith('__'));
101
+ options.filter(o => !(o === 'answers' && ownChoices)).forEach((o, i) => {
85
102
  const id = `r${i}`;
86
103
  questions[id] = { type: 'noul', criteria: criteriaOf(o, choices), instructions: { option: o, question: `Does the latest message do what \`${o}\` describes?` } };
87
104
  index.push({ id, option: o });
@@ -110,12 +127,11 @@ export function decodeReplyRankLlm(text, request) {
110
127
  return { option };
111
128
  }
112
129
  /**
113
- * System One's answers as the canonical reply: the highest-rated option when it clears the cut, else the default
114
- * (`answers`), with every option's probability (for logs). A declared choice that clears the cut is taken over `answers`
115
- * ("picks none of them") and `new_request`; only `abort` keeps its rank (owner 2026-10-28: a reply rated its menu
116
- * choice 0.65 and `answers` 0.81, and the menu was asked again). Throws if any question is unanswered.
130
+ * System One's answers as the canonical reply: the highest-rated option that clears its own cut (REPLY_CUTS, else the
131
+ * threshold), a cleared choice before `answers` and `new_request`; when none does, the default (`answers`: asked again at a choice question, the answer at an open one), with
132
+ * every option's probability (for logs). Throws if any question is unanswered.
117
133
  */
118
- export function decodeReplyRankSystemOne(answers, request, { threshold = REPLY_YES_THRESHOLD } = {}) {
134
+ export function decodeReplyRankSystemOne(answers, request, { threshold = REPLY_YES_THRESHOLD, cuts = REPLY_CUTS } = {}) {
119
135
  const scores = {};
120
136
  for (const q of request.systemOne.index) {
121
137
  const noul = Number(answers?.[q.id]?.noul);
@@ -123,10 +139,12 @@ export function decodeReplyRankSystemOne(answers, request, { threshold = REPLY_Y
123
139
  throw new Error(`System One: question ${q.id} unanswered`);
124
140
  scores[q.option] = noul;
125
141
  }
126
- const ranked = request.systemOne.index.map(q => q.option).sort((a, b) => scores[b] - scores[a]);
127
- const choice = ranked.find(o => o.startsWith(CHOICE_PREFIX) && scores[o] >= threshold);
128
- const best = choice && ranked[0] !== 'abort' ? choice : ranked[0];
129
- if (best && scores[best] >= threshold) {
142
+ const cleared = request.systemOne.index.map(q => q.option).filter(o => scores[o] >= (cuts[o] ?? threshold)).sort((a, b) => scores[b] - scores[a]);
143
+ // A cleared choice is taken over `answers` and `new_request` (owner 2026-10-28: a request that names an offered option
144
+ // is that option, "Representative." is the agent); only `abort` keeps its rank.
145
+ const choice = cleared.find(o => o.startsWith(CHOICE_PREFIX));
146
+ const best = choice && (cleared[0] === 'answers' || cleared[0] === 'new_request') ? choice : cleared[0];
147
+ if (best) {
130
148
  return { option: best, scores, fallback: false };
131
149
  }
132
150
  else {
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:9fa908654ad5402560cc1cf51e4cd57d1bff01735b4e31d6fc0bb4f5f519ceb1";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:bbc5a083f51750e417f571d17b1d5880f511fc5506a76f235c6740e55dad7640";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.14.1",
3
+ "version": "1.15.0",
4
4
  "description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
5
5
  "license": "MIT",
6
6
  "author": "Ron Pinkas",