jsfe 1.36.0 → 1.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/docs/flow-docs.d.ts.map +1 -1
- package/dist/docs/flow-docs.js +4 -3
- package/dist/forms/contracts.js +3 -3
- package/dist/forms/flows.d.ts +8 -3
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +27 -17
- package/dist/forms/index.d.ts +1 -1
- package/dist/forms/index.js +1 -1
- package/dist/runtime-identity.js +1 -1
- package/docs/architecture-guide.md +60 -7
- package/docs/copilot-reference.md +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -387,7 +387,7 @@ means a reply that picks none of them. A picked choice sets `responseVariable` t
|
|
|
387
387
|
`answers` sets `{ key: '' }` unless `responseParameters` are declared too. The request carries the options it offers
|
|
388
388
|
(`request.options`); a host validates a reply against those, never a fixed list. The form readers use it: an unread
|
|
389
389
|
answer with any words goes to the ranker with the question's choices (`exact` ones too, since `exact` is how words match
|
|
390
|
-
a choice; never the YES of a firm confirmation
|
|
390
|
+
a choice; never the YES of a firm confirmation, which a form's confirmation is when an action it authorizes is `confirm.exact`; a NO always) and the escape every question supports, `__agent` (a person);
|
|
391
391
|
text is never offered. **Arguments in an unread answer** (jsfe 1.36): the asked input's `rank_parameters` (`[{ name, description, type?, reads? }]`) are extracted when the ranker says the reply answers the question, and a choice's own `parameters` when it is picked (`responseParameters` may name the array variable that holds them). Each value is read by the form's reader as if said alone: `reads` names the input it answers, else it answers the asked question, so that question's rules decide a number's field. Two readings of one input that differ take nothing, and nothing read asks again, as before. A form's `confirm.rank_parameters` do the same at its confirmation (no `reads`: read as a correction there); a value read changes the form, which is confirmed again. A charge or a cancellation is guarded by the action's `confirm: {exact: true}`, not by the choice's
|
|
392
392
|
`exact`. A confirmation's exhausted attempts go to the `give-up` worker.
|
|
393
393
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;EA+RC"}
|
package/dist/docs/flow-docs.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { resolveSetDataReferences } from '../forms/declarations.js';
|
|
18
|
+
import { confirmationFirmness } from '../forms/flows.js';
|
|
18
19
|
export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log = console.log }) {
|
|
19
20
|
// A catalog or value written as {"$data": path} is documented as compilation resolves it.
|
|
20
21
|
const SETS = sets.map((s) => ({ ...s, flows: resolveSetDataReferences(s.flows) }));
|
|
@@ -354,7 +355,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
354
355
|
else { /* no menu */ }
|
|
355
356
|
return c.done();
|
|
356
357
|
}
|
|
357
|
-
function formChart(fm) {
|
|
358
|
+
function formChart(fm, performs) {
|
|
358
359
|
const c = builder();
|
|
359
360
|
if (fm.before && fm.before.length) {
|
|
360
361
|
c.steps(fm.before);
|
|
@@ -365,7 +366,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
365
366
|
c.then({ label: `${i.name}${i.label ? ' (' + i.label + ')' : ''}${extra ? ': ' + extra : ''}`, kind: 'input' });
|
|
366
367
|
}
|
|
367
368
|
if (fm.confirm) {
|
|
368
|
-
c.then({ label:
|
|
369
|
+
c.then({ label: `confirm (${confirmationFirmness(performs)})`, kind: 'decision' });
|
|
369
370
|
}
|
|
370
371
|
else { /* no confirmation */ }
|
|
371
372
|
if (fm.after && fm.after.length) {
|
|
@@ -414,7 +415,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
414
415
|
}
|
|
415
416
|
else { /* not a provider */ }
|
|
416
417
|
if (flow.form) {
|
|
417
|
-
out.push('', '### Form', '', formChart(flow.form));
|
|
418
|
+
out.push('', '### Form', '', formChart(flow.form, flow.performs));
|
|
418
419
|
}
|
|
419
420
|
else { /* not a form */ }
|
|
420
421
|
// A form the jsfe SDK compiled (the system set) carries its inputs as a variable; its steps are the engine's run-form loop.
|
package/dist/forms/contracts.js
CHANGED
|
@@ -5,7 +5,7 @@ import { INPUT_PROPOSALS_REQUIREMENT, hasInputProposals, proposalRuntimeSteps }
|
|
|
5
5
|
import { ACTION_TOOLS } from './action-tools.js';
|
|
6
6
|
import { normalizeHostRequirements } from '../host/capabilities.js';
|
|
7
7
|
import { validateActionProjection, actionProjectionExpression } from './action-projection.js';
|
|
8
|
-
import { formFlow, FIELD_PRESETS } from './flows.js';
|
|
8
|
+
import { formFlow, FIELD_PRESETS, confirmationFirmness } from './flows.js';
|
|
9
9
|
import { resolveFormInputs } from '../helpers/form-state.js';
|
|
10
10
|
const ROLES = new Set(['provider', 'performer', 'collector', 'gate', 'router', 'worker']);
|
|
11
11
|
const EFFECTS = new Set(['read', 'send', 'write', 'charge', 'cancel', 'handoff']);
|
|
@@ -137,7 +137,7 @@ export function defineProvider({ provides, contract, parameters = [], variables
|
|
|
137
137
|
{ id: `${flow.id}_provide`, type: 'FLOW', value: 'provide-data', callType: 'call', parameters: { provides: '{{provides}}', asked: '{{asked}}', reply: '{{userInput}}', ...(gateParameters !== undefined ? { gate_params: '{{provider_gate_arguments}}' } : {}) } }] }, { role: 'provider', ...contract, catalog: 'provides' });
|
|
138
138
|
}
|
|
139
139
|
export function defineForm({ contract, provides, ...spec }) {
|
|
140
|
-
const flow = formFlow({ ...spec, ...(provides ? { extraVars: { ...spec.extraVars, provides, questions: providerQuestions(provides) } } : {}) });
|
|
140
|
+
const flow = formFlow({ ...spec, confirm_soft: false, ...(provides ? { extraVars: { ...spec.extraVars, provides, questions: providerQuestions(provides) } } : {}) });
|
|
141
141
|
return declared(flow, { role: 'collector', ...contract, form: { inputs: 'inputs' }, ...(provides ? { catalog: 'provides' } : {}) });
|
|
142
142
|
}
|
|
143
143
|
export function definePerformer({ performs, contract, prepare, present, project = [], provides, ...spec }) {
|
|
@@ -158,7 +158,7 @@ export function definePerformer({ performs, contract, prepare, present, project
|
|
|
158
158
|
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}}' } }),
|
|
159
159
|
set('proposal', 'pf_record', "pf_action ? prepareFormAction(cargo, form_instance, pf_action, pf_request) : null"),
|
|
160
160
|
];
|
|
161
|
-
const flow = formFlow({ ...spec, confirm, confirmation_condition: "pf_action !== null && pf_action && typeof pf_action === 'object'",
|
|
161
|
+
const flow = formFlow({ ...spec, confirm, confirm_soft: confirmationFirmness(performs) === 'soft', confirmation_condition: "pf_action !== null && pf_action && typeof pf_action === 'object'",
|
|
162
162
|
extraVars: { ...spec.extraVars, ...(provides ? { provides, questions: providerQuestions(provides) } : {}), performs, projection: project, pf_action: null, pf_request: null, pf_record: null, pf_result: null, pf_receipt: null },
|
|
163
163
|
before_confirm: confirm ? [...prepareSteps, ...(spec.before_confirm || [])] : spec.before_confirm || [],
|
|
164
164
|
after: [...(confirm ? [] : prepareSteps),
|
package/dist/forms/flows.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The form shell: declare inputs, run the loop, hand the result on. `after` = the form's own steps once complete.
|
|
3
3
|
* `confirm` ({ question, question_es } — SAY-style templates that may read cargo.forms[<id>].values; build them in
|
|
4
|
-
* `before_confirm` SET steps when they need computing): the
|
|
5
|
-
* never an input.
|
|
4
|
+
* `before_confirm` SET steps when they need computing): the confirmation, asked every time after the loop and
|
|
5
|
+
* never an input. It is firm unless `confirm_soft` (definePerformer: no action it authorizes declares `confirm.exact`): a soft
|
|
6
|
+
* confirmation's YES is read as a soft input confirmation's and is offered to the reply ranker; a firm one's is neither. YES runs `after`; NO runs `declined`; a correction given there goes through the form's own matching
|
|
6
7
|
* and the form runs again (so whatever the correction made missing is asked).
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
@@ -12,7 +13,10 @@
|
|
|
12
13
|
*/
|
|
13
14
|
/** rank_parameters and a choice's parameters: [{ name, description, type?, reads? }] (reads: the input the value answers). */
|
|
14
15
|
export function validateRankParameters(id: any, name: any, what: any, list: any): void;
|
|
15
|
-
|
|
16
|
+
/** How firm a form's confirmation is: firm when an action it authorizes declares confirm.exact (charge, cancel, irreversible)
|
|
17
|
+
* or it authorizes none (a plain form); soft when it authorizes actions and none is exact (owner 2026-11-08). */
|
|
18
|
+
export function confirmationFirmness(performs: any): "firm" | "soft";
|
|
19
|
+
export function formFlow({ id, name, version, description, inputs, after, extraVars, parameters, confirm, confirm_soft, before_confirm, declined, stop, close_on_stop, on_no, resume_on_parent_change, before, done, primary, scope, validator, confirmation_condition, prompt, prompt_es, incomplete_outcome, on_exhausted, must_complete, first_task, first_task_es, acknowledge }: {
|
|
16
20
|
id: any;
|
|
17
21
|
name: any;
|
|
18
22
|
version: any;
|
|
@@ -22,6 +26,7 @@ export function formFlow({ id, name, version, description, inputs, after, extraV
|
|
|
22
26
|
extraVars?: {} | undefined;
|
|
23
27
|
parameters?: never[] | undefined;
|
|
24
28
|
confirm?: null | undefined;
|
|
29
|
+
confirm_soft?: boolean | undefined;
|
|
25
30
|
before_confirm?: never[] | undefined;
|
|
26
31
|
declined?: never[] | undefined;
|
|
27
32
|
stop?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6GA
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6GA;;;;;;;GAOG;AACH;;;;GAIG;AACH,8HAA8H;AAC9H,uFAIC;AAiBD;iHACiH;AACjH,qEAEC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAolBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA3yBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAogC9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -104,8 +104,9 @@ const FIELD_COLLECTORS = { 'collect-phone': 'phone', 'collect-email': 'email', '
|
|
|
104
104
|
/**
|
|
105
105
|
* The form shell: declare inputs, run the loop, hand the result on. `after` = the form's own steps once complete.
|
|
106
106
|
* `confirm` ({ question, question_es } — SAY-style templates that may read cargo.forms[<id>].values; build them in
|
|
107
|
-
* `before_confirm` SET steps when they need computing): the
|
|
108
|
-
* never an input.
|
|
107
|
+
* `before_confirm` SET steps when they need computing): the confirmation, asked every time after the loop and
|
|
108
|
+
* never an input. It is firm unless `confirm_soft` (definePerformer: no action it authorizes declares `confirm.exact`): a soft
|
|
109
|
+
* confirmation's YES is read as a soft input confirmation's and is offered to the reply ranker; a firm one's is neither. YES runs `after`; NO runs `declined`; a correction given there goes through the form's own matching
|
|
109
110
|
* and the form runs again (so whatever the correction made missing is asked).
|
|
110
111
|
*/
|
|
111
112
|
/**
|
|
@@ -144,7 +145,12 @@ function validateAcceptanceDeclarations(id, inputs) {
|
|
|
144
145
|
else { /* no invites, or a list of names */ }
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
/** How firm a form's confirmation is: firm when an action it authorizes declares confirm.exact (charge, cancel, irreversible)
|
|
149
|
+
* or it authorizes none (a plain form); soft when it authorizes actions and none is exact (owner 2026-11-08). */
|
|
150
|
+
export function confirmationFirmness(performs) {
|
|
151
|
+
return Array.isArray(performs) && performs.length && !performs.some((action) => action?.confirm?.exact) ? 'soft' : 'firm';
|
|
152
|
+
}
|
|
153
|
+
export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, confirm_soft = false, before_confirm = [], declined = [], stop = '', close_on_stop = false, on_no = '', resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null, must_complete = false, first_task = '', first_task_es = '', acknowledge = 'all' }) {
|
|
148
154
|
validateFormExhaustion(on_exhausted);
|
|
149
155
|
if (acknowledge !== 'all' && acknowledge !== 'data')
|
|
150
156
|
throw new Error(`FORM ${id}: acknowledge must be 'all' or 'data'`);
|
|
@@ -168,7 +174,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
168
174
|
// FLOW parameters are paths only: the question is built here, as a SAY-style template, then passed by name
|
|
169
175
|
SET(`${id}_confirm_q`, 'form_confirm_q', textValue(confirm.question)),
|
|
170
176
|
SET(`${id}_confirm_q_es`, 'form_confirm_q_es', textValue(confirm.question_es || confirm.question)),
|
|
171
|
-
FLOW(`${id}_confirm`, 'confirm-form', 'call', { form: '{{form_instance}}', question: '{{form_confirm_q}}', question_es: '{{form_confirm_q_es}}', phrases: '{{form_confirmation.phrases}}', words: '{{form_confirmation.words}}', clarifications: '{{form_confirmation.clarifications}}', no_keys: '{{form_confirmation.no_keys}}', interrupts: '{{form_confirmation.interrupts}}', invites: '{{form_confirmation.invites}}', named_fields: '{{form_confirmation.named_fields}}', reply_context: '{{form_confirmation.reply_context}}', rank_parameters: '{{form_confirmation.rank_parameters}}', exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
|
|
177
|
+
FLOW(`${id}_confirm`, 'confirm-form', 'call', { form: '{{form_instance}}', confirmation: confirm_soft === true ? 'soft' : 'firm', question: '{{form_confirm_q}}', question_es: '{{form_confirm_q_es}}', phrases: '{{form_confirmation.phrases}}', words: '{{form_confirmation.words}}', clarifications: '{{form_confirmation.clarifications}}', no_keys: '{{form_confirmation.no_keys}}', interrupts: '{{form_confirmation.interrupts}}', invites: '{{form_confirmation.invites}}', named_fields: '{{form_confirmation.named_fields}}', reply_context: '{{form_confirmation.reply_context}}', rank_parameters: '{{form_confirmation.rank_parameters}}', exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
|
|
172
178
|
{ id: `${id}_confirmed`, type: 'CASE', branches: {
|
|
173
179
|
[`condition: (cargo.formConfirm || {})[form_instance] === 'changed'`]: FLOW(`${id}_again`, id, 'replace', { parent_form: '{{parent_form}}', result_input: '{{result_input}}', form_instance: '{{form_instance}}' }),
|
|
174
180
|
// NO: the declined flow takes the form's place, so the action below never runs
|
|
@@ -536,13 +542,14 @@ const readFormChoice = {
|
|
|
536
542
|
FLOW('choice_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })]
|
|
537
543
|
};
|
|
538
544
|
// A confirmation's unread answer goes to the reply ranker with the confirmation question (owner 2026-10-27), never
|
|
539
|
-
// straight to the host. A
|
|
540
|
-
//
|
|
545
|
+
// straight to the host. A firm confirmation authorizes the form's action, so the ranker is offered NO, the form's other
|
|
546
|
+
// choices (a correction) and a person, never YES; a soft one (no exact action) is offered YES too (owner 2026-11-08:
|
|
547
|
+
// prod 11:07 "Sí, para enviar." at a ticket's confirmation). Result: cargo.confirmRanked = 'yes' | 'no' | 'changed' | 'none'
|
|
541
548
|
// (ConfirmForm reads it); `abort` ends every flow; `new_request` goes to intent detection.
|
|
542
549
|
const readConfirmReply = {
|
|
543
550
|
id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.4.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), says a named field read back is wrong (confirm.named_fields: that input is asked again), asks for a person, or is asked again; a withdrawal or a new request leaves with saved form progress. Never confirms.',
|
|
544
|
-
parameters: [{ name: 'form', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }, { name: 'named_fields', type: 'boolean' }, { name: 'reply_context', type: 'string' }, { name: 'rank_parameters', type: 'array' }, { name: 'invites', type: 'array' }],
|
|
545
|
-
variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null, chain: null, others: [], fields: [], field: null, picked: null, reading: null, named_fields: false, prompt: null, field_arguments: [], corrections: [], reply_context: '', stay: false, rank_parameters: [], invites: [], rank_params: [], args: null }),
|
|
551
|
+
parameters: [{ name: 'form', type: 'string' }, { name: 'confirmation', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }, { name: 'named_fields', type: 'boolean' }, { name: 'reply_context', type: 'string' }, { name: 'rank_parameters', type: 'array' }, { name: 'invites', type: 'array' }],
|
|
552
|
+
variables: vars({ confirmation: '', question: '', question_es: '', asked: '', ranked: [], reply: null, chain: null, others: [], fields: [], field: null, picked: null, reading: null, named_fields: false, prompt: null, field_arguments: [], corrections: [], reply_context: '', stay: false, rank_parameters: [], invites: [], rank_params: [], args: null }),
|
|
546
553
|
steps: [
|
|
547
554
|
SET('confirm_asked', 'asked', "(language === 'es' && question_es ? question_es : question)+(reply_context ? ' '+reply_context : '')"),
|
|
548
555
|
SET('confirm_stay', 'stay', "formMustComplete(cargo,form,'__confirm')"),
|
|
@@ -556,7 +563,7 @@ const readConfirmReply = {
|
|
|
556
563
|
SET('confirm_fields', 'fields', "named_fields === true || named_fields === 'true' ? (chain || []).filter(function(i){var v=cargo.forms[form] && cargo.forms[form].values;return i.form===form && !i.confirmation && Array.isArray(i.names) && i.names.length && v && v[i.name]!==undefined && v[i.name]!==null && v[i.name]!=='';}).map(function(i){var l=String(i.label || i.names[0]);return {key:'field:'+i.name,label:'says the '+l+' read back is wrong, or gives a different '+l,owner:{form:i.form,name:i.name}};}) : []"),
|
|
557
564
|
// A correction may name several fields ('it is Long Beach, apartment 4'): any field's pick extracts every named field's new value.
|
|
558
565
|
SET('confirm_field_arguments', 'field_arguments', "fields.map(function(c){var i=(chain || []).filter(function(i){return i.form===c.owner.form && i.name===c.owner.name;})[0];var l=String(i.label || i.names[0]);return {name:c.owner.name,type:'string',description:'The correct '+(i.argument_description || l)+' the caller gives now. Leave it empty when they give none.'};})"),
|
|
559
|
-
rankedChoices('confirm_ranked_choices', "[{key:'no',label:'no: declines or refuses what the question asks to confirm'}].concat(others.map(function(c){return Object.assign({key:c.key,label:c.label},c.parameters ? {parameters:c.parameters.map(function(p){return {name:p.name,type:p.type || 'string',description:p.description};})} : {});})).concat(fields.map(function(c){return {key:c.key,label:c.label,parameters:field_arguments};}))"),
|
|
566
|
+
rankedChoices('confirm_ranked_choices', "(confirmation==='soft' ? [{key:'yes',label:'yes: agrees to what the question asks to confirm'}] : []).concat([{key:'no',label:'no: declines or refuses what the question asks to confirm'}]).concat(others.map(function(c){return Object.assign({key:c.key,label:c.label},c.parameters ? {parameters:c.parameters.map(function(p){return {name:p.name,type:p.type || 'string',description:p.description};})} : {});})).concat(fields.map(function(c){return {key:c.key,label:c.label,parameters:field_arguments};}))"),
|
|
560
567
|
// confirm.rank_parameters: values extracted when the ranker says the reply answers (owner 2026-11-06), read as RANK_ARGUMENTS_JS says
|
|
561
568
|
SET('confirm_rank_params', 'rank_params', "(Array.isArray(rank_parameters) ? rank_parameters : []).map(function(p){return {name:p.name,type:p.type || 'string',description:p.description};})"),
|
|
562
569
|
rankedDispatch('confirm_dispatch', 'confirmation_not_matched', true, 'rank_params'),
|
|
@@ -584,7 +591,7 @@ const readConfirmReply = {
|
|
|
584
591
|
WHEN('confirm_correct', 'corrections.length', FLOW('confirm_correct_accept', 'collect-input-accept', 'call', { hits: '{{corrections}}' })),
|
|
585
592
|
WHEN('confirm_field_reset', 'field && !corrections.length', SIDE('confirm_field_ask', '(resetFormInputs(cargo, field.owner.form, [field.owner.name]), true)')),
|
|
586
593
|
WHEN('confirm_args_accept', '!picked && args.hits.length', FLOW('confirm_args_accept_flow', 'collect-input-accept', 'call', { hits: '{{args.hits}}' })),
|
|
587
|
-
SIDE('confirm_ranked', "(cargo.confirmRanked=reading && reading.hits.length || field || args.hits.length ? 'changed' : reply && reply.key==='
|
|
594
|
+
SIDE('confirm_ranked', "(cargo.confirmRanked=reading && reading.hits.length || field || args.hits.length ? 'changed' : reply && (reply.key==='yes' || reply.key==='no') ? reply.key : 'none',true)")
|
|
588
595
|
]
|
|
589
596
|
};
|
|
590
597
|
const collectContextualAnswer = formFlow({ id: 'collect-contextual-answer', name: 'CollectContextualAnswer', description: 'Collect one open-ended answer using contextual DISPATCH and preserve the caller wording.',
|
|
@@ -627,7 +634,10 @@ const acceptedEffects = {
|
|
|
627
634
|
SET('ie_rest', 'ca_rest', 'readings.slice(1)'),
|
|
628
635
|
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)")),
|
|
629
636
|
SET('ie_say_en', 'ca_ack', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message || ('That ' + (ca_h.label || ca_h.name) + \" doesn't look right.\")) : ca_h.silent === true || ca_h.fromPending || !formAcknowledges(cargo, ca_h) ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Thank you, I already have your ' + ca_h.label + '.') : ca_h.outcome === 'changed' && ca_h.origin !== 'activation' ? ('Okay, I noted your ' + ca_h.label + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Got it, your ' + ca_h.label + '.') : ''"),
|
|
630
|
-
|
|
637
|
+
// The Spanish rejection names the label without an article or adjective ("No pude usar eso como ciudad o código postal."): labels are
|
|
638
|
+
// feminine and masculine, and some carry their own article; 'Ese ' + label + ' no parece correcto' was wrong for every feminine one
|
|
639
|
+
// (owner 2026-11-07; prod "Ese ciudad o código postal…").
|
|
640
|
+
SET('ie_say_es', 'ca_ack_es', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message_es || ca_h.message || ('No pude usar eso como ' + (ca_h.label_es || ca_h.label || ca_h.name) + '.')) : ca_h.silent === true || ca_h.fromPending || !formAcknowledges(cargo, ca_h) ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Gracias, ya tengo su ' + ca_h.label_es + '.') : ca_h.outcome === 'changed' && ca_h.origin !== 'activation' ? ('Listo, anoté su ' + ca_h.label_es + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Entendido, su ' + ca_h.label_es + '.') : ''"),
|
|
631
641
|
WHEN('ie_say', 'ca_ack', SAY('ie_ack', '{{ca_ack}}', '{{ca_ack_es}}')),
|
|
632
642
|
// While a field's callback runs, a reply cannot fill the form's callback fields, so a question in it cannot run them again (formInputChain).
|
|
633
643
|
SET('ie_after_mark', 'noop', "(ca_h.after && ca_h.outcome !== 'known' && ca_h.outcome !== 'rejected' && ca_h.outcome !== 'pending' && cargo.forms[ca_h.form] ? (cargo.forms[ca_h.form].afterRunning = true) : true, true)"),
|
|
@@ -674,12 +684,12 @@ const disambiguate = {
|
|
|
674
684
|
// ── confirm-form: the exact confirmation of a finished form; a correction is matched against the form chain ───────
|
|
675
685
|
const confirmForm = {
|
|
676
686
|
id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
|
|
677
|
-
description: "Worker: ask a form's confirmation (
|
|
678
|
-
parameters: ['form', 'question', 'question_es', 'exit_context', 'exit_context_es', 'phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'invites', 'rank_parameters'].map((n) => ({ name: n, type: ['phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'invites', 'rank_parameters'].includes(n) ? 'array' : 'string', description: n })).concat([{ name: 'named_fields', type: 'boolean', description: "true: an answer that names one of the form's inputs (its names) without a value re-asks only that input" }]),
|
|
679
|
-
variables: vars({ cf_clarify: null, named_fields: false, reply_context: '', rank_parameters: [], cf_must: false, cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_current: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, phrases: null, words: [], clarifications: [], no_keys: ['2'], interrupts: [], invites: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
|
|
687
|
+
description: "Worker: ask a form's confirmation (YES / NO, keys 1 and 2; firm when an action it authorizes is exact; otherwise soft: YES is read as a soft confirmation's and offered to the reply ranker). 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 answer with words it cannot read goes to the reply ranker (read-confirm-reply); an unclear one is asked again, and after two the give-up worker takes over. Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
|
|
688
|
+
parameters: ['form', 'confirmation', 'question', 'question_es', 'exit_context', 'exit_context_es', 'phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'invites', 'rank_parameters'].map((n) => ({ name: n, type: ['phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'invites', 'rank_parameters'].includes(n) ? 'array' : 'string', description: n })).concat([{ name: 'named_fields', type: 'boolean', description: "true: an answer that names one of the form's inputs (its names) without a value re-asks only that input" }]),
|
|
689
|
+
variables: vars({ confirmation: '', cf_clarify: null, named_fields: false, reply_context: '', rank_parameters: [], cf_must: false, cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_current: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, phrases: null, words: [], clarifications: [], no_keys: ['2'], interrupts: [], invites: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
|
|
680
690
|
steps: [
|
|
681
691
|
SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', cargo.confirmRanked = '', true)"),
|
|
682
|
-
SET('cf_choices', 'cf_choices', `[{key:'yes',phrases:['1'].concat(Array.isArray(phrases)?phrases:Array.isArray(words)?words:[]),exact:
|
|
692
|
+
SET('cf_choices', 'cf_choices', `[{key:'yes',phrases:['1'].concat(Array.isArray(phrases)?phrases:Array.isArray(words)?words:[]).concat(confirmation==='soft'?${JSON.stringify(ANSWER_WORDS.yes)}:[]),exact:confirmation!=='soft'},{key:'no',phrases:(Array.isArray(no_keys)?no_keys:['2']).concat(${JSON.stringify(ANSWER_WORDS.no)}),exact:true}].concat((Array.isArray(interrupts)?interrupts:[]).map(function(event){return {key:'event:'+event.key,phrases:event.phrases ?? event.words,exact:event.exact!==false};}))`),
|
|
683
693
|
SET('cf_initial_chain', 'cf_chain', CHAIN_JS),
|
|
684
694
|
SET('cf_amount_context', 'cf_amount', "cf_chain.some(function(input){return input.profile && input.profile.kind==='amount';})"),
|
|
685
695
|
SET('cf_prompt', 'cf_prompt', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'en')"),
|
|
@@ -713,11 +723,11 @@ const confirmForm = {
|
|
|
713
723
|
'condition: cf_m.hits.length > 0': FLOW('cf_accept', 'collect-input-accept', 'call', { hits: '{{cf_m.hits}}' }),
|
|
714
724
|
"condition: cargo.choice === 'no'": SET('cf_declined', 'cf_result', "'no'"),
|
|
715
725
|
"condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',phrases:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'clarification'"),
|
|
716
|
-
'condition: cf_words > 0': FLOW('cf_elsewhere', 'read-confirm-reply', 'call', { form: '{{form}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}', named_fields: '{{named_fields}}', reply_context: '{{reply_context}}', rank_parameters: '{{rank_parameters}}', invites: '{{invites}}' }),
|
|
726
|
+
'condition: cf_words > 0': FLOW('cf_elsewhere', 'read-confirm-reply', 'call', { form: '{{form}}', confirmation: '{{confirmation}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}', named_fields: '{{named_fields}}', reply_context: '{{reply_context}}', rank_parameters: '{{rank_parameters}}', invites: '{{invites}}' }),
|
|
717
727
|
default: SET('cf_unclear', 'cf_result', "'unclear'")
|
|
718
728
|
} },
|
|
719
729
|
// the ranker's reading of an unread answer: a decline is the NO; an answer that declines nothing is asked again
|
|
720
|
-
SET('cf_ranked', 'cf_result', "cf_result ? cf_result : cargo.confirmRanked === 'no' ? 'no' : cargo.confirmRanked === 'none' ? 'unclear' : ''"),
|
|
730
|
+
SET('cf_ranked', 'cf_result', "cf_result ? cf_result : cargo.confirmRanked === 'yes' ? 'yes' : cargo.confirmRanked === 'no' ? 'no' : cargo.confirmRanked === 'none' ? 'unclear' : ''"),
|
|
721
731
|
SET('cf_changed', 'cf_result', "cf_result ? cf_result : 'changed'"),
|
|
722
732
|
// an unclear answer is asked again (the form runs again and reaches this question), at most twice
|
|
723
733
|
SET('cf_tries_count', 'cf_tries', "cf_result === 'unclear' ? missFormInput(cargo,form,'__confirm') : (clearFormMisses(cargo,form,'__confirm'),0)"),
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { explainSession } from "./inspection.js";
|
|
|
5
5
|
export { continuationBreaks } from "./continuation-compat.js";
|
|
6
6
|
export { inheritFlowDeclaration } from "./declaration-capture.js";
|
|
7
7
|
export { withSafetyRevision, CONTINUATION_SAFETY_REQUIREMENT } from "./continuation-safety.js";
|
|
8
|
-
export { FIELD_PRESETS, formFlow, declinedFlow, textValue } from "./flows.js";
|
|
8
|
+
export { FIELD_PRESETS, formFlow, declinedFlow, textValue, confirmationFirmness } from "./flows.js";
|
|
9
9
|
export { linkFormSchemas, defineWorker, defineProvider, defineForm, definePerformer, validateFlowContracts, operationWorkers, providerQuestions, providerParameter } from "./contracts.js";
|
|
10
10
|
export { addressDraft, zipLocation, streetLocation, resolvedAddress } from "../helpers/address.js";
|
|
11
11
|
export { Form, Provider, Performer, Worker, compileExpression } from "./authoring.js";
|
package/dist/forms/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { FLOWS as FORM_FLOWS } from './flows.js';
|
|
|
5
5
|
import { FLOWS as CHOICE_FLOWS } from './choice-flow.js';
|
|
6
6
|
import { FLOWS as IDENTITY } from './identity-flows.js';
|
|
7
7
|
export const FLOWS = [...FORM_FLOWS, ...ADDRESS_FLOWS, ...CHOICE_FLOWS, ...IDENTITY].map(flow => withSafetyRevision(flow, 'dispatch-intake-2026-10-22'));
|
|
8
|
-
export { FIELD_PRESETS, formFlow, declinedFlow, textValue } from './flows.js';
|
|
8
|
+
export { FIELD_PRESETS, formFlow, declinedFlow, textValue, confirmationFirmness } from './flows.js';
|
|
9
9
|
export { FLOWS as IDENTITY_FLOWS } from './identity-flows.js';
|
|
10
10
|
export { linkFormSchemas, defineWorker, defineProvider, defineForm, definePerformer, validateFlowContracts, operationWorkers, providerQuestions, providerParameter } from './contracts.js';
|
|
11
11
|
export { compileFormPrompts } from './prompt-compiler.js';
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
1
|
+
export const JSFE_RUNTIME_FINGERPRINT = "sha256:452fe6731c7d9d4b43d0961d3eb33a429f0d3c2d7392acb968f4cbda7f514c95";
|
|
@@ -274,21 +274,74 @@ does this reply do with the question the flow asked? The model picks from a clos
|
|
|
274
274
|
| `new_request` | the caller clearly asks for something else | DISPATCH: the message goes to intent detection |
|
|
275
275
|
| `irrelevant` | the reply means nothing here: a stray word, an unfinished sentence, speech to someone else | the question is asked again; no flow moves |
|
|
276
276
|
|
|
277
|
-
**Arguments in an unread form answer** (jsfe 1.36, opt-in): an input's `rank_parameters` are extracted on `answers` and a profile choice's `parameters` on its pick; the form's own reader reads each value as if said alone (`reads`: the input it answers, else the asked question). The model only finds the words; validation is unchanged.
|
|
278
|
-
|
|
279
277
|
**Options with arguments.** A declared choice may carry `parameters` (`{name,type,description}`): a pick then extracts them from the reply with the same stage as a flow's parameters, and the flow reads `{ key, arguments }`. The pick stands when extraction finds nothing or fails. The ranker is generic, so the flow supplies the context: a form's `confirm.reply_context` says what a reply may contain. The address read-back uses this: "It's Long Beach, apartment 4" changes the city and the apartment and reads the address back, with no further question.
|
|
280
278
|
|
|
281
|
-
|
|
279
|
+
### Arguments in free-form answers
|
|
280
|
+
|
|
281
|
+
A caller rarely answers in the shape a reader expects: "Voy a hacer un pago de 100 dólares", "I'd like to fly to
|
|
282
|
+
Cancún the week after Easter". The readers cannot read such a sentence, so it goes to the reply ranker. A flow can
|
|
283
|
+
declare the values to look for in it (jsfe 1.36, opt-in). The ranker then decides what the reply does, the parameter
|
|
284
|
+
stage (the same one that extracts a flow's arguments in intent detection) finds the values, and **the form's own
|
|
285
|
+
readers decide what each value is.** The model only finds the words; every reader rule and validator still applies.
|
|
286
|
+
|
|
287
|
+
**Where to declare them.** Each is a list of `{ name, description, type?, reads? }`:
|
|
288
|
+
|
|
289
|
+
| Declared on | Extracted when | Example |
|
|
290
|
+
| --- | --- | --- |
|
|
291
|
+
| an input: `rank_parameters` | the ranker says the reply `answers` the question | the amount question, the travel search question |
|
|
292
|
+
| a profile choice: `parameters` | the ranker picks that choice: the asked question's own, or another input's option offered with it | "different amount" carries the amount |
|
|
293
|
+
| a form's confirmation: `confirm.rank_parameters` | the ranker says the reply `answers` the confirmation | "No, make it 200 dollars" at "I'll charge $119.77…" |
|
|
294
|
+
|
|
295
|
+
**`reads` decides which question a value answers.** With `reads: '<input>'` the value is read as that input's
|
|
296
|
+
answer, wherever the question was: it can only become that input. Without `reads` it is read as the answer to the
|
|
297
|
+
question asked, exactly as if the caller had said only that value, so that question's rules decide where it goes (a
|
|
298
|
+
key, a sub account number, an invited amount). At a confirmation, a value without `reads` is read as a correction there.
|
|
299
|
+
Use `reads` for a value the caller clearly meant for one field; leave it out for a value whose field the question's
|
|
300
|
+
rules should decide.
|
|
301
|
+
|
|
302
|
+
**How the values are taken:**
|
|
303
|
+
|
|
304
|
+
- Each value is read by the readers as if said alone, then validated and stored like a typed answer, with the same
|
|
305
|
+
acknowledgement, attempts and confirmation. A value read at a confirmation changes the form, which is confirmed again.
|
|
306
|
+
- Nothing read: the question is asked again, as without the declaration. Extraction failing behaves the same.
|
|
307
|
+
- Two readings of one input that differ take nothing: the question is asked again.
|
|
308
|
+
- A picked choice is applied first, then its arguments. An argument that answers the choice's own exclusive group
|
|
309
|
+
replaces it, being the more specific answer ("other amount" with $100 is the amount $100).
|
|
310
|
+
- Each reading carries `origin: 'rank_argument'` and `argument: '<name>'`, so the input's validator can tell an
|
|
311
|
+
extracted value from a typed one (the Curacao amount check treats an extracted definite amount as an amount, never a
|
|
312
|
+
key or a sub account number).
|
|
313
|
+
|
|
314
|
+
**Writing the descriptions.** The description is the whole instruction to the model, so it states what counts and what
|
|
315
|
+
does not: "The amount the caller definitely wants to pay now … Empty when no number is meant as the amount to pay now:
|
|
316
|
+
an amount already paid, owed or asked about is not one." Ask for the value as the caller said it ("as the digits they
|
|
317
|
+
said, with cents only if they said cents"): the reader applies the rules, and a model that normalises a value can change
|
|
318
|
+
which rule applies. Use `type: 'string'` (the default) for anything a reader reads.
|
|
319
|
+
|
|
320
|
+
**Cost.** One extraction call, only on replies that already went to the ranker; a reply the readers read never reaches
|
|
321
|
+
it.
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
{ name: 'selection', exclusive: true,
|
|
325
|
+
rank_parameters: [
|
|
326
|
+
{ name: 'amount', reads: 'amount', description: 'The amount the caller definitely wants to pay now, …' },
|
|
327
|
+
{ name: 'amount_candidate', description: 'A number that may be the amount to pay now but could answer another option of this question, …' }],
|
|
328
|
+
any_of: [ { name: 'option', profile: { kind: 'choice', choices: [ /* … */
|
|
329
|
+
{ key: 'amount', phrases: ['other amount', 'otro monto'], parameters: [{ name: 'amount', reads: 'amount', description: '…' }] } ] } },
|
|
330
|
+
{ name: 'amount', profile: { kind: 'amount', min: 0.01 } } ] }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Word readers and the ranker.** An input with `profile.whole_phrases` reads a choice from words only when the whole reply is accounted for: its phrase, another input's argument, courtesy, connecting words and the input's `template_words`. A reply with any other word goes to the ranker, so a word inside a sentence ("Quiero hacer una nueva compra" at a pay menu) never picks an option. Compound answers that carry arguments stay with the reader ("pay $50 with my Visa card"), because a picked option extracts only the arguments it declares. A proposal's confirmation offers the ranker its input's choices, so a correction ("yes, it was stolen") picks the changed candidate.
|
|
282
334
|
|
|
283
335
|
**A number that must name itself.** `input.confirm_out_of_order: {word, word_es, message, message_es}` (default off): out of order a number fills this input only beside its word, in either language ("account 5373850"). Alone it may be something else, a code said with an extra digit, so nothing is filled: the caller hears `message` (`{value}` the number, digit by digit on a call; `{word}` the word) and the current question again, which counts as an attempt at that question. It works at `collect-input` and `confirm-form` questions. At its own question the number needs no word. `authenticate-user`'s `code_confirm` takes the same declaration for a `code_validator`, whose flow reads it with the helpers `saysConfirmWord(answer, spec)` and `confirmOutOfOrderLine(spec, value, language, voice)`.
|
|
284
336
|
|
|
285
337
|
**A provider's menu.** `provides.menu.whole_phrases: true` (default off) applies the same rule to a menu: a fact's phrase answers only as the whole reply, and a phrase inside a longer sentence goes to the reply ranker, offered every fact with phrases, every action, `done` and a person. A fact pick is answered with every other fact that cleared `REPLY_ALSO_CUT` (0.6), so one sentence that asks two things hears both; an action pick runs alone. `answers` and `irrelevant` fall to the menu's plain DISPATCH, which hands the turn to the host without intent detection (a Guru answers from the user context the flow set); `new_request` runs intent detection. A reply with no phrase is unchanged. `menu.unanswered` handles a question the provider cannot answer: when a reply from its own menu comes back to it through intent detection with no fact to say, the caller hears the declared apology and offer (`provider-unanswered`) instead of the same facts again.
|
|
286
338
|
|
|
287
|
-
Some options are **never offered**, so the ranker cannot pick them: the YES of a firm confirmation or
|
|
288
|
-
confirmation, a `current_only` choice while another question is asked, and text. `exact` on a choice says how words
|
|
339
|
+
Some options are **never offered**, so the ranker cannot pick them: the YES of a firm confirmation (an input's, or a form's
|
|
340
|
+
confirmation of an exact action), a `current_only` choice while another question is asked, and text. `exact` on a choice says how words
|
|
289
341
|
match it, not what it does, so exact choices are offered. A charge or a cancellation is guarded by `confirm: {exact: true}`
|
|
290
342
|
on its action, whose form confirmation offers the ranker NO, corrections and a person: a ranker can understand "no thanks,
|
|
291
|
-
maybe later" as NO; it can never produce a YES to a payment.
|
|
343
|
+
maybe later" as NO; it can never produce a YES to a payment. A form whose actions are none of them exact (filing a request)
|
|
344
|
+
confirms softly: its YES is read as a soft confirmation's and is offered to the ranker too. Options that cost more when wrong need a higher score (a NO that would cancel what the
|
|
292
345
|
caller was confirming, or a new request that leaves the question unanswered). A reply that clears no option is
|
|
293
346
|
`answers`, and the question is asked again if nothing usable was in it.
|
|
294
347
|
|
|
@@ -357,7 +410,7 @@ Cancellation and Concurrency" explains each rule and the production incident beh
|
|
|
357
410
|
|
|
358
411
|
- **Choices.** A choice has a key, phrases in each language and, on a call, keypad keys. A key matches only as the
|
|
359
412
|
whole answer. Sensitive choices are `exact`.
|
|
360
|
-
- **Confirmations.** `confirmation: "firm"` for consent (a charge, a cancellation, storing a card
|
|
413
|
+
- **Confirmations.** `confirmation: "firm"` for consent (a charge, a cancellation, storing a card):
|
|
361
414
|
only confirmation words. `"soft"` for an offer ("Would you like a link?"): the wider YES vocabulary.
|
|
362
415
|
- **Proposals.** When the flow infers a value ("Is this the card ending in 4242?"), a proposal asks the caller to
|
|
363
416
|
approve that one value. It never authorizes the final action.
|
|
@@ -214,7 +214,7 @@ Declarations:
|
|
|
214
214
|
|
|
215
215
|
| Declaration | Meaning |
|
|
216
216
|
| --- | --- |
|
|
217
|
-
| input `confirmation:"firm"` / `"soft"` | A yes/no decision. Firm (consent to a charge, a cancellation, storing a card
|
|
217
|
+
| input `confirmation:"firm"` / `"soft"` | A yes/no decision. Firm (consent to a charge, a cancellation, storing a card): YES only by `CONFIRM_WORDS` or the option's own exact phrases. Soft (an offer): the wider YES vocabulary. NO by the NO vocabulary or the option's own phrases. |
|
|
218
218
|
| input `invites:[names]`, `form.confirm.invites` | The other inputs this question (or the read-back) also asks for ("NEW CARD, LINK, or a different amount" invites `amount`), in its form or an enclosing one; read as in order while it is asked. |
|
|
219
219
|
| profile `out_of_order:{…}` | A narrower signature for out-of-order reading: the payment card takes a bare last four when asked, but out of order only a card word or "ending in 4242". |
|
|
220
220
|
| `authenticate-user` `other_whole_answer:true` | At the code question "another account" switches only as the whole answer; inside a sentence the reply ranker decides, offered `another_account` and `no_other_account` ("I understand. We still need to secure access to your account.", then the question). Default off. |
|
|
@@ -290,6 +290,8 @@ Ordinary DISPATCH is the default uncertainty escape. An explicit contextual DISP
|
|
|
290
290
|
|
|
291
291
|
Declare `response` as a local object variable. A single open string parameter whose words are the value (a reason, a description) may add `"verbatim":true`: a confident `answers` then keeps the reply as said when extraction finds nothing. Never mark a field that names something to look up (a product, a city). The reply ranker answers `answers`, `abort`, `new_request` or `irrelevant` (a cleared `new_request` outranks it; nothing clear is `answers`; `irrelevant` extracts nothing and the question is asked again). `responseChoices` names a flow variable holding `[{key,label,route?,parameters?}]`: the ranker is also offered `choice:<key>`. A pick sets the response variable to `{key}`, plus `also` (other declared choices the reply clears at 0.6, by score) when there are any, plus `arguments` when the choice declares `parameters:[{name,type,description}]`. `route:"new_request"` makes a pick leave like a new request. `onNoIntent:"stay"`: a `new_request` that intent detection routes to no flow is an answer to this question, and the flow continues. `answers` retains the live continuation and runs the following recovery steps; an optional typed response writes a cloned normalized object before continuing. `new_request` routes the input through intent detection; `abort` ends every flow as a RETURN with the reply `cmd_request_aborted` and outcome `user_aborted` (no recovery stamp). Neither writes that response object. Typed contextual v1 permits primitive string/boolean/number/integer fields, not object/array/jsonSchema. Missing fields remain missing. Errors preserve the pending step and prior object. Context is supplied to that decision without overwriting saved conversation history. This is existing configured intent/extraction, not authorization and not a snapshot restore.
|
|
292
292
|
|
|
293
|
+
**A form question's values in a free-form sentence** (jsfe 1.36): declare `rank_parameters` on the input (extracted when the ranker says the reply answers), `parameters` on a choice (extracted when it is picked) or `confirm.rank_parameters` (at the form's confirmation), each `[{ name, description, type?, reads? }]`. Each value is read by the form's readers as if said alone: `reads` names the input it answers, else it answers the asked question. Never a DISPATCH of your own for this. Details and rules: architecture-guide.md, "Arguments in free-form answers".
|
|
294
|
+
|
|
293
295
|
## 7. Providers
|
|
294
296
|
|
|
295
297
|
Declare a catalog once in top-level `provides`, including on a Form/Performer that answers related questions. JSFE derives the public `asked` parameter and mid-form question catalog. Do not maintain a second vocabulary.
|
|
@@ -345,7 +347,7 @@ A Performer requires `form`, nonempty `performs`, `prepare` worker and `present`
|
|
|
345
347
|
| `subject` | Ownership path such as `form.data.account` or `form.data.subject`; bind account/service identity, not an unrelated session flag. |
|
|
346
348
|
| `gate:{flow?,verified:[paths]}` | Execution-time verification; rerun the declared flow before each fresh effect. |
|
|
347
349
|
| `flow`, `tool` | Installed executor worker and/or registered tool identity. |
|
|
348
|
-
| `confirm:{exact:true}` | Required for charge, cancel or `reversible:false`. This requires a real form confirmation question. |
|
|
350
|
+
| `confirm:{exact:true}` | Required for charge, cancel or `reversible:false`. This requires a real form confirmation question, and makes it firm (YES only by `CONFIRM_WORDS` or its own exact phrases, never offered to the reply ranker). A performer none of whose actions is exact (filing a request) confirms softly: the wider YES vocabulary, and YES is offered to the ranker. There is no separate attribute for this. |
|
|
349
351
|
| `outcomes` | Map declared executor statuses to business outcomes, including unknown results. |
|
|
350
352
|
| `retry` | Explicit policies: `after:[knownStatuses]`; unknown retry only for SEND with `unknown:"explicit"` and a new accepted recovery choice. |
|
|
351
353
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.1",
|
|
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",
|