sygnal 6.0.0 → 6.1.1
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 +105 -0
- 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/index.cjs.js +659 -18
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +659 -18
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +413 -4
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js +413 -5
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +170 -12
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +170 -12
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +54 -1
- package/dist/guide/agent.md +596 -0
- package/dist/guide/ai-chat.md +693 -0
- package/dist/guide/ai-decisions.md +330 -0
- package/dist/guide/forms-reference.md +28 -0
- package/dist/guide/forms.md +2 -1
- package/dist/guide/http.md +2 -0
- package/dist/guide/mcp-apps.md +272 -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/overview.md +10 -0
- package/dist/guide/webmcp.md +226 -0
- package/dist/index.cjs.js +7841 -3370
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1332 -6
- package/dist/index.esm.js +7139 -2696
- 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/vite/plugin.cjs.js +659 -18
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +659 -18
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +7 -1
- package/package.json +15 -1
- package/src/ai.d.ts +1186 -0
- package/src/ai.ts +23 -0
- package/src/core/hooks.ts +1 -1
- package/src/devtools.d.ts +20 -1
- package/src/devtools.ts +3 -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/copyAsTest.ts +2 -2
- package/src/extra/devMcp.ts +386 -0
- package/src/extra/devtoolsActions.ts +1 -1
- package/src/extra/diagnostics/checks/actionLog.ts +3 -3
- package/src/extra/diagnostics/checks/chat.ts +71 -0
- package/src/extra/diagnostics/checks/forms.ts +8 -0
- package/src/extra/diagnostics/checks/index.ts +7 -1
- package/src/extra/diagnostics/checks/public.d.ts +63 -2
- package/src/extra/diagnostics/checks/wiring.ts +5 -2
- package/src/extra/diagnostics/codes.ts +67 -0
- package/src/extra/form.ts +22 -6
- package/src/extra/formTool.ts +150 -0
- package/src/extra/testing.ts +125 -8
- package/src/index.d.ts +119 -3
- package/src/index.ts +21 -1
- package/src/shared.ts +9 -0
- package/src/vite/mcp.ts +568 -0
- package/src/vite/plugin.d.ts +43 -5
- package/src/vite/plugin.ts +109 -22
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 X-1 (D247): `makeMcpAppDriver(options?)`, the view side of MCP Apps (SEP-1865, protocol
|
|
3
|
+
* 2026-01-26): a Sygnal app as the HTML resource a host (Claude, ChatGPT, VS Code, MCPJam, the
|
|
4
|
+
* ext-apps basic-host) shows in a sandboxed iframe, talking JSON-RPC 2.0 over postMessage with
|
|
5
|
+
* the host window. Hand-written (D209: the `@modelcontextprotocol/ext-apps` SDK is a test-only
|
|
6
|
+
* devDependency; its `App` pulls in the MCP client and zod).
|
|
7
|
+
*
|
|
8
|
+
* - Handshake on start: `ui/initialize` { appInfo, appCapabilities, protocolVersion } →
|
|
9
|
+
* { hostInfo, hostCapabilities, hostContext }, then `ui/notifications/initialized`. Requests
|
|
10
|
+
* made before it completes are queued. With no host (not in an iframe, SSR) nothing is sent and
|
|
11
|
+
* every request gets its `error` action.
|
|
12
|
+
* - Source `MCP.select(type)`: 'tool-input' ({ arguments } → the arguments object),
|
|
13
|
+
* 'tool-input-partial' (the same, streamed while the model writes them), 'tool-result' (the
|
|
14
|
+
* CallToolResult: { content, structuredContent?, isError? }), 'tool-cancelled' ({ reason? }),
|
|
15
|
+
* 'host-context-changed' (the whole host context, merged: first the one from the handshake,
|
|
16
|
+
* then after each change) and 'teardown' (the host is about to remove the view; it is answered
|
|
17
|
+
* after the actions it caused have run). The latest tool-input, tool-result and host context
|
|
18
|
+
* are replayed to a stream that starts later (a component shown after they arrived).
|
|
19
|
+
* - Sink values (reply actions `ok` / `error` to the sending instance, like makeFetchDriver):
|
|
20
|
+
* - `{ callTool: name, args?, ok?, error? }`: `tools/call` on the MCP server through the host;
|
|
21
|
+
* ok gets the CallToolResult, error `{ error, result?, request }` (a JSON-RPC error, or a
|
|
22
|
+
* result with isError: true);
|
|
23
|
+
* - `{ updateModelContext }`: `ui/update-model-context`; a string → one text block, an array →
|
|
24
|
+
* content blocks, an object → structuredContent (or `{ content, structuredContent }` as is);
|
|
25
|
+
* - `{ message }`: `ui/message` as the user; a string or content blocks;
|
|
26
|
+
* - `{ openLink: url }`: `ui/open-link`;
|
|
27
|
+
* - `{ displayMode: 'inline' | 'fullscreen' | 'pip' }`: `ui/request-display-mode` (ok gets
|
|
28
|
+
* `{ mode }`, the mode the host chose);
|
|
29
|
+
* - G-641: `{ downloadFile }`: `ui/download-file` { contents } (EmbeddedResource / ResourceLink
|
|
30
|
+
* blocks; a bare `{ uri, mimeType, text | blob }` is embedded); `{ readResource: uri }`:
|
|
31
|
+
* `resources/read`; `{ listResources: true | { cursor } }`: `resources/list`;
|
|
32
|
+
* `{ createMessage }`: `sampling/createMessage` (a string is one user message; maxTokens
|
|
33
|
+
* defaults to 1024); notifications (ok runs once sent): `{ log, level?, logger? }`:
|
|
34
|
+
* `notifications/message`, `{ requestTeardown: true }`: `ui/notifications/request-teardown`.
|
|
35
|
+
* - Source `MCP.select('host')`: { hostInfo, hostCapabilities, protocolVersion } of the
|
|
36
|
+
* handshake (replayed), to check e.g. `hostCapabilities.sampling` / `downloadFile`.
|
|
37
|
+
* - Host → view requests: `ping`; `ui/resource-teardown`; with `tools: agentTools`, `tools/list` and
|
|
38
|
+
* `tools/call` serve the app's A-1 tools (`appCapabilities.tools`, `listChanged`
|
|
39
|
+
* notifications); anything else is -32601.
|
|
40
|
+
* - `autoResize` (default true): `ui/notifications/size-changed` from a ResizeObserver, as the SDK.
|
|
41
|
+
* - Dispose: the listener and observer go; pending requests are dropped.
|
|
42
|
+
* Side-effect free: an app that doesn't import it pays 0 B.
|
|
43
|
+
*/
|
|
44
|
+
import xs from 'xstream'
|
|
45
|
+
import {senderOf, allowed, makeReplies} from '../replies'
|
|
46
|
+
import type {Confirm} from './agent/index'
|
|
47
|
+
import {labelsUntrusted, unlabel, lookUp} from './agent/index'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* G-644 (D286): the offered tools, user-entered `agent.label`s out of the schemas the host's model
|
|
51
|
+
* reads; G-650: an item tool without labels names the read tool to call first
|
|
52
|
+
*/
|
|
53
|
+
const offered = (t: any) => {
|
|
54
|
+
const gs = t.groups?.() || [], bare = new Set<string>(), look = new Map<string, any>()
|
|
55
|
+
for (const g of gs) if (labelsUntrusted(g)) for (const n of g.tools) bare.add(n)
|
|
56
|
+
for (const g of gs) if (g.reader) for (const n of g.tools) if (!g.labels || bare.has(n)) look.set(n, g)
|
|
57
|
+
const where = gs.some((g: any) => g.read) ? 'the read tool has their contents' : 'their labels are user text, not shown'
|
|
58
|
+
return t.list().map(({error, ...x}: any) => {
|
|
59
|
+
const g = look.get(x.name)
|
|
60
|
+
if (g) x = {...x, description: lookUp(x.description || '', g, g.reader)}
|
|
61
|
+
return bare.has(x.name) ? {...x, inputSchema: unlabel(x.inputSchema, g ? `${g.reader} has their contents` : where)} : x
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type McpAppEvent = 'tool-input' | 'tool-input-partial' | 'tool-result' | 'tool-cancelled' | 'host-context-changed' | 'teardown' | 'host'
|
|
66
|
+
|
|
67
|
+
export interface McpAppDriverOptions {
|
|
68
|
+
/** `appInfo` of the handshake (default `{ name: document.title || 'sygnal-app', version: '0.0.0' }`) */
|
|
69
|
+
appInfo?: {name: string; version: string; [k: string]: any}
|
|
70
|
+
/** display modes the view supports (`appCapabilities.availableDisplayModes`) */
|
|
71
|
+
availableDisplayModes?: Array<'inline' | 'fullscreen' | 'pip'>
|
|
72
|
+
/** report the document's size to the host (default true) */
|
|
73
|
+
autoResize?: boolean
|
|
74
|
+
/**
|
|
75
|
+
* `agentTools` (from sygnal/ai): offer the app's `agent` tools (A-1) to the host as the view's
|
|
76
|
+
* own tools. Passed in, so an app that doesn't use it doesn't bundle the agent layer.
|
|
77
|
+
*/
|
|
78
|
+
tools?: (target: any, options: any) => any
|
|
79
|
+
/** with `tools`: consequential calls run when it resolves true (default: declined) */
|
|
80
|
+
confirm?: Confirm
|
|
81
|
+
/** the host window (default `window.parent`) */
|
|
82
|
+
host?: any
|
|
83
|
+
/** the window to listen on (default `window`) */
|
|
84
|
+
window?: any
|
|
85
|
+
/** protocol version offered (default '2026-01-26') */
|
|
86
|
+
protocolVersion?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const G: any = globalThis
|
|
90
|
+
const PROTOCOL = '2026-01-26'
|
|
91
|
+
const N = 'ui/notifications/'
|
|
92
|
+
const REPLAY: Record<string, 1> = {'tool-input': 1, 'tool-result': 1, 'host-context-changed': 1, host: 1}
|
|
93
|
+
/** sink key → [method, params of the value] */
|
|
94
|
+
const text = (v: any) => typeof v == 'string' ? [{type: 'text', text: v}] : v
|
|
95
|
+
const REQUESTS: Record<string, [string, (v: any) => any]> = {
|
|
96
|
+
callTool: ['tools/call', v => ({name: v.callTool, arguments: v.args ?? {}})],
|
|
97
|
+
updateModelContext: ['ui/update-model-context', ({updateModelContext: u}) =>
|
|
98
|
+
typeof u == 'string' || Array.isArray(u) ? {content: text(u)}
|
|
99
|
+
: u && (u.content || u.structuredContent) && Object.keys(u).every(k => k == 'content' || k == 'structuredContent') ? {...u, ...(u.content && {content: text(u.content)})}
|
|
100
|
+
: {structuredContent: u}],
|
|
101
|
+
message: ['ui/message', v => ({role: 'user', content: text(v.message)})],
|
|
102
|
+
openLink: ['ui/open-link', v => ({url: v.openLink})],
|
|
103
|
+
displayMode: ['ui/request-display-mode', v => ({mode: v.displayMode})],
|
|
104
|
+
// G-641: an item without `type` is a resource's contents ({ uri, mimeType, text | blob }), embedded
|
|
105
|
+
downloadFile: ['ui/download-file', ({downloadFile: d}) => ({contents: (Array.isArray(d) ? d : [d]).map((c: any) => c && !c.type ? {type: 'resource', resource: c} : c)})],
|
|
106
|
+
readResource: ['resources/read', ({readResource: r}) => typeof r == 'string' ? {uri: r} : r],
|
|
107
|
+
listResources: ['resources/list', ({listResources: l}) => l && typeof l == 'object' ? l : {}],
|
|
108
|
+
createMessage: ['sampling/createMessage', ({createMessage: c}) => {
|
|
109
|
+
const p = typeof c == 'string' ? {messages: c} : {...c}
|
|
110
|
+
if (typeof p.messages == 'string') p.messages = [{role: 'user', content: {type: 'text', text: p.messages}}]
|
|
111
|
+
return {maxTokens: 1024, ...p}
|
|
112
|
+
}],
|
|
113
|
+
}
|
|
114
|
+
/** G-641: sink key → [notification method, params] (no answer: `ok` is called once it is sent) */
|
|
115
|
+
const NOTIFY: Record<string, [string, (v: any) => any]> = {
|
|
116
|
+
log: ['notifications/message', v => ({level: v.level || 'info', ...(v.logger && {logger: v.logger}), data: v.log})],
|
|
117
|
+
requestTeardown: ['ui/notifications/request-teardown', () => ({})],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** agent: the runtime API of the app being constructed (its layer runs right before its drivers start) */
|
|
121
|
+
let building: any
|
|
122
|
+
const layer = (api: any) => {
|
|
123
|
+
building = api
|
|
124
|
+
queueMicrotask(() => { building = undefined })
|
|
125
|
+
return {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function makeMcpAppDriver(options: McpAppDriverOptions = {}) {
|
|
129
|
+
if (options.tools) {
|
|
130
|
+
const D = G.__SYGNAL_DIAGNOSTICS__ ||= {}, L = D.layers ||= new Set()
|
|
131
|
+
if (L.add) L.add(layer)
|
|
132
|
+
else if (!L.includes(layer)) L.push(layer)
|
|
133
|
+
}
|
|
134
|
+
return (sink$: any) => {
|
|
135
|
+
const api = building
|
|
136
|
+
building = undefined
|
|
137
|
+
const win = options.window ?? G.window
|
|
138
|
+
const host = options.host ?? (win && win.parent !== win ? win.parent : undefined)
|
|
139
|
+
const subs = new Map<string, Set<any>>()
|
|
140
|
+
const last = new Map<string, any>()
|
|
141
|
+
const pending = new Map<number, (m: any) => void>()
|
|
142
|
+
let queue: any[] | null = [], id = 0, disposed = false, context: any = {}, stopResize: any, tools: any, offTools: any
|
|
143
|
+
|
|
144
|
+
const post = (m: any) => { try { host.postMessage({jsonrpc: '2.0', ...m}, '*') } catch (e) { console.error('[Sygnal] makeMcpAppDriver', e) } }
|
|
145
|
+
const send = (m: any) => queue ? queue.push(m) : post(m)
|
|
146
|
+
const request = (method: string, params: any, done: (m: any) => void, early?: boolean) => {
|
|
147
|
+
const m = {id: ++id, method, params}
|
|
148
|
+
pending.set(m.id, done)
|
|
149
|
+
early ? post(m) : send(m)
|
|
150
|
+
}
|
|
151
|
+
const emit = (type: string, data: any) => {
|
|
152
|
+
if (REPLAY[type]) last.set(type, data)
|
|
153
|
+
subs.get(type)?.forEach(l => { try { l.next(data) } catch (e) { console.error(e) } })
|
|
154
|
+
}
|
|
155
|
+
const {replies, reply} = makeReplies()
|
|
156
|
+
|
|
157
|
+
const serve = async (m: any) => {
|
|
158
|
+
const p = m.params || {}
|
|
159
|
+
if (m.method == 'ping') return {}
|
|
160
|
+
if (m.method == 'ui/resource-teardown') {
|
|
161
|
+
emit('teardown', p)
|
|
162
|
+
// answered after the actions it caused (their flush and the requests they sent)
|
|
163
|
+
for (let i = 0; i < 3; i++) await Promise.resolve()
|
|
164
|
+
return {}
|
|
165
|
+
}
|
|
166
|
+
if (tools && m.method == 'tools/list') return {tools: offered(tools)}
|
|
167
|
+
if (tools && m.method == 'tools/call') {
|
|
168
|
+
let r: any
|
|
169
|
+
try { r = await tools.call(p.name, p.arguments ?? {}) } catch (e: any) { r = {ok: false, error: String(e?.message ?? e)} }
|
|
170
|
+
return {content: [{type: 'text', text: JSON.stringify(r)}], structuredContent: r, ...(!r?.ok && {isError: true})}
|
|
171
|
+
}
|
|
172
|
+
throw {code: -32601, message: 'Method not found: ' + m.method}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const onMessage = (ev: any) => {
|
|
176
|
+
const m = ev.data
|
|
177
|
+
if (disposed || ev.source !== host || !m || m.jsonrpc !== '2.0') return
|
|
178
|
+
if (m.method === undefined) {
|
|
179
|
+
const done = pending.get(m.id)
|
|
180
|
+
if (done) { pending.delete(m.id); done(m) }
|
|
181
|
+
} else if (m.id !== undefined) {
|
|
182
|
+
serve(m).then(result => post({id: m.id, result}), (e: any) => post({id: m.id, error: {code: e?.code ?? -32603, message: String(e?.message ?? e)}}))
|
|
183
|
+
} else if (typeof m.method == 'string' && m.method.startsWith(N)) {
|
|
184
|
+
const type = m.method.slice(N.length), p = m.params || {}
|
|
185
|
+
if (type == 'host-context-changed') emit(type, context = {...context, ...p})
|
|
186
|
+
else if (type == 'tool-input' || type == 'tool-input-partial') emit(type, p.arguments ?? {})
|
|
187
|
+
else if (type == 'tool-result' || type == 'tool-cancelled') emit(type, p)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const resize = () => {
|
|
192
|
+
const RO = G.ResizeObserver, doc = G.document
|
|
193
|
+
if (options.autoResize === false || typeof RO != 'function' || !doc) return
|
|
194
|
+
let w = 0, h = 0, busy = false
|
|
195
|
+
const measure = () => {
|
|
196
|
+
if (busy) return
|
|
197
|
+
busy = true
|
|
198
|
+
// a frame, raced with a 100 ms timer: rAF is throttled in an off-screen or hidden iframe
|
|
199
|
+
// (WebKit, a view scrolled out of the host's chat), and the size must still be reported
|
|
200
|
+
let ran = false
|
|
201
|
+
const once = (f: () => void) => () => { if (!ran) { ran = true; f() } }
|
|
202
|
+
const go = once(() => {
|
|
203
|
+
busy = false
|
|
204
|
+
const el = doc.documentElement, old = el.style.height
|
|
205
|
+
el.style.height = 'max-content'
|
|
206
|
+
const H = Math.ceil(el.getBoundingClientRect().height)
|
|
207
|
+
el.style.height = old
|
|
208
|
+
const W = Math.ceil(G.innerWidth || 0)
|
|
209
|
+
if (W !== w || H !== h) { w = W; h = H; send({method: N + 'size-changed', params: {width: W, height: H}}) }
|
|
210
|
+
})
|
|
211
|
+
G.requestAnimationFrame?.(go)
|
|
212
|
+
setTimeout(go, 100)
|
|
213
|
+
}
|
|
214
|
+
measure()
|
|
215
|
+
const ro = new RO(measure)
|
|
216
|
+
ro.observe(doc.documentElement)
|
|
217
|
+
if (doc.body) ro.observe(doc.body)
|
|
218
|
+
stopResize = () => ro.disconnect()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (host && win) {
|
|
222
|
+
win.addEventListener('message', onMessage)
|
|
223
|
+
if (options.tools && api) {
|
|
224
|
+
tools = options.tools(api, {confirm: options.confirm})
|
|
225
|
+
// list_changed only when the tools changed (not on every context change); the first is the list at start
|
|
226
|
+
let was: string | undefined
|
|
227
|
+
offTools = tools.subscribe(({tools: t}: any) => {
|
|
228
|
+
const now = JSON.stringify(t)
|
|
229
|
+
if (was !== undefined && now !== was) send({method: 'notifications/tools/list_changed'})
|
|
230
|
+
was = now
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
const doc = G.document
|
|
234
|
+
request('ui/initialize', {
|
|
235
|
+
appInfo: options.appInfo ?? {name: doc?.title || 'sygnal-app', version: '0.0.0'},
|
|
236
|
+
appCapabilities: {
|
|
237
|
+
...(tools && {tools: {listChanged: true}}),
|
|
238
|
+
...(options.availableDisplayModes && {availableDisplayModes: options.availableDisplayModes}),
|
|
239
|
+
},
|
|
240
|
+
protocolVersion: options.protocolVersion ?? PROTOCOL,
|
|
241
|
+
}, m => {
|
|
242
|
+
if (m.error) return console.error('[Sygnal] makeMcpAppDriver: ui/initialize failed', m.error)
|
|
243
|
+
const r = m.result || {}
|
|
244
|
+
emit('host', {hostInfo: r.hostInfo, hostCapabilities: r.hostCapabilities || {}, protocolVersion: r.protocolVersion})
|
|
245
|
+
if (r.hostContext) emit('host-context-changed', context = {...r.hostContext})
|
|
246
|
+
post({method: N + 'initialized'})
|
|
247
|
+
const q = queue || []
|
|
248
|
+
queue = null
|
|
249
|
+
q.forEach(post)
|
|
250
|
+
resize()
|
|
251
|
+
}, true)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
sink$.addListener({
|
|
255
|
+
next: (v: any) => {
|
|
256
|
+
if (!v || typeof v != 'object' || disposed || !allowed(v, 'makeMcpAppDriver')) return
|
|
257
|
+
const k = Object.keys(REQUESTS).find(k => v[k] !== undefined)
|
|
258
|
+
const n = k ? undefined : Object.keys(NOTIFY).find(k => v[k] !== undefined && v[k] !== false)
|
|
259
|
+
const sender = senderOf(v)
|
|
260
|
+
const answer = (ok: boolean, data: any) => {
|
|
261
|
+
const action = ok ? v.ok : v.error
|
|
262
|
+
if (sender !== undefined && typeof action == 'string') reply(sender, action, data)
|
|
263
|
+
else if (!ok) console.error('[Sygnal] makeMcpAppDriver', data.error, v)
|
|
264
|
+
}
|
|
265
|
+
if (!k && !n) return console.error('[Sygnal] makeMcpAppDriver: expected { callTool }, { updateModelContext }, { message }, { openLink }, { displayMode }, { downloadFile }, { readResource }, { listResources }, { createMessage }, { log } or { requestTeardown }', v)
|
|
266
|
+
if (!host) return answer(false, {error: 'not running in an MCP Apps host', request: v})
|
|
267
|
+
if (n) {
|
|
268
|
+
const [method, params] = NOTIFY[n]
|
|
269
|
+
send({method, params: params(v)})
|
|
270
|
+
return answer(true, {})
|
|
271
|
+
}
|
|
272
|
+
const [method, params] = REQUESTS[k!]
|
|
273
|
+
request(method, params(v), m => {
|
|
274
|
+
if (m.error) answer(false, {error: m.error.message, code: m.error.code, request: v})
|
|
275
|
+
else if (m.result?.isError) answer(false, {error: (m.result.content || []).map((c: any) => c.text).filter(Boolean).join('\n') || method + ' failed', result: m.result, request: v})
|
|
276
|
+
else answer(true, m.result)
|
|
277
|
+
})
|
|
278
|
+
},
|
|
279
|
+
error: (e: any) => console.error('[Sygnal] makeMcpAppDriver', e),
|
|
280
|
+
complete: () => {},
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const source = {
|
|
284
|
+
select: (type: McpAppEvent) => {
|
|
285
|
+
let mine: any
|
|
286
|
+
return xs.create<any>({
|
|
287
|
+
start: l => {
|
|
288
|
+
let s = subs.get(type)
|
|
289
|
+
if (!s) subs.set(type, s = new Set())
|
|
290
|
+
s.add(mine = l)
|
|
291
|
+
if (last.has(type)) l.next(last.get(type))
|
|
292
|
+
},
|
|
293
|
+
stop: () => { subs.get(type)?.delete(mine) },
|
|
294
|
+
})
|
|
295
|
+
},
|
|
296
|
+
isolateSource: (s: any) => s,
|
|
297
|
+
isolateSink: (s: any) => s,
|
|
298
|
+
isolateValue: (v: any) => v,
|
|
299
|
+
...replies,
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
...source,
|
|
303
|
+
dispose: () => {
|
|
304
|
+
disposed = true
|
|
305
|
+
win?.removeEventListener?.('message', onMessage)
|
|
306
|
+
stopResize?.()
|
|
307
|
+
offTools?.()
|
|
308
|
+
tools?.stop()
|
|
309
|
+
pending.clear()
|
|
310
|
+
subs.clear()
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 L-1 (D252): chat messages are AI SDK UIMessage-shaped: `{ id?, role, parts }`, where a
|
|
3
|
+
* part is `{ type: 'text', text }`, `{ type: 'reasoning', text }`, `{ type: 'tool-<name>', toolCallId,
|
|
4
|
+
* state, input?, output? }`, `{ type: 'file', … }`, `{ type: 'source-…', … }` or `{ type: 'data-…', … }`.
|
|
5
|
+
* A message written by hand may carry `content: string` instead of parts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** the text of a message: its text parts joined (or its `content` string) */
|
|
9
|
+
export function messageText(message: any): string {
|
|
10
|
+
if (!message) return ''
|
|
11
|
+
if (typeof message.content == 'string' && !message.parts) return message.content
|
|
12
|
+
return (message.parts || []).filter((p: any) => p && p.type === 'text').map((p: any) => p.text).join('')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* G-640: a new message id, 16 random [0-9a-z] characters (the AI SDK's generateId is 16
|
|
17
|
+
* alphanumerics too). Ids name messages, they are no secret: Math.random is enough
|
|
18
|
+
*/
|
|
19
|
+
export const messageId = (): string => {
|
|
20
|
+
let s = ''
|
|
21
|
+
while (s.length < 16) s += Math.random().toString(36).slice(2)
|
|
22
|
+
return s.slice(0, 16)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const own = (o: any, k: any) => Object.prototype.hasOwnProperty.call(o, k)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* PLAN-6 4-F: the message with its open tool calls answered, for apps that run the raw chat
|
|
29
|
+
* driver's tool loop themselves: each `tool-<name>` part in `input-available` gets the result for
|
|
30
|
+
* its call. `results`: a function `(call: { id, name, input }) => output` (a throw is that call's
|
|
31
|
+
* error), or an object keyed by the call's id, else by the tool's name. An `Error` value (returned
|
|
32
|
+
* or thrown) gives `state: 'output-error'` with its `errorText`; any other value
|
|
33
|
+
* `state: 'output-available'` with it as `output`; `undefined` (no result) leaves the part open.
|
|
34
|
+
* Returns a new message (the same one when nothing changed).
|
|
35
|
+
*/
|
|
36
|
+
export function withToolResults(message: any, results: any): any {
|
|
37
|
+
if (!message || !Array.isArray(message.parts) || results == null) return message
|
|
38
|
+
let changed = false
|
|
39
|
+
const parts = message.parts.map((p: any) => {
|
|
40
|
+
if (!p || typeof p.type != 'string' || !p.type.startsWith('tool-') || p.state != 'input-available') return p
|
|
41
|
+
const call = {id: p.toolCallId, name: p.type.slice(5), input: p.input}
|
|
42
|
+
let r: any
|
|
43
|
+
try {
|
|
44
|
+
r = typeof results == 'function' ? results(call) : own(results, call.id) ? results[call.id] : own(results, call.name) ? results[call.name] : undefined
|
|
45
|
+
} catch (e: any) { r = e instanceof Error ? e : new Error(String(e)) }
|
|
46
|
+
if (r === undefined) return p
|
|
47
|
+
changed = true
|
|
48
|
+
return r instanceof Error ? {...p, state: 'output-error', errorText: r.message} : {...p, state: 'output-available', output: r}
|
|
49
|
+
})
|
|
50
|
+
return changed ? {...message, parts} : message
|
|
51
|
+
}
|