@superpanel-sdk/runtime 0.2.2 → 0.2.3

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.
Files changed (39) hide show
  1. package/dist/builder/compile/compile-agenda.js +9 -4
  2. package/dist/cli/serve/index.js +60 -12
  3. package/dist/compute-host/index.d.ts +2 -0
  4. package/dist/compute-host/index.js +1 -0
  5. package/dist/compute-host/read-only-cache.d.ts +20 -0
  6. package/dist/compute-host/read-only-cache.js +61 -0
  7. package/dist/compute-host/source-compute-host.js +7 -2
  8. package/dist/core/context/types.d.ts +19 -1
  9. package/dist/core/functions/define/custom.d.ts +1 -1
  10. package/dist/core/functions/types.d.ts +4 -4
  11. package/dist/core/slot/index.d.ts +1 -1
  12. package/dist/core/slot/index.js +1 -1
  13. package/dist/core/slot/types.d.ts +2 -0
  14. package/dist/core/slot/write.d.ts +8 -1
  15. package/dist/core/slot/write.js +145 -38
  16. package/dist/core/stage/builders.d.ts +23 -5
  17. package/dist/core/stage/builders.js +3 -0
  18. package/dist/core/stage/drive.d.ts +1 -1
  19. package/dist/core/stage/drive.js +9 -8
  20. package/dist/core/stage/flatten.js +13 -11
  21. package/dist/core/stage/index.d.ts +8 -4
  22. package/dist/core/stage/index.js +5 -2
  23. package/dist/core/stage/interrupts.d.ts +28 -0
  24. package/dist/core/stage/interrupts.js +120 -0
  25. package/dist/core/stage/ivr-fields.d.ts +9 -0
  26. package/dist/core/stage/ivr-fields.js +75 -0
  27. package/dist/core/stage/transfer-fields.d.ts +6 -1
  28. package/dist/core/stage/transfer-fields.js +44 -3
  29. package/dist/core/stage/transfer-spec.d.ts +48 -0
  30. package/dist/core/stage/transfer-spec.js +113 -0
  31. package/dist/core/stage/types.d.ts +45 -14
  32. package/dist/core/stage/validate.js +105 -1
  33. package/dist/core/stage/walk.d.ts +5 -0
  34. package/dist/core/stage/walk.js +33 -10
  35. package/dist/fn-context/rpc/trpc-client.js +3 -0
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.js +2 -2
  38. package/dist/voice-service/types.d.ts +3 -0
  39. package/package.json +1 -1
@@ -4,6 +4,70 @@ import { formatFieldPath, parseFieldPath, requireFieldPath } from './path.js';
4
4
  import { getRowField, isReservedTableName, isRowIdField, mintRowId, slotFieldNames } from './row.js';
5
5
  import { cloneSnapshot, getSnapshotTable } from './snapshot.js';
6
6
  import { validateBareValue, validateExtractedField } from './value.js';
7
+ export function isRowWrite(value) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value))
9
+ return false;
10
+ const o = value;
11
+ return (typeof o.table === 'string' &&
12
+ typeof o.rowId === 'string' &&
13
+ o.values !== null &&
14
+ typeof o.values === 'object' &&
15
+ !Array.isArray(o.values));
16
+ }
17
+ export function isStructuredWrites(value) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value) || isRowWrite(value))
19
+ return false;
20
+ const keys = Object.keys(value);
21
+ return keys.length > 0 && keys.every((key) => key === 'fields' || key === 'rows');
22
+ }
23
+ export function normalizeWrites(writes) {
24
+ if (writes == null)
25
+ return null;
26
+ if (isRowWrite(writes)) {
27
+ return { fields: {}, rows: [writes] };
28
+ }
29
+ if (isStructuredWrites(writes)) {
30
+ const fields = writes.fields && typeof writes.fields === 'object' && !Array.isArray(writes.fields) ? writes.fields : {};
31
+ const rows = Array.isArray(writes.rows) ? writes.rows.filter(isRowWrite) : [];
32
+ if (Object.keys(fields).length === 0 && rows.length === 0)
33
+ return null;
34
+ return { fields, rows };
35
+ }
36
+ if (Object.keys(writes).length === 0)
37
+ return null;
38
+ return { fields: writes, rows: [] };
39
+ }
40
+ function boundRowId(tableName, current) {
41
+ if (!current)
42
+ return undefined;
43
+ if (tableName === 'contact')
44
+ return current.contactId;
45
+ if (tableName === 'case_contact')
46
+ return current.caseContactId;
47
+ if (tableName === 'case_file')
48
+ return current.caseFileId;
49
+ return undefined;
50
+ }
51
+ function writeTargetRow(schema, snapshot, tableName, rowId) {
52
+ const table = getCompiledTable(schema, tableName);
53
+ if (!table)
54
+ return null;
55
+ const data = getSnapshotTable(snapshot, tableName);
56
+ if (!data)
57
+ return null;
58
+ if (isSnapshotSingleRow(table) && !Array.isArray(data)) {
59
+ if (rowId && data.id && data.id !== rowId)
60
+ return null;
61
+ return data;
62
+ }
63
+ if (Array.isArray(data)) {
64
+ const id = rowId ?? boundRowId(tableName, snapshot.current);
65
+ if (!id)
66
+ return null;
67
+ return data.find((row) => row.id === id) ?? null;
68
+ }
69
+ return null;
70
+ }
7
71
  function extractedToFieldValue(extracted, updatedBy, now) {
8
72
  return makeFieldValue({
9
73
  reason: extracted.reason,
@@ -13,7 +77,7 @@ function extractedToFieldValue(extracted, updatedBy, now) {
13
77
  updatedAt: now,
14
78
  });
15
79
  }
16
- function assertWritableNormal(schema, path, options) {
80
+ function assertWritableNormal(schema, snapshot, path, options, rowId) {
17
81
  const { table, field: fieldName } = requireFieldPath(path);
18
82
  const tableDef = getCompiledTable(schema, table);
19
83
  const field = tableDef?.fields[fieldName];
@@ -49,23 +113,75 @@ function assertWritableNormal(schema, path, options) {
49
113
  },
50
114
  ];
51
115
  }
52
- if (!isSnapshotSingleRow(tableDef)) {
116
+ if (!isSnapshotSingleRow(tableDef) && !writeTargetRow(schema, snapshot, table, rowId)) {
53
117
  return [
54
118
  {
55
- code: 'multi-path-write',
56
- message: `Use Slot Filling / row APIs to write multi-row field ${path}`,
119
+ code: rowId ? 'row-not-found' : 'multi-path-write',
120
+ message: rowId
121
+ ? `Row ${rowId} not found in ${table}`
122
+ : `Use Slot Filling or { table, rowId, values } to write multi-row field ${path}`,
57
123
  path,
58
124
  },
59
125
  ];
60
126
  }
61
127
  return [];
62
128
  }
129
+ function applyBareFieldWrite(schema, original, next, path, value, rowId, options, now, errors, changed, history) {
130
+ const writeIssues = assertWritableNormal(schema, original, path, options, rowId);
131
+ if (writeIssues.length > 0) {
132
+ errors.push(...writeIssues);
133
+ return;
134
+ }
135
+ const parsed = parseFieldPath(path);
136
+ if (!parsed) {
137
+ errors.push({ code: 'unknown-field', message: `Invalid field path: ${path}`, path });
138
+ return;
139
+ }
140
+ const field = getCompiledTable(schema, parsed.table)?.fields[parsed.field];
141
+ if (!field) {
142
+ errors.push({ code: 'unknown-field', message: `Unknown field: ${path}`, path });
143
+ return;
144
+ }
145
+ const valueIssues = validateBareValue(field, value, path, {
146
+ allowNull: !field.derived && field.nullable,
147
+ });
148
+ if (valueIssues.length > 0) {
149
+ errors.push(...valueIssues);
150
+ return;
151
+ }
152
+ const tableData = writeTargetRow(schema, next, parsed.table, rowId);
153
+ if (!tableData)
154
+ return;
155
+ const prev = getRowField(tableData, parsed.field);
156
+ if (!prev)
157
+ return;
158
+ const committed = commitFieldWrite(prev, makeFieldValue({
159
+ value: (value ?? null),
160
+ state: 'filled',
161
+ reason: null,
162
+ updatedBy: options.source,
163
+ updatedAt: now,
164
+ }));
165
+ tableData[parsed.field] = committed.field;
166
+ if (committed.changed) {
167
+ changed.push(parsed.path);
168
+ if (committed.historyEntry) {
169
+ history.push({
170
+ path: parsed.path,
171
+ entry: committed.historyEntry,
172
+ rowId: typeof tableData.id === 'string' && tableData.id ? tableData.id : undefined,
173
+ });
174
+ }
175
+ }
176
+ }
63
177
  /**
64
178
  * Apply bare Writes (function / human / import / platform) onto a snapshot.
65
179
  * Validates type/enum and write permissions. Detects C5 changes and appends history.
180
+ * Multi-row tables: pass `{ table, rowId, values }` (or `rows` on StructuredWrites).
66
181
  */
67
182
  export function applyWrites(schema, snapshot, writes, options) {
68
- if (!writes || Object.keys(writes).length === 0) {
183
+ const normalized = normalizeWrites(writes);
184
+ if (!normalized) {
69
185
  return { snapshot, changed: [], history: [] };
70
186
  }
71
187
  const now = options.now ?? Date.now();
@@ -73,38 +189,21 @@ export function applyWrites(schema, snapshot, writes, options) {
73
189
  const changed = [];
74
190
  const history = [];
75
191
  const errors = [];
76
- for (const [path, value] of Object.entries(writes)) {
77
- const writeIssues = assertWritableNormal(schema, path, options);
78
- if (writeIssues.length > 0) {
79
- errors.push(...writeIssues);
192
+ for (const [path, value] of Object.entries(normalized.fields)) {
193
+ applyBareFieldWrite(schema, snapshot, next, path, value, undefined, options, now, errors, changed, history);
194
+ }
195
+ for (const row of normalized.rows) {
196
+ if (!row.table || !row.rowId) {
197
+ errors.push({
198
+ code: 'row-not-found',
199
+ message: 'Row write requires table and rowId',
200
+ path: row.table || undefined,
201
+ });
80
202
  continue;
81
203
  }
82
- const parsed = parseFieldPath(path);
83
- const field = getCompiledTable(schema, parsed.table).fields[parsed.field];
84
- const valueIssues = validateBareValue(field, value, path, {
85
- allowNull: !field.derived && field.nullable,
86
- });
87
- if (valueIssues.length > 0) {
88
- errors.push(...valueIssues);
89
- continue;
90
- }
91
- const tableData = getSnapshotTable(next, parsed.table);
92
- const prev = getRowField(tableData, parsed.field);
93
- if (!prev)
94
- continue;
95
- const committed = commitFieldWrite(prev, makeFieldValue({
96
- value: (value ?? null),
97
- state: 'filled',
98
- reason: null,
99
- updatedBy: options.source,
100
- updatedAt: now,
101
- }));
102
- tableData[parsed.field] = committed.field;
103
- if (committed.changed) {
104
- changed.push(parsed.path);
105
- if (committed.historyEntry) {
106
- history.push({ path: parsed.path, entry: committed.historyEntry });
107
- }
204
+ for (const [fieldName, value] of Object.entries(row.values)) {
205
+ const path = formatFieldPath(row.table, fieldName);
206
+ applyBareFieldWrite(schema, snapshot, next, path, value, row.rowId, options, now, errors, changed, history);
108
207
  }
109
208
  }
110
209
  if (errors.length > 0) {
@@ -201,7 +300,11 @@ export function applySlotFilling(schema, snapshot, output, options = {}) {
201
300
  if (committed.changed) {
202
301
  changed.add(path);
203
302
  if (committed.historyEntry) {
204
- history.push({ path, entry: committed.historyEntry });
303
+ history.push({
304
+ path,
305
+ entry: committed.historyEntry,
306
+ rowId: typeof tableData.id === 'string' && tableData.id ? tableData.id : undefined,
307
+ });
205
308
  }
206
309
  }
207
310
  }
@@ -241,7 +344,7 @@ export function applySlotFilling(schema, snapshot, output, options = {}) {
241
344
  if (committed.changed) {
242
345
  changed.add(path);
243
346
  if (committed.historyEntry) {
244
- history.push({ path, entry: committed.historyEntry });
347
+ history.push({ path, entry: committed.historyEntry, rowId: id });
245
348
  }
246
349
  }
247
350
  }
@@ -310,7 +413,11 @@ export function markAskedUnknown(schema, snapshot, fields, options = {}) {
310
413
  if (committed.changed) {
311
414
  changed.push(parsed.path);
312
415
  if (committed.historyEntry) {
313
- history.push({ path: parsed.path, entry: committed.historyEntry });
416
+ history.push({
417
+ path: parsed.path,
418
+ entry: committed.historyEntry,
419
+ rowId: typeof tableData.id === 'string' && tableData.id ? tableData.id : undefined,
420
+ });
314
421
  }
315
422
  }
316
423
  }
@@ -1,6 +1,7 @@
1
1
  import type { FuncRef } from '../functions/func-ref.js';
2
2
  import type { FieldPath } from '../slot/path.js';
3
- import type { ActionNode, AgendaHeader, AgendaTree, AuthoringNode, AuthoringWhen, BranchNode, FinishWhen, OnNoProgress, SectionNode, SwitchOnNode, TopicNode, TransferNode } from './types.js';
3
+ import type { ActionNode, AgendaHeader, AgendaTree, AuthoringNode, AuthoringWhen, BranchNode, FinishWhen, IvrDigit, IvrFallbackAction, IvrNode, OnNoProgress, SectionNode, SwitchOnNode, TopicNode, TransferNode } from './types.js';
4
+ import type { TransferDial, TransferMode, TransferSay } from './transfer-spec.js';
4
5
  export declare function topic(id: string, config: {
5
6
  label: string;
6
7
  applyWhen: AuthoringWhen;
@@ -20,10 +21,27 @@ export declare function transfer(id: string, config: {
20
21
  label: string;
21
22
  applyWhen: AuthoringWhen;
22
23
  numbers: string[];
23
- mode: 'cold' | 'warm';
24
- say: string;
25
- unsuccessfulSay: string | null;
26
- }): TransferNode;
24
+ mode: TransferMode;
25
+ say: TransferSay;
26
+ unsuccessfulSay: TransferSay | null;
27
+ } & Omit<TransferDial, 'numbers' | 'mode' | 'say' | 'unsuccessfulSay'>): TransferNode;
28
+ export declare function ivr(id: string, config: {
29
+ label: string;
30
+ applyWhen: AuthoringWhen;
31
+ onDtmfResult: FuncRef;
32
+ finishWhen: FinishWhen;
33
+ fallbackAction: IvrFallbackAction;
34
+ confirmTimeoutMs: number;
35
+ pauseAfterSayMs: number;
36
+ loopSay: string;
37
+ enterSay: string | null;
38
+ succeedSay: string | null;
39
+ unsuccessSay: string | null;
40
+ finishOnKey?: IvrDigit | null;
41
+ minDigits?: number | null;
42
+ maxDigits?: number | null;
43
+ validateDigits?: IvrDigit[] | null;
44
+ }): IvrNode;
27
45
  export declare function section(id: string, config: {
28
46
  label: string;
29
47
  applyWhen: AuthoringWhen;
@@ -7,6 +7,9 @@ export function action(id, config) {
7
7
  export function transfer(id, config) {
8
8
  return { kind: 'transfer', id, ...config };
9
9
  }
10
+ export function ivr(id, config) {
11
+ return { kind: 'ivr', id, ...config };
12
+ }
10
13
  export function section(id, config, children) {
11
14
  return { kind: 'section', id, label: config.label, applyWhen: config.applyWhen, children };
12
15
  }
@@ -21,7 +21,7 @@ export type StageDriveResult = {
21
21
  fused: boolean;
22
22
  };
23
23
  /**
24
- * Walk until a topic/transfer/complete, executing actions inline.
24
+ * Walk until a topic/transfer/ivr/complete, executing actions inline.
25
25
  * Action returning null is skipped for this tick; writes trigger a full re-walk.
26
26
  */
27
27
  export declare function driveAgenda(items: FlatAgendaItem[], state: StageDriveState, compute: ComputeHost, hooks: StageDriveHooks, options?: {
@@ -1,8 +1,9 @@
1
+ import { normalizeWrites } from '../slot/write.js';
1
2
  import { evaluateWhen } from './finish-when.js';
2
3
  import { unresolvedFinishWhenFields, walkAgenda } from './walk.js';
3
4
  export const DEFAULT_MAX_ACTIONS_PER_TURN = 10;
4
5
  /**
5
- * Walk until a topic/transfer/complete, executing actions inline.
6
+ * Walk until a topic/transfer/ivr/complete, executing actions inline.
6
7
  * Action returning null is skipped for this tick; writes trigger a full re-walk.
7
8
  */
8
9
  export async function driveAgenda(items, state, compute, hooks, options = {}) {
@@ -11,11 +12,11 @@ export async function driveAgenda(items, state, compute, hooks, options = {}) {
11
12
  let currentPlacementId = state.currentPlacementId;
12
13
  const skip = new Set();
13
14
  let actionsRun = 0;
14
- let lastWalk = await walkAgenda(items, snapshot, { currentPlacementId, compute, skipPlacementIds: skip });
15
+ let lastWalk;
15
16
  while (true) {
16
17
  lastWalk = await walkAgenda(items, snapshot, { currentPlacementId, compute, skipPlacementIds: skip });
17
18
  const hit = lastWalk.hit;
18
- if (!hit || hit.kind === 'topic' || hit.kind === 'transfer') {
19
+ if (!hit || hit.kind === 'topic' || hit.kind === 'transfer' || hit.kind === 'ivr') {
19
20
  return {
20
21
  snapshot,
21
22
  currentPlacementId: hit?.placementId ?? currentPlacementId,
@@ -35,13 +36,13 @@ export async function driveAgenda(items, state, compute, hooks, options = {}) {
35
36
  }
36
37
  actionsRun += 1;
37
38
  const writes = await hooks.runAction(hit, snapshot);
38
- if (writes && Object.keys(writes).length > 0) {
39
- snapshot = await hooks.applyWrites(writes, snapshot);
40
- skip.clear();
41
- currentPlacementId = hit.placementId;
39
+ if (!writes || !normalizeWrites(writes)) {
40
+ skip.add(hit.placementId);
42
41
  continue;
43
42
  }
44
- skip.add(hit.placementId);
43
+ snapshot = await hooks.applyWrites(writes, snapshot);
44
+ skip.clear();
45
+ currentPlacementId = hit.placementId;
45
46
  }
46
47
  }
47
48
  export async function advanceAgenda(items, state, compute, hooks, options = {}) {
@@ -1,6 +1,7 @@
1
1
  import { resolveFuncName } from '../functions/func-ref.js';
2
2
  import { isOtherwise, jsonRules } from './condition.js';
3
3
  import { andCompiledWhen, compileAuthoringWhen, compileWhen, whenFields } from './finish-when.js';
4
+ import { ivrResultField } from './ivr-fields.js';
4
5
  import { transferResultField } from './transfer-fields.js';
5
6
  function isTopicNode(node) {
6
7
  return node.kind === 'topic';
@@ -11,6 +12,9 @@ function isActionNode(node) {
11
12
  function isTransferNode(node) {
12
13
  return node.kind === 'transfer';
13
14
  }
15
+ function isIvrNode(node) {
16
+ return node.kind === 'ivr';
17
+ }
14
18
  function isSectionNode(node) {
15
19
  return node.kind === 'section';
16
20
  }
@@ -41,15 +45,8 @@ function toAgendaBlock(node) {
41
45
  say: node.say,
42
46
  };
43
47
  }
44
- return {
45
- kind: 'transfer',
46
- id: node.id,
47
- label: node.label,
48
- numbers: node.numbers,
49
- mode: node.mode,
50
- say: node.say,
51
- unsuccessfulSay: node.unsuccessfulSay,
52
- };
48
+ const { applyWhen: _applyWhen, ...block } = node;
49
+ return block;
53
50
  }
54
51
  function compileBlock(block, functions, issues, path) {
55
52
  try {
@@ -63,6 +60,11 @@ function compileBlock(block, functions, issues, path) {
63
60
  const run = resolveFuncName(block.run, functions);
64
61
  return { ...block, run, finishWhen };
65
62
  }
63
+ if (block.kind === 'ivr') {
64
+ const finishWhen = compileWhen(block.finishWhen, functions);
65
+ const onDtmfResult = resolveFuncName(block.onDtmfResult, functions);
66
+ return { ...block, onDtmfResult, finishWhen, resultField: ivrResultField(block.id) };
67
+ }
66
68
  const resultField = transferResultField(block.id);
67
69
  const finishWhen = compileWhen(jsonRules({
68
70
  fact: resultField,
@@ -341,7 +343,7 @@ function asSectionPlacement(node, usedIds, path, issues) {
341
343
  children: node.children.map((child, i) => asPlacement(child, usedIds, `${path}/${node.id}[${i}]`, issues)),
342
344
  };
343
345
  }
344
- if (isTopicNode(node) || isActionNode(node) || isTransferNode(node)) {
346
+ if (isTopicNode(node) || isActionNode(node) || isTransferNode(node) || isIvrNode(node)) {
345
347
  return {
346
348
  kind: 'node',
347
349
  placementId: uniquePlacementId(node.id, usedIds),
@@ -371,7 +373,7 @@ export function extractBlocksFromTree(tree, blocks = {}) {
371
373
  return blocks;
372
374
  function visit(nodes) {
373
375
  for (const node of nodes) {
374
- if (isTopicNode(node) || isActionNode(node) || isTransferNode(node)) {
376
+ if (isTopicNode(node) || isActionNode(node) || isTransferNode(node) || isIvrNode(node)) {
375
377
  blocks[node.id] = toAgendaBlock(node);
376
378
  }
377
379
  else if (isSectionNode(node) || node.kind === 'branch') {
@@ -1,11 +1,15 @@
1
- export type { ActionBlock, ActionNode, AgendaBlock, AgendaConfig, AgendaHeader, AgendaTree, ApplyWhen, AuthoringNode, AuthoringWhen, BranchNode, Channel, CompiledActionBlock, CompiledAgenda, CompiledAgendaBlock, CompiledTopicBlock, CompiledTransferBlock, CompiledWhen, Condition, FirstMessage, FinishWhen, FlatActionItem, FlatAgendaItem, FlatNodePlacement, FlatTopicItem, FlatTransferItem, Interrupt, SequenceAgendas, JsonRulesCondition, NodePlacement, OnNoProgress, OtherwiseSentinel, Placement, SectionNode, SectionPlacement, SwitchOnNode, TopicBlock, TopicNode, TransferBlock, TransferNode, When, } from './types.js';
1
+ export type { ActionBlock, ActionNode, AgendaBlock, AgendaConfig, AgendaHeader, AgendaTree, ApplyWhen, AuthoringNode, AuthoringWhen, BranchNode, Channel, CompiledActionBlock, CompiledAgenda, CompiledAgendaBlock, CompiledTopicBlock, CompiledIvrBlock, CompiledTransferBlock, CompiledWhen, Condition, FirstMessage, FinishWhen, FlatActionItem, FlatAgendaItem, FlatIvrItem, FlatNodePlacement, FlatTopicItem, FlatTransferItem, DtmfResult, IvrBlock, IvrDigit, IvrFallbackAction, IvrNode, Interrupt, SequenceAgendas, JsonRulesCondition, NodePlacement, OnNoProgress, OtherwiseSentinel, Placement, SectionNode, SectionPlacement, SwitchOnNode, TopicBlock, TopicNode, TransferBlock, TransferInterruptAction, TransferNode, When, } from './types.js';
2
+ export type { TransferArguments, TransferCallerIdShown, TransferConfirmKey, TransferConfirmMethod, TransferDial, TransferFallbackAction, TransferMode, TransferSay, } from './transfer-spec.js';
3
+ export { E164_NUMBER, SIP_URI, interpolateFromSnapshot, interpolateTemplate, isTransferDestinationLiteral, resolveTransferArguments, resolveTransferDestinations, resolveTransferSay, transferArgumentsJson, validateTransferDestinations, } from './transfer-spec.js';
2
4
  export { collectJsonRulesFacts, evaluateJsonRules, extractTemplateFields, isOtherwise, jsonRules, looksLikeJsonRules, normalizeJsonRules, otherwise, parseJsonRules, type ConditionEval, } from './condition.js';
3
5
  export { andCompiledWhen, compileAuthoringWhen, compileFinishWhen, compileWhen, evaluateFinishWhen, evaluateWhen, finishWhenFields, notAnyWhen, whenFields, type FinishWhenEval, type WhenEval, } from './finish-when.js';
4
- export { action, agenda, branch, section, switchOn, topic, transfer } from './builders.js';
6
+ export { action, agenda, branch, ivr, section, switchOn, topic, transfer } from './builders.js';
5
7
  export { extractBlocksFromTree, flattenAgendaConfig, flattenAgendaPlacements, flattenAgendaTree, isAgendaTree, isSequenceAgendas, normalizeSequenceAgendas, treeToPlacements, type FlattenOptions, type FlattenResult, } from './flatten.js';
6
- export { LAST_TRANSFER_RESULT_FIELD, collectTransferBlocks, registerTransferSessionFields, transferFieldKey, transferResultField, } from './transfer-fields.js';
8
+ export { CASE_LAST_TRANSFER_AT, CASE_LAST_TRANSFER_RESULT, CASE_TRANSFER_ATTEMPT_COUNT, LAST_TRANSFER_RESULT_FIELD, collectTransferBlocks, registerTransferSessionFields, transferFieldKey, transferResultField, } from './transfer-fields.js';
9
+ export { collectIvrBlocks, ivrAttempted, ivrResultField, registerIvrSessionFields, type IvrAttemptResult, } from './ivr-fields.js';
7
10
  export { fatalAgendaIssues, validateCompiledAgenda, type ValidateAgendaOptions } from './validate.js';
8
- export { unresolvedFinishWhenFields, walkAgenda, type WalkDecision, type WalkHitReason, type WalkOptions, type WalkResult, type WalkSkip, type WalkSkipReason, } from './walk.js';
11
+ export { RESERVED_INTERRUPT_TOOL_NAMES, collectTransferInterruptHeaders, headerHasTransferInterrupt, CONTACT_LANGUAGE_FIELD, getContactLang, inboundIvrCollect, isSpokenLanguage, ivrOf, languageFieldOf, validateInterrupts, type SpokenLanguage, } from './interrupts.js';
12
+ export { unresolvedFinishWhenFields, walkAgenda, type WalkDecision, type WalkHitReason, type WalkOptions, type WalkResult, type WalkSkip, type WalkSkipReason, type WalkStats, } from './walk.js';
9
13
  export { simulateAgenda, type SimulateResult, type SimulatedItem, type SimulatedItemStatus } from './simulate.js';
10
14
  export { DEFAULT_MAX_ACTIONS_PER_TURN, advanceAgenda, driveAgenda, transferWrites, watchdogHint, type AdvanceResult, type StageDriveHooks, type StageDriveResult, type StageDriveState, } from './drive.js';
11
15
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,12 @@
1
+ export { E164_NUMBER, SIP_URI, interpolateFromSnapshot, interpolateTemplate, isTransferDestinationLiteral, resolveTransferArguments, resolveTransferDestinations, resolveTransferSay, transferArgumentsJson, validateTransferDestinations, } from './transfer-spec.js';
1
2
  export { collectJsonRulesFacts, evaluateJsonRules, extractTemplateFields, isOtherwise, jsonRules, looksLikeJsonRules, normalizeJsonRules, otherwise, parseJsonRules, } from './condition.js';
2
3
  export { andCompiledWhen, compileAuthoringWhen, compileFinishWhen, compileWhen, evaluateFinishWhen, evaluateWhen, finishWhenFields, notAnyWhen, whenFields, } from './finish-when.js';
3
- export { action, agenda, branch, section, switchOn, topic, transfer } from './builders.js';
4
+ export { action, agenda, branch, ivr, section, switchOn, topic, transfer } from './builders.js';
4
5
  export { extractBlocksFromTree, flattenAgendaConfig, flattenAgendaPlacements, flattenAgendaTree, isAgendaTree, isSequenceAgendas, normalizeSequenceAgendas, treeToPlacements, } from './flatten.js';
5
- export { LAST_TRANSFER_RESULT_FIELD, collectTransferBlocks, registerTransferSessionFields, transferFieldKey, transferResultField, } from './transfer-fields.js';
6
+ export { CASE_LAST_TRANSFER_AT, CASE_LAST_TRANSFER_RESULT, CASE_TRANSFER_ATTEMPT_COUNT, LAST_TRANSFER_RESULT_FIELD, collectTransferBlocks, registerTransferSessionFields, transferFieldKey, transferResultField, } from './transfer-fields.js';
7
+ export { collectIvrBlocks, ivrAttempted, ivrResultField, registerIvrSessionFields, } from './ivr-fields.js';
6
8
  export { fatalAgendaIssues, validateCompiledAgenda } from './validate.js';
9
+ export { RESERVED_INTERRUPT_TOOL_NAMES, collectTransferInterruptHeaders, headerHasTransferInterrupt, CONTACT_LANGUAGE_FIELD, getContactLang, inboundIvrCollect, isSpokenLanguage, ivrOf, languageFieldOf, validateInterrupts, } from './interrupts.js';
7
10
  export { unresolvedFinishWhenFields, walkAgenda, } from './walk.js';
8
11
  export { simulateAgenda } from './simulate.js';
9
12
  export { DEFAULT_MAX_ACTIONS_PER_TURN, advanceAgenda, driveAgenda, transferWrites, watchdogHint, } from './drive.js';
@@ -0,0 +1,28 @@
1
+ import type { FieldPath } from '../slot/path.js';
2
+ import type { CompiledSlotSchema, ValidationIssue } from '../slot/types.js';
3
+ import type { AgendaHeader, FlatAgendaItem, FlatIvrItem, Interrupt, IvrDigit, IvrFallbackAction } from './types.js';
4
+ export type SpokenLanguage = string;
5
+ export declare const CONTACT_LANGUAGE_FIELD: "contact.language";
6
+ export declare const RESERVED_INTERRUPT_TOOL_NAMES: Set<string>;
7
+ export declare function isSpokenLanguage(value: unknown): value is SpokenLanguage;
8
+ export declare function ivrOf(items: FlatAgendaItem[] | null | undefined): FlatIvrItem | null;
9
+ export declare function languageFieldOf(): FieldPath;
10
+ /** Voice language. Empty / missing / non-language value → english. */
11
+ export declare function getContactLang(readField?: (path: FieldPath) => unknown): SpokenLanguage;
12
+ export declare function inboundIvrCollect(items: FlatAgendaItem[] | null | undefined): {
13
+ id: string;
14
+ fallbackAction: IvrFallbackAction;
15
+ confirmTimeoutMs: number;
16
+ pauseAfterSayMs: number;
17
+ finishOnKey?: IvrDigit | null;
18
+ minDigits?: number | null;
19
+ maxDigits?: number | null;
20
+ validateDigits: IvrDigit[];
21
+ } | null;
22
+ export declare function headerHasTransferInterrupt(header: AgendaHeader | null | undefined): boolean;
23
+ export declare function collectTransferInterruptHeaders(headers: Array<AgendaHeader | null | undefined>): boolean;
24
+ export declare function validateInterrupts(interrupts: Interrupt[], options: {
25
+ schema: CompiledSlotSchema;
26
+ agendaId?: string;
27
+ }): ValidationIssue[];
28
+ //# sourceMappingURL=interrupts.d.ts.map
@@ -0,0 +1,120 @@
1
+ import { getCompiledTable } from '../slot/compile-schema.js';
2
+ import { validateTransferDestinations } from './transfer-spec.js';
3
+ export const CONTACT_LANGUAGE_FIELD = 'contact.language';
4
+ export const RESERVED_INTERRUPT_TOOL_NAMES = new Set(['voice_turn', 'record_opt_out', 'advance']);
5
+ export function isSpokenLanguage(value) {
6
+ return typeof value === 'string' && value.trim().length > 0;
7
+ }
8
+ export function ivrOf(items) {
9
+ return items?.find((item) => item.kind === 'ivr') ?? null;
10
+ }
11
+ export function languageFieldOf() {
12
+ return CONTACT_LANGUAGE_FIELD;
13
+ }
14
+ function looksLikeSpokenLanguage(value) {
15
+ return isSpokenLanguage(value) && /[a-zA-Z]/.test(value);
16
+ }
17
+ /** Voice language. Empty / missing / non-language value → english. */
18
+ export function getContactLang(readField) {
19
+ if (!readField)
20
+ return 'english';
21
+ const raw = readField(CONTACT_LANGUAGE_FIELD);
22
+ return looksLikeSpokenLanguage(raw) ? raw.trim() : 'english';
23
+ }
24
+ export function inboundIvrCollect(items) {
25
+ const ivr = ivrOf(items);
26
+ if (!ivr)
27
+ return null;
28
+ const validateDigits = (ivr.validateDigits ?? []).filter((digit) => Boolean(digit?.trim()));
29
+ if (!ivr.finishOnKey && validateDigits.length === 0)
30
+ return null;
31
+ return {
32
+ id: ivr.id,
33
+ fallbackAction: ivr.fallbackAction,
34
+ confirmTimeoutMs: ivr.confirmTimeoutMs,
35
+ pauseAfterSayMs: ivr.pauseAfterSayMs,
36
+ finishOnKey: ivr.finishOnKey,
37
+ minDigits: ivr.minDigits,
38
+ maxDigits: ivr.maxDigits,
39
+ validateDigits,
40
+ };
41
+ }
42
+ export function headerHasTransferInterrupt(header) {
43
+ return header?.interrupts.some((interrupt) => interrupt.action.type === 'transfer') ?? false;
44
+ }
45
+ export function collectTransferInterruptHeaders(headers) {
46
+ return headers.some((header) => headerHasTransferInterrupt(header));
47
+ }
48
+ export function validateInterrupts(interrupts, options) {
49
+ const issues = [];
50
+ const prefix = `${options.agendaId ?? 'agenda'}/header/interrupts`;
51
+ const names = new Set();
52
+ const priorities = new Set();
53
+ for (const interrupt of interrupts) {
54
+ const path = `${prefix}/${interrupt.id}`;
55
+ if (RESERVED_INTERRUPT_TOOL_NAMES.has(interrupt.tool.name)) {
56
+ issues.push({
57
+ code: 'interrupt-tool-reserved',
58
+ message: `Interrupt tool name '${interrupt.tool.name}' is reserved`,
59
+ path,
60
+ });
61
+ }
62
+ if (names.has(interrupt.tool.name)) {
63
+ issues.push({
64
+ code: 'interrupt-tool-duplicate',
65
+ message: `Interrupt tool name '${interrupt.tool.name}' is not unique`,
66
+ path,
67
+ });
68
+ }
69
+ names.add(interrupt.tool.name);
70
+ if (priorities.has(interrupt.priority)) {
71
+ issues.push({
72
+ code: 'interrupt-priority-tie',
73
+ message: `Interrupt priority ${interrupt.priority} is not unique`,
74
+ path,
75
+ });
76
+ }
77
+ priorities.add(interrupt.priority);
78
+ if (interrupt.action.type === 'switch-language') {
79
+ if (interrupt.action.languages.length === 0) {
80
+ issues.push({
81
+ code: 'interrupt-languages-empty',
82
+ message: 'switch-language languages must not be empty',
83
+ path: `${path}/languages`,
84
+ });
85
+ }
86
+ if (!getCompiledTable(options.schema, 'contact')?.fields.language) {
87
+ issues.push({
88
+ code: 'interrupt-language-field',
89
+ message: `switch-language requires ${CONTACT_LANGUAGE_FIELD}`,
90
+ path: `${path}/languages`,
91
+ });
92
+ }
93
+ }
94
+ if (interrupt.action.type === 'transfer') {
95
+ issues.push(...validateTransferDestinations(interrupt.action.numbers, {
96
+ path: `${path}/numbers`,
97
+ code: 'interrupt-transfer-number',
98
+ }));
99
+ issues.push(...validateCriteriaList(interrupt.action.escalateCriteria, `${path}/escalateCriteria`));
100
+ issues.push(...validateCriteriaList(interrupt.action.noEscalationCriteria, `${path}/noEscalationCriteria`));
101
+ }
102
+ }
103
+ return issues;
104
+ }
105
+ function validateCriteriaList(list, path) {
106
+ if (!list)
107
+ return [];
108
+ const issues = [];
109
+ for (let index = 0; index < list.length; index++) {
110
+ if (list[index]?.trim())
111
+ continue;
112
+ issues.push({
113
+ code: 'interrupt-escalation-criteria',
114
+ message: 'escalation criteria entry must not be empty',
115
+ path: `${path}[${index}]`,
116
+ });
117
+ }
118
+ return issues;
119
+ }
120
+ //# sourceMappingURL=interrupts.js.map
@@ -0,0 +1,9 @@
1
+ import type { FieldPath } from '../slot/path.js';
2
+ import type { CompiledSlotSchema, RuntimeSlotsSnapshot } from '../slot/types.js';
3
+ import type { AgendaBlock, AuthoringNode, FlatIvrItem, IvrBlock } from './types.js';
4
+ export type IvrAttemptResult = 'collected' | 'failed';
5
+ export declare function ivrResultField(nodeId: string): FieldPath;
6
+ export declare function ivrAttempted(item: FlatIvrItem, snapshot: RuntimeSlotsSnapshot): boolean;
7
+ export declare function collectIvrBlocks(blocks: Record<string, AgendaBlock>, nodes?: AuthoringNode[]): IvrBlock[];
8
+ export declare function registerIvrSessionFields(schema: CompiledSlotSchema, ivrIds: Iterable<string>): CompiledSlotSchema;
9
+ //# sourceMappingURL=ivr-fields.d.ts.map