jsfe 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -769,12 +769,22 @@ return aiResponse;
769
769
  is already repaired: the next turn starts with fresh intent detection. Log or record the error;
770
770
  no rollback is needed. A deliberate `RETURN ''` (which ends every flow) is not flagged.
771
771
 
772
+ Native FORM collection in 1.7.2 acknowledges an unreadable answer once, then yields the current
773
+ utterance while retaining its draft. Explicit recovery metadata distinguishes resumption, required
774
+ restart, unknown-action hold and no promised recovery; business completion remains independent.
775
+ See the [retry/yield/recovery contract](docs/forms.md#retry-yield-and-recovery-contract-172).
776
+
772
777
  ### Host Responsibilities: Tool Calls, Cancellation and Concurrency
773
778
 
774
779
  A flow can take actions in the outside world through `CALL-TOOL` — charge a card, send an SMS,
775
780
  create a ticket. For ordinary `CALL-TOOL` flows, **the session returned by `updateActivity` records
776
781
  which steps already ran**; the host owns its persistence. Native performers can additionally use an
777
- explicitly attached durable action journal. Neither mode removes the host responsibilities below.
782
+ explicitly attached durable action journal (the native default). An explicit
783
+ `engine.actionPersistence = 'session-only'` opt-out retains confirmation and saved session action guards
784
+ without journal storage. It cannot prevent a repeated charge after an effect succeeds and its updated
785
+ session save is lost. The reference host selects it with `jsfe: {actionPersistence:'session-only'}`;
786
+ `durableActions:true` conflicts with that setting. Keep each existing conversation on its original mode.
787
+ Neither policy removes the host responsibilities below.
778
788
  Getting these rules wrong can repeat an action the user asked for once.
779
789
 
780
790
  The reference host defaults `config.jsfe` to `{durableActions:false,sessionEncoding:'plain'}`. Durable
@@ -1,6 +1,6 @@
1
1
  /** Internal transport ports. Registered by WorkflowEngine; no network implementation lives in JSFE. */
2
2
  export const ACTION_TOOLS = ['claim', 'complete'].map(operation => ({
3
- id: `jsfe:action-${operation}`, name: `JSFE action ${operation}`, description: `Await the configured durable action journal ${operation} port.`,
3
+ id: `jsfe:action-${operation}`, name: `JSFE action ${operation}`, description: `Await the configured guarded action ${operation} port.`,
4
4
  parameters: { type: 'object', properties: { record: { type: 'object' }, action: { type: 'object' }, ...(operation === 'complete' ? { outcome: { type: ['object', 'null'] } } : {}) }, required: ['record', 'action'], additionalProperties: false },
5
5
  implementation: { type: 'local', function: operation === 'claim' ? 'jsfeClaimAction' : 'jsfeCompleteAction', retries: 0, timeout: 10000 },
6
6
  }));
@@ -114,7 +114,7 @@ export function definePerformer({ performs, contract, prepare, present, project
114
114
  call('prepare', prepare, { form: '{{form_instance}}' }),
115
115
  set('request', 'pf_request', 'cargo.actionRequest'),
116
116
  set('select', 'pf_action', 'selectFormAction(cargo, form_instance, performs)'),
117
- when('bad_request', "!(pf_action === null || pf_action && typeof pf_action === 'object' && pf_action.action) || pf_action !== null && (!pf_request || typeof pf_request !== 'object')", { id: `${spec.id}_prepare_failed`, type: 'FLOW', value: 'give-up', callType: 'reboot', parameters: { context: '{{exit_context}}', context_es: '{{exit_context_es}}' } }),
117
+ when('bad_request', "!(pf_action === null || pf_action && typeof pf_action === 'object' && pf_action.action) || pf_action !== null && (!pf_request || typeof pf_request !== 'object')", { id: `${spec.id}_prepare_failed`, type: 'FLOW', value: 'give-up', callType: 'reboot', recovery: { version: 1, mode: 'none', reason: 'performer_request_invalid' }, parameters: { context: '{{exit_context}}', context_es: '{{exit_context_es}}' } }),
118
118
  set('proposal', 'pf_record', "pf_action ? prepareFormAction(cargo, form_instance, pf_action, pf_request) : null"),
119
119
  ];
120
120
  const flow = formFlow({ ...spec, confirm, confirmation_condition: "pf_action !== null && pf_action && typeof pf_action === 'object'",
@@ -129,7 +129,7 @@ export function definePerformer({ performs, contract, prepare, present, project
129
129
  { ...set('receipt_applied', 'noop', 'pf_receipt ? consumeActionReceipt(cargo,pf_receipt) : true'), requireTrue: true },
130
130
  call('present', present, { form: '{{form_instance}}', request: '{{pf_request}}', result: '{{pf_result}}' }),
131
131
  ...(spec.after || [])] });
132
- return declared(flow, { role: 'performer', ...contract, requires: ['durableActions', ...normalizeHostRequirements(contract?.requires, spec.id)], actions: 'performs', form: { inputs: 'inputs' }, prepares: prepare, presents: present, projects: 'projection' });
132
+ return declared(flow, { role: 'performer', ...contract, requires: ['guardedActions', ...normalizeHostRequirements(contract?.requires, spec.id)], actions: 'performs', form: { inputs: 'inputs' }, prepares: prepare, presents: present, projects: 'projection' });
133
133
  }
134
134
  /** A registered tool gets one generated adapter with explicit args; no dynamic tool-name interpreter is needed. */
135
135
  export function operationWorkers(tools) {
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAsFA;;;;;;GAMG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAiSD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAlcM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAknB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAyFA;;;;;;GAMG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAoTD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAtdM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsoB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -28,7 +28,10 @@ const FLOW = (id, value, callType = 'call', parameters) => ({ id, type: 'FLOW',
28
28
  const WHEN = (id, condition, step) => ({ id, type: 'CASE', branches: { [`condition: ${condition}`]: step, default: NOOP(`${id}_skip`) } });
29
29
  const vars = (o) => Object.fromEntries(Object.entries(o).map(([k, v]) => [k, { type: Array.isArray(v) ? 'array' : v === null || typeof v === 'object' ? 'object' : typeof v, value: v }]));
30
30
  const incompleteAttrs = (form, reason) => ({ outcome: `{{formIncompleteOutcome(cargo,${form},{outcome:'unresolved',reason:${reason}}).outcome}}`, reason: `{{formIncompleteOutcome(cargo,${form},{outcome:'unresolved',reason:${reason}}).reason}}` });
31
- const incompleteDispatch = (id, form, reason) => ({ id, type: 'DISPATCH', ...incompleteAttrs(form, reason) });
31
+ const recoveryAttrs = (form, reason, failure = false, mode = 'resume') => { const call = `formConversationOutcome(cargo,${form},${reason},${failure},${JSON.stringify(mode)})`; return { outcome: `{{${call}.outcome}}`, reason: `{{${call}.reason}}`, recovery: { version: 1, mode: `{{${call}.recovery.mode}}`, reason: `{{${call}.recovery.reason}}`, form: `{{${call}.recovery.form || ''}}`, flow: `{{${call}.recovery.flow || ''}}` } }; };
32
+ const incompleteDispatch = (id, form, reason) => ({ id, type: 'DISPATCH', ...recoveryAttrs(form, reason, true, 'none') });
33
+ const yieldDispatch = (id, form, reason) => ({ id, type: 'DISPATCH', ...recoveryAttrs(form, reason) });
34
+ const recoveryStamp = (id, form, reason, mode = 'resume') => ({ ...SET(id, 'noop', 'true'), recovery: recoveryAttrs(form, reason, true, mode).recovery });
32
35
  const optionalIncomplete = (id, condition, form, reason) => WHEN(id, `(${condition}) && !!formIncompleteOutcome(cargo,${form},{outcome:'unresolved',reason:${reason}}).owner`, incompleteDispatch(id + '_dispatch', form, reason));
33
36
  /** A SET value that yields this text: with {{ }} it is a template (as is); without, a JS string literal. */
34
37
  export const textValue = (text) => (String(text).includes('{{') ? String(text) : JSON.stringify(String(text)));
@@ -115,7 +118,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
115
118
  SET(`${id}_open`, 'form_instance', `openFormInstance(cargo, ${JSON.stringify(id)}, inputs, { parent: parent_form || null, input: result_input || '', context:!!parent_form && !result_input, resume: form_instance || (!parent_form ? (cargo.formTasks || {})[${JSON.stringify(id)}] || '' : ''), providers: provides, questions: questions, incomplete_outcome: ${incompleteExpression}, on_exhausted: form_on_exhausted })`),
116
119
  SIDE(`${id}_task`, `(!parent_form ? (cargo.formTasks = cargo.formTasks || {}, cargo.formTasks[${JSON.stringify(id)}] = form_instance) : true, true)`),
117
120
  SIDE(`${id}_provider_reset`, '(delete cargo.forms[form_instance].providerRequests, delete cargo.forms[form_instance].activeGates, delete cargo.forms[form_instance].providerGateActive, true)'),
118
- SIDE(`${id}_pass_reset`, '(cargo.forms[form_instance].replays = 0, form_internal_resume ? true : (cargo.forms[form_instance].attempts = {}), true)'),
121
+ SIDE(`${id}_pass_reset`, '(cargo.forms[form_instance].replays = 0, form_internal_resume ? true : (cargo.forms[form_instance].attempts = {},cargo.forms[form_instance].misses={},cargo.forms[form_instance].feedback={}), true)'),
119
122
  SET(`${id}_arguments`, 'form_arguments', `rememberFormArguments(cargo.forms[form_instance], currentContext, ${JSON.stringify(parameters.filter(p => !p.internal && !/^internal\b/i.test(p.description || '')).map(p => p.name))}, form_internal_resume)`),
120
123
  ...parameters.filter(p => !p.internal && !/^internal\b/i.test(p.description || '')).map(p => SET(`${id}_restore_${p.name}`, p.name, `form_internal_resume ? form_arguments[${JSON.stringify(p.name)}] : currentContext[${JSON.stringify(p.name)}]`)),
121
124
  ...before,
@@ -137,6 +140,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
137
140
  WHEN(`${id}_resolve_validation_exhausted`, "form_validation === 'failed'", SET(`${id}_validation_exhaustion`, 'form_exhaustion', "exhaustFormInput(cargo,form_instance,((cargo.formCheck || {}).invalid || [])[0] ? cargo.formCheck.invalid[0].name : '')")),
138
141
  WHEN(`${id}_recover_validation_exhausted`, 'form_exhaustion', FLOW(`${id}_validation_recovery`, '{{form_exhaustion.flow}}', 'reboot', { form: '{{form_exhaustion.form}}', failed_form: '{{form_exhaustion.failed_form}}', input: '{{form_exhaustion.input}}', failed_input: '{{form_exhaustion.failed_input}}', reason: '{{form_exhaustion.reason}}' })),
139
142
  optionalIncomplete(`${id}_optional_validation_failed`, "form_validation === 'failed'", 'form_instance', "form_instance+'_incomplete'"),
143
+ WHEN(`${id}_validation_failed_recovery`, "form_validation === 'failed'", recoveryStamp(`${id}_validation_stamp`, 'form_instance', "'validation_exhausted'", 'none')),
140
144
  WHEN(`${id}_validation_failed`, "form_validation === 'failed'", FLOW(`${id}_validation_give_up`, 'give-up', 'reboot', { context: '{{exit_context}}', context_es: '{{exit_context_es}}' })),
141
145
  WHEN(`${id}_validation_message`, "form_validation === 'retry' && cargo.formCheck.message", SAY(`${id}_validation_say`, '{{cargo.formCheck.message}}', '{{cargo.formCheck.message_es}}')),
142
146
  WHEN(`${id}_validation_retry`, "form_validation === 'retry'", FLOW(`${id}_validate_again`, id, 'replace', { parent_form: '{{parent_form}}', result_input: '{{result_input}}', form_instance: '{{form_instance}}' })),
@@ -198,26 +202,31 @@ const collectInput = {
198
202
  id: 'collect-input', name: 'CollectInput', version: '1.0.0', scope: 'isolated',
199
203
  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).",
200
204
  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 })),
201
- variables: vars({ 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_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: '' }),
205
+ variables: vars({ 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_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: '' }),
202
206
  steps: [
203
207
  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; })()"),
204
208
  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]"),
205
- WHEN('ci_no_spec', '!ci_spec', END('ci_no_spec_end', incompleteAttrs('form', "'collect_input_spec'"))),
209
+ WHEN('ci_no_spec', '!ci_spec', END('ci_no_spec_end', recoveryAttrs('form', "'collect_input_spec'", true, 'none'))),
206
210
  SET('ci_count_missed', 'ci_missed', "Number(((cargo.forms[form] || {}).attempts || {})[current] || 0)"),
207
211
  SET('ci_max_attempts', 'ci_max', 'Number(ci_spec.max_attempts || 3)'),
212
+ SET('ci_context_misses', 'ci_misses', 'Number(((cargo.forms[form] || {}).misses || {})[current] || 0)'),
213
+ SET('ci_context_limit', 'ci_miss_limit', 'Number(ci_spec.max_misses || 2)'),
214
+ WHEN('ci_context_yield', 'ci_misses >= ci_miss_limit', yieldDispatch('ci_misses_dispatch', 'form', "'input_misses_exhausted'")),
208
215
  SET('ci_context', 'ci_ctx', "typeof exit_context !== 'undefined' && exit_context ? exit_context : 'your request'"),
209
216
  SET('ci_context_es', 'ci_ctx_es', "typeof exit_context_es !== 'undefined' && exit_context_es ? exit_context_es : 'su solicitud'"),
210
217
  WHEN('ci_resolve_exhausted', 'ci_missed >= ci_max', SET('ci_exhausted_policy', 'ci_exhaustion', 'exhaustFormInput(cargo,form,current)')),
211
218
  WHEN('ci_declared_exhausted', 'ci_exhaustion', FLOW('ci_exhausted_recovery', '{{ci_exhaustion.flow}}', 'reboot', { form: '{{ci_exhaustion.form}}', failed_form: '{{ci_exhaustion.failed_form}}', input: '{{ci_exhaustion.input}}', failed_input: '{{ci_exhaustion.failed_input}}', reason: '{{ci_exhaustion.reason}}' })),
212
219
  optionalIncomplete('ci_optional_exhausted', 'ci_missed >= ci_max', 'form', "form+'_incomplete'"),
220
+ WHEN('ci_failure_recovery', 'ci_missed >= ci_max', recoveryStamp('ci_failure_stamp', 'form', "'validation_exhausted'")),
213
221
  WHEN('ci_give_up', 'ci_missed >= ci_max', FLOW('ci_give_up_flow', 'give-up', 'reboot', { context: '{{ci_ctx}}', context_es: '{{ci_ctx_es}}' })),
214
222
  FLOW('ci_prepare', 'prepare-input', 'call', { form: '{{form}}', input: '{{current}}' }),
215
223
  // `refresh`: a flow run before EVERY ask, which computes what the question shows into cargo.forms[form].data
216
224
  WHEN('ci_refresh', 'ci_spec.refresh', FLOW('ci_refresh_flow', '{{ci_spec.refresh}}', 'call', { form: '{{form}}', input: '{{current}}' })),
217
225
  SET('ci_refetch_spec', 'ci_spec', "(function(){var spec=cargo.forms[form].specs.filter(function(s){return s.name===current;})[0];return spec&&spec.any_of?Object.assign({},spec,{profile:{kind:'group'}}):spec;})()"),
218
226
  SET('ci_data', 'ci_d', '(cargo.forms[form] || {}).data || {}'),
219
- SET('ci_question', 'ci_q', "ci_spec.question_from && ci_d[ci_spec.question_from] ? ci_d[ci_spec.question_from] : (ci_missed > 0 && ci_spec.retry ? ci_spec.retry : ci_spec.question)"),
220
- SET('ci_question_es', 'ci_q_es', "ci_spec.question_from && (ci_d[ci_spec.question_from + '_es'] || ci_d[ci_spec.question_from]) ? (ci_d[ci_spec.question_from + '_es'] || ci_d[ci_spec.question_from]) : (ci_missed > 0 && (ci_spec.retry_es || ci_spec.retry) ? (ci_spec.retry_es || ci_spec.retry) : (ci_spec.question_es || ci_spec.question))"),
227
+ SET('ci_question', 'ci_q', "formInputPrompt(cargo,form,current,ci_spec,ci_d,'en')"),
228
+ SET('ci_question_es', 'ci_q_es', "formInputPrompt(cargo,form,current,ci_spec,ci_d,'es')"),
229
+ SIDE('ci_feedback_consumed', 'consumeFormFeedback(cargo,form,current)'),
221
230
  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 : [])"),
222
231
  SET('ci_keypad', 'ci_digits', 'ci_spec.digits || null'),
223
232
  SET('ci_is_amount', 'ci_amount', "ci_spec.amount === 'marked' ? 'marked' : (ci_spec.amount === true || ci_spec.profile.kind === 'amount')"),
@@ -236,6 +245,7 @@ const collectInput = {
236
245
  // The labels and validators ride on each reading, so the presets stay in this one flow.
237
246
  SET('ci_enrich', 'ci_m', 'enrichFormMatches(ci_m, ci_chain, form, ci_names, current)'),
238
247
  SET('ci_question_key', 'ci_qkey', "ci_m.questions.length ? ci_m.questions[0] : !ci_m.hits.length && !ci_m.ambiguous.length && Array.isArray(questions) && questions.length && provides ? pickChoice(cargo.choiceText, questions, false) : ''"),
248
+ WHEN('ci_understood', 'ci_m.hits.length || ci_m.ambiguous.length || ci_m.questions.length || ci_qkey && ci_qkey.indexOf("__") !== 0', SIDE('ci_clear_misses', 'clearFormMisses(cargo,form,current)')),
239
249
  SET('ci_count_words', 'ci_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function (w) { return /[a-záéíóúüñ]/i.test(w); }).length"),
240
250
  // Independent field clauses are committed before a provider can suspend on its gate.
241
251
  WHEN('ci_question_ambiguity', 'ci_m.questions.length > 0 && ci_m.ambiguous.length > 0', FLOW('ci_question_clarify', 'disambiguate-input', 'call', { readings: '{{ci_m.ambiguous}}', hits: '{{ci_m.hits}}', remaining: '{{ci_m.remaining}}' })),
@@ -248,8 +258,8 @@ const collectInput = {
248
258
  'condition: ci_m.hits.length > 0': FLOW('ci_accept', 'collect-input-accept', 'call', { hits: '{{ci_m.hits}}' }),
249
259
  "condition: ci_m.questions.length > 0": END('ci_question_handled'),
250
260
  "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}}' }),
251
- 'condition: ci_words >= 3': incompleteDispatch('ci_elsewhere', 'form', "form+'_incomplete'"),
252
- default: SIDE('ci_miss', "(cargo.forms[form].attempts = cargo.forms[form].attempts || {}, cargo.forms[form].attempts[current] = ci_missed + 1, true)")
261
+ 'condition: ci_words >= 3': yieldDispatch('ci_elsewhere', 'form', "'input_not_matched'"),
262
+ default: SIDE('ci_miss', "(missFormInput(cargo,form,current),true)")
253
263
  } },
254
264
  ],
255
265
  };
@@ -317,7 +327,7 @@ const acceptedEffects = {
317
327
  SET('ie_first', 'ca_h', 'readings.length ? readings[0] : null'),
318
328
  WHEN('ie_none', '!ca_h', END('ie_end')),
319
329
  SET('ie_rest', 'ca_rest', 'readings.slice(1)'),
320
- 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, true)")),
330
+ 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)")),
321
331
  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 + '.') : ''"),
322
332
  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 + '.') : ''"),
323
333
  WHEN('ie_say', 'ca_ack', SAY('ie_ack', '{{ca_ack}}', '{{ca_ack_es}}')),
@@ -330,7 +340,7 @@ const disambiguate = {
330
340
  id: 'disambiguate-input', name: 'DisambiguateInput', version: '1.0.0', scope: 'isolated',
331
341
  description: "Worker: the answer fits two or more inputs at the same scope (or a declared confirm_against pair). Stores the unambiguous readings, asks which input the rest was for (keys 1..n, never 8), stores the pick. No pick: nothing stored, not a miss. Params: readings, hits.",
332
342
  parameters: [{ name: 'readings', type: 'array', description: 'The ambiguous readings' }, { name: 'hits', type: 'array', description: 'Readings to store as they are' }, { name: 'remaining', type: 'array' }],
333
- variables: vars({ di_keys: [], di_choices: [], di_q: '', di_q_es: '', di_pick: null, di_value: '', remaining: [], di_next: [], di_rest: [] }),
343
+ variables: vars({ di_form: '', di_input: '', di_prompt: '', di_prompt_es: '', di_words: 0, di_misses: 0, di_keys: [], di_choices: [], di_q: '', di_q_es: '', di_pick: null, di_value: '', remaining: [], di_next: [], di_rest: [] }),
334
344
  steps: [
335
345
  WHEN('di_store_clear', 'Array.isArray(hits) && hits.length > 0', FLOW('di_store_hits', 'collect-input-accept', 'call', { hits: '{{hits}}' })),
336
346
  SET('di_number', 'di_keys', `${KEYS_JS}(readings.length)`),
@@ -338,8 +348,19 @@ const disambiguate = {
338
348
  SET('di_said', 'di_value', "readings[0].pii ? '' : String(readings[0].value)"),
339
349
  SET('di_question', 'di_q', "(di_value ? 'Is ' + di_value + ' your ' : 'Is that your ') + readings.map(function (r) { return r.label; }).join(' or your ') + '? ' + readings.map(function (r, i) { return (cargo.voice ? 'Press ' + di_keys[i] + ' or say ' : 'Say ') + r.label.toUpperCase(); }).join(', ') + '.'"),
340
350
  SET('di_question_es', 'di_q_es', "(di_value ? '¿' + di_value + ' es su ' : '¿Eso es su ') + readings.map(function (r) { return r.label_es; }).join(' o su ') + '? ' + readings.map(function (r, i) { return (cargo.voice ? 'Presione ' + di_keys[i] + ' o diga ' : 'Diga ') + r.label_es.toUpperCase(); }).join(', ') + '.'"),
341
- FLOW('di_ask', 'ask-choice', 'call', { question: '{{di_q}}', question_es: '{{di_q_es}}', choices: '{{di_choices}}', accept_other: true, digits: { min: 1, max: 1 } }),
351
+ SET('di_owner', 'di_form', 'readings[0].form'),
352
+ SET('di_counter', 'di_input', "'__ambiguity:'+readings.map(function(r){return r.form+':'+r.name;}).sort().join('|')"),
353
+ SET('di_prompt', 'di_prompt', "formInputPrompt(cargo,di_form,di_input,{question:di_q,question_es:di_q_es},{},'en')"),
354
+ SET('di_prompt_es', 'di_prompt_es', "formInputPrompt(cargo,di_form,di_input,{question:di_q,question_es:di_q_es},{},'es')"),
355
+ SIDE('di_feedback_consumed', 'consumeFormFeedback(cargo,di_form,di_input)'),
356
+ FLOW('di_ask', 'ask-choice', 'call', { question: '{{di_prompt}}', question_es: '{{di_prompt_es}}', choices: '{{di_choices}}', accept_other: true, digits: { min: 1, max: 1 } }),
342
357
  SET('di_picked', 'di_pick', "/^r\\d+$/.test(String(cargo.choice || '')) ? [readings[Number(String(cargo.choice).slice(1))]] : null"),
358
+ WHEN('di_understood', 'di_pick', SIDE('di_clear_misses', 'clearFormMisses(cargo,di_form,di_input)')),
359
+ SET('di_word_count', 'di_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function(w){return /[a-záéíóúüñ]/i.test(w);}).length"),
360
+ WHEN('di_context', '!di_pick && di_words >= 3', yieldDispatch('di_context_yield', 'di_form', "'ambiguity_not_resolved'")),
361
+ WHEN('di_miss', '!di_pick', SET('di_miss_count', 'di_misses', 'missFormInput(cargo,di_form,di_input)')),
362
+ WHEN('di_exhausted', '!di_pick && di_misses >= 2', yieldDispatch('di_misses_yield', 'di_form', "'ambiguity_misses_exhausted'")),
363
+ WHEN('di_retry', '!di_pick', FLOW('di_retry_question', 'disambiguate-input', 'replace', { readings: '{{readings}}', hits: [], remaining: '{{remaining}}' })),
343
364
  WHEN('di_store_pick', 'di_pick', FLOW('di_store_pick_flow', 'collect-input-accept', 'call', { hits: '{{di_pick}}' })),
344
365
  SET('di_remaining_first', 'di_next', 'Array.isArray(remaining) && remaining.length ? remaining[0] : []'),
345
366
  SET('di_remaining_rest', 'di_rest', 'Array.isArray(remaining) ? remaining.slice(1) : []'),
@@ -351,13 +372,16 @@ const confirmForm = {
351
372
  id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
352
373
  description: "Worker: ask a form's confirmation (exact YES / NO; keys 1 and 2). A correction instead ('no, make it $40') is read against the form chain and stored, and the result is 'changed' so the form runs again; an unclear short answer is asked again, a long unrelated one is DISPATCHed. Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
353
374
  parameters: ['form', 'question', 'question_es', 'exit_context', 'exit_context_es', 'words', 'interrupts', 'no_keys', 'clarifications'].map((n) => ({ name: n, type: ['words', 'interrupts', 'no_keys', 'clarifications'].includes(n) ? 'array' : 'string', description: n })),
354
- variables: vars({ cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_result: '', cf_words: 0, cf_tries: 0, words: [], clarifications: [], no_keys: ['2'], interrupts: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
375
+ variables: vars({ cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, words: [], clarifications: [], no_keys: ['2'], interrupts: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
355
376
  steps: [
356
377
  SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', true)"),
357
378
  SET('cf_choices', 'cf_choices', "[{key:'yes',words:['1'].concat(Array.isArray(words)?words:[]),exact:true},{key:'no',words:Array.isArray(no_keys)?no_keys:['2']}].concat((Array.isArray(interrupts)?interrupts:[]).map(function(event){return {key:'event:'+event.key,words:event.words,exact:event.exact!==false};}))"),
358
379
  SET('cf_initial_chain', 'cf_chain', CHAIN_JS),
359
380
  SET('cf_amount_context', 'cf_amount', "cf_chain.some(function(input){return input.profile && input.profile.kind==='amount';})"),
360
- FLOW('cf_ask', 'ask-choice', 'call', { amount: '{{cf_amount}}', question: '{{question}}', question_es: '{{question_es}}', choices: '{{cf_choices}}', accept_other: true, reask_long: true, digits: { min: 1, max: 1 }, exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
381
+ SET('cf_prompt', 'cf_prompt', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'en')"),
382
+ SET('cf_prompt_es', 'cf_prompt_es', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'es')"),
383
+ SIDE('cf_feedback_consumed', "consumeFormFeedback(cargo,form,'__confirm')"),
384
+ FLOW('cf_ask', 'ask-choice', 'call', { amount: '{{cf_amount}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}', choices: '{{cf_choices}}', accept_other: true, reask_long: true, digits: { min: 1, max: 1 }, exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
361
385
  SET('cf_event', 'cf_event', "(Array.isArray(interrupts)?interrupts:[]).filter(function(event){return cargo.choice==='event:'+event.key;}).map(function(event){return event.flow;})[0] || ''"),
362
386
  WHEN('cf_interrupt', 'cf_event', FLOW('cf_event_flow', '{{cf_event}}', 'reboot', { form: '{{form}}', event: '{{cargo.choice}}', text: '{{cargo.choiceText}}' })),
363
387
  SET('cf_build_chain', 'cf_chain', CHAIN_JS),
@@ -376,17 +400,16 @@ const confirmForm = {
376
400
  'condition: cf_m.ambiguous.length > 0': FLOW('cf_ambiguous', 'disambiguate-input', 'call', { readings: '{{cf_m.ambiguous}}', hits: '{{cf_m.hits}}', remaining: '{{cf_m.remaining}}' }),
377
401
  'condition: cf_m.hits.length > 0': FLOW('cf_accept', 'collect-input-accept', 'call', { hits: '{{cf_m.hits}}' }),
378
402
  "condition: cargo.choice === 'no'": SET('cf_declined', 'cf_result', "'no'"),
379
- "condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',words:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'unclear'"),
380
- 'condition: cf_words >= 3': incompleteDispatch('cf_elsewhere', 'form', "form+'_unconfirmed'"),
403
+ "condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',words:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'clarification'"),
404
+ 'condition: cf_words >= 3': yieldDispatch('cf_elsewhere', 'form', "'confirmation_not_matched'"),
381
405
  default: SET('cf_unclear', 'cf_result', "'unclear'")
382
406
  } },
383
407
  SET('cf_changed', 'cf_result', "cf_result ? cf_result : 'changed'"),
384
408
  // an unclear short answer is asked again (the form runs again and reaches this question), at most twice
385
- SET('cf_tries_count', 'cf_tries', "cf_result === 'unclear' ? (cargo.forms[form] ? ((cargo.forms[form].attempts = cargo.forms[form].attempts || {}).__confirm = Number(cargo.forms[form].attempts.__confirm || 0) + 1) : 9) : 0"),
386
- optionalIncomplete('cf_optional_exhausted', 'cf_tries > 2', 'form', "form+'_unconfirmed'"),
387
- WHEN('cf_give_up', 'cf_tries > 2', FLOW('cf_give_up_flow', 'give-up', 'reboot', { context: '{{exit_context}}', context_es: '{{exit_context_es}}' })),
409
+ SET('cf_tries_count', 'cf_tries', "cf_result === 'unclear' ? missFormInput(cargo,form,'__confirm') : (clearFormMisses(cargo,form,'__confirm'),0)"),
410
+ WHEN('cf_misses_exhausted', 'cf_tries >= 2', yieldDispatch('cf_misses_dispatch', 'form', "'confirmation_misses_exhausted'")),
388
411
  WHEN('cf_bind_revision', "cf_result === 'yes'", SIDE('cf_bind', 'confirmFormRevision(cargo, form)')),
389
- SIDE('cf_store', "(cargo.formConfirm[form] = cf_result === 'unclear' ? 'changed' : cf_result, true)"),
412
+ SIDE('cf_store', "(cargo.formConfirm[form] = cf_result === 'unclear' || cf_result === 'clarification' ? 'changed' : cf_result, true)"),
390
413
  ],
391
414
  };
392
415
  // ── form-done: the result goes to the parent (or cargo.formResults); the form is cleared ─────────────────────────────
@@ -497,7 +520,7 @@ const provideData = {
497
520
  parameters: [{ name: 'form', type: 'string', description: 'Originating FORM instance for task-owned deferred questions' }, { name: 'provides', type: 'object', description: 'The provider catalog' }, { name: 'asked', type: 'string', description: 'Fact or set key' }, { name: 'asked_keys', type: 'array', description: 'Independent requested facts or sets' }, { name: 'no_menu', type: 'string', description: "'true': answer only (a question asked inside a form)" }, { name: 'gate_params', type: 'object', description: 'Identifiers the caller already gave, passed to the gate flow as gate_params' }, { name: 'no_gate', type: 'string', description: "'true': the caller is inside a task on the verified account; the gate flow is not run (the verified paths are still checked)" }],
498
521
  variables: vars({ form: '', pd_request: null, pd_deferred: null, pd_ready: [], asked_keys: [], no_gate: '', pd_close: false, pd_who: '', no_menu: '', gate_params: null, pd_keys: [], pd_menu: [], pd_mkeys: [], pd_q: '', pd_q_es: '', pd_gated: false, pd_ok: false }),
499
522
  steps: [
500
- WHEN('pd_no_catalog', "!provides || typeof provides !== 'object' || !Array.isArray(provides.facts)", END('pd_no_catalog_end', { outcome: 'unresolved', reason: 'provide_data_catalog' })),
523
+ WHEN('pd_no_catalog', "!provides || typeof provides !== 'object' || !Array.isArray(provides.facts)", END('pd_no_catalog_end', { outcome: 'unresolved', reason: 'provide_data_catalog', recovery: { version: 1, mode: 'none', reason: 'provide_data_catalog' } })),
501
524
  SET('pd_resolve', 'pd_keys', "(function () { var requested=Array.isArray(asked_keys)&&asked_keys.length?asked_keys:[typeof asked!=='undefined'?String(asked||''):''];return requested.reduce(function(keys,a){var set=(provides.sets||[]).filter(function(x){return x.key===a;})[0];var facts=set?set.facts:provides.facts.some(function(f){return f.key===a;})?[a]:[];return keys.concat(facts.filter(function(key){return keys.indexOf(key)<0;}));},[]); })()"),
502
525
  // Nothing specific asked: offer the sets and facts as a menu (keys 1..n, never 8; at most 7 options shown).
503
526
  SET('pd_menu_items', 'pd_menu', "(provides.sets || []).concat(provides.facts).slice(0, 7)"),
@@ -515,7 +538,7 @@ const provideData = {
515
538
  SET('pd_deferred', 'pd_deferred', `(${finishProviderRequest.toString()})(cargo,pd_request.owner,provides,pd_keys)`),
516
539
  SET('pd_merged_keys', 'pd_keys', 'pd_deferred.keys'),
517
540
  SET('pd_gate_check', 'pd_ok', `!pd_gated || [].concat(provides.gate.verified).every(function (p) { return !!${PATH_JS}(p); })`),
518
- WHEN('pd_not_verified', '!pd_ok', END('pd_not_verified_end', { outcome: 'unresolved', reason: 'provide_data_not_verified' })),
541
+ WHEN('pd_not_verified', '!pd_ok', END('pd_not_verified_end', { outcome: 'unresolved', reason: 'provide_data_not_verified', recovery: { version: 1, mode: 'none', reason: 'provide_data_not_verified' } })),
519
542
  WHEN('pd_load', 'provides.source && provides.source.flow', FLOW('pd_load_flow', 'load-source', 'call', { source: '{{provides.source}}' })),
520
543
  FLOW('pd_answer', 'answer-facts', 'call', { provides: '{{provides}}', keys: '{{pd_keys}}' }),
521
544
  WHEN('pd_deferred_more', 'pd_deferred.remaining.length > 0', FLOW('pd_answer_deferred', 'provide-queued-data', 'call', { form: '{{form}}', requests: '{{pd_deferred.remaining}}' })),
@@ -584,7 +607,7 @@ const loadSource = {
584
607
  WHEN('ls_begin', 'ls_stale', SIDE('ls_clear_old', 'beginSourceRefresh(cargo, source)')),
585
608
  WHEN('ls_load', 'ls_stale', FLOW('ls_loader', '{{source.flow}}', 'call')),
586
609
  SET('ls_loaded', 'ls_have', 'ls_stale ? completeSourceRefresh(cargo, source) : true'),
587
- WHEN('ls_failed', '!ls_have', FLOW('ls_give_up', 'give-up', 'reboot', { context: 'your request', context_es: 'su solicitud' })),
610
+ WHEN('ls_failed', '!ls_have', { ...FLOW('ls_give_up', 'give-up', 'reboot', { context: 'your request', context_es: 'su solicitud' }), recovery: { version: 1, mode: 'none', reason: 'source_unavailable' } }),
588
611
  ],
589
612
  };
590
613
  const answerFacts = {
@@ -0,0 +1,14 @@
1
+ /** beginFormAction has already checked completeness, exact confirmation, gate, revision and ownership. */
2
+ export function claimSessionAction(cargo: any, record: any, action: any): {
3
+ status: string;
4
+ outcome: any;
5
+ } | {
6
+ status: string;
7
+ outcome?: undefined;
8
+ };
9
+ /** Cache the first observed outcome before returning it. No receipt or durable recovery checkpoint is created. */
10
+ export function completeSessionAction(cargo: any, record: any, action: any, outcome: any): {
11
+ status: string;
12
+ outcome: any;
13
+ };
14
+ //# sourceMappingURL=session-actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-actions.d.ts","sourceRoot":"","sources":["../../src/forms/session-actions.js"],"names":[],"mappings":"AAcA,0GAA0G;AAC1G;;;;;;EAOC;AAED,kHAAkH;AAClH;;;EAUC"}
@@ -0,0 +1,40 @@
1
+ /** Non-durable transport policy. All state belongs to the conversation, never the engine instance. */
2
+ import { formSignature } from '../helpers/form-actions.js';
3
+ const hold = reason => ({ status: 'hold', outcome: { status: 'unknown', reason } });
4
+ function ownedAction(cargo, record, action) {
5
+ if (!record?.key || record.action !== action?.action)
6
+ throw new Error('Session action identity mismatch');
7
+ const owner = Object.values(cargo?.forms || {}).find(form => form.actions?.[action.action]?.key === record.key);
8
+ const owned = owner?.actions[action.action];
9
+ if (!owned || owned.signature !== record.signature || owned.scope !== record.scope ||
10
+ formSignature(owned.request) !== formSignature(record.request))
11
+ throw new Error('Session action ownership mismatch');
12
+ if (owned.durableKey)
13
+ throw new Error('A durable action cannot be migrated to session-only execution');
14
+ return owned;
15
+ }
16
+ /** beginFormAction has already checked completeness, exact confirmation, gate, revision and ownership. */
17
+ export function claimSessionAction(cargo, record, action) {
18
+ const owned = ownedAction(cargo, record, action);
19
+ if (owned.sessionOutcome)
20
+ return { status: 'replay', outcome: structuredClone(owned.sessionOutcome) };
21
+ if (owned.status !== 'attempted' || owned.sessionClaimed)
22
+ return hold('action_result_unresolved');
23
+ owned.sessionClaimed = true;
24
+ cargo.actionJournal[owned.key] = owned;
25
+ return { status: 'execute' };
26
+ }
27
+ /** Cache the first observed outcome before returning it. No receipt or durable recovery checkpoint is created. */
28
+ export function completeSessionAction(cargo, record, action, outcome) {
29
+ const owned = ownedAction(cargo, record, action);
30
+ if (!owned.sessionClaimed)
31
+ throw new Error('Session action completion without claim');
32
+ if (!owned.sessionOutcome) {
33
+ if (!['attempted', 'unknown'].includes(owned.status))
34
+ throw new Error('Session action is not pending');
35
+ owned.sessionOutcome = outcome && Object.hasOwn(action.outcomes || {}, outcome.status)
36
+ ? structuredClone(outcome) : { status: 'unknown', reason: 'invalid_action_result' };
37
+ cargo.actionJournal[owned.key] = owned;
38
+ }
39
+ return { status: owned.sessionOutcome.status === 'unknown' ? 'unknown' : 'completed', outcome: structuredClone(owned.sessionOutcome) };
40
+ }
@@ -0,0 +1,28 @@
1
+ export function clearFormMisses(cargo: any, id: any, input: any): boolean;
2
+ export function missFormInput(cargo: any, id: any, input: any): any;
3
+ /** Explicit retry wins. Otherwise acknowledge only an actual unrecognized answer, once per re-ask. */
4
+ export function formInputPrompt(cargo: any, id: any, input: any, spec: any, data?: {}, language?: string): any;
5
+ export function consumeFormFeedback(cargo: any, id: any, input: any): boolean;
6
+ /** Recovery refers to the actual owning root, and never permits a fresh attempt at an unknown action. */
7
+ export function formConversationOutcome(cargo: any, id: any, reason: any, failure?: boolean, recoveryMode?: string): {
8
+ outcome: any;
9
+ reason: any;
10
+ recovery: {
11
+ version: number;
12
+ mode: string;
13
+ reason: any;
14
+ form?: undefined;
15
+ flow?: undefined;
16
+ };
17
+ } | {
18
+ outcome: any;
19
+ reason: any;
20
+ recovery: {
21
+ version: number;
22
+ mode: string;
23
+ reason: any;
24
+ form: any;
25
+ flow: any;
26
+ };
27
+ };
28
+ //# sourceMappingURL=form-conversation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-conversation.d.ts","sourceRoot":"","sources":["../../src/helpers/form-conversation.js"],"names":[],"mappings":"AAGA,0EAGC;AACD,oEAGC;AACD,sGAAsG;AACtG,+GAMC;AACD,8EAAwI;AAExI,yGAAyG;AACzG;;;;;;;;;;;;;;;;;;;;EAUC"}
@@ -0,0 +1,57 @@
1
+ /** Shared FORM retry, contextual yield and recovery policy; all state is conversation-owned. */
2
+ import { formIncompleteOutcome } from './form-state.js';
3
+ export function clearFormMisses(cargo, id, input) {
4
+ const state = cargo.forms?.[id];
5
+ if (!state)
6
+ return true;
7
+ state.misses ||= {};
8
+ state.feedback ||= {};
9
+ delete state.misses[input];
10
+ delete state.feedback[input];
11
+ return true;
12
+ }
13
+ export function missFormInput(cargo, id, input) {
14
+ const state = cargo.forms?.[id];
15
+ if (!state)
16
+ throw new Error('Cannot record a miss without its owned form');
17
+ state.misses ||= {};
18
+ state.feedback ||= {};
19
+ state.misses[input] = Number(state.misses[input] || 0) + 1;
20
+ state.feedback[input] = 'miss';
21
+ return state.misses[input];
22
+ }
23
+ /** Explicit retry wins. Otherwise acknowledge only an actual unrecognized answer, once per re-ask. */
24
+ export function formInputPrompt(cargo, id, input, spec, data = {}, language = 'en') {
25
+ const es = language === 'es', feedback = cargo.forms?.[id]?.feedback?.[input];
26
+ const question = spec.question_from && (es ? (data[spec.question_from + '_es'] || data[spec.question_from]) : data[spec.question_from]) || (es ? (spec.question_es || spec.question) : spec.question) || '';
27
+ const retry = es ? spec.retry_es : spec.retry;
28
+ if (feedback && retry)
29
+ return retry;
30
+ return (feedback === 'miss' ? (es ? 'Disculpe, no le entendí. ' : "Sorry, I didn't catch that. ") : '') + question;
31
+ }
32
+ export function consumeFormFeedback(cargo, id, input) { if (cargo.forms?.[id]?.feedback)
33
+ delete cargo.forms[id].feedback[input]; return true; }
34
+ /** Recovery refers to the actual owning root, and never permits a fresh attempt at an unknown action. */
35
+ export function formConversationOutcome(cargo, id, reason, failure = false, recoveryMode = 'resume') {
36
+ const completed = formIncompleteOutcome(cargo, id, { outcome: 'unresolved', reason });
37
+ if (completed.owner && ['resolved', 'deflected'].includes(completed.outcome))
38
+ return { outcome: completed.outcome, reason: completed.reason, recovery: { version: 1, mode: 'none', reason: completed.reason } };
39
+ const seen = new Set();
40
+ let state = cargo.forms?.[id], root = state;
41
+ while (state && !seen.has(state.id)) {
42
+ seen.add(state.id);
43
+ root = state;
44
+ state = cargo.forms?.[state.parent];
45
+ }
46
+ if (!root)
47
+ return { outcome: 'unresolved', reason, recovery: { version: 1, mode: 'none', reason } };
48
+ const belongs = form => { const visited = new Set(); while (form && !visited.has(form.id)) {
49
+ if (form.id === root.id)
50
+ return true;
51
+ visited.add(form.id);
52
+ form = cargo.forms?.[form.parent];
53
+ } return false; };
54
+ const unknown = Object.values(cargo.forms || {}).filter(belongs).some(form => Object.values(form.actions || {}).some(record => ['attempted', 'unknown'].includes(record.status)));
55
+ const mode = unknown ? 'hold' : recoveryMode, why = unknown ? 'action_result_unknown' : completed.owner ? completed.reason : reason;
56
+ return { outcome: unknown ? 'unresolved' : completed.owner ? completed.outcome : failure ? 'unresolved' : 'pending', reason: why, recovery: { version: 1, mode, reason: why, form: root.id, flow: root.definition } };
57
+ }
@@ -81,7 +81,7 @@ export function formPreparation(cargo: any, id: any, input: any): {
81
81
  export function queueFormReadings(cargo: any, hits?: any[]): boolean;
82
82
  export function pendingFormReadings(cargo: any, id: any): any[];
83
83
  export function formWorkLimit(cargo: any, id: any): number;
84
- export function applyFormValidation(cargo: any, id: any, result: any): "failed" | "retry" | "valid";
84
+ export function applyFormValidation(cargo: any, id: any, result: any): "failed" | "valid" | "retry";
85
85
  /** Invalidation is transitive and independent of declaration order. */
86
86
  export function invalidateFormInputs(cargo: any, id: any, changed: any, preserve?: any[], preserveNodes?: any[]): any[];
87
87
  /** An intentional retry resets the named inputs as well as all of their dependent state. Unknown actions stay held. */
@@ -1 +1 @@
1
- {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAMA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEAsBC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEAuCC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;QA8BC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AACD,qFASC;AACD,oGAoCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGA4CC;AACD,8DAQC;AACD,8EAeC;AAljBM,8CAAmE"}
1
+ {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAMA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEAuBC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEAuCC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;QA8BC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AACD,qFASC;AACD,oGAoCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGA4CC;AACD,8DAQC;AACD,8EAeC;AAnjBM,8CAAmE"}
@@ -35,6 +35,8 @@ export function resolveFormInputs(inputs, presets = {}) {
35
35
  validateMoneyOptions(input.profile.money);
36
36
  }
37
37
  validateFormExhaustion(input.on_exhausted);
38
+ if (input.max_misses !== undefined && (!Number.isInteger(input.max_misses) || input.max_misses < 1))
39
+ throw new Error('FORM max_misses must be a positive integer');
38
40
  if (input.max_attempts !== undefined && (!Number.isInteger(input.max_attempts) || input.max_attempts < 1))
39
41
  throw new Error('FORM max_attempts must be a positive integer');
40
42
  if (input.any_of)
@@ -300,7 +302,7 @@ export function openFormInstance(cargo, definition, specs, { parent = null, inpu
300
302
  cargo.formSequence = Number(cargo.formSequence || 0) + 1;
301
303
  const id = `${definition}:${cargo.formSequence}`;
302
304
  cargo.forms[id] = { id, definition, task: owner?.task || task || id, parent, result_input: input,
303
- on_exhausted: on_exhausted || '', incomplete_outcome: incomplete, context_only: !!context, parent_revision: owner?.revision ?? null, specs, values: {}, accepted: {}, errors: {}, pending: {}, seeds: {}, attempts: {}, prepared: {}, data: {}, arguments: {}, contexts: {}, children: {}, childResults: {}, revision: 0,
305
+ on_exhausted: on_exhausted || '', incomplete_outcome: incomplete, context_only: !!context, parent_revision: owner?.revision ?? null, specs, values: {}, accepted: {}, errors: {}, pending: {}, seeds: {}, attempts: {}, misses: {}, feedback: {}, prepared: {}, data: {}, arguments: {}, contexts: {}, children: {}, childResults: {}, revision: 0,
304
306
  providers: providers || owner?.providers || null, questions: questions?.length ? questions : owner?.questions || [], status: 'collecting' };
305
307
  if (owner) {
306
308
  if (context) {
@@ -1,4 +1,9 @@
1
1
  export namespace STANDARD_FUNCTIONS {
2
+ export { clearFormMisses };
3
+ export { missFormInput };
4
+ export { formInputPrompt };
5
+ export { consumeFormFeedback };
6
+ export { formConversationOutcome };
2
7
  export { readMoneyAmount };
3
8
  export { isMoneyAmount };
4
9
  export { moneyIntentAllows };
@@ -88,6 +93,11 @@ export namespace STANDARD_FUNCTIONS {
88
93
  export { urlToSpeech };
89
94
  export { textWithUrlToSpeech };
90
95
  }
96
+ import { clearFormMisses } from './form-conversation.js';
97
+ import { missFormInput } from './form-conversation.js';
98
+ import { formInputPrompt } from './form-conversation.js';
99
+ import { consumeFormFeedback } from './form-conversation.js';
100
+ import { formConversationOutcome } from './form-conversation.js';
91
101
  import { readMoneyAmount } from './money.js';
92
102
  import { isMoneyAmount } from './money.js';
93
103
  import { moneyIntentAllows } from './money.js';
@@ -181,5 +191,5 @@ import { validatePhone } from './validators.js';
181
191
  import { validateEmail } from './validators.js';
182
192
  import { urlToSpeech } from './url-speech.js';
183
193
  import { textWithUrlToSpeech } from './url-speech.js';
184
- export { readMoneyAmount, isMoneyAmount, moneyIntentAllows, 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, 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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
194
+ export { clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, 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, 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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
185
195
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAkBmF,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;qCAAZ,YAAY;gCAlB/D,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;4BAUtD,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;kCAC6mB,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;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACzoB,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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAAwG,wBAAwB;8BAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmB7C,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;qCAAZ,YAAY;gCAlB/D,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;4BAUtD,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;kCAC6mB,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;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACzoB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
@@ -1,3 +1,4 @@
1
+ import { clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome } from './form-conversation.js';
1
2
  import { takeChoiceOffer } from './choice-offer.js';
2
3
  import { consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified } from './form-actions.js';
3
4
  /**
@@ -29,9 +30,10 @@ import { resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, fo
29
30
  import { checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
30
31
  import { validateDigits, validatePhone, validateEmail } from './validators.js';
31
32
  import { urlToSpeech, textWithUrlToSpeech } from './url-speech.js';
32
- export { readMoneyAmount, isMoneyAmount, moneyIntentAllows, 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, 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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
33
+ export { clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, 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, 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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
33
34
  /** The functions flows call by name, ready to spread into a host's approved functions. */
34
35
  export const STANDARD_FUNCTIONS = {
36
+ clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome,
35
37
  readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
36
38
  readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
37
39
  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, 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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance,
@@ -1,6 +1,8 @@
1
1
  /** Copy/normalize declaration data so subsequent caller mutations cannot change admission. */
2
2
  export function normalizeHostRequirements(requirements: any, flowId: any): readonly any[];
3
- /** Every declared requirement needs an explicitly supplied own boolean true. Legacy flows declare none. */
3
+ /** Durable is the native default; session-only must be selected explicitly by the host. */
4
+ export function actionPersistenceMode(mode?: string): "durable" | "session-only";
5
+ /** Boolean requirements remain strict. guardedActions additionally accepts the explicit session-only policy. */
4
6
  export function assertFlowHostCapabilities(flows: any, capabilities?: {}): boolean;
5
7
  /** Declarative flow admission. No engine imports, I/O, or implicit host capability activation. */
6
8
  export class JSFEHostCapabilityError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/host/capabilities.js"],"names":[],"mappings":"AAWA,8FAA8F;AAC9F,0FASC;AAED,2GAA2G;AAC3G,mFAiBC;AAzCD,kGAAkG;AAClG;IACG,uDAMC;IAHE,aAA2C;IAC3C,YAA0B;IAC1B,gBAAkC;CAEvC"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/host/capabilities.js"],"names":[],"mappings":"AAWA,8FAA8F;AAC9F,0FASC;AAED,2FAA2F;AAC3F,iFAKC;AAED,gHAAgH;AAChH,mFA0BC;AA1DD,kGAAkG;AAClG;IACG,uDAMC;IAHE,aAA2C;IAC3C,YAA0B;IAC1B,gBAAkC;CAEvC"}