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.
- package/CHANGELOG.md +716 -0
- package/README.md +24 -9
- package/dist/ai.cjs.js +115 -0
- package/dist/ai.cjs.js.map +1 -0
- package/dist/ai.esm.js +2 -0
- package/dist/ai.esm.js.map +1 -0
- package/dist/astro/client.cjs.js +16 -9
- package/dist/astro/client.cjs.js.map +1 -1
- package/dist/astro/client.mjs +16 -9
- package/dist/astro/client.mjs.map +1 -1
- package/dist/astro/index.cjs.js +958 -92
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +958 -93
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3396 -165
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3396 -165
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +1840 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1827 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +3190 -296
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +3190 -296
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +310 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/agent.md +588 -0
- package/dist/guide/ai-chat.md +693 -0
- package/dist/guide/ai-decisions.md +330 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +409 -0
- package/dist/guide/forms.md +251 -0
- package/dist/guide/http.md +301 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/mcp-apps.md +272 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/ai-form-fill.md +163 -0
- package/dist/guide/recipes/ai-summarize.md +155 -0
- package/dist/guide/recipes/ai-support-inbox.md +190 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +43 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/webmcp.md +226 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +19288 -7408
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4466 -552
- package/dist/index.esm.js +19274 -7461
- package/dist/index.esm.js.map +1 -1
- package/dist/jsx-dev-runtime.cjs.js +26 -334
- package/dist/jsx-dev-runtime.cjs.js.map +1 -1
- package/dist/jsx-dev-runtime.esm.js +19 -327
- package/dist/jsx-dev-runtime.esm.js.map +1 -1
- package/dist/jsx-runtime.cjs.js +26 -334
- package/dist/jsx-runtime.cjs.js.map +1 -1
- package/dist/jsx-runtime.esm.js +19 -327
- package/dist/jsx-runtime.esm.js.map +1 -1
- package/dist/jsx.cjs.js +10 -314
- package/dist/jsx.cjs.js.map +1 -1
- package/dist/jsx.esm.js +8 -315
- package/dist/jsx.esm.js.map +1 -1
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +4 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -1
- package/dist/vike/ClientOnly.mjs +4 -14
- package/dist/vike/ClientOnly.mjs.map +1 -1
- package/dist/vike/{+config.js → config/+config.js} +9 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +165 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +165 -95
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +59 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +60 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +919 -89
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +919 -90
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +172 -103
- package/package.json +112 -14
- package/src/ai.d.ts +1186 -0
- package/src/ai.ts +23 -0
- package/src/astro/client.ts +27 -16
- package/src/astro/index.d.ts +24 -0
- package/src/astro/index.ts +69 -3
- package/src/astro/server.ts +8 -2
- package/src/collection.ts +6 -93
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +9 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +9 -7
- package/src/cycle/dom/controlledInputModule.ts +23 -6
- package/src/cycle/dom/enrichEventStream.ts +25 -47
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -56
- package/src/cycle/dom/mockDOMSource.ts +21 -1
- package/src/cycle/dom/modules.ts +16 -3
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +19 -3
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +170 -0
- package/src/devtools.ts +37 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/ai/agent/index.ts +484 -0
- package/src/extra/ai/answers.ts +115 -0
- package/src/extra/ai/chat/behavior.ts +336 -0
- package/src/extra/ai/chat/driver.ts +271 -0
- package/src/extra/ai/chat/memoryTransport.ts +61 -0
- package/src/extra/ai/chat/output.ts +37 -0
- package/src/extra/ai/commandBar.ts +281 -0
- package/src/extra/ai/decide.ts +104 -0
- package/src/extra/ai/index.ts +42 -0
- package/src/extra/ai/link.ts +90 -0
- package/src/extra/ai/mcpApp.ts +314 -0
- package/src/extra/ai/messages.ts +51 -0
- package/src/extra/ai/schema/index.ts +166 -0
- package/src/extra/ai/schema/jsonSchema.ts +72 -0
- package/src/extra/ai/schema/strict.ts +152 -0
- package/src/extra/ai/transports/agui.ts +177 -0
- package/src/extra/ai/transports/anthropicMessages.ts +175 -0
- package/src/extra/ai/transports/chatCompletions.ts +105 -0
- package/src/extra/ai/transports/chromePrompt.ts +77 -0
- package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
- package/src/extra/ai/transports/fromAISDK.ts +126 -0
- package/src/extra/ai/transports/openResponses.ts +105 -0
- package/src/extra/ai/transports/shared.ts +125 -0
- package/src/extra/ai/transports/tools.ts +59 -0
- package/src/extra/ai/transports/uiMessageStream.ts +107 -0
- package/src/extra/ai/webmcp.ts +260 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devMcp.ts +386 -0
- package/src/extra/devtools.ts +101 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/chat.ts +71 -0
- package/src/extra/diagnostics/checks/collections.ts +1 -1
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +20 -11
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +17 -2
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +171 -0
- package/src/extra/diagnostics/checks/index.ts +103 -4
- package/src/extra/diagnostics/checks/inspect.ts +140 -32
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +10 -5
- package/src/extra/diagnostics/checks/public.d.ts +115 -7
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
- package/src/extra/diagnostics/checks/shared.ts +86 -2
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +100 -7
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +10 -67
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +69 -10
- package/src/extra/diagnostics/codes.ts +335 -23
- package/src/extra/diagnostics/index.ts +34 -9
- package/src/extra/diagnostics/legacy.ts +17 -0
- package/src/extra/driverFactories.ts +83 -19
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +284 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/formTool.ts +150 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +1 -1
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +90 -217
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +2550 -175
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/index.d.ts +2754 -127
- package/src/index.ts +45 -4
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +256 -133
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +57 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -121
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +7 -1
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/mcp.ts +568 -0
- package/src/vite/plugin.d.ts +47 -3
- package/src/vite/plugin.ts +262 -25
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/vike/+config.cjs.js +0 -66
- package/dist/vike/+config.cjs.js.map +0 -1
- package/dist/vike/+config.js.map +0 -1
- package/src/component.ts +0 -2301
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -184
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -201
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- 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})
|