sygnal 6.1.1 → 6.3.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 +100 -0
- package/README.md +3 -1
- package/dist/ai.cjs.js +36 -0
- package/dist/ai.cjs.js.map +1 -1
- package/dist/ai.esm.js +1 -1
- package/dist/astro/server.cjs.js +3 -3
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3 -3
- package/dist/astro/server.mjs.map +1 -1
- package/dist/diagnostics.cjs.js +174 -6
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +174 -6
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/adapters.md +2 -1
- package/dist/guide/agent.md +119 -7
- package/dist/guide/ai-cards.md +319 -0
- package/dist/guide/ai-chat.md +64 -16
- package/dist/guide/ai-compose.md +881 -0
- package/dist/guide/ai-decisions.md +17 -1
- package/dist/guide/behaviors.md +1 -1
- package/dist/guide/error-boundaries.md +2 -1
- package/dist/guide/forms.md +1 -1
- package/dist/guide/http.md +5 -4
- package/dist/guide/mcp-apps.md +4 -2
- package/dist/guide/persistence.md +1 -1
- package/dist/guide/recipes/ai-form-fill.md +1 -1
- package/dist/guide/recipes/ai-summarize.md +1 -1
- package/dist/guide/recipes/ai-support-inbox.md +1 -1
- package/dist/guide/resources.md +11 -10
- package/dist/guide/ui/combobox.md +1 -1
- package/dist/guide/undo.md +1 -1
- package/dist/guide/web-components.md +1 -0
- package/dist/guide/webmcp.md +1 -2
- package/dist/index.cjs.js +5842 -370
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +679 -13
- package/dist/index.esm.js +5832 -370
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vike/onRenderClient.cjs.js +3 -2
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +3 -2
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/llms.txt +8 -6
- package/package.json +2 -1
- package/src/ai.d.ts +683 -12
- package/src/ai.ts +6 -0
- package/src/collection.ts +2 -1
- package/src/core/instance.ts +6 -3
- package/src/core/markers/lazy.ts +7 -3
- package/src/core/markers/portal.ts +5 -2
- package/src/core/markers/suspense.ts +3 -2
- package/src/core/markers/transition.ts +6 -3
- package/src/core/registry.ts +9 -5
- package/src/cycle/dom/IsolateModule.ts +25 -22
- package/src/extra/ai/agent/index.ts +111 -26
- package/src/extra/ai/chat/behavior.ts +368 -87
- package/src/extra/ai/chat/driver.ts +7 -4
- package/src/extra/ai/compose/agent.ts +55 -0
- package/src/extra/ai/compose/ask.ts +147 -0
- package/src/extra/ai/compose/behavior.ts +183 -0
- package/src/extra/ai/compose/catalog.ts +211 -0
- package/src/extra/ai/compose/composed.ts +61 -0
- package/src/extra/ai/compose/decisions.ts +1649 -0
- package/src/extra/ai/compose/diag.ts +33 -0
- package/src/extra/ai/compose/index.ts +23 -0
- package/src/extra/ai/compose/layout.ts +441 -0
- package/src/extra/ai/compose/llm.ts +1120 -0
- package/src/extra/ai/compose/render.ts +159 -0
- package/src/extra/ai/compose/testing.ts +71 -0
- package/src/extra/ai/compose/types.ts +159 -0
- package/src/extra/ai/decide.ts +97 -1
- package/src/extra/ai/index.ts +6 -1
- package/src/extra/ai/webmcp.ts +5 -3
- package/src/extra/diagnostics/checks/next.ts +86 -2
- package/src/extra/diagnostics/codes.ts +64 -3
- package/src/extra/fetchDriver.ts +58 -27
- package/src/extra/lens.ts +15 -0
- package/src/extra/ssr.ts +3 -3
- package/src/extra/testing.ts +2 -0
- package/src/extra/widget.ts +3 -3
- package/src/index.d.ts +42 -1
- package/src/index.ts +8 -0
- package/src/lazy.ts +3 -2
- package/src/portal.ts +5 -2
- package/src/pragma/index.ts +2 -1
- package/src/suspense.ts +5 -2
- package/src/switchable.ts +2 -1
- package/src/transition.ts +5 -2
- package/src/vike/onRenderClient.ts +3 -2
package/src/ai.ts
CHANGED
|
@@ -21,3 +21,9 @@ export {anthropicMessages, agui, fromAISDK, strictSchemas} from './index'
|
|
|
21
21
|
export {makeMcpAppDriver} from './index'
|
|
22
22
|
// A-3 (3-A3, experimental, D291)
|
|
23
23
|
export {formTool} from './index'
|
|
24
|
+
// PLAN-9 (A)
|
|
25
|
+
export {defineCatalog, compose, composeWith, fromTree, toTree, removeNode} from './index'
|
|
26
|
+
// PLAN-9 H: chat client tools and cards
|
|
27
|
+
export {openToolCalls, askCard} from './index'
|
|
28
|
+
// PLAN-9 M-1
|
|
29
|
+
export {Composed} from './index'
|
package/src/collection.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {h} from './cycle/dom/snabbdom';
|
|
2
|
-
// the core's
|
|
2
|
+
// the core's host for this marker, registered on import (D157): unlike Portal, Transition and
|
|
3
|
+
// Suspense (G-751), not on first use, because the lowercase tag works without an import (docs)
|
|
3
4
|
import './core/hosts/collection';
|
|
4
5
|
|
|
5
6
|
/** `<Collection of={Item} from="items" />`: a marker vnode the core's Collection host renders */
|
package/src/core/instance.ts
CHANGED
|
@@ -52,12 +52,15 @@ function extractSlots(children: any[]): [any[], Record<string, any[]>] {
|
|
|
52
52
|
return [slots.default || [], slots]
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* an instance's id among its parent's children (D456): its vnode `key`, else its `id` prop, else
|
|
57
|
+
* its path in the view
|
|
58
|
+
*/
|
|
56
59
|
const idOf = (n: any, path: string) => {
|
|
57
|
-
const id = n.data?.props?.id
|
|
60
|
+
const id = n.key ?? n.data?.props?.id
|
|
58
61
|
let s: any
|
|
59
62
|
// (G-298: an id JSON can't encode, a BigInt, is used as its string)
|
|
60
|
-
if (id) try { s = JSON.stringify(id) } catch (_) { s = String(id) }
|
|
63
|
+
if (id != null) try { s = JSON.stringify(id) } catch (_) { s = String(id) }
|
|
61
64
|
return `${n.sel}::${(s && s.replace(/"/g, '')) || path}`
|
|
62
65
|
}
|
|
63
66
|
|
package/src/core/markers/lazy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PLAN-4.6 core: lazy() components (R2), registered
|
|
2
|
+
* PLAN-4.6 core: lazy() components (R2), registered by the first `lazy()` call (G-751).
|
|
3
3
|
* A view resolver (registry `resolvers`): until the import resolves, the LazyWrapper itself is
|
|
4
4
|
* the component (it renders the data-sygnal-lazy="loading" placeholder Suspense waits for, or
|
|
5
5
|
* the error placeholder); once it has, the loaded component replaces it under the same id (a
|
|
@@ -15,7 +15,7 @@ import {resolvers} from '../registry'
|
|
|
15
15
|
/** wrapper -> the owners waiting for it (`done`: settled, nobody waits any more) */
|
|
16
16
|
export const waiting = new WeakMap<any, Set<any> & {done?: boolean}>()
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const resolve = (view: any, owner: any) => {
|
|
19
19
|
if (!view.__sygnalLazy) return
|
|
20
20
|
if (view.__sygnalLazyLoaded()) return view.__sygnalLazyLoadedComponent || undefined
|
|
21
21
|
let w = waiting.get(view)
|
|
@@ -37,4 +37,8 @@ resolvers.push((view, owner) => {
|
|
|
37
37
|
set.add(owner)
|
|
38
38
|
owner.dispose$?.().addListener({next: () => set.delete(owner)})
|
|
39
39
|
}
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let on: any
|
|
43
|
+
/** G-751: registers the resolver once; `lazy()` calls it, not the module's import */
|
|
44
|
+
export const enableLazy = () => { on ||= resolvers.push(resolve) }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PLAN-4.6 core: the Portal marker (R2), registered
|
|
2
|
+
* PLAN-4.6 core: the Portal marker (R2), registered by the public `Portal` when used (G-751).
|
|
3
3
|
* A template rewrite (registry `pres`): the `portal` marker becomes a hidden placeholder whose
|
|
4
4
|
* hooks patch its children into the target with a separate snabbdom patch function. As today
|
|
5
5
|
* (the 5.x core's createPortalPlaceholder):
|
|
@@ -93,4 +93,7 @@ export function portalPlaceholder(target: string, children: any[]): any {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
// (widgets among its children: `pres.widget.$k` rewrites them, extra/widget.ts; the core doesn't walk them)
|
|
97
|
+
const pre = (n: any, o: any, x: string) => portalPlaceholder(n.data?.props?.target, (pres.widget as any)?.$k(n.children || [], o, x) || n.children || [])
|
|
98
|
+
/** G-751: registers the rewrite; the public `Portal` calls it when used (its `$reg`), not on import */
|
|
99
|
+
export const enablePortal = () => { pres.portal = pre }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PLAN-4.6 core: Suspense (R2), registered
|
|
2
|
+
* PLAN-4.6 core: Suspense (R2), registered by the public `Suspense` when used (G-751). A
|
|
3
3
|
* post-processor (registry `posts`) of the injected vnode of an instance whose template has a
|
|
4
4
|
* `suspense` marker (the 5.x core's processSuspensePost):
|
|
5
5
|
* - the core injects a child whose READY is false with data-sygnal-ready="false", and a lazy
|
|
@@ -38,4 +38,5 @@ export function suspensePost(v: any): any {
|
|
|
38
38
|
return out ? {...v, children: out} : v
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/** G-751: registers the post-processor; the public `Suspense` calls it when used (its `$reg`) */
|
|
42
|
+
export const enableSuspense = () => { posts.suspense = suspensePost }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PLAN-4.6 core: the Transition marker (R2), registered
|
|
3
|
-
*
|
|
2
|
+
* PLAN-4.6 core: the Transition marker (R2), registered by the public `Transition`
|
|
3
|
+
* when used (G-751). A template rewrite (registry `pres`): the `transition` marker is replaced by its first
|
|
4
4
|
* child (walked on, at the marker's path), with snabbdom hooks that add the classes as today
|
|
5
5
|
* (the 5.x core's applyTransitionHooks):
|
|
6
6
|
* `${name}-enter|leave-from` + `-active`, two frames later `-from` becomes `-to`, and at the end
|
|
@@ -57,7 +57,7 @@ export function transitionHooks(vnode: any, p: {name?: string, duration?: number
|
|
|
57
57
|
// (no global symbol registry entry per name)
|
|
58
58
|
const T = Symbol()
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const pre = (n: any) => {
|
|
61
61
|
const child = n.children?.[0], p = n.data?.props || {}, k = p.name + ' ' + p.duration
|
|
62
62
|
// no element child: the text child (or the marker itself, left as it is: today's behaviour)
|
|
63
63
|
if (!child?.sel) return child || n
|
|
@@ -68,3 +68,6 @@ pres.transition = (n) => {
|
|
|
68
68
|
// fragment or text has no element to animate: nothing happens
|
|
69
69
|
return child.sel == 'collection' ? {...child, data: {...child.data, tr: (v: any) => (v[T] ||= {})[k] ||= transitionHooks(v, p)}} : transitionHooks(child, p)
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/** G-751: registers the rewrite; the public `Transition` calls it when used (its `$reg`) */
|
|
73
|
+
export const enableTransition = () => { pres.transition = pre }
|
package/src/core/registry.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PLAN-4.6 core: the module registries (04-hooks-contract §2.1). A feature
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* PLAN-4.6 core: the module registries (04-hooks-contract §2.1). A feature fills them when it is
|
|
3
|
+
* used; the core only looks them up, so an app that never uses the feature pays nothing (D157).
|
|
4
|
+
* R2 fills hosts (core/hosts: Collection, Switchable) and the markers (core/markers: Portal,
|
|
5
|
+
* Transition, ClientOnly, Suspense, lazy). G-751: Portal, Transition and Suspense register their
|
|
6
|
+
* handler when a tag uses them (the pragma calls the marker's `$reg`) and `lazy()` its resolver,
|
|
7
|
+
* not on import: a module-level registration is kept by every bundle of the single-file build.
|
|
8
|
+
* Collection and Switchable register on import (the lowercase `<collection>` / `<switchable>`
|
|
9
|
+
* work without one); ClientOnly's is in runtime.ts (sygnal/vike can't reach the registry). R3
|
|
10
|
+
* adds the definition hooks (behaviors, undo, selection, pager, persist, resources).
|
|
7
11
|
*
|
|
8
12
|
* The entries are core modules: they get the owner's raw instance (hooks get InstanceViews).
|
|
9
13
|
*/
|
|
@@ -81,25 +81,25 @@ export class IsolateModule {
|
|
|
81
81
|
|
|
82
82
|
public createModule() {
|
|
83
83
|
const self = this;
|
|
84
|
-
// G-564: a vnode can be in the old tree and the new one (a component's cached
|
|
85
|
-
// Collection item's copy, a hoisted vnode).
|
|
86
|
-
// : <ul>{c}</ul
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// (
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
const
|
|
84
|
+
// G-564, G-750, G-753: a vnode can be in the old tree and the new one (a component's cached
|
|
85
|
+
// vnode, a Collection item's copy, a hoisted vnode). Under another parent element (a recreated
|
|
86
|
+
// ancestor: `alt ? <ol>{c}</ol> : <ul>{c}</ul>`; a keyed instance moved to another column, or
|
|
87
|
+
// into a new row that takes its old place among its siblings) snabbdom created it again and
|
|
88
|
+
// gave it the new element while the old tree still had it: its destroy hooks saw the new
|
|
89
|
+
// element (a widget inside wasn't unmounted, its scope lost the new element), its old parent
|
|
90
|
+
// removed the new element and left the old one (G-750), or inserted a new sibling before it,
|
|
91
|
+
// inside that sibling (NotFoundError, the patch stopped: G-753). Now an element that is
|
|
92
|
+
// created or patched takes a copy of each child that has an element and isn't one of its old
|
|
93
|
+
// children (only a mounted vnode has one), before it is created: the old tree keeps the vnode
|
|
94
|
+
// and its element. The copy's children array is its own, so its children are copied when it
|
|
95
|
+
// is created. After the patch (post) each vnode takes its copy's fields (its element) and its
|
|
96
|
+
// place again, so the next patch sees the same vnode (snabbdom's shortcut). A patch that
|
|
97
|
+
// throws keeps the copies (pre forgets them): they still work, as copies
|
|
98
|
+
const R: any[] = [];
|
|
99
|
+
const copy = (a: any[], i: number) => { R.push(a, i, a[i]); a[i] = {...a[i], children: a[i].children?.slice()} };
|
|
99
100
|
return {
|
|
100
101
|
pre() {
|
|
101
|
-
|
|
102
|
-
M.clear();
|
|
102
|
+
R.length = 0;
|
|
103
103
|
},
|
|
104
104
|
|
|
105
105
|
create(emptyVNode: VNode, vNode: VNode) {
|
|
@@ -109,7 +109,7 @@ export class IsolateModule {
|
|
|
109
109
|
if (Array.isArray(namespace)) {
|
|
110
110
|
self.insertElement(namespace, elm as Element);
|
|
111
111
|
}
|
|
112
|
-
children?.forEach((c: any) => c?.elm &&
|
|
112
|
+
children?.forEach((c: any, i: number) => c?.elm && copy(children, i));
|
|
113
113
|
},
|
|
114
114
|
|
|
115
115
|
update(oldVNode: VNode, vNode: VNode) {
|
|
@@ -126,21 +126,25 @@ export class IsolateModule {
|
|
|
126
126
|
if (Array.isArray(namespace)) {
|
|
127
127
|
self.insertElement(namespace, elm as Element);
|
|
128
128
|
}
|
|
129
|
+
// (oldVNode !== vNode here: snabbdom returns early for the same vnode; the Set only when a
|
|
130
|
+
// child isn't at its old index)
|
|
131
|
+
const oc = oldVNode.children, nc: any = vNode.children
|
|
132
|
+
let o: any
|
|
133
|
+
nc?.forEach((c: any, i: number) => c?.elm && c !== oc?.[i] && !(o ||= new Set(oc)).has(c) && copy(nc, i));
|
|
129
134
|
},
|
|
130
135
|
|
|
131
|
-
// (the elements as they are now: post runs after the vnodes got their new ones back)
|
|
132
136
|
destroy(vNode: VNode) {
|
|
133
|
-
vNode.children?.forEach(back);
|
|
134
137
|
self.vnodesBeingRemoved.push(vNode.elm as Element);
|
|
135
138
|
},
|
|
136
139
|
|
|
137
140
|
remove(vNode: VNode, cb: Function) {
|
|
138
141
|
self.vnodesBeingRemoved.push(vNode.elm as Element);
|
|
139
|
-
ret();
|
|
140
142
|
cb();
|
|
141
143
|
},
|
|
142
144
|
|
|
143
145
|
post() {
|
|
146
|
+
for (let i = 0; i < R.length; i += 3) R[i][R[i + 1]] = Object.assign(R[i + 2], R[i][R[i + 1]]);
|
|
147
|
+
R.length = 0;
|
|
144
148
|
// G-144: a removed root element is no longer one of its scope's roots (an element no
|
|
145
149
|
// scope has as a root isn't in any set: removing it does nothing)
|
|
146
150
|
for (const elm of self.vnodesBeingRemoved) {
|
|
@@ -148,7 +152,6 @@ export class IsolateModule {
|
|
|
148
152
|
(self.eventDelegator as EventDelegator).removeElement(elm);
|
|
149
153
|
}
|
|
150
154
|
self.vnodesBeingRemoved = [];
|
|
151
|
-
M.clear();
|
|
152
155
|
},
|
|
153
156
|
};
|
|
154
157
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* `t.tools()` / `t.callTool()` / `t.agentContext()` (A-4) all use it.
|
|
7
7
|
*
|
|
8
8
|
* Comp.agent = { name, description?, read?, label?, untrusted?,
|
|
9
|
-
* actions: { ACTION: { description, input?, consequential?, when?, idempotent? } }
|
|
9
|
+
* actions: { ACTION: { description, input?, consequential?, when?, idempotent?, until?, timeout? } },
|
|
10
|
+
* queries: { find: { description, input?, when?, run: (state, input) => result } } }
|
|
10
11
|
*
|
|
11
12
|
* It reaches the app only through the runtime API (`root`, `get`, `dispatch`, `flushed`,
|
|
12
13
|
* `addHooks`) and one hook layer, so the core pays 0 bytes:
|
|
@@ -25,12 +26,24 @@
|
|
|
25
26
|
* outcome measured by `wrapHandler` on that action only: success when a STATE handler changed
|
|
26
27
|
* the state structurally, a sink got a value or an EFFECT ran; `abort(reason)` refuses with the
|
|
27
28
|
* reason; otherwise "changed nothing" (`idempotent: true`: `{ ok: true, unchanged: true }`, D256);
|
|
29
|
+
* - `until`: an action whose result arrives later (a `resources` entry, a driver reply) answers
|
|
30
|
+
* once it is in state: `until: 'search'` (the resource is neither loading nor refreshing) or
|
|
31
|
+
* `until: (state) => boolean`, checked after the action's flush and after every flush that
|
|
32
|
+
* changed something; `{ ok: true, pending: true, note, state }` after `timeout` (default 10 s) or
|
|
33
|
+
* `cancel()` (the chat behavior's STOP / new turn); `removed: true` when the instance went away.
|
|
34
|
+
* Calls run one at a time, so a waiting call delays the next one;
|
|
35
|
+
* - queries: read-only tools (`readOnlyHint`) that take an input, for data too large for `read`
|
|
36
|
+
* or answers computed from the state: `run(state, input)` is a pure, synchronous function of the
|
|
37
|
+
* instance's state; a call validates the input as for actions, checks `when`, and returns
|
|
38
|
+
* `{ ok: true, result }` without dispatching anything (no confirm, no flush);
|
|
28
39
|
* - `when` and `read`: cached by state identity per instance (reducers are immutable);
|
|
29
40
|
* - HMR: given the `run()` result, it re-binds to `app.__runtime` after the root's dispose.
|
|
30
41
|
* 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,
|
|
32
|
-
* lossy input schema,
|
|
33
|
-
*
|
|
42
|
+
* `list({ all: true })` shows it with `error`), SYG241 `read` / `when` / `until` / `label` / a query threw,
|
|
43
|
+
* SYG243 a lossy input schema, SYG246 a query whose `run` is not a function or returned a promise,
|
|
44
|
+
* SYG247 a query with the tool name of an action (not offered), SYG248 `until` names no resource of
|
|
45
|
+
* the component (the call answers at once), SYG440 two declarations with one
|
|
46
|
+
* name, SYG441 Collection items with no or duplicate keys.
|
|
34
47
|
*/
|
|
35
48
|
import {abortReason, isAbort, ORIGINAL} from '../../../shared'
|
|
36
49
|
import {objIsEqual} from '../../../cycle/state/objIsEqual'
|
|
@@ -73,8 +86,8 @@ const dev = (code: string, decl: any, what: string, component: string, message:
|
|
|
73
86
|
if (!s) done.set(decl, s = new Set())
|
|
74
87
|
if (s.has(code + what)) return
|
|
75
88
|
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 }) }
|
|
89
|
+
// severities are passed (DEV_CODE_SEVERITY, kept out of the main bundle's table): 240 / 241 / 246 error, 243 / 247 / 248 / 440 / 441 warn
|
|
90
|
+
try { report(code, {component, message, severity: code < 'SYG242' || code == 'SYG246' ? 'error' : 'warn'}) } catch (e) { queueMicrotask(() => { throw e }) }
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
/** the string `enum` / `const` values anywhere in a JSON Schema (the app's own words: filter names, statuses) */
|
|
@@ -144,7 +157,7 @@ export const lookUp = (d: string, g: AgentGroup, read: string) => {
|
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
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}
|
|
160
|
+
interface Rec {tool: AgentTool; e: Entry; action?: string; query?: string; a?: any; key?: string; off?: 1}
|
|
148
161
|
interface Call {id: number; type: string; data: any; ran: boolean; out: Array<{kind: string; reason?: string; error?: any}>}
|
|
149
162
|
|
|
150
163
|
/**
|
|
@@ -164,6 +177,15 @@ export function agentTools(target: any, options: AgentToolsOptions = {}) {
|
|
|
164
177
|
const owners = new Map<any, Map<any, any>>()
|
|
165
178
|
const listeners = new Set<(change: {tools: AgentTool[]; context: Record<string, any>}) => void>()
|
|
166
179
|
let last: any, scheduled = false
|
|
180
|
+
// `until`: calls waiting for their action's result to land in state, checked after each flush that changed something
|
|
181
|
+
const waiters = new Set<() => void>(), cancels = new Set<() => void>()
|
|
182
|
+
let waking = false
|
|
183
|
+
const wake = () => {
|
|
184
|
+
if (!waiters.size || waking) return
|
|
185
|
+
waking = true
|
|
186
|
+
queueMicrotask(() => api.flushed().then(() => { waking = false; for (const w of [...waiters]) w() }))
|
|
187
|
+
}
|
|
188
|
+
const changed = () => { dirty(); wake() }
|
|
167
189
|
|
|
168
190
|
const dirty = () => {
|
|
169
191
|
ver++
|
|
@@ -174,10 +196,10 @@ export function agentTools(target: any, options: AgentToolsOptions = {}) {
|
|
|
174
196
|
}
|
|
175
197
|
const hooks = {
|
|
176
198
|
onCreate: dirty,
|
|
177
|
-
onPatch:
|
|
178
|
-
onReducer:
|
|
199
|
+
onPatch: changed,
|
|
200
|
+
onReducer: changed,
|
|
179
201
|
onDispose: (iv: any) => {
|
|
180
|
-
|
|
202
|
+
changed()
|
|
181
203
|
// HMR: run()'s hmr() disposes this app, starts its successor and swaps __runtime
|
|
182
204
|
if (iv.isRoot && isApp) queueMicrotask(rebind)
|
|
183
205
|
},
|
|
@@ -301,21 +323,28 @@ export function agentTools(target: any, options: AgentToolsOptions = {}) {
|
|
|
301
323
|
for (const e of entries) {
|
|
302
324
|
const d = e.decl
|
|
303
325
|
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
|
-
|
|
305
|
-
|
|
326
|
+
// the actions, then the queries (read-only, `run(state, input)`): one tool each
|
|
327
|
+
const defs: Array<[string, any, boolean]> = []
|
|
328
|
+
for (const k in d.actions) defs.push([k, d.actions[k], false])
|
|
329
|
+
for (const k in d.queries) defs.push([k, d.queries[k], true])
|
|
330
|
+
for (const [action, a, isQuery] of defs) {
|
|
331
|
+
const name = e.name + '_' + snake(action), comp = e.ivs[0].name, where = `agent.${isQuery ? 'queries' : 'actions'}.${action}`
|
|
332
|
+
if (isQuery && recs.has(name)) { dev('SYG247', d, action, comp, `${where} has the tool name ${name} of an action; the query is not offered (rename it)`); continue }
|
|
333
|
+
if (isQuery && typeof a?.run != 'function') { dev('SYG246', d, action, comp, `${where}.run is not a function; the query is not offered`); continue }
|
|
306
334
|
const c = a.input ? toJsonSchema(a.input) : undefined
|
|
307
335
|
if (c?.schema) enumsOf(c.schema, e.enums)
|
|
308
|
-
const tool: AgentTool = {name, description: a.description, inputSchema: c?.schema || {type: 'object', properties: {}},
|
|
309
|
-
|
|
336
|
+
const tool: AgentTool = {name, description: a.description, inputSchema: c?.schema || {type: 'object', properties: {}},
|
|
337
|
+
annotations: isQuery ? {readOnlyHint: true, ...(d.untrusted && {untrustedContentHint: true})} : a.consequential ? {consequentialHint: true} : {}}
|
|
338
|
+
const rec: Rec = isQuery ? {e, query: action, a, tool} : {e, action, a, tool}
|
|
310
339
|
recs.set(name, rec)
|
|
311
|
-
if (e.item) seen.set(name, {e: {...e, ivs: []}
|
|
340
|
+
if (e.item) seen.set(name, {...rec, e: {...e, ivs: []}})
|
|
312
341
|
if (c?.error) {
|
|
313
342
|
tool.error = `SYG240 ${c.error}`
|
|
314
343
|
rec.off = 1
|
|
315
|
-
dev('SYG240', d, action, comp,
|
|
344
|
+
dev('SYG240', d, action, comp, `${where}.input: ${c.error}; the tool is not offered`)
|
|
316
345
|
continue
|
|
317
346
|
}
|
|
318
|
-
if (c?.lossy) dev('SYG243', d, action, comp,
|
|
347
|
+
if (c?.lossy) dev('SYG243', d, action, comp, `${where}.input: the model doesn't see ${c.lossy}`)
|
|
319
348
|
const live = e.ivs.filter(iv => whenOk(iv, action, a.when))
|
|
320
349
|
if (!live.length) { rec.off = 1; continue }
|
|
321
350
|
if (e.item) {
|
|
@@ -396,8 +425,8 @@ export function agentTools(target: any, options: AgentToolsOptions = {}) {
|
|
|
396
425
|
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
426
|
}
|
|
398
427
|
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]}
|
|
428
|
+
const {e, a, action, query} = r
|
|
429
|
+
if (!action && !query) return {ok: true, state: context()[e.name]}
|
|
401
430
|
const key = r.key || 'id', view = e.view
|
|
402
431
|
let raw = args ?? {}, k: any
|
|
403
432
|
if (e.item) {
|
|
@@ -418,18 +447,20 @@ export function agentTools(target: any, options: AgentToolsOptions = {}) {
|
|
|
418
447
|
}
|
|
419
448
|
let iv = resolve()
|
|
420
449
|
if (!iv) return e.item ? missing(e, view, k) : {ok: false, error: `${e.name} is not on the page`}
|
|
421
|
-
if (
|
|
450
|
+
if (query) return ask(name, iv, query, a, data)
|
|
451
|
+
const type = action as string
|
|
452
|
+
if (!whenOk(iv, type, a.when)) return {ok: false, error: `${name} is not available now`, state: resultState(iv)}
|
|
422
453
|
if (a.consequential) {
|
|
423
454
|
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)})}
|
|
455
|
+
const info: ConfirmInfo = {tool: name, component: iv.name, action: type, description: a.description, input: data, ...(e.item && {key: k, label: labelOf(iv)})}
|
|
425
456
|
if (!(typeof c == 'function' ? await c(info) : c === true)) return {ok: false, error: 'the user declined'}
|
|
426
457
|
// the app may have changed while the user decided: resolve and check again
|
|
427
458
|
iv = resolve()
|
|
428
459
|
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,
|
|
460
|
+
if (!whenOk(iv, type, a.when)) return {ok: false, error: `${name} is not available any more`, state: resultState(iv)}
|
|
430
461
|
}
|
|
431
|
-
const rec: Call = pending = {id: iv.id, type
|
|
432
|
-
api.dispatch(iv.id,
|
|
462
|
+
const rec: Call = pending = {id: iv.id, type, data, ran: false, out: []}
|
|
463
|
+
api.dispatch(iv.id, type, data, 'agent')
|
|
433
464
|
await api.flushed()
|
|
434
465
|
if (pending === rec) pending = null
|
|
435
466
|
if (current === rec) current = null
|
|
@@ -440,12 +471,66 @@ export function agentTools(target: any, options: AgentToolsOptions = {}) {
|
|
|
440
471
|
if (iv.disposed && e.item) iv = resolve() || iv
|
|
441
472
|
const gone = iv.disposed
|
|
442
473
|
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}
|
|
474
|
+
if (out.some(x => x.kind == 'changed' || x.kind == 'sent' || x.kind == 'effect')) return gone ? {ok: true, removed: true, state} : a.until ? settle(name, iv, type, a) : {ok: true, state}
|
|
444
475
|
if (refused) return {ok: false, error: `${action} was refused: ${refused.reason}`, state}
|
|
445
476
|
return a.idempotent ? {ok: true, unchanged: true, state} : {ok: false, error: `${action} changed nothing (already so, or the input matched nothing)`, state}
|
|
446
477
|
}
|
|
447
478
|
|
|
479
|
+
/** `until` as a predicate: a resource name is done when that resource is neither loading nor refreshing */
|
|
480
|
+
const doneTest = (iv: any, action: string, u: any): ((s: any) => boolean) | undefined => {
|
|
481
|
+
if (typeof u == 'function') return u
|
|
482
|
+
if (typeof u != 'string') return
|
|
483
|
+
const res = iv.def.view.resources
|
|
484
|
+
if (!res || !(u in res)) {
|
|
485
|
+
dev('SYG248', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until names '${u}', which is not one of ${iv.name}'s resources${res ? ` (${Object.keys(res).join(', ')})` : ''}; the call answers at once`)
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
return (s: any) => { const r = s?.[u]; return !r || (r.status != 'loading' && !r.refreshing) }
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* `until`: answer once the action's result is in state (after this flush or a later one), or
|
|
492
|
+
* with `pending` on `timeout` / `cancel()`; `removed` when the instance went away meanwhile
|
|
493
|
+
*/
|
|
494
|
+
function settle(name: string, iv: any, action: string, a: any): AgentResult | Promise<AgentResult> {
|
|
495
|
+
const test = doneTest(iv, action, a.until)
|
|
496
|
+
const holds = () => {
|
|
497
|
+
if (!test) return true
|
|
498
|
+
try { return !!test(iv.state) } catch (err: any) {
|
|
499
|
+
dev('SYG241', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until threw: ${err?.message ?? err}`)
|
|
500
|
+
return true
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const result = (extra?: any): AgentResult => ({ok: true, ...extra, state: iv.disposed ? resultState(api.get(iv.parentId)) : resultState(iv)})
|
|
504
|
+
if (holds()) return result()
|
|
505
|
+
return new Promise(resolve => {
|
|
506
|
+
const finish = (extra?: any) => { waiters.delete(check); cancels.delete(cancel); clearTimeout(timer); resolve(result(extra)) }
|
|
507
|
+
const cancel = () => finish({pending: true, note: `${name} is still in progress: its result will be in the app state later`})
|
|
508
|
+
const check = () => stopped ? cancel() : iv.disposed ? finish({removed: true}) : holds() && finish()
|
|
509
|
+
const timer = setTimeout(cancel, a.timeout ?? 10000)
|
|
510
|
+
waiters.add(check)
|
|
511
|
+
cancels.add(cancel)
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** a query call: `run(state, input)` on the instance's state now; nothing is dispatched */
|
|
516
|
+
function ask(name: string, iv: any, query: string, q: any, data: any): AgentResult {
|
|
517
|
+
if (!whenOk(iv, query, q.when)) return {ok: false, error: `${name} is not available now`}
|
|
518
|
+
let result: any
|
|
519
|
+
try { result = q.run(iv.state, data) } catch (err: any) {
|
|
520
|
+
const m = err?.message ?? err
|
|
521
|
+
dev('SYG241', declOf(iv), 'query' + query, iv.name, `agent.queries.${query}.run threw: ${m}`)
|
|
522
|
+
return {ok: false, error: `${name} failed: ${m}`}
|
|
523
|
+
}
|
|
524
|
+
if (result && typeof result.then == 'function') {
|
|
525
|
+
dev('SYG246', declOf(iv), 'promise' + query, iv.name, `agent.queries.${query}.run returned a promise: a query is a pure, synchronous function of the state`)
|
|
526
|
+
return {ok: false, error: `${name} is not available: it returned a promise`}
|
|
527
|
+
}
|
|
528
|
+
return {ok: true, result}
|
|
529
|
+
}
|
|
530
|
+
|
|
448
531
|
return {
|
|
532
|
+
/** answer every call waiting on `until` now, as `pending` (a chat turn stopped or replaced) */
|
|
533
|
+
cancel() { for (const c of [...cancels]) c() },
|
|
449
534
|
/** the offered tools; `{ all: true }` adds declared tools that can't be offered (`error`, SYG240) */
|
|
450
535
|
list,
|
|
451
536
|
/** run a tool as an agent: resolves with the result after the action's flush */
|
|
@@ -457,7 +542,7 @@ export function agentTools(target: any, options: AgentToolsOptions = {}) {
|
|
|
457
542
|
dirty()
|
|
458
543
|
return () => { listeners.delete(fn) }
|
|
459
544
|
},
|
|
460
|
-
stop() { stopped = true; off?.(); listeners.clear() },
|
|
545
|
+
stop() { stopped = true; off?.(); listeners.clear(); for (const c of [...cancels]) c() },
|
|
461
546
|
/** 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
547
|
groups(): AgentGroup[] {
|
|
463
548
|
const rs = [...build().values()]
|