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,314 @@
1
+ /*
2
+ * PLAN-6 X-1 (D247): `makeMcpAppDriver(options?)`, the view side of MCP Apps (SEP-1865, protocol
3
+ * 2026-01-26): a Sygnal app as the HTML resource a host (Claude, ChatGPT, VS Code, MCPJam, the
4
+ * ext-apps basic-host) shows in a sandboxed iframe, talking JSON-RPC 2.0 over postMessage with
5
+ * the host window. Hand-written (D209: the `@modelcontextprotocol/ext-apps` SDK is a test-only
6
+ * devDependency; its `App` pulls in the MCP client and zod).
7
+ *
8
+ * - Handshake on start: `ui/initialize` { appInfo, appCapabilities, protocolVersion } →
9
+ * { hostInfo, hostCapabilities, hostContext }, then `ui/notifications/initialized`. Requests
10
+ * made before it completes are queued. With no host (not in an iframe, SSR) nothing is sent and
11
+ * every request gets its `error` action.
12
+ * - Source `MCP.select(type)`: 'tool-input' ({ arguments } → the arguments object),
13
+ * 'tool-input-partial' (the same, streamed while the model writes them), 'tool-result' (the
14
+ * CallToolResult: { content, structuredContent?, isError? }), 'tool-cancelled' ({ reason? }),
15
+ * 'host-context-changed' (the whole host context, merged: first the one from the handshake,
16
+ * then after each change) and 'teardown' (the host is about to remove the view; it is answered
17
+ * after the actions it caused have run). The latest tool-input, tool-result and host context
18
+ * are replayed to a stream that starts later (a component shown after they arrived).
19
+ * - Sink values (reply actions `ok` / `error` to the sending instance, like makeFetchDriver):
20
+ * - `{ callTool: name, args?, ok?, error? }`: `tools/call` on the MCP server through the host;
21
+ * ok gets the CallToolResult, error `{ error, result?, request }` (a JSON-RPC error, or a
22
+ * result with isError: true);
23
+ * - `{ updateModelContext }`: `ui/update-model-context`; a string → one text block, an array →
24
+ * content blocks, an object → structuredContent (or `{ content, structuredContent }` as is);
25
+ * - `{ message }`: `ui/message` as the user; a string or content blocks;
26
+ * - `{ openLink: url }`: `ui/open-link`;
27
+ * - `{ displayMode: 'inline' | 'fullscreen' | 'pip' }`: `ui/request-display-mode` (ok gets
28
+ * `{ mode }`, the mode the host chose);
29
+ * - G-641: `{ downloadFile }`: `ui/download-file` { contents } (EmbeddedResource / ResourceLink
30
+ * blocks; a bare `{ uri, mimeType, text | blob }` is embedded); `{ readResource: uri }`:
31
+ * `resources/read`; `{ listResources: true | { cursor } }`: `resources/list`;
32
+ * `{ createMessage }`: `sampling/createMessage` (a string is one user message; maxTokens
33
+ * defaults to 1024); notifications (ok runs once sent): `{ log, level?, logger? }`:
34
+ * `notifications/message`, `{ requestTeardown: true }`: `ui/notifications/request-teardown`.
35
+ * - Source `MCP.select('host')`: { hostInfo, hostCapabilities, protocolVersion } of the
36
+ * handshake (replayed), to check e.g. `hostCapabilities.sampling` / `downloadFile`.
37
+ * - Host → view requests: `ping`; `ui/resource-teardown`; with `tools: agentTools`, `tools/list` and
38
+ * `tools/call` serve the app's A-1 tools (`appCapabilities.tools`, `listChanged`
39
+ * notifications); anything else is -32601.
40
+ * - `autoResize` (default true): `ui/notifications/size-changed` from a ResizeObserver, as the SDK.
41
+ * - Dispose: the listener and observer go; pending requests are dropped.
42
+ * Side-effect free: an app that doesn't import it pays 0 B.
43
+ */
44
+ import xs from 'xstream'
45
+ import {senderOf, allowed, makeReplies} from '../replies'
46
+ import type {Confirm} from './agent/index'
47
+ import {labelsUntrusted, unlabel, lookUp} from './agent/index'
48
+
49
+ /**
50
+ * G-644 (D286): the offered tools, user-entered `agent.label`s out of the schemas the host's model
51
+ * reads; G-650: an item tool without labels names the read tool to call first
52
+ */
53
+ const offered = (t: any) => {
54
+ const gs = t.groups?.() || [], bare = new Set<string>(), look = new Map<string, any>()
55
+ for (const g of gs) if (labelsUntrusted(g)) for (const n of g.tools) bare.add(n)
56
+ for (const g of gs) if (g.reader) for (const n of g.tools) if (!g.labels || bare.has(n)) look.set(n, g)
57
+ const where = gs.some((g: any) => g.read) ? 'the read tool has their contents' : 'their labels are user text, not shown'
58
+ return t.list().map(({error, ...x}: any) => {
59
+ const g = look.get(x.name)
60
+ if (g) x = {...x, description: lookUp(x.description || '', g, g.reader)}
61
+ return bare.has(x.name) ? {...x, inputSchema: unlabel(x.inputSchema, g ? `${g.reader} has their contents` : where)} : x
62
+ })
63
+ }
64
+
65
+ export type McpAppEvent = 'tool-input' | 'tool-input-partial' | 'tool-result' | 'tool-cancelled' | 'host-context-changed' | 'teardown' | 'host'
66
+
67
+ export interface McpAppDriverOptions {
68
+ /** `appInfo` of the handshake (default `{ name: document.title || 'sygnal-app', version: '0.0.0' }`) */
69
+ appInfo?: {name: string; version: string; [k: string]: any}
70
+ /** display modes the view supports (`appCapabilities.availableDisplayModes`) */
71
+ availableDisplayModes?: Array<'inline' | 'fullscreen' | 'pip'>
72
+ /** report the document's size to the host (default true) */
73
+ autoResize?: boolean
74
+ /**
75
+ * `agentTools` (from sygnal/ai): offer the app's `agent` tools (A-1) to the host as the view's
76
+ * own tools. Passed in, so an app that doesn't use it doesn't bundle the agent layer.
77
+ */
78
+ tools?: (target: any, options: any) => any
79
+ /** with `tools`: consequential calls run when it resolves true (default: declined) */
80
+ confirm?: Confirm
81
+ /** the host window (default `window.parent`) */
82
+ host?: any
83
+ /** the window to listen on (default `window`) */
84
+ window?: any
85
+ /** protocol version offered (default '2026-01-26') */
86
+ protocolVersion?: string
87
+ }
88
+
89
+ const G: any = globalThis
90
+ const PROTOCOL = '2026-01-26'
91
+ const N = 'ui/notifications/'
92
+ const REPLAY: Record<string, 1> = {'tool-input': 1, 'tool-result': 1, 'host-context-changed': 1, host: 1}
93
+ /** sink key → [method, params of the value] */
94
+ const text = (v: any) => typeof v == 'string' ? [{type: 'text', text: v}] : v
95
+ const REQUESTS: Record<string, [string, (v: any) => any]> = {
96
+ callTool: ['tools/call', v => ({name: v.callTool, arguments: v.args ?? {}})],
97
+ updateModelContext: ['ui/update-model-context', ({updateModelContext: u}) =>
98
+ typeof u == 'string' || Array.isArray(u) ? {content: text(u)}
99
+ : u && (u.content || u.structuredContent) && Object.keys(u).every(k => k == 'content' || k == 'structuredContent') ? {...u, ...(u.content && {content: text(u.content)})}
100
+ : {structuredContent: u}],
101
+ message: ['ui/message', v => ({role: 'user', content: text(v.message)})],
102
+ openLink: ['ui/open-link', v => ({url: v.openLink})],
103
+ displayMode: ['ui/request-display-mode', v => ({mode: v.displayMode})],
104
+ // G-641: an item without `type` is a resource's contents ({ uri, mimeType, text | blob }), embedded
105
+ downloadFile: ['ui/download-file', ({downloadFile: d}) => ({contents: (Array.isArray(d) ? d : [d]).map((c: any) => c && !c.type ? {type: 'resource', resource: c} : c)})],
106
+ readResource: ['resources/read', ({readResource: r}) => typeof r == 'string' ? {uri: r} : r],
107
+ listResources: ['resources/list', ({listResources: l}) => l && typeof l == 'object' ? l : {}],
108
+ createMessage: ['sampling/createMessage', ({createMessage: c}) => {
109
+ const p = typeof c == 'string' ? {messages: c} : {...c}
110
+ if (typeof p.messages == 'string') p.messages = [{role: 'user', content: {type: 'text', text: p.messages}}]
111
+ return {maxTokens: 1024, ...p}
112
+ }],
113
+ }
114
+ /** G-641: sink key → [notification method, params] (no answer: `ok` is called once it is sent) */
115
+ const NOTIFY: Record<string, [string, (v: any) => any]> = {
116
+ log: ['notifications/message', v => ({level: v.level || 'info', ...(v.logger && {logger: v.logger}), data: v.log})],
117
+ requestTeardown: ['ui/notifications/request-teardown', () => ({})],
118
+ }
119
+
120
+ /** agent: the runtime API of the app being constructed (its layer runs right before its drivers start) */
121
+ let building: any
122
+ const layer = (api: any) => {
123
+ building = api
124
+ queueMicrotask(() => { building = undefined })
125
+ return {}
126
+ }
127
+
128
+ export function makeMcpAppDriver(options: McpAppDriverOptions = {}) {
129
+ if (options.tools) {
130
+ const D = G.__SYGNAL_DIAGNOSTICS__ ||= {}, L = D.layers ||= new Set()
131
+ if (L.add) L.add(layer)
132
+ else if (!L.includes(layer)) L.push(layer)
133
+ }
134
+ return (sink$: any) => {
135
+ const api = building
136
+ building = undefined
137
+ const win = options.window ?? G.window
138
+ const host = options.host ?? (win && win.parent !== win ? win.parent : undefined)
139
+ const subs = new Map<string, Set<any>>()
140
+ const last = new Map<string, any>()
141
+ const pending = new Map<number, (m: any) => void>()
142
+ let queue: any[] | null = [], id = 0, disposed = false, context: any = {}, stopResize: any, tools: any, offTools: any
143
+
144
+ const post = (m: any) => { try { host.postMessage({jsonrpc: '2.0', ...m}, '*') } catch (e) { console.error('[Sygnal] makeMcpAppDriver', e) } }
145
+ const send = (m: any) => queue ? queue.push(m) : post(m)
146
+ const request = (method: string, params: any, done: (m: any) => void, early?: boolean) => {
147
+ const m = {id: ++id, method, params}
148
+ pending.set(m.id, done)
149
+ early ? post(m) : send(m)
150
+ }
151
+ const emit = (type: string, data: any) => {
152
+ if (REPLAY[type]) last.set(type, data)
153
+ subs.get(type)?.forEach(l => { try { l.next(data) } catch (e) { console.error(e) } })
154
+ }
155
+ const {replies, reply} = makeReplies()
156
+
157
+ const serve = async (m: any) => {
158
+ const p = m.params || {}
159
+ if (m.method == 'ping') return {}
160
+ if (m.method == 'ui/resource-teardown') {
161
+ emit('teardown', p)
162
+ // answered after the actions it caused (their flush and the requests they sent)
163
+ for (let i = 0; i < 3; i++) await Promise.resolve()
164
+ return {}
165
+ }
166
+ if (tools && m.method == 'tools/list') return {tools: offered(tools)}
167
+ if (tools && m.method == 'tools/call') {
168
+ let r: any
169
+ try { r = await tools.call(p.name, p.arguments ?? {}) } catch (e: any) { r = {ok: false, error: String(e?.message ?? e)} }
170
+ return {content: [{type: 'text', text: JSON.stringify(r)}], structuredContent: r, ...(!r?.ok && {isError: true})}
171
+ }
172
+ throw {code: -32601, message: 'Method not found: ' + m.method}
173
+ }
174
+
175
+ const onMessage = (ev: any) => {
176
+ const m = ev.data
177
+ if (disposed || ev.source !== host || !m || m.jsonrpc !== '2.0') return
178
+ if (m.method === undefined) {
179
+ const done = pending.get(m.id)
180
+ if (done) { pending.delete(m.id); done(m) }
181
+ } else if (m.id !== undefined) {
182
+ serve(m).then(result => post({id: m.id, result}), (e: any) => post({id: m.id, error: {code: e?.code ?? -32603, message: String(e?.message ?? e)}}))
183
+ } else if (typeof m.method == 'string' && m.method.startsWith(N)) {
184
+ const type = m.method.slice(N.length), p = m.params || {}
185
+ if (type == 'host-context-changed') emit(type, context = {...context, ...p})
186
+ else if (type == 'tool-input' || type == 'tool-input-partial') emit(type, p.arguments ?? {})
187
+ else if (type == 'tool-result' || type == 'tool-cancelled') emit(type, p)
188
+ }
189
+ }
190
+
191
+ const resize = () => {
192
+ const RO = G.ResizeObserver, doc = G.document
193
+ if (options.autoResize === false || typeof RO != 'function' || !doc) return
194
+ let w = 0, h = 0, busy = false
195
+ const measure = () => {
196
+ if (busy) return
197
+ busy = true
198
+ // a frame, raced with a 100 ms timer: rAF is throttled in an off-screen or hidden iframe
199
+ // (WebKit, a view scrolled out of the host's chat), and the size must still be reported
200
+ let ran = false
201
+ const once = (f: () => void) => () => { if (!ran) { ran = true; f() } }
202
+ const go = once(() => {
203
+ busy = false
204
+ const el = doc.documentElement, old = el.style.height
205
+ el.style.height = 'max-content'
206
+ const H = Math.ceil(el.getBoundingClientRect().height)
207
+ el.style.height = old
208
+ const W = Math.ceil(G.innerWidth || 0)
209
+ if (W !== w || H !== h) { w = W; h = H; send({method: N + 'size-changed', params: {width: W, height: H}}) }
210
+ })
211
+ G.requestAnimationFrame?.(go)
212
+ setTimeout(go, 100)
213
+ }
214
+ measure()
215
+ const ro = new RO(measure)
216
+ ro.observe(doc.documentElement)
217
+ if (doc.body) ro.observe(doc.body)
218
+ stopResize = () => ro.disconnect()
219
+ }
220
+
221
+ if (host && win) {
222
+ win.addEventListener('message', onMessage)
223
+ if (options.tools && api) {
224
+ tools = options.tools(api, {confirm: options.confirm})
225
+ // list_changed only when the tools changed (not on every context change); the first is the list at start
226
+ let was: string | undefined
227
+ offTools = tools.subscribe(({tools: t}: any) => {
228
+ const now = JSON.stringify(t)
229
+ if (was !== undefined && now !== was) send({method: 'notifications/tools/list_changed'})
230
+ was = now
231
+ })
232
+ }
233
+ const doc = G.document
234
+ request('ui/initialize', {
235
+ appInfo: options.appInfo ?? {name: doc?.title || 'sygnal-app', version: '0.0.0'},
236
+ appCapabilities: {
237
+ ...(tools && {tools: {listChanged: true}}),
238
+ ...(options.availableDisplayModes && {availableDisplayModes: options.availableDisplayModes}),
239
+ },
240
+ protocolVersion: options.protocolVersion ?? PROTOCOL,
241
+ }, m => {
242
+ if (m.error) return console.error('[Sygnal] makeMcpAppDriver: ui/initialize failed', m.error)
243
+ const r = m.result || {}
244
+ emit('host', {hostInfo: r.hostInfo, hostCapabilities: r.hostCapabilities || {}, protocolVersion: r.protocolVersion})
245
+ if (r.hostContext) emit('host-context-changed', context = {...r.hostContext})
246
+ post({method: N + 'initialized'})
247
+ const q = queue || []
248
+ queue = null
249
+ q.forEach(post)
250
+ resize()
251
+ }, true)
252
+ }
253
+
254
+ sink$.addListener({
255
+ next: (v: any) => {
256
+ if (!v || typeof v != 'object' || disposed || !allowed(v, 'makeMcpAppDriver')) return
257
+ const k = Object.keys(REQUESTS).find(k => v[k] !== undefined)
258
+ const n = k ? undefined : Object.keys(NOTIFY).find(k => v[k] !== undefined && v[k] !== false)
259
+ const sender = senderOf(v)
260
+ const answer = (ok: boolean, data: any) => {
261
+ const action = ok ? v.ok : v.error
262
+ if (sender !== undefined && typeof action == 'string') reply(sender, action, data)
263
+ else if (!ok) console.error('[Sygnal] makeMcpAppDriver', data.error, v)
264
+ }
265
+ if (!k && !n) return console.error('[Sygnal] makeMcpAppDriver: expected { callTool }, { updateModelContext }, { message }, { openLink }, { displayMode }, { downloadFile }, { readResource }, { listResources }, { createMessage }, { log } or { requestTeardown }', v)
266
+ if (!host) return answer(false, {error: 'not running in an MCP Apps host', request: v})
267
+ if (n) {
268
+ const [method, params] = NOTIFY[n]
269
+ send({method, params: params(v)})
270
+ return answer(true, {})
271
+ }
272
+ const [method, params] = REQUESTS[k!]
273
+ request(method, params(v), m => {
274
+ if (m.error) answer(false, {error: m.error.message, code: m.error.code, request: v})
275
+ else if (m.result?.isError) answer(false, {error: (m.result.content || []).map((c: any) => c.text).filter(Boolean).join('\n') || method + ' failed', result: m.result, request: v})
276
+ else answer(true, m.result)
277
+ })
278
+ },
279
+ error: (e: any) => console.error('[Sygnal] makeMcpAppDriver', e),
280
+ complete: () => {},
281
+ })
282
+
283
+ const source = {
284
+ select: (type: McpAppEvent) => {
285
+ let mine: any
286
+ return xs.create<any>({
287
+ start: l => {
288
+ let s = subs.get(type)
289
+ if (!s) subs.set(type, s = new Set())
290
+ s.add(mine = l)
291
+ if (last.has(type)) l.next(last.get(type))
292
+ },
293
+ stop: () => { subs.get(type)?.delete(mine) },
294
+ })
295
+ },
296
+ isolateSource: (s: any) => s,
297
+ isolateSink: (s: any) => s,
298
+ isolateValue: (v: any) => v,
299
+ ...replies,
300
+ }
301
+ return {
302
+ ...source,
303
+ dispose: () => {
304
+ disposed = true
305
+ win?.removeEventListener?.('message', onMessage)
306
+ stopResize?.()
307
+ offTools?.()
308
+ tools?.stop()
309
+ pending.clear()
310
+ subs.clear()
311
+ },
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * PLAN-6 L-1 (D252): chat messages are AI SDK UIMessage-shaped: `{ id?, role, parts }`, where a
3
+ * part is `{ type: 'text', text }`, `{ type: 'reasoning', text }`, `{ type: 'tool-<name>', toolCallId,
4
+ * state, input?, output? }`, `{ type: 'file', … }`, `{ type: 'source-…', … }` or `{ type: 'data-…', … }`.
5
+ * A message written by hand may carry `content: string` instead of parts.
6
+ */
7
+
8
+ /** the text of a message: its text parts joined (or its `content` string) */
9
+ export function messageText(message: any): string {
10
+ if (!message) return ''
11
+ if (typeof message.content == 'string' && !message.parts) return message.content
12
+ return (message.parts || []).filter((p: any) => p && p.type === 'text').map((p: any) => p.text).join('')
13
+ }
14
+
15
+ /**
16
+ * G-640: a new message id, 16 random [0-9a-z] characters (the AI SDK's generateId is 16
17
+ * alphanumerics too). Ids name messages, they are no secret: Math.random is enough
18
+ */
19
+ export const messageId = (): string => {
20
+ let s = ''
21
+ while (s.length < 16) s += Math.random().toString(36).slice(2)
22
+ return s.slice(0, 16)
23
+ }
24
+
25
+ const own = (o: any, k: any) => Object.prototype.hasOwnProperty.call(o, k)
26
+
27
+ /**
28
+ * PLAN-6 4-F: the message with its open tool calls answered, for apps that run the raw chat
29
+ * driver's tool loop themselves: each `tool-<name>` part in `input-available` gets the result for
30
+ * its call. `results`: a function `(call: { id, name, input }) => output` (a throw is that call's
31
+ * error), or an object keyed by the call's id, else by the tool's name. An `Error` value (returned
32
+ * or thrown) gives `state: 'output-error'` with its `errorText`; any other value
33
+ * `state: 'output-available'` with it as `output`; `undefined` (no result) leaves the part open.
34
+ * Returns a new message (the same one when nothing changed).
35
+ */
36
+ export function withToolResults(message: any, results: any): any {
37
+ if (!message || !Array.isArray(message.parts) || results == null) return message
38
+ let changed = false
39
+ const parts = message.parts.map((p: any) => {
40
+ if (!p || typeof p.type != 'string' || !p.type.startsWith('tool-') || p.state != 'input-available') return p
41
+ const call = {id: p.toolCallId, name: p.type.slice(5), input: p.input}
42
+ let r: any
43
+ try {
44
+ r = typeof results == 'function' ? results(call) : own(results, call.id) ? results[call.id] : own(results, call.name) ? results[call.name] : undefined
45
+ } catch (e: any) { r = e instanceof Error ? e : new Error(String(e)) }
46
+ if (r === undefined) return p
47
+ changed = true
48
+ return r instanceof Error ? {...p, state: 'output-error', errorText: r.message} : {...p, state: 'output-available', output: r}
49
+ })
50
+ return changed ? {...message, parts} : message
51
+ }
@@ -0,0 +1,166 @@
1
+ /*
2
+ * PLAN-6 A-1 (0-S4): the input contract of agent tools and of structured chat output.
3
+ *
4
+ * An `input` (or a chat request's `output`) is a Standard Schema that also implements Standard
5
+ * JSON Schema (Zod 4.2+, ArkType 2.1.28+, Valibot through `toStandardJsonSchema()`), or a plain
6
+ * JSON Schema wrapped by `jsonSchema()` (./jsonSchema.ts, D262). The model always gets the
7
+ * INPUT-side JSON Schema (it produces what `validate` receives), after the portable
8
+ * normalization below, which every consumer (the agent layer, WebMCP, the chat behavior, the
9
+ * chat driver's `output`, t.tools()) shares:
10
+ * - `$schema` / `$id` dropped, `oneOf` -> `anyOf`, Zod's safe-integer bounds dropped, a closed
11
+ * tuple's `items: false` -> `maxItems` (Ollama rejects boolean subschemas);
12
+ * - a root self-reference (`"$ref": "#"`, Zod's recursion) moved into `$defs.__root` (Ollama
13
+ * rejects `#`, and wrapping changes what it means); ArkType's `$ref`-only root inlined;
14
+ * - a non-object root wrapped as `{ value }` (`required`, `additionalProperties: false`); the
15
+ * unwrap is lenient: arguments without a `value` key are taken as the value itself (0-S4: both
16
+ * local models sent a wrapped object union bare).
17
+ * A schema part with no JSON Schema form (refinements, a Date) is retried with the library's
18
+ * lossy option (`lossy`: SYG243, a dev warning; validation still enforces it); no form at all is
19
+ * `error` (SYG240). Conversions are cached by schema object (ArkType rebuilds `~standard` on
20
+ * every read, G-608). The strict layer for OpenAI / Anthropic is the transports' (Phase 2), opt-in
21
+ through `strictSchemas` (./strict.ts, D285); both walk schemas with `walk` below.
22
+ */
23
+
24
+ /** The result of converting a schema: the tool schema, or why there is none (SYG240) */
25
+ export interface Converted {
26
+ /** the normalized, input-side JSON Schema; always an object root */
27
+ schema?: any
28
+ /** the root was wrapped as `{ value }` */
29
+ wrapped?: boolean
30
+ /** a lossy conversion (SYG243): what the library couldn't express */
31
+ lossy?: string
32
+ /** no JSON Schema form (SYG240) */
33
+ error?: string
34
+ }
35
+
36
+ // each library's "give me what you can" switch (Standard JSON Schema's libraryOptions)
37
+ const LOSSY: Record<string, any> = {zod: {unrepresentable: 'any'}, valibot: {errorMode: 'ignore'}, arktype: {fallback: (c: any) => c.base}}
38
+ const SAFE = 9007199254740991
39
+ const cache = new WeakMap<object, Converted>()
40
+
41
+ const msg = (e: any) => String(e?.message || e).split('\n')[0].slice(0, 200)
42
+ export const isObj = (x: any): boolean => !!x && typeof x == 'object' && !Array.isArray(x)
43
+
44
+ /**
45
+ * The JSON Schema a model gets for `schema` (a Standard Schema with Standard JSON Schema, or
46
+ * `jsonSchema()`): `{ schema, wrapped, lossy? }`, or `{ error }` when it has no JSON Schema form.
47
+ * Cached by schema object.
48
+ */
49
+ export function toJsonSchema(input: any): Converted {
50
+ if (!input || (typeof input != 'object' && typeof input != 'function')) return {error: 'not a Standard Schema'}
51
+ let c = cache.get(input)
52
+ if (!c) cache.set(input, c = convert(input))
53
+ return c
54
+ }
55
+
56
+ function convert(input: any): Converted {
57
+ const st = input['~standard']
58
+ if (typeof st?.validate != 'function') return {error: input.type || input.properties || input.anyOf || input.enum ? 'a plain JSON Schema: wrap it with jsonSchema()' : 'not a Standard Schema'}
59
+ const conv = st.jsonSchema?.input, v = st.vendor
60
+ if (typeof conv != 'function') return {error: `the ${v} schema has no JSON Schema form` + (v == 'valibot' ? ': wrap it with toStandardJsonSchema() from @valibot/to-json-schema' : v == 'zod' ? ' (Zod Mini): use jsonSchema(z.toJSONSchema(s), { validate: s })' : '')}
61
+ let js: any, lossy: string | undefined
62
+ try { js = conv({target: 'draft-2020-12'}) } catch (e) {
63
+ try { if (!LOSSY[v]) throw 0; js = conv({target: 'draft-2020-12', libraryOptions: LOSSY[v]}) } catch (_) { return {error: 'no JSON Schema form: ' + msg(e)} }
64
+ lossy = msg(e)
65
+ }
66
+ return {...normalize(js), ...(lossy && {lossy})}
67
+ }
68
+
69
+ // G-629: every subschema, bottom-up (shared with the strict layer, ./strict.ts). The values of
70
+ // `properties` / `$defs` / ... maps are schemas, the maps themselves aren't (a property named
71
+ // `oneOf` or `items` is a name, not a keyword), and `const` / `enum` / `default` / `examples` are
72
+ // data (a default of `{ oneOf: 1 }` stays as it is)
73
+ const MAPS = /^(properties|\$defs|definitions|patternProperties|dependentSchemas)$/
74
+ const DATA = /^(const|enum|default|examples)$/
75
+ export const walk = (s: any, f: (o: any) => any): any => {
76
+ if (Array.isArray(s)) return s.map(x => walk(x, f))
77
+ if (!s || typeof s != 'object') return s
78
+ const o: any = {}
79
+ for (const k in s) {
80
+ const v = s[k]
81
+ o[k] = DATA.test(k) ? v : MAPS.test(k) && isObj(v) ? Object.fromEntries(Object.entries(v).map(([n, x]) => [n, walk(x, f)])) : walk(v, f)
82
+ }
83
+ return f(o)
84
+ }
85
+ const rewriteRefs = (s: any, to: string) => walk(s, o => {
86
+ const r = o.$ref
87
+ if (typeof r == 'string' && (r == '#' || r[1] == '/') && !/^#\/(\$defs|definitions)\//.test(r)) o.$ref = to + r.slice(1)
88
+ return o
89
+ })
90
+
91
+ /** the portable normalization (see the header): `{ schema, wrapped }` */
92
+ export function normalize(schema: any): {schema: any; wrapped: boolean} {
93
+ let {$schema, $id, ...s} = walk(schema, o => {
94
+ if ('oneOf' in o && !('anyOf' in o)) { o.anyOf = o.oneOf; delete o.oneOf }
95
+ if (o.type == 'integer' && o.minimum == -SAFE && o.maximum == SAFE) { delete o.minimum; delete o.maximum }
96
+ if (o.prefixItems && o.items === false) { delete o.items; o.maxItems ??= o.prefixItems.length }
97
+ return o
98
+ })
99
+ const m = typeof s.$ref == 'string' && /^#\/\$defs\/(.+)$/.exec(s.$ref)
100
+ if (m && s.$defs?.[m[1]]?.type == 'object' && Object.keys(s).length == 2) s = {...s.$defs[m[1]], $defs: s.$defs}
101
+ const {$defs, description, ...rest} = s
102
+ const defs: any = {...$defs}
103
+ const selfRef = /"\$ref":"#(?!\/\$defs\/|\/definitions\/)/.test(JSON.stringify(s))
104
+ if (selfRef) {
105
+ for (const k in defs) defs[k] = rewriteRefs(defs[k], '#/$defs/__root')
106
+ defs.__root = rewriteRefs(rest, '#/$defs/__root')
107
+ }
108
+ if (s.type == 'object') return {schema: selfRef ? {...defs.__root, ...(description && {description}), $defs: defs} : s, wrapped: false}
109
+ let value = selfRef ? {$ref: '#/$defs/__root'} : rest
110
+ if (description) value = {...value, description}
111
+ const out: any = {type: 'object', properties: {value}, required: ['value'], additionalProperties: false}
112
+ if (Object.keys(defs).length) out.$defs = defs
113
+ return {schema: out, wrapped: true}
114
+ }
115
+
116
+ export const resolve = (s: any, root: any) => {
117
+ const m = /^#\/\$defs\/(.+)$/.exec(s.$ref)
118
+ return m ? root.$defs?.[m[1]] : s.$ref == '#' ? root : s
119
+ }
120
+
121
+ /**
122
+ * D264: small local models send "3" for an integer and "true" for a boolean. Where the JSON
123
+ * Schema says number / integer / boolean (and not string), a string that parses as one becomes
124
+ * one. Nothing else changes; validation still decides.
125
+ */
126
+ export function repair(value: any, schema: any, root: any = schema): any {
127
+ if (schema?.$ref) schema = resolve(schema, root)
128
+ if (!schema) return value
129
+ const types = [].concat(schema.type ?? []) as string[]
130
+ if (typeof value == 'string' && !types.includes('string')) {
131
+ const t = value.trim()
132
+ if ((types.includes('integer') || types.includes('number')) && /^-?\d+(\.\d+)?$/.test(t)) {
133
+ const n = Number(t)
134
+ if (types.includes('number') || Number.isInteger(n)) return n
135
+ }
136
+ if (types.includes('boolean') && (t == 'true' || t == 'false')) return t == 'true'
137
+ }
138
+ if (Array.isArray(value)) return value.map((x, i) => repair(x, schema.prefixItems?.[i] ?? schema.items, root))
139
+ if (isObj(value)) {
140
+ const o = {...value}
141
+ for (const b of schema.anyOf || [schema]) {
142
+ const bb = b?.$ref ? resolve(b, root) : b
143
+ for (const k in bb?.properties) if (k in o) o[k] = repair(o[k], bb.properties[k], root)
144
+ }
145
+ return o
146
+ }
147
+ if (typeof value == 'string' && schema.anyOf) for (const b of schema.anyOf) { const r = repair(value, b, root); if (r !== value) return r }
148
+ return value
149
+ }
150
+
151
+ /** One validation failure: the schema's message and the path as keys */
152
+ export interface Issue {message: string; path?: PropertyKey[]}
153
+
154
+ /**
155
+ * A model's arguments for `schema` -> the validated value: the lenient `{ value }` unwrap,
156
+ * `repair` (unless `repair: false`), then the schema's own validation. Resolves with
157
+ * `{ value }` (the schema's output), `{ issues }`, or `{ error }` (no JSON Schema form)
158
+ */
159
+ export async function parseInput(schema: any, args: any, options: {repair?: boolean} = {}): Promise<{value?: any; issues?: Issue[]; error?: string}> {
160
+ const c = toJsonSchema(schema)
161
+ if (c.error) return {error: c.error}
162
+ let v = c.wrapped && isObj(args) && 'value' in args ? args.value : args
163
+ if (options.repair !== false) v = repair(v, c.wrapped ? c.schema.properties.value : c.schema, c.schema)
164
+ const r = await schema['~standard'].validate(v)
165
+ return r.issues ? {issues: r.issues.map((i: any) => ({message: i.message, ...(i.path && {path: i.path.map((p: any) => isObj(p) ? p.key : p)})}))} : {value: r.value}
166
+ }
@@ -0,0 +1,72 @@
1
+ /*
2
+ * PLAN-6 A-1 (D262, 0-S4 §4): plain JSON Schema as a tool `input` (or a chat `output`), only
3
+ * through `jsonSchema(json[, { validate }])`. It returns a Standard Schema that is also a
4
+ * Standard JSON Schema, so it takes the same path as Zod / Valibot / ArkType. Without
5
+ * `validate`, a subset validator checks the value: type (integer and type arrays too), enum,
6
+ * const, required, properties, additionalProperties (false or a schema), items, prefixItems,
7
+ * anyOf / oneOf, minLength / maxLength, pattern, minimum / maximum, minItems / maxItems and local
8
+ * `$ref`s (`#`, `#/$defs/…`, `#/definitions/…`). format, allOf, not, if/then/else, multipleOf,
9
+ * uniqueItems, exclusive* and the rest are accepted unchecked. `{ validate: s }` keeps a
10
+ * library's own validation (Zod Mini: `jsonSchema(z.toJSONSchema(s), { validate: s })`).
11
+ * A separate module: tree-shaken unless imported.
12
+ */
13
+ import type {Issue} from './index'
14
+
15
+ const T = (v: any) => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v
16
+ const is = (t: string, v: any) => t == 'integer' ? Number.isInteger(v) : t == 'number' ? typeof v == 'number' && isFinite(v) : T(v) == t
17
+ const S = JSON.stringify
18
+
19
+ /** the subset check: the issues of `v` against `s` (empty when valid) */
20
+ export function check(s: any, v: any, root: any = s, path: PropertyKey[] = [], out: Issue[] = []): Issue[] {
21
+ const bad = (message: string) => (out.push({message, path}), out)
22
+ if (s === false) return bad('no value allowed')
23
+ if (!s || s === true) return out
24
+ if (s.$ref) return check(s.$ref == '#' ? root : root.$defs?.[s.$ref.slice(8)] ?? root.definitions?.[s.$ref.slice(14)], v, root, path, out)
25
+ const types = [].concat(s.type ?? []) as string[]
26
+ if (types.length && !types.some(t => is(t, v))) return bad(`expected ${types.join(' or ')}, got ${T(v)}`)
27
+ if (s.enum && !s.enum.some((e: any) => S(e) == S(v))) return bad(`expected one of ${S(s.enum)}`)
28
+ if ('const' in s && S(s.const) != S(v)) return bad(`expected ${S(s.const)}`)
29
+ const any = s.anyOf ?? s.oneOf
30
+ if (any && !any.some((b: any) => !check(b, v, root, path).length)) return bad('matches no alternative')
31
+ if (typeof v == 'string') {
32
+ if (v.length < (s.minLength ?? 0)) bad(`at least ${s.minLength} characters`)
33
+ if (v.length > (s.maxLength ?? Infinity)) bad(`at most ${s.maxLength} characters`)
34
+ if (s.pattern && !new RegExp(s.pattern, 'u').test(v)) bad(`must match ${s.pattern}`)
35
+ }
36
+ if (typeof v == 'number') {
37
+ if (v < (s.minimum ?? -Infinity)) bad(`at least ${s.minimum}`)
38
+ if (v > (s.maximum ?? Infinity)) bad(`at most ${s.maximum}`)
39
+ }
40
+ if (Array.isArray(v)) {
41
+ if (v.length < (s.minItems ?? 0)) bad(`at least ${s.minItems} items`)
42
+ if (v.length > (s.maxItems ?? Infinity)) bad(`at most ${s.maxItems} items`)
43
+ const pre = s.prefixItems ?? []
44
+ v.forEach((x, i) => check(i < pre.length ? pre[i] : s.items, x, root, [...path, i], out))
45
+ } else if (T(v) == 'object') {
46
+ for (const k of s.required ?? []) if (!(k in v)) bad(`${k} is required`)
47
+ const p = s.properties ?? {}
48
+ for (const k in v) check(k in p ? p[k] : s.additionalProperties, v[k], root, [...path, k], out)
49
+ }
50
+ return out
51
+ }
52
+
53
+ /**
54
+ * A plain JSON Schema as a Standard Schema with Standard JSON Schema: the schema goes to the
55
+ * model as is (normalized), and the subset validator (or `options.validate`, a Standard Schema)
56
+ * checks the value
57
+ */
58
+ export function jsonSchema(schema: any, options: {validate?: any} = {}): any {
59
+ const own = options.validate?.['~standard']
60
+ const out = () => schema
61
+ return {
62
+ '~standard': {
63
+ version: 1,
64
+ vendor: 'sygnal',
65
+ validate: own ? (v: any) => own.validate(v) : (value: any) => {
66
+ const issues = check(schema, value)
67
+ return issues.length ? {issues} : {value}
68
+ },
69
+ jsonSchema: {input: out, output: out},
70
+ },
71
+ }
72
+ }