sygnal 5.4.0 → 6.1.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 (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,484 @@
1
+ /*
2
+ * PLAN-6 A-1 (0-S2): the agent layer. `agentTools(app | runtime, options)` turns the `agent`
3
+ * statics of an app's shown instances into tools, and runs tool calls as actions with
4
+ * `cause: 'agent'`, through the same queue, reducers and flush as a click. The chat behavior
5
+ * (L-3), WebMCP (A-2), the command bar (M-3), the dev endpoint (E-1) and renderComponent's
6
+ * `t.tools()` / `t.callTool()` / `t.agentContext()` (A-4) all use it.
7
+ *
8
+ * Comp.agent = { name, description?, read?, label?, untrusted?,
9
+ * actions: { ACTION: { description, input?, consequential?, when?, idempotent? } } }
10
+ *
11
+ * It reaches the app only through the runtime API (`root`, `get`, `dispatch`, `flushed`,
12
+ * `addHooks`) and one hook layer, so the core pays 0 bytes:
13
+ * - discovery: a walk of the shown instances (`InstanceView.shown`: hidden Switchable pages stay
14
+ * alive and are filtered), redone at most once per flush after onCreate / onDispose / onPatch /
15
+ * onReducer marked it dirty; subscribers hear about a changed tool list or context once per
16
+ * flush (after `flushed()`). A `setState` on a hidden page is published on the next flush that
17
+ * fires one of those hooks (G-595);
18
+ * - Collection items: one tool per item declaration with a key parameter (`id`, or `item` when
19
+ * the input has its own `id`, D265), an enum of the live keys (`state.id`, G-592) labelled by
20
+ * `agent.label(state)` (D258; consumers strip user-entered labels with `unlabel`, G-644); a key hidden by the Collection's filter gets an error that says
21
+ * so (D257);
22
+ * - a call: serialized (D260), then the tool, the input (`parseInput`: unwrap, repair, validate),
23
+ * the target, `when`, `confirm` for a consequential action (awaited BEFORE dispatch, then the
24
+ * target resolved and `when` checked again), dispatch with cause 'agent', `flushed()`, and the
25
+ * outcome measured by `wrapHandler` on that action only: success when a STATE handler changed
26
+ * the state structurally, a sink got a value or an EFFECT ran; `abort(reason)` refuses with the
27
+ * reason; otherwise "changed nothing" (`idempotent: true`: `{ ok: true, unchanged: true }`, D256);
28
+ * - `when` and `read`: cached by state identity per instance (reducers are immutable);
29
+ * - HMR: given the `run()` result, it re-binds to `app.__runtime` after the root's dispose.
30
+ * Diagnostics (dev, once each): SYG240 an input with no JSON Schema form (the tool is left out;
31
+ * `list({ all: true })` shows it with `error`), SYG241 `read` / `when` / `label` threw, SYG243 a
32
+ * lossy input schema, SYG440 two declarations with one name, SYG441 Collection items with no or
33
+ * duplicate keys.
34
+ */
35
+ import {abortReason, isAbort, ORIGINAL} from '../../../shared'
36
+ import {objIsEqual} from '../../../cycle/state/objIsEqual'
37
+ import {report} from '../../diagnostics/index'
38
+ import {toJsonSchema, parseInput, isObj} from '../schema/index'
39
+
40
+ /** One tool, as an agent sees it */
41
+ export interface AgentTool {
42
+ name: string
43
+ description: string
44
+ inputSchema: any
45
+ annotations: {readOnlyHint?: true; consequentialHint?: true; untrustedContentHint?: true}
46
+ /** a declared tool that isn't offered (SYG240), in `list({ all: true })` only */
47
+ error?: string
48
+ }
49
+ export interface ConfirmInfo {tool: string; component: string; action: string; description: string; input: any; key?: any; label?: string}
50
+ export type Confirm = boolean | ((info: ConfirmInfo) => boolean | Promise<boolean>)
51
+ export type AgentResult = {ok: boolean; [k: string]: any}
52
+ /** internal (A-2): one declaration's offered tools */
53
+ export interface AgentGroup {name: string; untrusted?: boolean; read: boolean; tools: string[]; enums: Set<string>; labels?: Record<string, string>; reader?: string}
54
+ export interface AgentToolsOptions {
55
+ /** consequential calls: resolve true to run (default: decline) */
56
+ confirm?: Confirm
57
+ /** run calls one at a time, in call order (default true, D260) */
58
+ serial?: boolean
59
+ /** numeric / boolean strings coerced where the schema says so (default true, D264) */
60
+ repair?: boolean
61
+ /** only this instance (an InstanceView or id) and its descendants */
62
+ from?: any
63
+ /** only these components' declarations */
64
+ components?: any[]
65
+ }
66
+
67
+ const snake = (s: string) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/\W+/g, '_').toLowerCase()
68
+ const keyOf = (iv: any) => iv.state?.id
69
+ const done = new WeakMap<object, Set<string>>()
70
+ /** a dev diagnostic, once per (declaration, what); 'error' mode rethrows outside the caller */
71
+ const dev = (code: string, decl: any, what: string, component: string, message: string) => {
72
+ let s = done.get(decl)
73
+ if (!s) done.set(decl, s = new Set())
74
+ if (s.has(code + what)) return
75
+ s.add(code + what)
76
+ // severities are passed (DEV_CODE_SEVERITY, kept out of the main bundle's table): 240 / 241 error, 243 / 440 / 441 warn
77
+ try { report(code, {component, message, severity: code < 'SYG242' ? 'error' : 'warn'}) } catch (e) { queueMicrotask(() => { throw e }) }
78
+ }
79
+
80
+ /** the string `enum` / `const` values anywhere in a JSON Schema (the app's own words: filter names, statuses) */
81
+ function enumsOf(s: any, out: Set<string>, d = 0): Set<string> {
82
+ if (!s || typeof s != 'object' || d > 20) return out
83
+ if (Array.isArray(s)) { for (const x of s) enumsOf(x, out, d + 1); return out }
84
+ for (const v of Array.isArray(s.enum) ? s.enum : []) if (typeof v == 'string') out.add(v)
85
+ if (typeof s.const == 'string') out.add(s.const)
86
+ for (const k in s) if (k != 'enum' && k != 'const' && s[k] && typeof s[k] == 'object') enumsOf(s[k], out, d + 1)
87
+ return out
88
+ }
89
+
90
+ /** keys whose string values are the app's own (ids and enum-like fields), not user text (G-623) */
91
+ const OWN_KEYS = /^(id|status|type|kind)$/
92
+
93
+ /**
94
+ * Whether a `read` projection holds user text, when its declaration doesn't say (`untrusted`
95
+ * unset; WebMCP's SYG244, the chat behavior's app-state block). The rule (G-623), kept simple on
96
+ * purpose: any string is user text except a value under a key named `id`, `status`, `type` or
97
+ * `kind` (at any depth), and a value that is one of the string enum / const values of the same
98
+ * declaration's action inputs (a filter name the model can set). Declare `untrusted` to be exact.
99
+ */
100
+ export function hasUserText(v: any, enums?: Set<string>, d = 0): boolean {
101
+ if (d > 20) return false
102
+ if (typeof v == 'string') return !enums?.has(v)
103
+ if (!v || typeof v != 'object') return false
104
+ if (Array.isArray(v)) return v.some(x => hasUserText(x, enums, d + 1))
105
+ return Object.keys(v).some(k => !OWN_KEYS.test(k) && hasUserText(v[k], enums, d + 1))
106
+ }
107
+
108
+ /**
109
+ * D286 / G-644: whether an item declaration's `agent.label` texts are user-entered: `untrusted` as
110
+ * declared, else inferred by hasUserText (so any label text but the app's own enum values; declare
111
+ * `untrusted: false` to keep the labels in the schema). Shared by WebMCP, chat and the command bar
112
+ */
113
+ export const labelsUntrusted = (g: AgentGroup): boolean =>
114
+ !!g.labels && Object.keys(g.labels).length > 0 && (g.untrusted ?? hasUserText(Object.values(g.labels), g.enums))
115
+
116
+ /**
117
+ * D286 / G-644: an item tool's key parameter (`id` / `item`: an enum with "Which todo (1: buy
118
+ * milk; …)") with its ids only: `agent.label` text stays out of the schema, which agents read as
119
+ * instructions. `where` says where the labels are instead ("the read tool has their contents")
120
+ */
121
+ export const unlabel = (schema: any, where: string): any => {
122
+ const props = schema?.properties
123
+ if (!props) return schema
124
+ let changed = false
125
+ const out: any = {}
126
+ for (const k in props) {
127
+ const p = props[k], d = p?.description
128
+ if (p && Array.isArray(p.enum) && typeof d == 'string' && /^Which \S+ \(/.test(d)) {
129
+ out[k] = {...p, description: d.slice(0, d.indexOf(' (')) + ` (ids ${p.enum.join(', ')}; ${where})`}
130
+ changed = true
131
+ } else out[k] = p
132
+ }
133
+ return changed ? {...schema, properties: out} : schema
134
+ }
135
+
136
+ /**
137
+ * G-650: the sentence an item tool's description gets when its key parameter has no labels (unlabel)
138
+ * and the labels are behind a read tool: without it, small models guess an id instead of reading
139
+ * first. Fixed text (the read tool's and the declaration's names), never a label
140
+ */
141
+ export const lookUp = (d: string, g: AgentGroup, read: string) => {
142
+ d = d.trim()
143
+ return d + (!d ? '' : /[.!?]$/.test(d) ? ' ' : '. ') + `Call ${read} first to find the ${g.name}'s id.`
144
+ }
145
+
146
+ interface Entry {name: string; decl: any; ivs: any[]; item: boolean; view: any; enums: Set<string>}
147
+ interface Rec {tool: AgentTool; e: Entry; action?: string; a?: any; key?: string; off?: 1}
148
+ interface Call {id: number; type: string; data: any; ran: boolean; out: Array<{kind: string; reason?: string; error?: any}>}
149
+
150
+ /**
151
+ * The live tool set of an app (the `run()` result, which it follows through HMR) or of a
152
+ * runtime API (`app.__runtime`, renderComponent's)
153
+ */
154
+ export function agentTools(target: any, options: AgentToolsOptions = {}) {
155
+ const isApp = !!target && '__runtime' in target
156
+ let api: any = isApp ? target.__runtime : target
157
+ let off: (() => void) | undefined, stopped = false, ver = 0, built = -1
158
+ let entries: Entry[] = [], recs = new Map<string, Rec>()
159
+ /** item tools seen before: a call when no item is shown (all filtered out) explains why */
160
+ const seen = new Map<string, Rec>()
161
+ let current: Call | null = null, pending: Call | null = null
162
+ const reads = new WeakMap<any, {s: any; v: any}>(), whens = new WeakMap<any, Map<string, {s: any; v: boolean}>>()
163
+ /** item component -> its Collection owners and their `from` (hidden-by-filter errors) */
164
+ const owners = new Map<any, Map<any, any>>()
165
+ const listeners = new Set<(change: {tools: AgentTool[]; context: Record<string, any>}) => void>()
166
+ let last: any, scheduled = false
167
+
168
+ const dirty = () => {
169
+ ver++
170
+ if (!listeners.size || scheduled || stopped) return
171
+ scheduled = true
172
+ // a microtask first: onReducer runs before the write schedules the flush
173
+ queueMicrotask(() => api.flushed().then(() => { scheduled = false; publish() }))
174
+ }
175
+ const hooks = {
176
+ onCreate: dirty,
177
+ onPatch: dirty,
178
+ onReducer: dirty,
179
+ onDispose: (iv: any) => {
180
+ dirty()
181
+ // HMR: run()'s hmr() disposes this app, starts its successor and swaps __runtime
182
+ if (iv.isRoot && isApp) queueMicrotask(rebind)
183
+ },
184
+ onHostProps: (owner: any, _sel: string, props: any) => {
185
+ if (typeof props.of == 'function') {
186
+ let m = owners.get(props.of)
187
+ if (!m) owners.set(props.of, m = new Map())
188
+ m.set(owner, props.from)
189
+ }
190
+ },
191
+ onAction: (iv: any, a: any) => {
192
+ current = null
193
+ const p = pending
194
+ if (p && a.cause === 'agent' && iv.id === p.id && a.type === p.type && a.data === p.data) { p.ran = true; current = p; pending = null }
195
+ },
196
+ wrapHandler: (iv: any, type: string, sink: string, fn: any) => {
197
+ const c = current
198
+ if (!c || iv.id !== c.id || type !== c.type) return
199
+ // a constant entry isn't wrapped: its outcome is known (the core: `true` / undefined send the data)
200
+ if (typeof fn != 'function') {
201
+ if (sink != 'EFFECT') c.out.push({kind: outcome(sink, iv.state, fn === undefined || fn === true ? c.data : fn)})
202
+ return
203
+ }
204
+ const w = function (this: any, pre: any) {
205
+ abortReason.r = undefined
206
+ let v: any
207
+ try { v = fn.apply(this, arguments) } catch (e) { c.out.push({kind: 'threw', error: e}); throw e }
208
+ const reason = abortReason.r
209
+ abortReason.r = undefined
210
+ c.out.push(sink == 'EFFECT' ? {kind: 'effect'} : {kind: outcome(sink, pre, v), reason})
211
+ return v
212
+ }
213
+ ;(w as any)[ORIGINAL] = fn[ORIGINAL] || fn
214
+ return w
215
+ },
216
+ }
217
+ // STATE: changed / same (the object it was given, or a deep-equal one); another sink: sent
218
+ const outcome = (sink: string, pre: any, v: any) => isAbort(v) ? 'abort' : sink != 'STATE' ? (typeof v == 'symbol' ? 'abort' : 'sent')
219
+ : v === pre || (v !== undefined && objIsEqual(pre, v, 50)) ? 'same' : 'changed'
220
+
221
+ const attach = () => { off = api.addHooks(hooks); dirty() }
222
+ function rebind() {
223
+ const next = target.__runtime
224
+ if (stopped || !next || next === api) return
225
+ off?.()
226
+ api = next
227
+ attach()
228
+ }
229
+ attach()
230
+
231
+ // ------------------------------------------------------------------ discovery
232
+ const declOf = (iv: any) => iv.def.view.agent
233
+ const project = (iv: any, d: any) => {
234
+ if (!d.read) return
235
+ const s = iv.state, c = reads.get(iv)
236
+ if (c && c.s === s) return c.v
237
+ let v: any
238
+ try { v = d.read(s) } catch (e: any) { dev('SYG241', d, 'read', iv.name, `agent.read threw: ${e?.message ?? e}`) }
239
+ reads.set(iv, {s, v})
240
+ return v
241
+ }
242
+ const whenOk = (iv: any, action: string, w?: (s: any) => boolean) => {
243
+ if (!w) return true
244
+ let m = whens.get(iv)
245
+ if (!m) whens.set(iv, m = new Map())
246
+ const s = iv.state, c = m.get(action)
247
+ if (c && c.s === s) return c.v
248
+ let v = false
249
+ try { v = !!w(s) } catch (e: any) { dev('SYG241', declOf(iv), 'when' + action, iv.name, `agent.actions.${action}.when threw: ${e?.message ?? e}`) }
250
+ m.set(action, {s, v})
251
+ return v
252
+ }
253
+ const labelOf = (iv: any) => {
254
+ const d = declOf(iv)
255
+ try { return d.label ? String(d.label(iv.state)) : undefined } catch (e: any) { dev('SYG241', d, 'label', iv.name, `agent.label threw: ${e?.message ?? e}`) }
256
+ }
257
+
258
+ function walk(iv: any, out: any[]) {
259
+ if (!iv || iv.disposed || !iv.shown) return
260
+ out.push(iv)
261
+ for (const c of iv.children()) walk(c, out)
262
+ }
263
+ function scan() {
264
+ if (isApp && target.__runtime !== api) rebind()
265
+ const all: any[] = [], by = new Map<string, Entry>()
266
+ const from = options.from
267
+ walk(from == null ? api.root : api.get(typeof from == 'object' ? from.id : from), all)
268
+ for (const iv of all) {
269
+ const d = declOf(iv)
270
+ if (!d || (options.components && !options.components.includes(iv.def.view))) continue
271
+ const item = iv.kind == 'item', name = snake(d.name ?? iv.name)
272
+ if (item) {
273
+ let m = owners.get(iv.def.view)
274
+ const p = iv.parentId !== undefined && api.get(iv.parentId)
275
+ if (!m) owners.set(iv.def.view, m = new Map())
276
+ if (p && !m.has(p)) m.set(p, undefined)
277
+ }
278
+ const e = by.get(name)
279
+ if (!e) by.set(name, {name, decl: d, ivs: [iv], item, view: iv.def.view, enums: new Set()})
280
+ else if (e.decl === d && e.item && item) e.ivs.push(iv)
281
+ else dev('SYG440', d, name, iv.name, `two agent declarations are named '${name}'; only the first is offered`)
282
+ }
283
+ for (const e of by.values()) if (e.item) {
284
+ const ks = new Set<string>()
285
+ for (const iv of e.ivs) {
286
+ const k = keyOf(iv)
287
+ // an item without an id is keyed by its index (the core's `_i<index>` uid part): unstable
288
+ if (k == null || /-_i\d+$/.test(iv.uid)) dev('SYG441', e.decl, 'nokey', iv.name, `items of '${e.name}' have no id: agents address them by index`)
289
+ else if (ks.has(k + '')) dev('SYG441', e.decl, 'dup', iv.name, `two items of '${e.name}' have the id ${JSON.stringify(k)}; calls reach the first`)
290
+ else ks.add(k + '')
291
+ }
292
+ }
293
+ entries = [...by.values()]
294
+ }
295
+
296
+ function build(): Map<string, Rec> {
297
+ if (built === ver && !(isApp && target.__runtime !== api)) return recs
298
+ scan()
299
+ built = ver
300
+ recs = new Map()
301
+ for (const e of entries) {
302
+ const d = e.decl
303
+ if (d.read) recs.set(e.name + '_read', {e, tool: {name: e.name + '_read', description: `Read ${e.name}` + (d.description ? `: ${d.description}` : ''), inputSchema: {type: 'object', properties: {}}, annotations: {readOnlyHint: true, ...(d.untrusted && {untrustedContentHint: true})}}})
304
+ for (const action in d.actions) {
305
+ const a = d.actions[action], name = e.name + '_' + snake(action), comp = e.ivs[0].name
306
+ const c = a.input ? toJsonSchema(a.input) : undefined
307
+ if (c?.schema) enumsOf(c.schema, e.enums)
308
+ const tool: AgentTool = {name, description: a.description, inputSchema: c?.schema || {type: 'object', properties: {}}, annotations: a.consequential ? {consequentialHint: true} : {}}
309
+ const rec: Rec = {e, action, a, tool}
310
+ recs.set(name, rec)
311
+ if (e.item) seen.set(name, {e: {...e, ivs: []}, action, a, tool})
312
+ if (c?.error) {
313
+ tool.error = `SYG240 ${c.error}`
314
+ rec.off = 1
315
+ dev('SYG240', d, action, comp, `agent.actions.${action}.input: ${c.error}; the tool is not offered`)
316
+ continue
317
+ }
318
+ if (c?.lossy) dev('SYG243', d, action, comp, `agent.actions.${action}.input: the model doesn't see ${c.lossy}`)
319
+ const live = e.ivs.filter(iv => whenOk(iv, action, a.when))
320
+ if (!live.length) { rec.off = 1; continue }
321
+ if (e.item) {
322
+ const s = tool.inputSchema, key = rec.key = !c?.wrapped && s.properties?.id ? 'item' : 'id'
323
+ const labels = d.label ? live.map(iv => `${keyOf(iv)}: ${labelOf(iv)}`).join('; ') : ''
324
+ tool.inputSchema = {...s, properties: {[key]: {enum: live.map(keyOf).filter(k => k != null), description: `Which ${e.name}` + (labels && ` (${labels})`)}, ...s.properties}, required: [key, ...(s.required || [])]}
325
+ }
326
+ }
327
+ }
328
+ return recs
329
+ }
330
+
331
+ function list(o: {all?: boolean} = {}): AgentTool[] {
332
+ const out: AgentTool[] = []
333
+ for (const r of build().values()) if (!r.off || (o.all && r.tool.error)) out.push(r.tool)
334
+ return out
335
+ }
336
+
337
+ /** the `read` projections by declaration name (an item declaration: an array with the keys) */
338
+ function context(): Record<string, any> {
339
+ build()
340
+ const ctx: Record<string, any> = {}
341
+ for (const e of entries) if (e.decl.read) {
342
+ ctx[e.name] = e.item ? e.ivs.map(iv => { const v = project(iv, e.decl); return {id: keyOf(iv), ...(isObj(v) ? v : {value: v})} }) : project(e.ivs[0], e.decl)
343
+ }
344
+ return ctx
345
+ }
346
+ function publish() {
347
+ if (stopped || !listeners.size) return
348
+ const now = {tools: list(), context: context()}
349
+ if (last && objIsEqual(last, now, 50)) return
350
+ last = now
351
+ for (const l of listeners) try { l(now) } catch (e) { queueMicrotask(() => { throw e }) }
352
+ }
353
+
354
+ // ------------------------------------------------------------------ calls
355
+ /** the nearest instance at or above `iv` with a `read`: its projection is the call's `state` */
356
+ const resultState = (iv: any) => {
357
+ for (let i = iv; i; i = i.parentId !== undefined ? api.get(i.parentId) : undefined) {
358
+ const d = declOf(i)
359
+ if (d?.read && !i.disposed) return project(i, d)
360
+ }
361
+ }
362
+ /** G-650: the read tool with an item declaration's data: its own, else the nearest ancestor's */
363
+ const readerOf = (e: Entry): string | undefined => {
364
+ const iv = e.ivs.find(iv => !iv.disposed)
365
+ for (let i = iv; i; i = i.parentId !== undefined ? api.get(i.parentId) : undefined) {
366
+ const d = declOf(i), n = d?.read && !i.disposed && snake(d.name ?? i.name) + '_read'
367
+ if (n && recs.has(n)) return n
368
+ }
369
+ }
370
+ /** the live keys (with labels) of an item tool, for an error */
371
+ const keysOf = (e: Entry) => e.ivs.filter(iv => !iv.disposed).map(iv => { const l = e.decl.label && labelOf(iv); return l ? `${keyOf(iv)} (${l})` : keyOf(iv) })
372
+ const missing = (e: Entry, view: any, k: any): AgentResult => {
373
+ const what = e.name, keys = e.ivs.filter(iv => !iv.disposed).map(keyOf)
374
+ // D257: an item a filter hides is disposed; its owner's array still has it
375
+ for (const [o, from] of owners.get(view) || []) {
376
+ if (o.disposed) continue
377
+ const s = o.state
378
+ for (const a of typeof from == 'string' ? [s?.[from]] : isObj(s) ? Object.values(s) : [s]) {
379
+ if (Array.isArray(a) && a.some(x => x?.id != null && String(x.id) === String(k))) return {ok: false, error: `the ${what} with id ${JSON.stringify(k)} is hidden by the current filter`, keys}
380
+ }
381
+ }
382
+ return {ok: false, error: `no ${what} with id ${JSON.stringify(k)}; ids: ${keysOf(e).join(', ') || 'none'}`, keys}
383
+ }
384
+
385
+ let chain: Promise<any> = Promise.resolve()
386
+ function call(name: string, args?: any, o: {confirm?: Confirm} = {}): Promise<AgentResult> {
387
+ if (options.serial === false) return run(name, args, o)
388
+ const p = chain.then(() => run(name, args, o))
389
+ chain = p.catch(() => {})
390
+ return p
391
+ }
392
+ async function run(name: string, args: any, o: {confirm?: Confirm}): Promise<AgentResult> {
393
+ if (stopped) return {ok: false, error: 'the agent tools were stopped'}
394
+ const r = build().get(name) || seen.get(name)
395
+ if (!r || (r.off && !r.tool.error && !r.e.item)) {
396
+ return r ? {ok: false, error: `${name} is not available now`, state: resultState(r.e.ivs[0])} : {ok: false, error: `no tool ${name}; tools: ${list().map(t => t.name).join(', ')}`}
397
+ }
398
+ if (r.tool.error) return {ok: false, error: r.tool.error}
399
+ const {e, a, action} = r
400
+ if (!action) return {ok: true, state: context()[e.name]}
401
+ const key = r.key || 'id', view = e.view
402
+ let raw = args ?? {}, k: any
403
+ if (e.item) {
404
+ if (!isObj(raw) || raw[key] == null) return {ok: false, error: `${name} needs ${key}: one of ${keysOf(e).join(', ') || 'none'}`, keys: e.ivs.map(keyOf)}
405
+ k = raw[key]
406
+ const {[key]: _, ...rest} = raw
407
+ raw = rest
408
+ }
409
+ let data: any
410
+ if (a.input) {
411
+ const p = await parseInput(a.input, raw, {repair: options.repair})
412
+ if (p.error || p.issues) return {ok: false, error: p.error || `invalid input for ${name}: ` + p.issues!.map(i => (i.path?.length ? i.path.join('.') + ': ' : '') + i.message).join('; '), ...(p.issues && {issues: p.issues})}
413
+ data = p.value
414
+ }
415
+ const resolve = () => {
416
+ const ivs = (build().get(name)?.e || e).ivs
417
+ return e.item ? ivs.find(iv => !iv.disposed && String(keyOf(iv)) === String(k)) : ivs.find(iv => !iv.disposed)
418
+ }
419
+ let iv = resolve()
420
+ if (!iv) return e.item ? missing(e, view, k) : {ok: false, error: `${e.name} is not on the page`}
421
+ if (!whenOk(iv, action, a.when)) return {ok: false, error: `${name} is not available now`, state: resultState(iv)}
422
+ if (a.consequential) {
423
+ const c = 'confirm' in o ? o.confirm : options.confirm
424
+ const info: ConfirmInfo = {tool: name, component: iv.name, action, description: a.description, input: data, ...(e.item && {key: k, label: labelOf(iv)})}
425
+ if (!(typeof c == 'function' ? await c(info) : c === true)) return {ok: false, error: 'the user declined'}
426
+ // the app may have changed while the user decided: resolve and check again
427
+ iv = resolve()
428
+ if (!iv) return e.item ? {...missing(e, view, k), error: `the ${e.name} with id ${JSON.stringify(k)} is gone`} : {ok: false, error: `${e.name} is not on the page any more`}
429
+ if (!whenOk(iv, action, a.when)) return {ok: false, error: `${name} is not available any more`, state: resultState(iv)}
430
+ }
431
+ const rec: Call = pending = {id: iv.id, type: action, data, ran: false, out: []}
432
+ api.dispatch(iv.id, action, data, 'agent')
433
+ await api.flushed()
434
+ if (pending === rec) pending = null
435
+ if (current === rec) current = null
436
+ if (!rec.ran) return {ok: false, error: `${name} did not run: its target was removed first`}
437
+ const out = rec.out, threw = out.find(x => x.kind == 'threw'), refused = out.find(x => x.kind == 'abort' && x.reason)
438
+ if (threw) return {ok: false, error: `${action} failed: ${threw.error?.message ?? threw.error}`, state: resultState(iv)}
439
+ // G-650: an item a filter moved to another Collection (a card to another column) is a new instance, not removed
440
+ if (iv.disposed && e.item) iv = resolve() || iv
441
+ const gone = iv.disposed
442
+ const state = gone ? resultState(api.get(iv.parentId)) : resultState(iv)
443
+ if (out.some(x => x.kind == 'changed' || x.kind == 'sent' || x.kind == 'effect')) return gone ? {ok: true, removed: true, state} : {ok: true, state}
444
+ if (refused) return {ok: false, error: `${action} was refused: ${refused.reason}`, state}
445
+ return a.idempotent ? {ok: true, unchanged: true, state} : {ok: false, error: `${action} changed nothing (already so, or the input matched nothing)`, state}
446
+ }
447
+
448
+ return {
449
+ /** the offered tools; `{ all: true }` adds declared tools that can't be offered (`error`, SYG240) */
450
+ list,
451
+ /** run a tool as an agent: resolves with the result after the action's flush */
452
+ call,
453
+ context,
454
+ /** called with `{ tools, context }` after each flush that changed either; returns unsubscribe */
455
+ subscribe(fn: (change: {tools: AgentTool[]; context: Record<string, any>}) => void) {
456
+ listeners.add(fn)
457
+ dirty()
458
+ return () => { listeners.delete(fn) }
459
+ },
460
+ stop() { stopped = true; off?.(); listeners.clear() },
461
+ /** internal (A-2, WebMCP; chat; the command bar): the offered tools grouped by declaration, with `untrusted` as declared and an item declaration's live labels (id → `agent.label`) */
462
+ groups(): AgentGroup[] {
463
+ const rs = [...build().values()]
464
+ return entries.map(e => {
465
+ const labels: Record<string, string> = {}
466
+ if (e.item && e.decl.label) for (const iv of e.ivs) {
467
+ const id = keyOf(iv), l = !iv.disposed && id != null ? labelOf(iv) : undefined
468
+ if (l !== undefined) labels[id] = l
469
+ }
470
+ return {name: e.name, untrusted: e.decl.untrusted, read: !!e.decl.read, tools: rs.filter(r => r.e === e && !r.off).map(r => r.tool.name), enums: e.enums, ...(e.item && e.decl.label && {labels}), ...(e.item && {reader: readerOf(e)})}
471
+ })
472
+ },
473
+ /** internal (M-3, the command bar): the live Collection items by declaration name, with their key and `agent.label` */
474
+ targets(): Array<{name: string; id: any; label?: string}> {
475
+ build()
476
+ const out: Array<{name: string; id: any; label?: string}> = []
477
+ for (const e of entries) if (e.item) for (const iv of e.ivs) {
478
+ const id = keyOf(iv)
479
+ if (!iv.disposed && id != null) out.push({name: e.name, id, label: labelOf(iv)})
480
+ }
481
+ return out
482
+ },
483
+ }
484
+ }
@@ -0,0 +1,115 @@
1
+ /*
2
+ * PLAN-6 M-2: decision reply fixtures for tests. `answers(questions, picks)` builds the reply a
3
+ * decision model would send for `decide({ questions })` (the dictionary form), so a test writes
4
+ * only what matters and the rest is filled in sensibly:
5
+ *
6
+ * await t.respond('HTTP', answers(questions, { topic: 'billing', urgent: true }), 'TRIAGED')
7
+ * await t.respond('HTTP', answers(questions, { topic: { choice: 'billing', confidence: 0.35 } }), 'TRIAGED')
8
+ *
9
+ * Picks per question type (an unknown question name or option throws, as a typo would otherwise
10
+ * pass silently):
11
+ * - choice: an option name, `{ choice?, confidence? }` or `{ probabilities }` (choice: the most
12
+ * likely). Default: the first option at confidence 0.9. The other options share the rest equally;
13
+ * - noul: `true` (0.95), `false` (0.05) or the probability; default false;
14
+ * - score: a level index (fractional: between two levels) or a level description, or
15
+ * `{ score, confidence? }`; default 0. The probabilities sit on the level(s) around it, so the
16
+ * weighted index is the score.
17
+ * `confidence` is what TypeSafe / Ollama `nimble` report: 1 minus the probabilities' entropy over
18
+ * its maximum (ln n), so 0 is "all options equally likely" and 1 is "certain" (a given confidence
19
+ * is reached by choosing the winner's probability).
20
+ *
21
+ * `answers.openai(questions, picks)` is the same reply in OpenAI Decisions' array form, for a
22
+ * `decide.openai()` request (its `parse` maps it back). Side-effect free.
23
+ */
24
+
25
+ const conf = (ps: number[]) => {
26
+ const n = ps.length
27
+ if (n < 2) return 1
28
+ let h = 0
29
+ for (const p of ps) if (p > 0) h -= p * Math.log(p)
30
+ return Math.max(0, Math.min(1, 1 - h / Math.log(n)))
31
+ }
32
+ /** the winner's probability that gives confidence `c` (the others share the rest) */
33
+ const winner = (n: number, c: number) => {
34
+ if (n < 2) return 1
35
+ let lo = 1 / n, hi = 1
36
+ for (let i = 0; i < 60; i++) {
37
+ const p = (lo + hi) / 2
38
+ conf([p, ...Array(n - 1).fill((1 - p) / (n - 1))]) < c ? lo = p : hi = p
39
+ }
40
+ return (lo + hi) / 2
41
+ }
42
+ const isObj = (x: any) => !!x && typeof x == 'object' && !Array.isArray(x)
43
+ const bad = (name: string, msg: string): never => { throw new Error(`[Sygnal] answers(): ${name}: ${msg}`) }
44
+
45
+ function choiceAnswer(name: string, q: any, pick: any) {
46
+ const opts = Object.keys(q.criteria || {})
47
+ if (!opts.length) bad(name, 'the choice has no options')
48
+ const check = (o: any) => opts.includes(String(o)) ? String(o) : bad(name, `${JSON.stringify(o)} is not an option (${opts.join(', ')})`)
49
+ let probabilities: Record<string, number>, choice: string
50
+ if (isObj(pick) && pick.probabilities) {
51
+ const given = pick.probabilities
52
+ Object.keys(given).forEach(check)
53
+ const sum = Object.values(given).reduce((a: number, b: any) => a + b, 0)
54
+ const left = opts.filter(o => !(o in given)), rest = Math.max(0, 1 - sum) / (left.length || 1)
55
+ probabilities = Object.fromEntries(opts.map(o => [o, o in given ? given[o] : rest]))
56
+ choice = pick.choice != null ? check(pick.choice) : opts.reduce((a, b) => probabilities[b] > probabilities[a] ? b : a)
57
+ } else {
58
+ choice = check(isObj(pick) ? pick.choice ?? opts[0] : pick ?? opts[0])
59
+ const c = isObj(pick) && typeof pick.confidence == 'number' ? pick.confidence : 0.9
60
+ const p = winner(opts.length, c)
61
+ probabilities = Object.fromEntries(opts.map(o => [o, o === choice ? p : (1 - p) / (opts.length - 1)]))
62
+ }
63
+ const confidence = isObj(pick) && typeof pick.confidence == 'number' ? pick.confidence : conf(Object.values(probabilities))
64
+ return {type: 'choice', choice, probabilities, confidence}
65
+ }
66
+
67
+ function scoreAnswer(name: string, q: any, pick: any) {
68
+ const levels: string[] = q.criteria || [], n = levels.length
69
+ if (n < 2) bad(name, 'a score needs 2 or more levels')
70
+ const v = isObj(pick) ? pick.score : pick
71
+ const x = v == null ? 0 : typeof v == 'string' ? (levels.indexOf(v) >= 0 ? levels.indexOf(v) : bad(name, `${JSON.stringify(v)} is not a level (${levels.join(', ')})`)) : v
72
+ if (typeof x != 'number' || !(x >= 0 && x <= n - 1)) bad(name, `the score must be a level index from 0 to ${n - 1} or a level description`)
73
+ const lo = Math.floor(x), f = x - lo, ps = levels.map((_, i) => i === lo ? 1 - f : i === lo + 1 ? f : 0)
74
+ const probabilities = Object.fromEntries(ps.map((p, i) => [String(i), p]))
75
+ const legend = Object.fromEntries(levels.map((l, i) => [String(i), l]))
76
+ return {type: 'score', score: x, legend, probabilities, confidence: isObj(pick) && typeof pick.confidence == 'number' ? pick.confidence : conf(ps)}
77
+ }
78
+
79
+ function noulAnswer(name: string, pick: any) {
80
+ const v = isObj(pick) ? pick.noul : pick
81
+ const p = v == null || v === false ? 0.05 : v === true ? 0.95 : v
82
+ if (typeof p != 'number' || !(p >= 0 && p <= 1)) bad(name, 'a noul pick is true, false or a probability from 0 to 1')
83
+ return {type: 'noul', noul: p}
84
+ }
85
+
86
+ /** a decision reply (dictionary form) for `questions`, with `picks` by question name */
87
+ function dictionary(questions: any, picks: any = {}, options: any = {}) {
88
+ const out: any = {}
89
+ for (const name in picks) if (!(name in (questions || {}))) bad(name, `no such question (${Object.keys(questions || {}).join(', ')})`)
90
+ for (const name in questions) {
91
+ const q = questions[name], pick = picks[name]
92
+ out[name] = q.type == 'choice' ? choiceAnswer(name, q, pick) : q.type == 'score' ? scoreAnswer(name, q, pick) : noulAnswer(name, pick)
93
+ }
94
+ return {model: options.model ?? 'test', answers: out, usage: options.usage ?? {input_tokens: 0, output_tokens: 0}}
95
+ }
96
+
97
+ const list = (m: Record<string, number>, legend?: Record<string, string>) =>
98
+ Object.keys(m).map(k => legend ? {value: +k, label: legend[k], probability: m[k]} : {value: k, probability: m[k]})
99
+
100
+ /** the same reply in OpenAI Decisions' array form (for a `decide.openai()` request) */
101
+ function openai(questions: any, picks: any = {}, options: any = {}) {
102
+ const d = dictionary(questions, picks, options)
103
+ return {
104
+ model: d.model,
105
+ answers: Object.keys(d.answers).map(name => {
106
+ const a = d.answers[name]
107
+ return a.type == 'noul' ? {type: 'predicate', name, probability: a.noul}
108
+ : a.type == 'choice' ? {type: 'choice', name, choice: a.choice, probabilities: list(a.probabilities), confidence: a.confidence}
109
+ : {type: 'score', name, score: a.score, probabilities: list(a.probabilities, a.legend), confidence: a.confidence}
110
+ }),
111
+ }
112
+ }
113
+
114
+ // (an Object.assign marked pure, not `answers.openai = …`: bundlers keep a property assignment)
115
+ export const answers = /*#__PURE__*/ Object.assign(dictionary, {openai})