@superpanel-sdk/runtime 0.2.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/README.md +28 -8
  2. package/dist/builder/codegen/emit.d.ts +29 -11
  3. package/dist/builder/codegen/emit.js +328 -61
  4. package/dist/builder/codegen/generate.d.ts +21 -0
  5. package/dist/builder/codegen/generate.js +152 -20
  6. package/dist/builder/company-import-graph.d.ts +2 -0
  7. package/dist/builder/company-import-graph.js +40 -8
  8. package/dist/builder/company-imports.d.ts +24 -0
  9. package/dist/builder/company-imports.js +114 -0
  10. package/dist/builder/compile/compile-sequence.d.ts +59 -8
  11. package/dist/builder/compile/compile-sequence.js +279 -77
  12. package/dist/builder/compile/function-code.d.ts +12 -2
  13. package/dist/builder/compile/function-code.js +87 -39
  14. package/dist/builder/compile/registry.d.ts +7 -3
  15. package/dist/builder/compile/registry.js +24 -23
  16. package/dist/builder/load.d.ts +70 -10
  17. package/dist/builder/load.js +250 -61
  18. package/dist/builder/paths.d.ts +3 -0
  19. package/dist/builder/paths.js +16 -0
  20. package/dist/builder/schedules.d.ts +23 -0
  21. package/dist/builder/schedules.js +102 -0
  22. package/dist/builder/slot-schema-validation.d.ts +4 -0
  23. package/dist/builder/slot-schema-validation.js +10 -0
  24. package/dist/builder/typecheck.d.ts +8 -0
  25. package/dist/builder/typecheck.js +105 -0
  26. package/dist/builder/types.d.ts +91 -19
  27. package/dist/builder/types.js +2 -1
  28. package/dist/cli/auth/auth.d.ts +37 -0
  29. package/dist/cli/auth/auth.js +211 -0
  30. package/dist/cli/auth/backend-url.d.ts +5 -0
  31. package/dist/cli/auth/backend-url.js +25 -0
  32. package/dist/cli/auth/clipboard.d.ts +9 -0
  33. package/dist/cli/auth/clipboard.js +38 -0
  34. package/dist/cli/auth/http.d.ts +20 -0
  35. package/dist/cli/auth/http.js +27 -0
  36. package/dist/cli/auth/index.d.ts +10 -0
  37. package/dist/cli/auth/index.js +27 -0
  38. package/dist/cli/auth/progress.d.ts +12 -0
  39. package/dist/cli/auth/progress.js +15 -0
  40. package/dist/cli/auth/serve-login.d.ts +8 -0
  41. package/dist/cli/auth/serve-login.js +24 -0
  42. package/dist/cli/compile/index.js +3 -0
  43. package/dist/cli/config.d.ts +10 -0
  44. package/dist/cli/config.js +52 -0
  45. package/dist/cli/index.js +28 -8
  46. package/dist/cli/serve/index.d.ts +73 -3
  47. package/dist/cli/serve/index.js +365 -116
  48. package/dist/cli/type-gen/index.js +9 -8
  49. package/dist/compiler.d.ts +7 -0
  50. package/dist/compiler.js +7 -0
  51. package/dist/compute-host/compute-host.d.ts +15 -23
  52. package/dist/compute-host/compute-host.js +25 -28
  53. package/dist/compute-host/index.d.ts +2 -4
  54. package/dist/compute-host/index.js +1 -2
  55. package/dist/compute-host/source-compute-host.d.ts +11 -9
  56. package/dist/compute-host/source-compute-host.js +46 -32
  57. package/dist/core/case-orchestrator/contact-hours.d.ts +50 -0
  58. package/dist/core/case-orchestrator/contact-hours.js +108 -0
  59. package/dist/core/case-orchestrator/follow-ups.d.ts +31 -0
  60. package/dist/core/case-orchestrator/follow-ups.js +103 -0
  61. package/dist/core/case-orchestrator/index.d.ts +6 -0
  62. package/dist/core/case-orchestrator/index.js +6 -0
  63. package/dist/core/case-orchestrator/render.d.ts +75 -0
  64. package/dist/core/case-orchestrator/render.js +239 -0
  65. package/dist/core/case-orchestrator/time.d.ts +11 -0
  66. package/dist/core/case-orchestrator/time.js +29 -0
  67. package/dist/core/case-orchestrator/tools.d.ts +29 -0
  68. package/dist/core/case-orchestrator/tools.js +31 -0
  69. package/dist/core/case-orchestrator/types.d.ts +753 -0
  70. package/dist/core/case-orchestrator/types.js +46 -0
  71. package/dist/core/case-orchestrator/validate.d.ts +11 -0
  72. package/dist/core/case-orchestrator/validate.js +63 -0
  73. package/dist/core/context/call.d.ts +291 -0
  74. package/dist/core/context/call.js +94 -0
  75. package/dist/core/context/channels.d.ts +60 -0
  76. package/dist/core/context/channels.js +35 -0
  77. package/dist/core/context/contact.d.ts +96 -6
  78. package/dist/core/context/contact.js +48 -2
  79. package/dist/core/context/email-message.d.ts +159 -0
  80. package/dist/core/context/email-message.js +59 -0
  81. package/dist/core/context/email.d.ts +97 -0
  82. package/dist/core/context/email.js +91 -0
  83. package/dist/core/context/exchanges.d.ts +142 -0
  84. package/dist/core/context/exchanges.js +63 -0
  85. package/dist/core/context/instances.d.ts +278 -0
  86. package/dist/core/context/instances.js +123 -0
  87. package/dist/core/context/kv.d.ts +113 -0
  88. package/dist/core/context/kv.js +60 -0
  89. package/dist/core/context/platform-registry.d.ts +509 -4
  90. package/dist/core/context/platform-registry.js +14 -2
  91. package/dist/core/context/platform.d.ts +2 -1
  92. package/dist/core/context/sms.d.ts +147 -0
  93. package/dist/core/context/sms.js +57 -0
  94. package/dist/core/context/types.d.ts +51 -48
  95. package/dist/core/context/types.js +1 -7
  96. package/dist/core/conversation/index.d.ts +31 -0
  97. package/dist/core/conversation/index.js +14 -0
  98. package/dist/core/functions/define/bind.d.ts +25 -0
  99. package/dist/core/functions/define/bind.js +14 -0
  100. package/dist/core/functions/define/case.d.ts +9 -0
  101. package/dist/core/functions/define/case.js +13 -0
  102. package/dist/core/functions/define/events.d.ts +20 -0
  103. package/dist/core/functions/define/events.js +21 -0
  104. package/dist/core/functions/define/no-case.d.ts +4 -0
  105. package/dist/core/functions/define/no-case.js +9 -0
  106. package/dist/core/functions/events.d.ts +247 -0
  107. package/dist/core/functions/events.js +32 -0
  108. package/dist/core/functions/func-ref.d.ts +11 -2
  109. package/dist/core/functions/schedules.d.ts +51 -0
  110. package/dist/core/functions/schedules.js +20 -0
  111. package/dist/core/functions/types.d.ts +62 -51
  112. package/dist/core/functions/types.js +6 -1
  113. package/dist/core/office-hours/index.d.ts +39 -0
  114. package/dist/core/office-hours/index.js +187 -0
  115. package/dist/core/page/define.d.ts +834 -0
  116. package/dist/core/page/define.js +477 -0
  117. package/dist/core/page/ops.d.ts +58 -0
  118. package/dist/core/page/ops.js +220 -0
  119. package/dist/core/page/render.d.ts +253 -0
  120. package/dist/core/page/render.js +840 -0
  121. package/dist/core/page/time-format.d.ts +19 -0
  122. package/dist/core/page/time-format.js +41 -0
  123. package/dist/core/page/timeline.d.ts +40 -0
  124. package/dist/core/page/timeline.js +2 -0
  125. package/dist/core/page/typed.d.ts +642 -0
  126. package/dist/core/page/typed.js +2 -0
  127. package/dist/core/page/workspace.d.ts +33 -0
  128. package/dist/core/page/workspace.js +231 -0
  129. package/dist/core/party/index.d.ts +3 -0
  130. package/dist/core/party/index.js +3 -0
  131. package/dist/core/party/schema.d.ts +137 -0
  132. package/dist/core/party/schema.js +80 -0
  133. package/dist/core/party/types.d.ts +40 -0
  134. package/dist/core/party/types.js +15 -0
  135. package/dist/core/sequence/define.d.ts +57 -0
  136. package/dist/core/sequence/define.js +10 -0
  137. package/dist/core/sequence/index.d.ts +63 -0
  138. package/dist/core/sequence/index.js +231 -0
  139. package/dist/core/slot/binding.d.ts +40 -0
  140. package/dist/core/slot/binding.js +2 -0
  141. package/dist/core/slot/company-metadata.d.ts +113 -0
  142. package/dist/core/slot/company-metadata.js +563 -0
  143. package/dist/core/slot/compile-schema.d.ts +19 -13
  144. package/dist/core/slot/compile-schema.js +51 -248
  145. package/dist/core/slot/contact-timezone.d.ts +110 -0
  146. package/dist/core/slot/contact-timezone.js +186 -0
  147. package/dist/core/slot/field.d.ts +2 -24
  148. package/dist/core/slot/field.js +0 -69
  149. package/dist/core/slot/id-ref.d.ts +17 -2
  150. package/dist/core/slot/id-ref.js +16 -8
  151. package/dist/core/slot/index.d.ts +12 -13
  152. package/dist/core/slot/index.js +11 -12
  153. package/dist/core/slot/naming.d.ts +15 -0
  154. package/dist/core/slot/naming.js +96 -0
  155. package/dist/core/slot/platform-fields.d.ts +119 -0
  156. package/dist/core/slot/platform-fields.js +121 -0
  157. package/dist/core/slot/ref.d.ts +16 -0
  158. package/dist/core/slot/ref.js +14 -0
  159. package/dist/core/slot/row.d.ts +2 -9
  160. package/dist/core/slot/row.js +13 -23
  161. package/dist/core/slot/schema-compat.d.ts +16 -0
  162. package/dist/core/slot/schema-compat.js +112 -0
  163. package/dist/core/slot/schema.d.ts +409 -76
  164. package/dist/core/slot/schema.js +178 -10
  165. package/dist/core/slot/types.d.ts +27 -94
  166. package/dist/core/slot/value.d.ts +26 -3
  167. package/dist/core/slot/value.js +89 -57
  168. package/dist/dev-overlay/protocol.d.ts +41 -8
  169. package/dist/fn-context/build-context.d.ts +3 -1
  170. package/dist/fn-context/build-context.js +20 -10
  171. package/dist/fn-context/fake/empty-api.js +9 -28
  172. package/dist/fn-context/fake/index.d.ts +11 -4
  173. package/dist/fn-context/fake/index.js +27 -2
  174. package/dist/fn-context/fn-params.d.ts +6 -0
  175. package/dist/fn-context/fn-params.js +13 -0
  176. package/dist/fn-context/index.d.ts +5 -5
  177. package/dist/fn-context/index.js +4 -4
  178. package/dist/fn-context/log.js +2 -2
  179. package/dist/fn-context/make-ctx.d.ts +11 -0
  180. package/dist/fn-context/make-ctx.js +18 -8
  181. package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +24 -0
  182. package/dist/fn-context/rpc/case-orchestrator-tools.js +13 -0
  183. package/dist/fn-context/rpc/index.d.ts +3 -1
  184. package/dist/fn-context/rpc/index.js +3 -1
  185. package/dist/fn-context/rpc/integration/index.d.ts +4 -4
  186. package/dist/fn-context/rpc/integration/index.js +1 -1
  187. package/dist/fn-context/rpc/integration/list-surfaces.d.ts +21 -0
  188. package/dist/fn-context/rpc/integration/{list-bindings.js → list-surfaces.js} +4 -4
  189. package/dist/fn-context/rpc/platform.d.ts +2 -2
  190. package/dist/fn-context/rpc/platform.js +1 -1
  191. package/dist/fn-context/rpc/records.d.ts +78 -0
  192. package/dist/fn-context/rpc/records.js +26 -0
  193. package/dist/fn-context/rpc/trpc-client.d.ts +2 -2
  194. package/dist/fn-context/rpc/trpc-client.js +88 -7
  195. package/dist/fn-context/rpc/trpc-init.d.ts +2 -2
  196. package/dist/fn-context/rpc/trpc-router-factory.d.ts +49 -3
  197. package/dist/fn-context/rpc/trpc-router-factory.js +2 -0
  198. package/dist/fn-context/run-scope.d.ts +4 -0
  199. package/dist/fn-context/run-scope.js +9 -0
  200. package/dist/fn-context/types.d.ts +36 -3
  201. package/dist/index.d.ts +37 -46
  202. package/dist/index.js +33 -33
  203. package/dist/logger.d.ts +7 -0
  204. package/dist/logger.js +41 -0
  205. package/dist/platform.d.ts +76 -0
  206. package/dist/platform.js +56 -0
  207. package/dist/voice-service/actions.d.ts +71 -4
  208. package/dist/voice-service/types.d.ts +42 -5
  209. package/dist/worker.d.ts +10 -0
  210. package/dist/worker.js +10 -0
  211. package/package.json +38 -19
  212. package/dist/builder/compile/compile-agenda.d.ts +0 -15
  213. package/dist/builder/compile/compile-agenda.js +0 -64
  214. package/dist/builder/merge.d.ts +0 -4
  215. package/dist/builder/merge.js +0 -19
  216. package/dist/compute-host/read-only-cache.d.ts +0 -20
  217. package/dist/compute-host/read-only-cache.js +0 -61
  218. package/dist/core/context/db.d.ts +0 -69
  219. package/dist/core/context/db.js +0 -44
  220. package/dist/core/context/docusign.d.ts +0 -134
  221. package/dist/core/context/docusign.js +0 -108
  222. package/dist/core/context/filevine.d.ts +0 -323
  223. package/dist/core/context/filevine.js +0 -254
  224. package/dist/core/context/http.d.ts +0 -9
  225. package/dist/core/context/http.js +0 -4
  226. package/dist/core/context/hubspot.d.ts +0 -250
  227. package/dist/core/context/hubspot.js +0 -191
  228. package/dist/core/context/twilio.d.ts +0 -50
  229. package/dist/core/context/twilio.js +0 -42
  230. package/dist/core/functions/define/case-context.d.ts +0 -14
  231. package/dist/core/functions/define/case-context.js +0 -15
  232. package/dist/core/functions/define/compute.d.ts +0 -7
  233. package/dist/core/functions/define/compute.js +0 -17
  234. package/dist/core/functions/define/custom.d.ts +0 -4
  235. package/dist/core/functions/define/custom.js +0 -8
  236. package/dist/core/functions/define/system.d.ts +0 -7
  237. package/dist/core/functions/define/system.js +0 -12
  238. package/dist/core/slot/deps.d.ts +0 -15
  239. package/dist/core/slot/deps.js +0 -88
  240. package/dist/core/slot/derived.d.ts +0 -32
  241. package/dist/core/slot/derived.js +0 -115
  242. package/dist/core/slot/platform-session-fields.d.ts +0 -38
  243. package/dist/core/slot/platform-session-fields.js +0 -48
  244. package/dist/core/slot/sf-schema.d.ts +0 -23
  245. package/dist/core/slot/sf-schema.js +0 -125
  246. package/dist/core/slot/sf-validate.d.ts +0 -14
  247. package/dist/core/slot/sf-validate.js +0 -132
  248. package/dist/core/slot/snapshot.d.ts +0 -18
  249. package/dist/core/slot/snapshot.js +0 -138
  250. package/dist/core/slot/write.d.ts +0 -48
  251. package/dist/core/slot/write.js +0 -426
  252. package/dist/core/stage/builders.d.ts +0 -54
  253. package/dist/core/stage/builders.js +0 -31
  254. package/dist/core/stage/condition.d.ts +0 -17
  255. package/dist/core/stage/condition.js +0 -198
  256. package/dist/core/stage/drive.d.ts +0 -43
  257. package/dist/core/stage/drive.js +0 -78
  258. package/dist/core/stage/finish-when.d.ts +0 -20
  259. package/dist/core/stage/finish-when.js +0 -82
  260. package/dist/core/stage/flatten.d.ts +0 -27
  261. package/dist/core/stage/flatten.js +0 -466
  262. package/dist/core/stage/index.d.ts +0 -15
  263. package/dist/core/stage/index.js +0 -13
  264. package/dist/core/stage/interrupts.d.ts +0 -28
  265. package/dist/core/stage/interrupts.js +0 -120
  266. package/dist/core/stage/ivr-fields.d.ts +0 -9
  267. package/dist/core/stage/ivr-fields.js +0 -75
  268. package/dist/core/stage/simulate.d.ts +0 -24
  269. package/dist/core/stage/simulate.js +0 -33
  270. package/dist/core/stage/transfer-fields.d.ts +0 -19
  271. package/dist/core/stage/transfer-fields.js +0 -129
  272. package/dist/core/stage/transfer-spec.d.ts +0 -48
  273. package/dist/core/stage/transfer-spec.js +0 -113
  274. package/dist/core/stage/types.d.ts +0 -208
  275. package/dist/core/stage/types.js +0 -2
  276. package/dist/core/stage/validate.d.ts +0 -11
  277. package/dist/core/stage/validate.js +0 -330
  278. package/dist/core/stage/walk.d.ts +0 -39
  279. package/dist/core/stage/walk.js +0 -119
  280. package/dist/fn-context/rpc/integration/list-bindings.d.ts +0 -21
  281. package/dist/fn-context/system-args.d.ts +0 -6
  282. package/dist/fn-context/system-args.js +0 -14
@@ -0,0 +1,840 @@
1
+ /**
2
+ * Page document → `RenderedPage` (15 §10.9 output). Runs on the platform, after the page script
3
+ * returned its document from the sandbox.
4
+ *
5
+ * The document is walked once. Every slot becomes a cell joined with its schema definition; its value
6
+ * is taken from what the platform itself read (`PageReads`), never from the document, and a slot
7
+ * whose row this run did not read is shown as plain read-only content (anti-forgery). Every action
8
+ * becomes a button with the function-call arguments the AI fills; what the page prefilled stays here,
9
+ * on the server. An action that cannot be pressed is left off the page.
10
+ *
11
+ * The rows on the page are the rows with a cell on it: a lookup cell, and an action's row parameter,
12
+ * choose among the rows of their table there (15 §10.15 point 9). So lookups and actions are rendered
13
+ * after the walk, once every row on the page is known.
14
+ *
15
+ * Cells are addressed by slot id `table#n.field` (`RowNames`: stable for a conversation, layout-free);
16
+ * actions and blocks by their page path (`injuries.rows.0.markError`), resolved against the very page
17
+ * the AI saw.
18
+ */
19
+ import { parentOf } from '../slot/company-metadata.js';
20
+ import { ROW_ID_FIELD } from '../slot/schema.js';
21
+ import { getCompiledTable, getTableField } from '../slot/compile-schema.js';
22
+ import { isChoose } from './define.js';
23
+ // ---------------------------------------------------------------------------------------------
24
+ // Row names (15 §10.6.2)
25
+ /**
26
+ * `table#n` for each row a conversation has seen: the same row keeps its name on every page of the
27
+ * conversation; names only grow and are never reused, so a late output naming a deleted row's name
28
+ * cannot reach another row. Serialisable (`toJSON` / `fromJSON`) so a session can keep it.
29
+ */
30
+ export class RowNames {
31
+ byId = new Map();
32
+ byName = new Map();
33
+ counters = new Map();
34
+ name(table, id) {
35
+ const known = this.byId.get(id);
36
+ if (known)
37
+ return known;
38
+ const n = (this.counters.get(table) ?? 0) + 1;
39
+ this.counters.set(table, n);
40
+ const name = `${table}#${n}`;
41
+ this.byId.set(id, name);
42
+ this.byName.set(name, { table, id });
43
+ return name;
44
+ }
45
+ resolve(name) {
46
+ return this.byName.get(name);
47
+ }
48
+ nameOf(id) {
49
+ return this.byId.get(id);
50
+ }
51
+ toJSON() {
52
+ return [...this.byName.entries()].map(([name, { table, id }]) => [name, table, id]);
53
+ }
54
+ static fromJSON(entries) {
55
+ const names = new RowNames();
56
+ for (const [name, table, id] of entries) {
57
+ names.byId.set(id, name);
58
+ names.byName.set(name, { table, id });
59
+ const n = Number(name.slice(name.lastIndexOf('#') + 1));
60
+ if (n > (names.counters.get(table) ?? 0))
61
+ names.counters.set(table, n);
62
+ }
63
+ return names;
64
+ }
65
+ }
66
+ export class PageDocumentError extends Error {
67
+ constructor(message) {
68
+ super(message);
69
+ this.name = 'PageDocumentError';
70
+ }
71
+ }
72
+ const MAX_DEPTH = 32;
73
+ /** Channel actions only a call can do; by text only `end` (17b §9.8). */
74
+ const CALL_ONLY = new Set(['transfer', 'keypad', 'announce', 'voicemail']);
75
+ export function renderDocument(input) {
76
+ const perform = performOf(input.document);
77
+ if (perform?.action.$action === 'channel' && input.channel !== undefined && input.channel !== 'call' && CALL_ONLY.has(perform.action.channel)) {
78
+ throw new PageDocumentError(`the page function returned ${perform.action.channel}, which only a call can do (this is ${input.channel})`);
79
+ }
80
+ if (perform) {
81
+ // An action to run now: rendered as a button on a page of its own, so its rows are checked the same way.
82
+ const inner = renderDocument({ ...input, document: { action: perform.action } });
83
+ const action = inner.actions['action'];
84
+ if (!action)
85
+ throw new PageDocumentError(`the action the page function returned cannot run: ${inner.warnings.join('; ') || 'no row to run it on'}`);
86
+ return { ...inner, auto: { action, key: perform.key ?? actionKey(perform.action), wait: perform.wait ?? true } };
87
+ }
88
+ const { schema, reads, names } = input;
89
+ if (!isPlainObject(input.document))
90
+ throw new PageDocumentError('the page script must return an object');
91
+ const cells = {};
92
+ const actions = {};
93
+ const rows = {};
94
+ const warnings = [];
95
+ /** Rows with a cell on the page, by table (page names). */
96
+ const cellRows = new Map();
97
+ const pendingActions = [];
98
+ const extractions = pageExtractions(input.document, schema, warnings);
99
+ const rowName = (table, id) => {
100
+ const name = names.name(table, id);
101
+ if (!rows[name]) {
102
+ const parent = parentOf(getCompiledTable(schema, table));
103
+ const parentValue = parent ? reads.row(id)?.slots[parent.field]?.value : null;
104
+ rows[name] = { table, id, parentId: typeof parentValue === 'string' ? parentValue : null };
105
+ }
106
+ return name;
107
+ };
108
+ const rowsOnPage = (table) => [...(cellRows.get(table) ?? [])];
109
+ const cellOf = (key, path, value) => {
110
+ const refCell = isRefFieldCell(value) ? value : null;
111
+ const fieldCell = isFieldCell(value) ? value : refCell ? { $field: true, slot: refCell.slot, ...(refCell.readOnly ? { readOnly: true } : {}) } : null;
112
+ const slot = fieldCell ? fieldCell.slot : value;
113
+ if (!isSlotLike(slot))
114
+ return null;
115
+ const ref = slot.ref;
116
+ const read = reads.row(ref.id);
117
+ const table = getCompiledTable(schema, ref.table);
118
+ const def = table?.fields[ref.field];
119
+ if (!read || read.table !== ref.table || !def || !(ref.field in read.slots)) {
120
+ warnings.push(`${path}: a slot this run did not read (${ref.table}.${ref.field}); shown as read-only text`);
121
+ return { kind: 'text', key, path, value: displayable(slot.value) };
122
+ }
123
+ const truth = read.slots[ref.field];
124
+ const name = rowName(ref.table, ref.id);
125
+ const onPage = cellRows.get(ref.table) ?? new Set();
126
+ onPage.add(name);
127
+ cellRows.set(ref.table, onPage);
128
+ const slotId = `${name}.${ref.field}`;
129
+ const isParent = parentOf(table)?.field === ref.field;
130
+ const isLookup = Boolean(def.ref) && !isParent;
131
+ // A ref field is placed with refField: its candidates and their labels are the page's (26 §16.2 #7).
132
+ if (isLookup && fieldCell && !refCell)
133
+ throw new PageDocumentError(`${path}: ${ref.table}.${ref.field} is a ref field: place it with refField(slot, { options })`);
134
+ if (refCell && !isLookup)
135
+ throw new PageDocumentError(`${path}: refField(...) is for a ref field; ${ref.table}.${ref.field} is ${isParent ? 'the parent' : 'not a ref'}`);
136
+ const offered = refCell ? refOptions(path, refCell, def.ref.table) : null;
137
+ // The page decides (19-page-writable-cells): a slot placed as is is read-only, `field(...)` opens
138
+ // it. Never the row id or the parent. A lookup opens where its table's rows are on the page
139
+ // (settled below): the AI cannot make up a row id.
140
+ const fixed = isParent || ref.field === ROW_ID_FIELD;
141
+ if (fieldCell && !fieldCell.readOnly && fixed) {
142
+ warnings.push(`${path}: field(...) cannot open ${ref.table}.${ref.field} (${isParent ? 'the parent' : 'the row id'}); it stays read-only`);
143
+ }
144
+ // A ref field with no row to offer is shown, not opened: nothing to choose (data, not a page mistake).
145
+ const placeWritable = fieldCell !== null && !fieldCell.readOnly && !fixed && (!offered || offered.length > 0);
146
+ const existing = cells[slotId];
147
+ if (existing) {
148
+ existing.writable ||= placeWritable;
149
+ existing.paths.push(path);
150
+ }
151
+ else {
152
+ const cell = {
153
+ slotId,
154
+ ref: { table: ref.table, id: ref.id, field: ref.field },
155
+ row: name,
156
+ label: def.label,
157
+ type: def.dataType,
158
+ choices: def.options,
159
+ value: truth.value,
160
+ state: truth.state,
161
+ reason: truth.reason,
162
+ updatedAt: truth.updatedAt,
163
+ writable: placeWritable,
164
+ paths: [path],
165
+ };
166
+ if (def.description)
167
+ cell.description = def.description;
168
+ if (def.extraction)
169
+ cell.extraction = def.extraction;
170
+ if (isLookup)
171
+ cell.type = 'ref';
172
+ cells[slotId] = cell;
173
+ }
174
+ if (offered) {
175
+ const target = cells[slotId];
176
+ // Writable, the rows offered; read-only, only the labels (who the current value is).
177
+ if (placeWritable)
178
+ target.options = [...new Set([...(target.options ?? []), ...offered.map((o) => o.name)])];
179
+ target.optionLabels = { ...target.optionLabels, ...Object.fromEntries(offered.map((o) => [o.name, o.label])) };
180
+ }
181
+ const node = { kind: 'cell', key, path, slotId, readOnly: !placeWritable };
182
+ // A ref field's definition is always on the page: its meaning is not in its name (26 §16.2 #7).
183
+ if (fieldCell?.expand || refCell)
184
+ node.expand = true;
185
+ return node;
186
+ };
187
+ /** A refField's rows: each must be a row this run read, of the table the field points at (it goes on the page). */
188
+ const refOptions = (path, cell, target) => cell.options.map((option) => {
189
+ const read = reads.row(option.id);
190
+ if (!read || read.table !== target)
191
+ throw new PageDocumentError(`${path}: refField offers ${option.id}, not a ${target} row this run read`);
192
+ return { name: rowName(target, option.id), label: option.label };
193
+ });
194
+ const walk = (key, path, value, depth) => {
195
+ if (depth > MAX_DEPTH)
196
+ throw new PageDocumentError(`${path || 'page'}: nested deeper than ${MAX_DEPTH}`);
197
+ if (value === undefined || typeof value === 'function')
198
+ return null;
199
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
200
+ return { kind: 'text', key, path, value };
201
+ }
202
+ if (isAction(value)) {
203
+ pendingActions.push({ path, value });
204
+ return { kind: 'action', key, path, actionId: path };
205
+ }
206
+ const cell = cellOf(key, path, value);
207
+ if (cell)
208
+ return cell;
209
+ if (Array.isArray(value)) {
210
+ const children = value
211
+ .map((item, i) => walk(String(i), join(path, String(i)), item, depth + 1))
212
+ .filter((n) => n !== null);
213
+ return { kind: 'block', key, path, list: true, children };
214
+ }
215
+ if (isPlainObject(value)) {
216
+ if (isRowLike(value)) {
217
+ warnings.push(`${path}: a whole row is not a cell; put its slots on the page`);
218
+ }
219
+ const block = { kind: 'block', key, path, list: false, children: [] };
220
+ if (typeof value.$title === 'string')
221
+ block.title = value.$title;
222
+ if (typeof value.$description === 'string')
223
+ block.description = value.$description;
224
+ pageMarks(block, value, path, depth === 0, warnings);
225
+ for (const [childKey, child] of Object.entries(value)) {
226
+ if (childKey.startsWith('$'))
227
+ continue;
228
+ // A button's id is the keys joined with '.': a key with a '.' in it would collide with a nested one (26 §16.2 #3).
229
+ if (childKey.includes('.'))
230
+ throw new PageDocumentError(`${join(path, childKey) || 'page'}: a key on the page cannot contain '.' (${JSON.stringify(childKey)})`);
231
+ const node = walk(childKey, join(path, childKey), child, depth + 1);
232
+ if (node)
233
+ block.children.push(node);
234
+ }
235
+ return block;
236
+ }
237
+ return { kind: 'text', key, path, value: String(value) };
238
+ };
239
+ const root = walk('', '', input.document, 0);
240
+ // Every row on the page is known now: what lookups and row parameters may choose.
241
+ const env = { schema, reads, rowName, rowsOnPage, warnings, ...(input.channel ? { channel: input.channel } : {}) };
242
+ for (const { path, value } of pendingActions) {
243
+ const rendered = renderAction(path, value, env);
244
+ if (rendered)
245
+ actions[path] = rendered;
246
+ }
247
+ // An action that cannot be pressed is not on the page.
248
+ const prune = (block) => {
249
+ block.children = block.children.filter((child) => child.kind !== 'action' || child.actionId in actions);
250
+ for (const child of block.children)
251
+ if (child.kind === 'block')
252
+ prune(child);
253
+ };
254
+ prune(root);
255
+ const page = { root, cells, actions, rows, warnings };
256
+ if (extractions.size)
257
+ page.extractions = Object.fromEntries(extractions);
258
+ return page;
259
+ }
260
+ /**
261
+ * The action as the AI presses it, or null when it cannot be pressed on this page: with a warning when
262
+ * the page placed it wrong; silently when a required row choice has no row on the page (no injury yet
263
+ * to link a provider to), which is the data, not a mistake.
264
+ */
265
+ function renderAction(path, action, env) {
266
+ const rendered = renderActionOf(path, action, env);
267
+ if (!rendered?.input)
268
+ return rendered;
269
+ const missing = unfillable(rendered.input.properties, rendered.input.required);
270
+ if (!missing)
271
+ return rendered;
272
+ // Said, so whoever reads the logs knows why the AI had no such button (25 §10).
273
+ env.warnings.push(`${path}: ${rendered.name} left off the page: no ${missing.table} row to choose for ${missing.key}`);
274
+ return null;
275
+ }
276
+ /** The first required row choice with no row to choose, at any depth; null when every one can be filled. */
277
+ function unfillable(properties, required, prefix = '') {
278
+ for (const key of required ?? []) {
279
+ const prop = properties[key];
280
+ if (prop?.type === 'ref' && !prop.enum.length)
281
+ return { key: `${prefix}${key}`, table: prop.table };
282
+ if (prop?.type === 'object') {
283
+ const inner = unfillable(prop.properties, prop.required, `${prefix}${key}.`);
284
+ if (inner)
285
+ return inner;
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+ function renderActionOf(path, action, env) {
291
+ const { schema, reads, rowName, warnings } = env;
292
+ if (action.$action === 'channel') {
293
+ // Not on this channel (a transfer by text): the types keep it to `ctx.call`, so reaching here is a bug.
294
+ if (env.channel !== undefined && env.channel !== 'call' && CALL_ONLY.has(action.channel)) {
295
+ throw new PageDocumentError(`${path}: ${action.channel} is a call action, and this is ${env.channel} (use ctx.call only on the call page)`);
296
+ }
297
+ return channelButton(path, action);
298
+ }
299
+ if (action.$action === 'createRow') {
300
+ const table = getCompiledTable(schema, action.table);
301
+ if (!table)
302
+ throw new PageDocumentError(`${path}: createRow on ${action.table}, not a table of this sequence`);
303
+ const parent = parentOf(table);
304
+ for (const key of Object.keys(action.prefill)) {
305
+ if (!getTableField(table, key))
306
+ throw new PageDocumentError(`${path}: createRow(${action.table}) prefills ${key}, not a field of it`);
307
+ }
308
+ const properties = {};
309
+ for (const fieldName of action.fields) {
310
+ if (!getTableField(table, fieldName))
311
+ throw new PageDocumentError(`${path}: createRow field ${action.table}.${fieldName} is not in this sequence`);
312
+ if (parent?.field === fieldName)
313
+ throw new PageDocumentError(`${path}: the parent ${fieldName} is prefilled, not a field to fill`);
314
+ if (fieldName in action.prefill)
315
+ continue;
316
+ properties[fieldName] = expandParam(path, { $schemaField: `${action.table}.${fieldName}` }, env);
317
+ }
318
+ // Where the new row hangs: prefilled, else the one parent this run's reads of the table were anchored on.
319
+ let parentId = null;
320
+ if (parent) {
321
+ const preset = action.prefill[parent.field];
322
+ const parents = reads.parentsOf(action.table);
323
+ parentId = typeof preset === 'string' ? preset : parents.length === 1 ? parents[0] : null;
324
+ if (!parentId) {
325
+ warnings.push(`${path}: createRow(${action.table}) needs ${parent.field} prefilled: this run read ${parents.length} parents of it; left off the page`);
326
+ return null;
327
+ }
328
+ // The parent row must be a row this run read, of the parent field's table: it goes on the page,
329
+ // so pressing the button can write under it.
330
+ const parentRow = reads.row(parentId);
331
+ if (!parentRow || parentRow.table !== parent.table) {
332
+ warnings.push(`${path}: createRow(${action.table}) parent ${parentId} is not a ${parent.table} row this run read; left off the page`);
333
+ return null;
334
+ }
335
+ rowName(parentRow.table, parentId);
336
+ }
337
+ const set = { ...action.prefill };
338
+ if (parent)
339
+ delete set[parent.field];
340
+ const under = parent && parentId ? ` under ${rowName(parent.table, parentId)}` : '';
341
+ return {
342
+ id: path,
343
+ kind: 'createRow',
344
+ name: action.label ?? `Add ${table.label}`,
345
+ description: action.description ?? `Adds a new ${table.label} (${action.table})${under}.`,
346
+ input: { type: 'object', properties, required: action.required.filter((r) => r in properties) },
347
+ server: { kind: 'createRow', table: action.table, parentId, set, fields: action.fields, required: action.required },
348
+ };
349
+ }
350
+ if (action.$action === 'deleteRow') {
351
+ const read = reads.row(action.rowId);
352
+ if (!read) {
353
+ warnings.push(`${path}: deleteRow on a row this run did not read; left off the page`);
354
+ return null;
355
+ }
356
+ const name = rowName(read.table, action.rowId);
357
+ return {
358
+ id: path,
359
+ kind: 'deleteRow',
360
+ name: action.label ?? `Delete ${name}`,
361
+ description: action.description ?? `Delete ${name} and everything under it.`,
362
+ input: null,
363
+ server: { kind: 'deleteRow', table: read.table, rowId: action.rowId, ...(action.reason ? { reason: action.reason } : {}) },
364
+ };
365
+ }
366
+ if (action.$action === 'message')
367
+ return messageButton(path, action, env);
368
+ if (action.$action === 'followUp')
369
+ return followUpButton(path, action);
370
+ if (action.$action === 'function') {
371
+ const { input, prefill } = actionInput(path, action.name, action.parameters, action.prefill ?? {}, env);
372
+ return {
373
+ id: path,
374
+ kind: 'function',
375
+ name: action.label ?? action.name,
376
+ description: action.description ?? `Runs ${action.name}.`,
377
+ input,
378
+ server: { kind: 'function', name: action.name, ...(action.say ? { say: action.say } : {}), ...(Object.keys(prefill).length ? { prefill } : {}) },
379
+ };
380
+ }
381
+ const { input, prefill } = actionInput(path, action.name, action.parameters, action.prefill, env);
382
+ return {
383
+ id: path,
384
+ kind: 'custom',
385
+ name: action.name,
386
+ description: action.description,
387
+ input,
388
+ server: { kind: 'custom', name: action.name, prefill },
389
+ };
390
+ }
391
+ /**
392
+ * What the AI fills of an action's parameters, and what the page fixed. A prefilled row is checked and
393
+ * put on the page; a `choose(...)` (25 §10) becomes the AI's choice among exactly those rows, each shown
394
+ * with its label.
395
+ */
396
+ function actionInput(path, name, parameters, given, env) {
397
+ const declared = parameters?.properties ?? {};
398
+ const properties = {};
399
+ const prefill = {};
400
+ for (const key of Object.keys(given)) {
401
+ if (!(key in declared))
402
+ throw new PageDocumentError(`${path}: ${name} prefills ${key}, not one of its parameters`);
403
+ }
404
+ for (const [key, param] of Object.entries(declared)) {
405
+ const value = given[key];
406
+ if (isChoose(value)) {
407
+ properties[key] = chooseParam(`${path}: ${name}({ ${key} })`, param, value, env);
408
+ continue;
409
+ }
410
+ if (key in given) {
411
+ checkPrefill(`${path}: ${name}({ ${key} })`, param, value, env);
412
+ prefill[key] = value;
413
+ continue;
414
+ }
415
+ properties[key] = expandParam(`${path}.${key}`, param, env);
416
+ }
417
+ const input = Object.keys(properties).length
418
+ ? { type: 'object', properties, required: (parameters?.required ?? []).filter((r) => r in properties) }
419
+ : null;
420
+ return { input, prefill };
421
+ }
422
+ /** A row parameter's candidates, as `choose(...)` gave them: rows this run read, of the parameter's table. */
423
+ function chooseParam(where, param, chosen, env) {
424
+ const table = isSchemaFieldParam(param) ? rowTableOf(where, param, env.schema) : null;
425
+ if (!table)
426
+ throw new PageDocumentError(`${where}: choose(...) is for a row parameter (schemaField('table.id') or a lookup)`);
427
+ if (chosen.table !== null && chosen.table !== table)
428
+ throw new PageDocumentError(`${where}: choose(...) gives ${chosen.table} rows, the parameter takes ${table}`);
429
+ const names = [];
430
+ const labels = {};
431
+ for (const option of chosen.options) {
432
+ const read = env.reads.row(option.id);
433
+ if (!read || read.table !== table)
434
+ throw new PageDocumentError(`${where}: choose(...) offers ${option.id}, not a ${table} row this run read`);
435
+ const rowName = env.rowName(table, option.id);
436
+ names.push(rowName);
437
+ labels[rowName] = option.label;
438
+ }
439
+ // Each row with its label, as a ref field's candidates are written (26 §16.2 #7).
440
+ const help = isSchemaFieldParam(param) ? (param.extraction ?? schemaFieldOf(where, param, env.schema).def?.extraction) : undefined;
441
+ return { type: 'ref', table, enum: names, labels, ...(help ? { description: help } : {}) };
442
+ }
443
+ /** `table.field` of a schema-field parameter, checked against this sequence. */
444
+ function schemaFieldOf(where, param, schema) {
445
+ const [table, field, ...rest] = param.$schemaField.split('.');
446
+ const def = table ? getCompiledTable(schema, table) : undefined;
447
+ if (!table || !field || rest.length || !def)
448
+ throw new PageDocumentError(`${where}: schemaField('${param.$schemaField}') is not a table.field of this sequence`);
449
+ if (field === 'id')
450
+ return { table, field, def: null };
451
+ const fieldDef = getTableField(def, field);
452
+ if (!fieldDef)
453
+ throw new PageDocumentError(`${where}: schemaField('${param.$schemaField}') is not a field of this sequence`);
454
+ return { table, field, def: fieldDef };
455
+ }
456
+ /** The table a parameter chooses a row of (`table.id`, a lookup), or null. */
457
+ function rowTableOf(where, param, schema) {
458
+ const { table, def } = schemaFieldOf(where, param, schema);
459
+ if (!def)
460
+ return table;
461
+ return def.ref ? def.ref.table : null;
462
+ }
463
+ /** A parameter as JSON Schema for the AI: schema fields filled in from the schema, row choices as enums. */
464
+ function expandParam(where, param, env) {
465
+ if (isSchemaFieldParam(param)) {
466
+ const rowTable = rowTableOf(where, param, env.schema);
467
+ const { table, def } = schemaFieldOf(where, param, env.schema);
468
+ if (rowTable) {
469
+ const names = env.rowsOnPage(rowTable);
470
+ const help = param.extraction ?? def?.extraction;
471
+ return { type: 'ref', table: rowTable, enum: names, ...(help ? { description: help } : {}) };
472
+ }
473
+ // The page's extraction replaces the schema's; type and allowed values stay the schema's.
474
+ const fieldDef = param.extraction !== undefined ? { ...def, extraction: param.extraction } : def;
475
+ return fieldDef.dataType === 'contact_schedule' ? scheduleProperty(fieldDef) : scalarProperty(fieldDef);
476
+ }
477
+ if (param.type === 'array') {
478
+ const out = { type: 'array', items: expandParam(`${where}[]`, param.items, env) };
479
+ if (param.description)
480
+ out.description = param.description;
481
+ return out;
482
+ }
483
+ if (param.type === 'object') {
484
+ const properties = {};
485
+ for (const [key, child] of Object.entries(param.properties))
486
+ properties[key] = expandParam(`${where}.${key}`, child, env);
487
+ const out = { type: 'object', properties };
488
+ if (param.required)
489
+ out.required = [...param.required];
490
+ if (param.description)
491
+ out.description = param.description;
492
+ return out;
493
+ }
494
+ return { ...param };
495
+ }
496
+ /**
497
+ * A prefilled row argument, at any depth (`{ target: { id } }`, a list of rows), must be a row this run
498
+ * read, of its table: it then goes on the page, so the action may write it.
499
+ */
500
+ function checkPrefill(where, param, value, env) {
501
+ if (value === null || value === undefined)
502
+ return;
503
+ if (isSchemaFieldParam(param)) {
504
+ const rowTable = rowTableOf(where, param, env.schema);
505
+ if (!rowTable)
506
+ return;
507
+ const read = typeof value === 'string' ? env.reads.row(value) : undefined;
508
+ if (!read || read.table !== rowTable)
509
+ throw new PageDocumentError(`${where} is not a ${rowTable} row this run read`);
510
+ env.rowName(read.table, value);
511
+ return;
512
+ }
513
+ if (param.type === 'array') {
514
+ if (!Array.isArray(value))
515
+ throw new PageDocumentError(`${where} must be a list`);
516
+ value.forEach((item, i) => checkPrefill(`${where}[${i}]`, param.items, item, env));
517
+ return;
518
+ }
519
+ if (param.type === 'object') {
520
+ if (!isPlainObject(value))
521
+ throw new PageDocumentError(`${where} must be an object`);
522
+ for (const [key, child] of Object.entries(param.properties))
523
+ checkPrefill(`${where}.${key}`, child, value[key], env);
524
+ }
525
+ }
526
+ /** A schema field's argument: the field's own type and options. */
527
+ function scalarProperty(def) {
528
+ const help = describeField(def);
529
+ if (def.dataType === 'multi_select')
530
+ return { type: 'multi_select', enum: def.options ?? [], ...help };
531
+ if (def.dataType === 'select')
532
+ return { type: 'select', enum: def.options ?? [], ...help };
533
+ return { type: def.dataType, ...help };
534
+ }
535
+ /** A `contact_schedule` field: its own structure (`ContactSchedule`), not text. */
536
+ function scheduleProperty(def) {
537
+ const window = {
538
+ type: 'object',
539
+ properties: {
540
+ start: { type: 'string', description: 'HH:mm' },
541
+ end: { type: 'string', description: 'HH:mm, after start; may be 24:00' },
542
+ },
543
+ required: ['start', 'end'],
544
+ };
545
+ const windows = { type: 'array', items: window, description: 'sorted by start, not overlapping; empty: no contact' };
546
+ const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
547
+ return {
548
+ type: 'object',
549
+ 'x-type': 'contact_schedule',
550
+ ...describeField(def),
551
+ properties: {
552
+ weekly: {
553
+ type: 'object',
554
+ nullable: true,
555
+ description: 'all seven weekdays; null: no weekly restriction',
556
+ properties: Object.fromEntries(days.map((day) => [day, windows])),
557
+ required: [...days],
558
+ },
559
+ dateOverrides: {
560
+ type: 'array',
561
+ description: 'dates whose windows replace weekly',
562
+ items: {
563
+ type: 'object',
564
+ properties: { dates: { type: 'array', items: { type: 'string', description: 'YYYY-MM-DD' } }, windows },
565
+ required: ['dates', 'windows'],
566
+ },
567
+ },
568
+ },
569
+ required: ['weekly', 'dateOverrides'],
570
+ };
571
+ }
572
+ /**
573
+ * An argument's help (26 §16.2 #5a): how to fill it, the page's (`schemaField(…, { extraction })`) or else
574
+ * the field's; none when neither says. The label is not repeated in it; how its type is written is the
575
+ * system message's (26 §15.8).
576
+ */
577
+ function describeField(def) {
578
+ return def.extraction ? { description: def.extraction } : {};
579
+ }
580
+ // ---------------------------------------------------------------------------------------------
581
+ function join(path, key) {
582
+ return path ? `${path}.${key}` : key;
583
+ }
584
+ function isPlainObject(value) {
585
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
586
+ }
587
+ function isSlotLike(value) {
588
+ if (!isPlainObject(value) || !isPlainObject(value.ref) || !('state' in value))
589
+ return false;
590
+ const ref = value.ref;
591
+ return typeof ref.table === 'string' && typeof ref.id === 'string' && typeof ref.field === 'string';
592
+ }
593
+ function isFieldCell(value) {
594
+ return isPlainObject(value) && value.$field === true;
595
+ }
596
+ function isRefFieldCell(value) {
597
+ return isPlainObject(value) && value.$refField === true && Array.isArray(value.options);
598
+ }
599
+ function isAction(value) {
600
+ return (isPlainObject(value) &&
601
+ (value.$action === 'createRow' ||
602
+ value.$action === 'deleteRow' ||
603
+ value.$action === 'custom' ||
604
+ (value.$action === 'function' && typeof value.name === 'string') ||
605
+ (value.$action === 'message' && typeof value.key === 'string') ||
606
+ (value.$action === 'followUp' && Array.isArray(value.channels)) ||
607
+ (value.$action === 'channel' && typeof value.channel === 'string')));
608
+ }
609
+ /** The page function returned an action to run now (`perform(…)`, or the action itself). */
610
+ function performOf(document) {
611
+ if (isPlainObject(document) && document.$perform === true && isAction(document.action))
612
+ return document;
613
+ if (isAction(document))
614
+ return { $perform: true, action: document };
615
+ return null;
616
+ }
617
+ /**
618
+ * The run key of a button on the page (17b §7): the action's own key, whoever runs it — the model
619
+ * pressing it, or the page function returning it — not its place on the page, which moves with rows.
620
+ */
621
+ export function runKeyOf(action) {
622
+ const server = action.server;
623
+ if (server.kind === 'custom')
624
+ return server.name;
625
+ if (server.kind === 'createRow')
626
+ return `createRow:${server.table}`;
627
+ if (server.kind === 'deleteRow')
628
+ return `deleteRow:${server.rowId}`;
629
+ if (server.kind === 'function')
630
+ return server.name;
631
+ if (server.kind === 'message' || server.kind === 'followUp')
632
+ return server.key;
633
+ return actionKey(server.spec);
634
+ }
635
+ /** The run key of an action the page function returned, when it names none. */
636
+ export function actionKey(action) {
637
+ if (action.$action === 'custom')
638
+ return action.name;
639
+ if (action.$action === 'function')
640
+ return action.name;
641
+ if (action.$action === 'message' || action.$action === 'followUp')
642
+ return action.key;
643
+ if (action.$action === 'createRow')
644
+ return `createRow:${action.table}`;
645
+ if (action.$action === 'deleteRow')
646
+ return `deleteRow:${action.rowId}`;
647
+ if (action.channel === 'keypad' || action.channel === 'announce')
648
+ return action.key;
649
+ return action.channel;
650
+ }
651
+ /** Marks removed with the todos (24 §2.3): a page still writing one is told what replaced it. */
652
+ /** The marks a page object may carry: `$title`, `$description` on any block; `$guide`, `$extraction` on the root. */
653
+ const KNOWN_MARKS = new Set(['$title', '$description', '$guide', '$extraction']);
654
+ const REMOVED_MARKS = {
655
+ $say: 'write $guide on the page\'s root',
656
+ $done: 'show what is done as a result, or stop guiding to it',
657
+ $locked: 'leave out what the AI must not touch; say why in plain text',
658
+ $summary: 'nothing is folded any more',
659
+ $recommend: 'write $guide on the page\'s root',
660
+ };
661
+ /**
662
+ * The page's `$extraction` (26 §15.3): how to fill a field on this page, by `table.field`, replacing the
663
+ * schema's for every place it is on the page. One per field for the whole page, so a field means the
664
+ * same everywhere on it. A `table.field` the schema does not have is warned and left out.
665
+ */
666
+ function pageExtractions(document, schema, warnings) {
667
+ const out = new Map();
668
+ const value = isPlainObject(document) ? document.$extraction : undefined;
669
+ if (value === undefined || value === null)
670
+ return out;
671
+ if (!isPlainObject(value))
672
+ throw new PageDocumentError('page: $extraction must be an object of table.field → text');
673
+ for (const [key, text] of Object.entries(value)) {
674
+ if (typeof text !== 'string')
675
+ throw new PageDocumentError(`page: $extraction['${key}'] must be text`);
676
+ const [table, field, ...rest] = key.split('.');
677
+ if (!table || !field || rest.length || !getCompiledTable(schema, table)?.fields[field]) {
678
+ warnings.push(`page: $extraction['${key}'] is not a table.field of the schema; left out`);
679
+ continue;
680
+ }
681
+ out.set(key, text);
682
+ }
683
+ return out;
684
+ }
685
+ /** `$guide` and `$extraction` on the root (24 §2.1, 26 §15.3); on any other block they are refused, as the removed marks are. */
686
+ function pageMarks(block, value, path, isRoot, warnings) {
687
+ const where = path || 'page';
688
+ for (const [mark, instead] of Object.entries(REMOVED_MARKS)) {
689
+ if (value[mark] !== undefined)
690
+ throw new PageDocumentError(`${where}: ${mark} was removed (24 §2.3): ${instead}`);
691
+ }
692
+ // A misspelt mark (`$titel`) would otherwise do nothing, silently (26 §16.2 #4).
693
+ for (const mark of Object.keys(value)) {
694
+ if (mark.startsWith('$') && !KNOWN_MARKS.has(mark))
695
+ warnings.push(`${where}: ${mark} is not a page mark (${[...KNOWN_MARKS].join(', ')}); ignored`);
696
+ }
697
+ if (!isRoot && value.$extraction !== undefined)
698
+ throw new PageDocumentError(`${where}: $extraction is the page's: write it on the page's root, one per page (26 §15.3)`);
699
+ if (value.$guide === undefined || value.$guide === null)
700
+ return;
701
+ if (typeof value.$guide !== 'string')
702
+ throw new PageDocumentError(`${where}: $guide must be text`);
703
+ if (!isRoot)
704
+ throw new PageDocumentError(`${where}: $guide is the page's: write it on the page's root, one per page (24 §2.1)`);
705
+ if (value.$guide.trim())
706
+ block.guide = value.$guide;
707
+ }
708
+ const CHANNEL_NAMES = {
709
+ transfer: ['Transfer to a person', 'Hand the call to a person. Press it only when the customer wants a person or the page says so.'],
710
+ keypad: ['Collect a keypress', 'The customer answers by pressing a key on their phone.'],
711
+ announce: ['Play a fixed statement', 'Plays a fixed statement, word for word.'],
712
+ end: ['End the conversation', 'Ends the conversation after your last line (say goodbye in the same step).'],
713
+ voicemail: ['Leave the voicemail', 'Leaves a fixed message after the beep, then hangs up.'],
714
+ };
715
+ const BACKGROUND_TRANSFER = [
716
+ 'Call a person in the background',
717
+ 'Calls a person while you keep helping the customer; the customer is put through once the person takes the call. Press it only when the customer wants a person or the page says so.',
718
+ ];
719
+ /** A channel action as a button: nothing for the AI to fill; what it does stays on the server. */
720
+ function channelButton(path, action) {
721
+ const [name, description] = action.channel === 'transfer' && action.mode === 'background' ? BACKGROUND_TRANSFER : CHANNEL_NAMES[action.channel];
722
+ const label = 'label' in action ? action.label : undefined;
723
+ const text = 'description' in action ? action.description : undefined;
724
+ return {
725
+ id: path,
726
+ kind: 'channel',
727
+ name: label ?? name,
728
+ description: text ?? (action.channel === 'keypad' ? `${description} It says: "${action.say}"` : description),
729
+ input: null,
730
+ server: { kind: 'channel', spec: action },
731
+ };
732
+ }
733
+ /**
734
+ * A text or an email as a button (25 §8): the contact it goes to must be a contact row this run read (it
735
+ * goes on the page); what the page left out (`body`, an email's `subject`) the AI fills.
736
+ */
737
+ function messageButton(path, action, env) {
738
+ if (action.to !== undefined) {
739
+ const read = env.reads.row(action.to);
740
+ if (!read || read.table !== 'contact') {
741
+ env.warnings.push(`${path}: ${action.key} goes to ${action.to}, not a contact this run read; left off the page`);
742
+ return null;
743
+ }
744
+ env.rowName('contact', action.to);
745
+ }
746
+ const what = action.channel === 'sms' ? 'text' : 'email';
747
+ const properties = {};
748
+ if (action.channel === 'email' && action.subject === undefined)
749
+ properties.subject = { type: 'string', description: 'The email\'s subject line' };
750
+ if (action.body === undefined)
751
+ properties.body = { type: 'string', description: `The ${what} to send, in full, in the customer's language` };
752
+ const to = action.to !== undefined ? env.rowName('contact', action.to) : 'the customer';
753
+ const server = { kind: 'message', channel: action.channel, key: action.key, to: action.to ?? null };
754
+ if (action.body !== undefined)
755
+ server.body = action.body;
756
+ if (action.subject !== undefined)
757
+ server.subject = action.subject;
758
+ if (action.say !== undefined)
759
+ server.say = action.say;
760
+ return {
761
+ id: path,
762
+ kind: 'message',
763
+ name: action.label ?? (action.channel === 'sms' ? 'Send a text' : 'Send an email'),
764
+ description: action.description ??
765
+ (action.body !== undefined
766
+ ? `Sends the fixed ${what} "${action.label ?? action.key}" to ${to} now.`
767
+ : `Sends a ${what} you write to ${to} now.`),
768
+ input: Object.keys(properties).length ? { type: 'object', properties, required: Object.keys(properties) } : null,
769
+ server,
770
+ };
771
+ }
772
+ const CHANNEL_WORDS = { call: 'a call', sms: 'a text', email: 'an email' };
773
+ /** The follow-up button (25 §5): the AI gives the channel, the time and the customer's own words. */
774
+ function followUpButton(path, action) {
775
+ const ways = action.channels.map((c) => CHANNEL_WORDS[c]).join(' or ');
776
+ return {
777
+ id: path,
778
+ kind: 'followUp',
779
+ name: action.label ?? 'Book the follow-up they asked for',
780
+ description: action.description ??
781
+ `The customer asked for ${ways} later, or agreed to one, at a time they said: press it then, and tell them it is booked. Not for a follow-up they did not ask for.`,
782
+ input: {
783
+ type: 'object',
784
+ properties: {
785
+ channel: { type: 'select', enum: [...action.channels], description: 'How they want to be reached' },
786
+ at: { type: 'datetime', description: 'When, as they said it, with the offset of their time zone; for a vague time ("tomorrow afternoon") a time inside it' },
787
+ customer_quote: { type: 'string', description: 'Their own words asking for it or agreeing to it, as said' },
788
+ note: { type: 'string', description: 'What it is for, in a few words' },
789
+ },
790
+ required: ['channel', 'at', 'customer_quote'],
791
+ },
792
+ server: { kind: 'followUp', key: action.key, channels: [...action.channels] },
793
+ };
794
+ }
795
+ function isSchemaFieldParam(value) {
796
+ return isPlainObject(value) && typeof value.$schemaField === 'string';
797
+ }
798
+ function isRowLike(value) {
799
+ return typeof value.id === 'string' && Object.values(value).some(isSlotLike);
800
+ }
801
+ function displayable(value) {
802
+ if (value === null || value === undefined)
803
+ return null;
804
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
805
+ return value;
806
+ return JSON.stringify(value);
807
+ }
808
+ /**
809
+ * The page to show when the page script failed (15 §10.9): the last good page, every cell read-only
810
+ * and no actions, marked stale; without one, an empty page.
811
+ */
812
+ export function fallbackPage(last, reason) {
813
+ if (!last) {
814
+ return {
815
+ root: { kind: 'block', key: '', path: '', list: false, children: [] },
816
+ cells: {},
817
+ actions: {},
818
+ rows: {},
819
+ warnings: [reason],
820
+ fallback: { reason, stale: false },
821
+ };
822
+ }
823
+ const readOnly = (node) => node.kind === 'cell'
824
+ ? { ...node, readOnly: true }
825
+ : node.kind === 'action'
826
+ ? null
827
+ : node.kind === 'block'
828
+ ? { ...node, children: node.children.map(readOnly).filter((n) => n !== null) }
829
+ : node;
830
+ return {
831
+ root: readOnly(last.root),
832
+ cells: Object.fromEntries(Object.entries(last.cells).map(([id, cell]) => [id, { ...cell, writable: false }])),
833
+ actions: {},
834
+ rows: last.rows,
835
+ ...(last.extractions ? { extractions: last.extractions } : {}),
836
+ warnings: [...last.warnings, reason],
837
+ fallback: { reason, stale: true },
838
+ };
839
+ }
840
+ //# sourceMappingURL=render.js.map