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,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 L-1 / L-4: an in-memory chat transport whose streams are driven by hand. renderComponent's
|
|
3
|
+
* LLM fake runs the real chat driver over it (as the HTTP fake runs makeFetchDriver over an
|
|
4
|
+
* in-memory fetch), and the driver's own tests use it. Internal: not exported from 'sygnal/ai'.
|
|
5
|
+
*
|
|
6
|
+
* memoryTransport({ onStream?, onIdle? }).stream(request, signal) returns an async iterable and
|
|
7
|
+
* registers a handle synchronously (so a test sees it right after the sink emitted):
|
|
8
|
+
* handle.request, handle.signal, handle.live (not ended, failed or aborted), handle.aborted
|
|
9
|
+
* handle.push(...events) ChatEvents, or strings (text deltas)
|
|
10
|
+
* handle.end(finish?) a finish event ({ reason?, usage? }), then done
|
|
11
|
+
* handle.fail(error) the iterator throws it
|
|
12
|
+
* `onIdle(handle)` runs when the driver has consumed every queued event and waits for more (the
|
|
13
|
+
* fake's "end of a frame").
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const toEvent = (c: any) => typeof c == 'string' ? {type: 'text', delta: c} : c;
|
|
17
|
+
const abortError = () => Object.assign(new Error('The operation was aborted.'), {name: 'AbortError'});
|
|
18
|
+
|
|
19
|
+
export function memoryTransport(opts: {onStream?: (h: any) => void; onIdle?: (h: any) => void} = {}) {
|
|
20
|
+
const streams: any[] = [];
|
|
21
|
+
const stream = (request: any, signal: AbortSignal) => {
|
|
22
|
+
const queue: any[] = [];
|
|
23
|
+
let ended = false, failed: any, waiting: any = null;
|
|
24
|
+
const wake = () => {
|
|
25
|
+
if (!waiting) return;
|
|
26
|
+
const w = waiting;
|
|
27
|
+
waiting = null;
|
|
28
|
+
pull(w[0], w[1]);
|
|
29
|
+
};
|
|
30
|
+
const h: any = {
|
|
31
|
+
request, signal, live: true, aborted: false,
|
|
32
|
+
push: (...evs: any[]) => { if (h.live) { queue.push(...evs.map(toEvent)); wake(); } return h; },
|
|
33
|
+
end: (finish?: any) => {
|
|
34
|
+
if (h.live) { queue.push({type: 'finish', ...finish}); ended = true; h.live = false; wake(); }
|
|
35
|
+
return h;
|
|
36
|
+
},
|
|
37
|
+
fail: (e: any) => {
|
|
38
|
+
if (h.live) { failed = e ?? new Error('stream failed'); h.live = false; wake(); }
|
|
39
|
+
return h;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const pull = (resolve: any, reject: any) => {
|
|
43
|
+
if (signal.aborted) return reject(abortError());
|
|
44
|
+
if (queue.length) return resolve({value: queue.shift(), done: false});
|
|
45
|
+
if (failed) return reject(failed);
|
|
46
|
+
if (ended) return resolve({value: undefined, done: true});
|
|
47
|
+
waiting = [resolve, reject];
|
|
48
|
+
opts.onIdle?.(h);
|
|
49
|
+
};
|
|
50
|
+
signal.addEventListener('abort', () => { h.live = false; h.aborted = true; wake(); });
|
|
51
|
+
streams.push(h);
|
|
52
|
+
opts.onStream?.(h);
|
|
53
|
+
return {
|
|
54
|
+
[Symbol.asyncIterator]: () => ({
|
|
55
|
+
next: () => new Promise((resolve, reject) => pull(resolve, reject)),
|
|
56
|
+
return: () => { h.live = false; return Promise.resolve({value: undefined, done: true}); },
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
return {stream, streams};
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {isStandardSchema, validateWith} from '../../standardSchema';
|
|
2
|
+
import {toJsonSchema} from '../schema/index';
|
|
3
|
+
import {unstrict} from '../schema/strict';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* PLAN-6 L-1 structured output (0-S4, G-604): a request's `output` is a Standard Schema with
|
|
7
|
+
* Standard JSON Schema (or 1-A's jsonSchema()). The transport sends `outputJsonSchema(output)`;
|
|
8
|
+
* the driver reads the reply text with `readOutput()` and puts the validated value on `ok`
|
|
9
|
+
* (a failure is the `error` reply with `issues`).
|
|
10
|
+
*
|
|
11
|
+
* The JSON Schema comes from A-1's schema module (toJsonSchema: the portable normalization,
|
|
12
|
+
* cached per schema object, `{ value }` wrapping; jsonSchema() for plain JSON Schema).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** the JSON Schema a transport sends for `output`: `{ schema, wrapped }` (a non-object root wrapped as `{ value }`), or undefined */
|
|
16
|
+
export function outputJsonSchema(output: any): {schema: Record<string, any>; wrapped: boolean} | undefined {
|
|
17
|
+
const c = toJsonSchema(output);
|
|
18
|
+
return c.schema ? {schema: c.schema, wrapped: !!c.wrapped} : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const invalid = (message: string) => Object.assign(new Error(message), {name: 'ValidationError', issues: [{message}]});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* the structured output in a reply's text: JSON (a ```json fence allowed), unwrapped from
|
|
25
|
+
* `{ value }` when the schema was wrapped (a bare value accepted too), then validated. Nulls for
|
|
26
|
+
* keys the schema leaves optional and doesn't allow null for are dropped first (L-2: a transport's
|
|
27
|
+
* strict mode makes the model send them; small models send them too), as `undefined` would be
|
|
28
|
+
*/
|
|
29
|
+
export async function readOutput(output: any, text: string): Promise<any> {
|
|
30
|
+
let v: any;
|
|
31
|
+
const body = text.trim().replace(/^```(?:json)?\s*([\s\S]*?)\s*```$/, '$1');
|
|
32
|
+
try { v = JSON.parse(body); } catch (_) { throw invalid(`The structured output is not JSON: ${body.length > 80 ? body.slice(0, 80) + '…' : body}`); }
|
|
33
|
+
const js = outputJsonSchema(output);
|
|
34
|
+
if (js) v = unstrict(v, js.schema);
|
|
35
|
+
if (js?.wrapped && v && typeof v == 'object' && !Array.isArray(v) && Object.keys(v).length == 1 && 'value' in v) v = v.value;
|
|
36
|
+
return isStandardSchema(output) ? validateWith(output, v) : v;
|
|
37
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 M-3: the `commandBar` behavior, "do one thing in this app" from one line of text, on a
|
|
3
|
+
* decision model instead of a chat model (Experiment 5: 8/8 with local `nimble`, ~570 ms).
|
|
4
|
+
*
|
|
5
|
+
* TodoApp.uses = { cmd: commandBar({ input: '.command', decide: { url: '/api/decide', model: 'jev-latest' },
|
|
6
|
+
* below: 0.6, escalate: 'assistant', approve: '.cmd-approve', deny: '.cmd-deny' }) }
|
|
7
|
+
*
|
|
8
|
+
* Slice (state.cmd): { text, status, command, pending, unsure, result, error }
|
|
9
|
+
* - text: the input's value (cleared when a command ran);
|
|
10
|
+
* - status: 'ready' | 'deciding' (the decision request is out) | 'running' (the action runs, or
|
|
11
|
+
* waits for APPROVE / DENY) | 'error' (the decision request failed: `error`);
|
|
12
|
+
* - command: the command being handled (null when ready);
|
|
13
|
+
* - pending: the confirm info (AgentConfirmInfo) of a consequential action, else null;
|
|
14
|
+
* - unsure: why the last command wasn't run, for the view: `{ command, reason, tool, description,
|
|
15
|
+
* target, label, confidence }` with reason 'confidence' (below `below`), 'no-action' (the model
|
|
16
|
+
* picked "none of these"), 'target' (an item action without a clear item) or 'input' (the
|
|
17
|
+
* action needs an argument the bar can't fill); null otherwise. With `escalate` the command
|
|
18
|
+
* goes to the chat behavior instead (`result.escalated`);
|
|
19
|
+
* - result: the last outcome, `{ command, tool, input, ...AgentResult }` (ok, error, state, ...),
|
|
20
|
+
* or `{ command, escalated: '<chat key>', reason, confidence }`; error: the last failure, else null.
|
|
21
|
+
*
|
|
22
|
+
* Actions ('cmd.X'): RUN (the input's Enter, or the form's submit, and a click of the optional
|
|
23
|
+
* `run` selector, D288; a string as data runs that command), APPROVE, DENY, DONE (a command ran: the result; a host entry 'cmd.DONE' runs after it).
|
|
24
|
+
* Internal: INPUT (typing), DECIDED / FAILED (the fetch driver's reply actions), ASK.
|
|
25
|
+
*
|
|
26
|
+
* One decision request per command (through the app's fetch driver, `decide()`), two questions:
|
|
27
|
+
* - action: a `choice` over the host's and its shown descendants' `agent` actions (D249; `agent:
|
|
28
|
+
* [Comp, …]` narrows), keyed by tool name, described by the action's description, plus
|
|
29
|
+
* `none` ("none of these"). An action whose input is one enum (or boolean) field becomes one
|
|
30
|
+
* option per value (`todos_set_filter=done`), so the model picks the argument too;
|
|
31
|
+
* - target (only when an item action exists): a `choice` over the live Collection item keys,
|
|
32
|
+
* described by `agent.label` (D258), plus `none`. User-entered labels (A-1's labelsUntrusted,
|
|
33
|
+
* G-644) stay out of the criteria, which a model reads like instructions: the option is described
|
|
34
|
+
* by kind and id, and its label goes into the decision's `state.labels` (option -> label).
|
|
35
|
+
* The decision `state` is `{ command, app, labels? }`, `app` being the `read` projections (A-1's context).
|
|
36
|
+
* Then the call runs through A-1's `call()` (validation, repair, no-op detection, cause 'agent',
|
|
37
|
+
* `confirm` → `pending` for a consequential action).
|
|
38
|
+
*
|
|
39
|
+
* Free text arguments (ADD's text): a decision model only picks among options, so the text is
|
|
40
|
+
* taken from the command by a heuristic, documented as one (G-634, freeText below): a quoted part
|
|
41
|
+
* ("…", '…', “…”) if there is one, else the command without its lead-in ("remind me to call mom" →
|
|
42
|
+
* "call mom", "put milk on my list" → "milk"); a question gives undefined. It is validated like any
|
|
43
|
+
* agent input. `freeText: (command, tool) => string | undefined` replaces it;
|
|
44
|
+
* undefined means "can't tell": the command is escalated (or `unsure`, reason 'input'). Actions with
|
|
45
|
+
* other inputs (numbers, several fields) are offered too, and escalated / unsure when picked: a
|
|
46
|
+
* chat model fills arguments, a decision model doesn't.
|
|
47
|
+
*
|
|
48
|
+
* It reaches the runtime through the shared link (./link.ts, D283), as `chat` does.
|
|
49
|
+
*/
|
|
50
|
+
import xs from 'xstream'
|
|
51
|
+
import {defineBehavior} from '../behaviors'
|
|
52
|
+
import {ABORT} from '../../shared'
|
|
53
|
+
import {agentTools, labelsUntrusted} from './agent/index'
|
|
54
|
+
import {decide as decideRequest} from './decide'
|
|
55
|
+
import {linked, engineOf, checkLinked} from './link'
|
|
56
|
+
|
|
57
|
+
interface Opt {tool: string; args?: any; free?: string; needs?: 1; description: string}
|
|
58
|
+
interface Asked {command: string; opts: Map<string, Opt>; targets: Map<string, {name: string; id: any; label?: string}>; items: Map<string, {key: string; name: string}>}
|
|
59
|
+
interface Engine {api: any; iv: any; k: string; o: any; tools?: any; asked?: Asked; turn: number; answer?: (ok: boolean) => void; stop(): void}
|
|
60
|
+
|
|
61
|
+
const engineFor = (props: any, k: string, o: any): Engine | undefined => engineOf<Engine>(props, k, (api, iv) => {
|
|
62
|
+
const en: Engine = {api, iv, k, o, turn: 0, stop() { en.turn++; en.answer?.(false); en.tools?.stop() }}
|
|
63
|
+
return en
|
|
64
|
+
})
|
|
65
|
+
const dispatch = (e: Engine, type: string, data: any, cause: string) => e.api.dispatch(e.iv.id, e.k + '.' + type, data, cause)
|
|
66
|
+
|
|
67
|
+
const toolsOf = (e: Engine) => e.tools ||= agentTools(e.api, {
|
|
68
|
+
from: e.iv.id,
|
|
69
|
+
...(Array.isArray(e.o.agent) && {components: e.o.agent}),
|
|
70
|
+
confirm: (info: any) => new Promise<boolean>(resolve => {
|
|
71
|
+
const turn = e.turn
|
|
72
|
+
e.answer = ok => { e.answer = undefined; resolve(ok) }
|
|
73
|
+
turn === e.turn ? dispatch(e, 'ASK', info, 'reply') : e.answer(false)
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// G-634: the free-text heuristic's words. Fillers before the request, the verbs that ask to add
|
|
78
|
+
// something, the nouns that name an entry, and a destination at the end ("… to my list")
|
|
79
|
+
const FILLER = "please|pls|hey|ok(?:ay)?|so|just|(?:can|could|would|will) you|i(?: want| need| have|'d like| would like) to|let'?s|go ahead and|(?:do not|don'?t) forget to|remember to|make sure to"
|
|
80
|
+
const VERBS = 'add|create|make|put|insert|append|include|new|remind|remember|note|track|jot(?: down)?|write(?: down)?|note down'
|
|
81
|
+
const NOUNS = 'todo|to-do|task|item|entry|note|reminder'
|
|
82
|
+
const words = (x: string) => x.toLowerCase().replace(/[^a-z' -]/g, '').trim()
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The documented heuristic (G-634): a quoted part if there is one; else the command without its
|
|
86
|
+
* lead-in, which is: fillers ("please", "can you", "I need to", "don't forget to"), then a verb
|
|
87
|
+
* asking to add (add, create, put, remind, jot down, …, or the first word of the action's
|
|
88
|
+
* `description`) with what follows it ("me to", "about", an article, "a new todo:", "a task to",
|
|
89
|
+
* "to my list:"; the last word of the description counts as a noun), and a destination at the end
|
|
90
|
+
* ("to the list", "on my shopping list"). A command with no verb keeps its words ("buy eggs").
|
|
91
|
+
* undefined ("can't tell": the command escalates) for a question or nothing left.
|
|
92
|
+
*/
|
|
93
|
+
export const freeText = (command: string, description = ''): string | undefined => {
|
|
94
|
+
const q = /"([^"]+)"|“([^”]+)”|'([^']+)'/.exec(command)
|
|
95
|
+
if (q) return (q[1] ?? q[2] ?? q[3]).trim() || undefined
|
|
96
|
+
const c = command.trim()
|
|
97
|
+
if (c.endsWith('?')) return
|
|
98
|
+
const d = words(description).split(/\s+/), v = d.length > 1 ? `|${d[0]}` : '', n = d.length > 1 ? `|${d[d.length - 1]}s?` : ''
|
|
99
|
+
const noun = `(?:(?:a|an|the|another|one|my)\\s+)?(?:new\\s+)?(?:${NOUNS}${n})\\b\\s*(?:(?::|-|to|that|saying|called|named)\\s+)?`
|
|
100
|
+
const list = `(?:to|on|onto|in|into)\\s+(?:the|my|our)\\s+(?:[\\w-]+\\s+)?(?:list|${NOUNS}${n})s?`
|
|
101
|
+
const t = c.replace(/[.!]+$/, '')
|
|
102
|
+
.replace(new RegExp(`^(?:(?:${FILLER})\\s+)*(?:(?:${VERBS}${v})\\b\\s*:?\\s*(?:(?:me|us)\\s+)?(?:${list}\\s*:\\s*|(?:to|that|about)\\s+)?(?:${noun}|(?:a|an|the)\\s+)?)?(?:${noun})?`, 'i'), '')
|
|
103
|
+
.replace(new RegExp(`\\s+(?:${list}|please|for me)$`, 'i'), '')
|
|
104
|
+
return t.trim() || undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** the questions for `command` over the live tools; `e.asked` maps the answers back */
|
|
108
|
+
function ask(e: Engine, command: string) {
|
|
109
|
+
const t = toolsOf(e), opts = new Map<string, Opt>(), items = new Map<string, {key: string; name: string}>()
|
|
110
|
+
const groups = new Map<string, string>(), bare = new Set<string>()
|
|
111
|
+
for (const g of t.groups()) {
|
|
112
|
+
for (const n of g.tools) groups.set(n, g.name)
|
|
113
|
+
if (labelsUntrusted(g)) bare.add(g.name)
|
|
114
|
+
}
|
|
115
|
+
const targets = t.targets()
|
|
116
|
+
const itemNames = new Set(targets.map((x: any) => x.name))
|
|
117
|
+
for (const tool of t.list()) {
|
|
118
|
+
if (tool.annotations.readOnlyHint) continue
|
|
119
|
+
const s = tool.inputSchema || {}, name = groups.get(tool.name) || ''
|
|
120
|
+
const props = {...s.properties}, req = new Set<string>(s.required || [])
|
|
121
|
+
if (itemNames.has(name) && s.required?.[0] && props[s.required[0]]?.enum) {
|
|
122
|
+
const key = s.required[0]
|
|
123
|
+
items.set(tool.name, {key, name})
|
|
124
|
+
delete props[key]
|
|
125
|
+
req.delete(key)
|
|
126
|
+
}
|
|
127
|
+
const fields = Object.keys(props), base = {tool: tool.name, description: tool.description}
|
|
128
|
+
const f = fields.length == 1 ? fields[0] : '', p = f && props[f]
|
|
129
|
+
// one option per value: an enum, a boolean, or `anyOf` consts (described literals: their own descriptions)
|
|
130
|
+
const values: any[] | null = !p ? null : Array.isArray(p.enum) ? p.enum.map((v: any) => [v]) : p.type == 'boolean' ? [[true], [false]]
|
|
131
|
+
: Array.isArray(p.anyOf) && p.anyOf.every((x: any) => x && 'const' in x) ? p.anyOf.map((x: any) => [x.const, x.description]) : null
|
|
132
|
+
if (!fields.length || (!req.size && !values)) opts.set(tool.name, {...base, args: {}})
|
|
133
|
+
else if (values) for (const [v, d] of values) opts.set(`${tool.name}=${v}`, {...base, args: {[f]: v}, description: d || `${tool.description}: ${v}`})
|
|
134
|
+
else if (p && p.type == 'string') opts.set(tool.name, {...base, free: f})
|
|
135
|
+
else opts.set(tool.name, {...base, needs: 1})
|
|
136
|
+
}
|
|
137
|
+
const many = new Set(targets.map((x: any) => x.name)).size > 1, tmap = new Map<string, any>()
|
|
138
|
+
for (const x of targets) tmap.set(many ? `${x.name} ${x.id}` : String(x.id), x)
|
|
139
|
+
const kinds = [...new Set(targets.map((x: any) => x.name))]
|
|
140
|
+
const questions: any = {
|
|
141
|
+
action: {type: 'choice', instructions: 'Which app action does the command ask for?',
|
|
142
|
+
criteria: {...Object.fromEntries([...opts].map(([k, v]) => [k, v.description])), none: 'None of these actions'}},
|
|
143
|
+
}
|
|
144
|
+
// G-644: user-entered labels are data: they go into the decision's `state` (`labels`: option ->
|
|
145
|
+
// label), and the criteria (read like instructions) name the option by kind and id only
|
|
146
|
+
const labels: Record<string, string> = {}
|
|
147
|
+
if (items.size && tmap.size) {
|
|
148
|
+
const what = kinds.length == 1 ? kinds[0] : 'item'
|
|
149
|
+
const desc = (k: string, x: any) => {
|
|
150
|
+
if (x.label == null) return `${x.name} ${x.id}`
|
|
151
|
+
if (!bare.has(x.name)) return x.label
|
|
152
|
+
labels[k] = x.label
|
|
153
|
+
return `The ${x.name} with id ${x.id} (its label: state.labels["${k}"])`
|
|
154
|
+
}
|
|
155
|
+
questions.target = {type: 'choice', instructions: `Which existing ${what} does the command refer to (none if it names no existing ${what})?`
|
|
156
|
+
+ (bare.size ? " Each option's label is in state.labels." : ''),
|
|
157
|
+
criteria: {none: `No existing ${what}`, ...Object.fromEntries([...tmap].map(([k, x]) => [k, desc(k, x)]))}}
|
|
158
|
+
}
|
|
159
|
+
e.asked = {command, opts, targets: tmap, items}
|
|
160
|
+
const ctx = t.context()
|
|
161
|
+
return {questions, state: {command, ...(Object.keys(ctx).length && {app: ctx}), ...(Object.keys(labels).length && {labels})}}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** one step's outcome per (slice, data): STATE and the sink / EFFECT of an action read the same one */
|
|
165
|
+
const memo = (f: (s: any, d: any, e?: Engine) => any) => {
|
|
166
|
+
let l: any[] = []
|
|
167
|
+
return (s: any, d: any, e?: Engine) => l[0] === s && l[1] === d && l[2] === e ? l[3] : (l = [s, d, e, f(s, d, e)])[3]
|
|
168
|
+
}
|
|
169
|
+
const busy = (s: any) => s.status == 'deciding' || s.status == 'running'
|
|
170
|
+
const commandOf = (s: any, d: any) => (typeof d == 'string' ? d : s.text).trim()
|
|
171
|
+
|
|
172
|
+
const runStep = /*#__PURE__*/ memo((s, d, e) => {
|
|
173
|
+
const command = commandOf(s, d)
|
|
174
|
+
if (busy(s) || !command) return
|
|
175
|
+
const next = {...s, status: 'deciding', command, unsure: null, error: null, pending: null}
|
|
176
|
+
if (!e) return {s: {...next, status: 'error', command: null, error: 'the command bar is not connected to its app (SYG442)'}}
|
|
177
|
+
e.turn++
|
|
178
|
+
return {s: next, ...ask(e, command)}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
/** what a decision means: run a tool, or why not */
|
|
182
|
+
const planStep = /*#__PURE__*/ memo((s, d, e) => {
|
|
183
|
+
const asked = e?.asked
|
|
184
|
+
if (s.status != 'deciding' || !asked || asked.command !== s.command) return
|
|
185
|
+
const o = e!.o, below = o.below ?? 0.6, command = asked.command
|
|
186
|
+
const a = d?.answers?.action, tg = d?.answers?.target
|
|
187
|
+
const opt = a && asked.opts.get(a.choice)
|
|
188
|
+
const item = opt && asked.items.get(opt.tool)
|
|
189
|
+
const target = item && tg && asked.targets.get(tg.choice)
|
|
190
|
+
let reason: string | undefined, args: any
|
|
191
|
+
if (!opt) reason = 'no-action'
|
|
192
|
+
else if (!(a.confidence >= below)) reason = 'confidence'
|
|
193
|
+
else if (item && (!target || target.name !== item.name || !(tg.confidence >= below))) reason = 'target'
|
|
194
|
+
else if (opt.needs) reason = 'input'
|
|
195
|
+
else if (opt.free) {
|
|
196
|
+
const x = o.freeText ? o.freeText(command, opt.tool) : freeText(command, opt.description)
|
|
197
|
+
if (typeof x != 'string' || !x) reason = 'input'
|
|
198
|
+
else args = {[opt.free]: x}
|
|
199
|
+
} else args = opt.args
|
|
200
|
+
const why = {tool: opt?.tool ?? null, description: opt?.description ?? null, target: target?.id ?? null, label: target?.label ?? null,
|
|
201
|
+
confidence: reason == 'target' ? tg?.confidence ?? 0 : a?.confidence ?? 0}
|
|
202
|
+
if (reason) {
|
|
203
|
+
return o.escalate
|
|
204
|
+
? {s: {...s, status: 'ready', command: null, result: {command, escalated: o.escalate, reason, ...why}}, escalate: command}
|
|
205
|
+
: {s: {...s, status: 'ready', command: null, unsure: {command, reason, ...why}}}
|
|
206
|
+
}
|
|
207
|
+
if (item) args = {...args, [item.key]: target!.id}
|
|
208
|
+
return {s: {...s, status: 'running'}, call: {tool: opt.tool, input: args}}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
async function runCall(e: Engine, command: string, c: {tool: string; input: any}) {
|
|
212
|
+
const turn = e.turn
|
|
213
|
+
let r: any
|
|
214
|
+
try { r = await toolsOf(e).call(c.tool, c.input) } catch (err: any) { r = {ok: false, error: String(err?.message ?? err)} }
|
|
215
|
+
if (turn === e.turn) dispatch(e, 'DONE', {command, ...c, ...r}, 'reply')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The command bar behavior (PLAN-6 M-3): `uses = { cmd: commandBar({ input, form, run, decide, below,
|
|
220
|
+
* escalate, agent, sink, approve, deny, freeText }) }`. Types: src/ai.d.ts.
|
|
221
|
+
*/
|
|
222
|
+
export const commandBar = (options: any = {}): any => {
|
|
223
|
+
const {sink = 'HTTP', input, form, run, approve, deny} = options
|
|
224
|
+
const on = (DOM: any, sel: any, ev = 'click', o?: any) => DOM.select(sel).events(ev, o)
|
|
225
|
+
const E = engineFor
|
|
226
|
+
const b = defineBehavior({
|
|
227
|
+
initialState: {text: '', status: 'ready', command: null, pending: null, unsure: null, result: null, error: null},
|
|
228
|
+
intent: (so: any) => {
|
|
229
|
+
const {DOM} = so
|
|
230
|
+
checkLinked(so, 'commandBar', 'it runs no commands')
|
|
231
|
+
// Enter in the field runs it (a form's submit instead when `form` is given); D288: and a
|
|
232
|
+
// click of `run` (a Go button: the field's text)
|
|
233
|
+
const runs = [
|
|
234
|
+
form ? on(DOM, form, 'submit', {preventDefault: true})
|
|
235
|
+
: input && on(DOM, input, 'keydown').filter((ev: any) => ev.key === 'Enter' && !ev.isComposing).map((ev: any) => { ev.preventDefault?.(); return typeof ev.target?.value == 'string' ? ev.target.value : undefined }),
|
|
236
|
+
run && on(DOM, run).mapTo(undefined),
|
|
237
|
+
].filter(Boolean)
|
|
238
|
+
return {
|
|
239
|
+
...(input && {INPUT: on(DOM, input, 'input').map((ev: any) => ev.target.value)}),
|
|
240
|
+
...(runs.length && {RUN: runs.length == 1 ? runs[0] : xs.merge(...runs)}),
|
|
241
|
+
...(approve && {APPROVE: on(DOM, approve)}),
|
|
242
|
+
...(deny && {DENY: on(DOM, deny)}),
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
model: {
|
|
246
|
+
INPUT: (s: any, v: any) => typeof v == 'string' && v !== s.text ? {...s, text: v} : ABORT,
|
|
247
|
+
RUN: {
|
|
248
|
+
STATE: (s: any, d: any, _n: any, p: any, o: any, k: string) => runStep(s, d, E(p, k, o))?.s || ABORT,
|
|
249
|
+
[sink]: (s: any, d: any, _n: any, p: any, o: any, k: string) => {
|
|
250
|
+
const r = runStep(s, d, E(p, k, o))
|
|
251
|
+
if (!r?.questions) return ABORT
|
|
252
|
+
const keys = {ok: k + '.DECIDED', error: k + '.FAILED'}
|
|
253
|
+
return typeof o.decide == 'function' ? {...o.decide({state: r.state, questions: r.questions}), ...keys}
|
|
254
|
+
: decideRequest({...o.decide, state: r.state, questions: r.questions, ...keys})
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
DECIDED: {
|
|
258
|
+
STATE: (s: any, d: any, _n: any, p: any, o: any, k: string) => planStep(s, d, E(p, k, o))?.s || ABORT,
|
|
259
|
+
EFFECT: (s: any, d: any, next: any, p: any, o: any, k: string) => {
|
|
260
|
+
const e = E(p, k, o), r = planStep(s, d, e)
|
|
261
|
+
if (r?.call) void runCall(e!, s.command, r.call)
|
|
262
|
+
else if (r?.escalate) next(o.escalate + '.SEND', r.escalate)
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
FAILED: (s: any, d: any) => s.status == 'deciding' ? {...s, status: 'error', command: null, error: d?.error?.message ?? String(d?.error ?? 'error')} : ABORT,
|
|
266
|
+
ASK: (s: any, info: any) => ({...s, pending: info}),
|
|
267
|
+
APPROVE: {
|
|
268
|
+
STATE: (s: any) => s.pending ? {...s, pending: null} : ABORT,
|
|
269
|
+
EFFECT: (_s: any, _d: any, _n: any, p: any, o: any, k: string) => { E(p, k, o)?.answer?.(true) },
|
|
270
|
+
},
|
|
271
|
+
DENY: {
|
|
272
|
+
STATE: (s: any) => s.pending ? {...s, pending: null} : ABORT,
|
|
273
|
+
EFFECT: (_s: any, _d: any, _n: any, p: any, o: any, k: string) => { E(p, k, o)?.answer?.(false) },
|
|
274
|
+
},
|
|
275
|
+
// a command ran (or was refused): the result; the input is cleared when it was this command
|
|
276
|
+
DONE: (s: any, d: any) => s.status != 'running' ? ABORT
|
|
277
|
+
: {...s, status: 'ready', command: null, pending: null, result: d, text: d?.ok && s.text.trim() === d.command ? '' : s.text},
|
|
278
|
+
},
|
|
279
|
+
})(options)
|
|
280
|
+
return linked(b)
|
|
281
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 M-1: decisions (TypeSafe Jev, Ollama `nimble`/`clef`, OpenRouter, AI Gateway, OpenAI
|
|
3
|
+
* Decisions) are plain HTTP requests, so there is no driver here: `decide()` builds a
|
|
4
|
+
* `makeFetchDriver` request that works as a reply-action request (`ok` / `error`) and as a
|
|
5
|
+
* `resources` entry. Types and docs: src/ai.d.ts.
|
|
6
|
+
*
|
|
7
|
+
* Dictionary form (TypeSafe `/v1/systemone`, Ollama ≥ 0.35 `/v1/systemone`; verified against
|
|
8
|
+
* Ollama 0.40.2 + nimble):
|
|
9
|
+
* request { model, state, questions: { name: { type, instructions, criteria? } }, images? }
|
|
10
|
+
* choice: criteria { option: description | null }; noul: criteria { true, false }
|
|
11
|
+
* (optional); score: criteria [level descriptions, lowest first]
|
|
12
|
+
* response { model, answers: { name: { type: 'choice', choice, probabilities, confidence }
|
|
13
|
+
* | { type: 'noul', noul } | { type: 'score', score, legend, probabilities, confidence } },
|
|
14
|
+
* usage: { input_tokens, output_tokens } }
|
|
15
|
+
*
|
|
16
|
+
* OpenAI Decisions (`POST /v1/decisions`, the array form): `decide.openai()` sends
|
|
17
|
+
* { model, input, questions: [{ type: 'predicate' | 'choice' | 'score', name, instructions,
|
|
18
|
+
* choices?: [{ value, description }], levels?: [{ label, description }] }] }
|
|
19
|
+
* and its `parse` maps `{ answers: [{ type, name, … }] }` back to the dictionary form, so the
|
|
20
|
+
* reply action / resource data has the same shape whichever form was sent (a refusal becomes
|
|
21
|
+
* `{ type: 'refusal' }` under the question's name).
|
|
22
|
+
*
|
|
23
|
+
* Side-effect free (0 B for apps that don't import it).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** a `choice` question: pick one option (criteria: option → description, or an array of option names) */
|
|
27
|
+
export const choice = (instructions: any, criteria: any) => ({
|
|
28
|
+
type: 'choice',
|
|
29
|
+
instructions,
|
|
30
|
+
criteria: Array.isArray(criteria) ? Object.fromEntries(criteria.map(c => [c, null])) : criteria,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** a `noul` question: the probability of yes (criteria: `{ true, false }` descriptions) */
|
|
34
|
+
export const noul = (instructions: any, criteria?: any) =>
|
|
35
|
+
criteria ? {type: 'noul', instructions, criteria} : {type: 'noul', instructions};
|
|
36
|
+
|
|
37
|
+
/** a `score` question: an ordered scale, lowest first (the answer is the weighted level index) */
|
|
38
|
+
export const score = (instructions: any, levels: any) => ({type: 'score', instructions, criteria: levels});
|
|
39
|
+
|
|
40
|
+
/** the dictionary-form request (TypeSafe, Ollama, OpenRouter, AI Gateway) */
|
|
41
|
+
function dictionary({url = '/api/decide', model, state, questions, images, ...rest}: any = {}) {
|
|
42
|
+
const json: any = {model, state, questions};
|
|
43
|
+
if (images) json.images = images;
|
|
44
|
+
return {url, method: 'POST', json, ...rest};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const text = (x: any) => typeof x == 'string' ? x : JSON.stringify(x);
|
|
48
|
+
// the dictionary form takes base64 files; OpenAI takes data URLs (the type from the magic bytes)
|
|
49
|
+
const dataUrl = (b64: string) => b64.startsWith('data:') ? b64
|
|
50
|
+
: `data:image/${b64.startsWith('/9j/') ? 'jpeg' : b64.startsWith('UklGR') ? 'webp' : 'png'};base64,${b64}`;
|
|
51
|
+
|
|
52
|
+
/** the questions in OpenAI's array form */
|
|
53
|
+
export const toOpenAI = (questions: any) => Object.keys(questions || {}).map(name => {
|
|
54
|
+
const q = questions[name];
|
|
55
|
+
const c = q.criteria;
|
|
56
|
+
const instructions = text(q.instructions);
|
|
57
|
+
if (q.type == 'choice') {
|
|
58
|
+
return {type: 'choice', name, instructions,
|
|
59
|
+
choices: Object.keys(c).map(value => c[value] == null ? {value} : {value, description: c[value]})};
|
|
60
|
+
}
|
|
61
|
+
if (q.type == 'score') return {type: 'score', name, instructions, levels: c.map((d: string) => ({label: d, description: d}))};
|
|
62
|
+
// a predicate has no criteria: they go into the instructions
|
|
63
|
+
return {type: 'predicate', name,
|
|
64
|
+
instructions: c ? `${instructions}\nTrue: ${c.true}\nFalse: ${c.false}` : instructions};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// a refusal (or a type this mapping doesn't know) keeps its fields but `name`
|
|
68
|
+
const withoutName = ({name, ...a}: any) => a;
|
|
69
|
+
|
|
70
|
+
const toMap = (list: any[], k: string) => {
|
|
71
|
+
const out: any = {};
|
|
72
|
+
(list || []).forEach(p => { out[String(p[k])] = p.probability; });
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** an OpenAI Decisions reply in the dictionary form */
|
|
77
|
+
export const fromOpenAI = (reply: any) => {
|
|
78
|
+
if (!reply || !Array.isArray(reply.answers)) return reply;
|
|
79
|
+
const answers: any = {};
|
|
80
|
+
reply.answers.forEach((a: any) => {
|
|
81
|
+
const {type, name} = a;
|
|
82
|
+
answers[name] = type == 'predicate' ? {type: 'noul', noul: a.probability}
|
|
83
|
+
: type == 'choice' ? {type, choice: a.choice, probabilities: toMap(a.probabilities, 'value'), confidence: a.confidence}
|
|
84
|
+
: type == 'score' ? {type, score: a.score,
|
|
85
|
+
legend: Object.fromEntries((a.probabilities || []).map((p: any) => [String(p.value), p.label])),
|
|
86
|
+
probabilities: toMap(a.probabilities, 'value'), confidence: a.confidence}
|
|
87
|
+
: withoutName(a);
|
|
88
|
+
});
|
|
89
|
+
return {...reply, answers};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const parseOpenAI = async (res: any) => fromOpenAI(await res.json());
|
|
93
|
+
|
|
94
|
+
/** the same questions as an OpenAI Decisions request; the reply arrives in the dictionary form */
|
|
95
|
+
function openai({url = '/api/decide', model, state, questions, images, ...rest}: any = {}) {
|
|
96
|
+
const input = images && images.length
|
|
97
|
+
? [{role: 'user', content: [{type: 'input_text', text: text(state)},
|
|
98
|
+
...images.map((b: string) => ({type: 'input_image', image_url: dataUrl(b)}))]}]
|
|
99
|
+
: text(state);
|
|
100
|
+
return {url, method: 'POST', json: {model, input, questions: toOpenAI(questions)}, parse: parseOpenAI, ...rest};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** the dictionary-form request (TypeSafe, Ollama, OpenRouter, AI Gateway); `decide.openai()`: the same as an OpenAI Decisions request */
|
|
104
|
+
export const decide = /*#__PURE__*/ Object.assign(dictionary, {openai})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 (D240, D253): the implementation behind 'sygnal/ai'. It lives in the main package so
|
|
3
|
+
* there is one copy of the reply helpers and the diagnostics module (G-581) and renderComponent's
|
|
4
|
+
* fakes can use it; src/index.ts exports these names and src/ai.ts re-exports them from the
|
|
5
|
+
* external 'sygnal', which is the documented import. Every module here must be side-effect free
|
|
6
|
+
* so apps that don't use it pay 0 B (the size gate).
|
|
7
|
+
*
|
|
8
|
+
* Layout (Phase 1): messages.ts (L-1 message helpers), chat/ (L-1 driver, L-2 transports),
|
|
9
|
+
* agent/ (A-1 layer), schema/ (A-1 input contract), decide.ts (M-1).
|
|
10
|
+
*/
|
|
11
|
+
export {messageText, withToolResults} from './messages'
|
|
12
|
+
export {decide, choice, noul, score} from './decide'
|
|
13
|
+
// A-1 (1-A): the agent layer and the schema input contract
|
|
14
|
+
export {agentTools} from './agent/index'
|
|
15
|
+
export {toJsonSchema, parseInput} from './schema/index'
|
|
16
|
+
export {jsonSchema} from './schema/jsonSchema'
|
|
17
|
+
// L-1 (1-L): the chat driver; outputJsonSchema is the structured-output seam transports use (1-A fills it)
|
|
18
|
+
export {makeChatDriver} from './chat/driver'
|
|
19
|
+
export {outputJsonSchema} from './chat/output'
|
|
20
|
+
// A-2 (2-W): WebMCP (experimental, D241)
|
|
21
|
+
export {experimentalExposeWebMcp} from './webmcp'
|
|
22
|
+
// L-3 (2-C): the chat behavior
|
|
23
|
+
export {chat} from './chat/behavior'
|
|
24
|
+
// L-2 (2-T): transports, wave 1 (D248), each its own module (tree-shaken), and the Open Responses
|
|
25
|
+
// event encoder (D273)
|
|
26
|
+
export {openResponses} from './transports/openResponses'
|
|
27
|
+
export {chatCompletions} from './transports/chatCompletions'
|
|
28
|
+
export {uiMessageStream} from './transports/uiMessageStream'
|
|
29
|
+
export {chromePrompt} from './transports/chromePrompt'
|
|
30
|
+
export {encodeOpenResponses} from './transports/encodeOpenResponses'
|
|
31
|
+
// M-2 / M-3 (3-M): decision reply fixtures and the command bar
|
|
32
|
+
export {answers} from './answers'
|
|
33
|
+
// PLAN-6 A-3 (experimental, D241, D291): form(schema, { tool: formTool({ … }) })
|
|
34
|
+
export {formTool} from '../formTool'
|
|
35
|
+
export {commandBar} from './commandBar'
|
|
36
|
+
// L-2 (3-W2): transports, wave 2 (D248), and the opt-in strict layer (D285)
|
|
37
|
+
export {anthropicMessages} from './transports/anthropicMessages'
|
|
38
|
+
export {agui} from './transports/agui'
|
|
39
|
+
export {fromAISDK} from './transports/fromAISDK'
|
|
40
|
+
export {strictSchemas} from './schema/strict'
|
|
41
|
+
// X-1 (3-X): MCP Apps, the view side
|
|
42
|
+
export {makeMcpAppDriver} from './mcpApp'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 (D283): how the sygnal/ai behaviors (`chat`, L-3; `commandBar`, M-3) reach their app's
|
|
3
|
+
* runtime, with 0 core bytes. The agent layer (A-1) needs the app's runtime API and the host
|
|
4
|
+
* instance, which a behavior's handlers don't get. The first factory call (`chat()`,
|
|
5
|
+
* `commandBar()`) adds one hook-layer factory to the core bridge's `layers`
|
|
6
|
+
* (`globalThis.__SYGNAL_DIAGNOSTICS__.layers`, which every App reads when it is constructed, as
|
|
7
|
+
* the dev entries do), so each app made after it hands the layer its runtime API. The layer's
|
|
8
|
+
* onAction (called synchronously right before an action's handlers) records the instance and its
|
|
9
|
+
* pre-action state; a handler takes them when its `props.state` is that state.
|
|
10
|
+
*
|
|
11
|
+
* G-625: an app made before the first factory call (a host loaded lazily into a running app)
|
|
12
|
+
* didn't read the layer at construction, so the first factory call also adds it to the apps that
|
|
13
|
+
* are already running and can be found without core changes: the one `run()` registers as
|
|
14
|
+
* `window.__SYGNAL_DEVTOOLS_APP__` (the first live app; its `__runtime.addHooks`). Another app
|
|
15
|
+
* started before it (a second `run()`, a `sygnal/element` custom element) can't be found: its
|
|
16
|
+
* hosts are reported as SYG442 and run without the link.
|
|
17
|
+
*
|
|
18
|
+
* Per (host instance, uses key) a behavior keeps one engine (`engineOf`); the layer's onDispose
|
|
19
|
+
* stops the host's engines.
|
|
20
|
+
*/
|
|
21
|
+
import {error as logError} from '../diagnostics/legacy'
|
|
22
|
+
|
|
23
|
+
const G: any = globalThis
|
|
24
|
+
/** marks a `uses` value whose host needs the link (`linked()`) */
|
|
25
|
+
const AI = Symbol.for('sygnal.aiBehavior')
|
|
26
|
+
|
|
27
|
+
interface Ctx {api: any; iv: any; pre: any}
|
|
28
|
+
export interface Engine {api: any; iv: any; k: string; stop(): void}
|
|
29
|
+
|
|
30
|
+
/** the instance whose action is being handled (set by the layer's onAction; one object per app) */
|
|
31
|
+
let cur: Ctx | undefined
|
|
32
|
+
const engines = new WeakMap<any, Map<string, Engine>>()
|
|
33
|
+
/** the dispose$ of each linked host an app with the layer created (SYG442 otherwise) */
|
|
34
|
+
const known = new WeakSet<any>()
|
|
35
|
+
const isHost = (iv: any) => {
|
|
36
|
+
const u = iv.def.view.uses
|
|
37
|
+
for (const k in u) if (u[k]?.[AI]) return true
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const layer = (api: any) => {
|
|
42
|
+
const c: Ctx = {api, iv: null, pre: null}
|
|
43
|
+
return {
|
|
44
|
+
onCreate: (iv: any) => { if (isHost(iv)) known.add(iv.sources.dispose$) },
|
|
45
|
+
onAction: (iv: any) => { c.iv = iv; c.pre = iv.state; cur = c },
|
|
46
|
+
onDispose: (iv: any) => { engines.get(iv)?.forEach(e => e.stop()) },
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** the runtimes the layer was added to after they started (G-625) */
|
|
51
|
+
const late = new WeakSet<any>()
|
|
52
|
+
|
|
53
|
+
/** install the layer (apps made from now on read it) and mark the behavior value `b` as needing it */
|
|
54
|
+
export function linked<B>(b: B): B {
|
|
55
|
+
const D = G.__SYGNAL_DIAGNOSTICS__ ||= {}, L = D.layers ||= new Set()
|
|
56
|
+
if (!(L.has ? L.has(layer) : L.includes(layer))) {
|
|
57
|
+
L.add ? L.add(layer) : L.push(layer)
|
|
58
|
+
// G-625: the app already running (registered by run())
|
|
59
|
+
const rt = (typeof window != 'undefined' ? window as any : G).__SYGNAL_DEVTOOLS_APP__?.__runtime
|
|
60
|
+
if (rt && typeof rt.addHooks == 'function' && !late.has(rt)) {
|
|
61
|
+
late.add(rt)
|
|
62
|
+
rt.addHooks(layer(rt))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
;(b as any)[AI] = true
|
|
66
|
+
return b
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The engine of the host whose action runs now, made by `make(api, iv)` on first use (undefined
|
|
71
|
+
* when the app has no layer, or `props` isn't the acting instance's)
|
|
72
|
+
*/
|
|
73
|
+
export function engineOf<E extends Engine>(props: any, k: string, make: (api: any, iv: any) => E): E | undefined {
|
|
74
|
+
const c = cur
|
|
75
|
+
if (!c || !props || props.state !== c.pre || c.iv.disposed) return
|
|
76
|
+
const iv = c.iv
|
|
77
|
+
let m = engines.get(iv)
|
|
78
|
+
if (!m) engines.set(iv, m = new Map())
|
|
79
|
+
let e = m.get(k) as E | undefined
|
|
80
|
+
if (!e) m.set(k, e = make(c.api, iv))
|
|
81
|
+
return e
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const warned = new Set<string>()
|
|
85
|
+
/** SYG442, once per behavior: an intent's host has no link (its app was made before the factory call) */
|
|
86
|
+
export function checkLinked(sources: any, name: string, without: string) {
|
|
87
|
+
if (known.has(sources.dispose$) || warned.has(name)) return
|
|
88
|
+
warned.add(name)
|
|
89
|
+
logError('SYG442', name, `the ${name} behavior has no connection to its app (the app was started before the first ${name}() call, and isn't the page's first app): ${without}`, `Call ${name}() in a module imported before run(), not only in a component loaded later`)
|
|
90
|
+
}
|