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
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Actions ('assistant.X'): SEND (the form's submit: the prompt; or a string as data), STOP (aborts
|
|
25
25
|
* the stream, keeps the partial text and reasoning; calls that already ran keep their results,
|
|
26
|
-
* G-626
|
|
26
|
+
* G-626; a call waiting on an action's `until` answers as pending: the tool set's cancel(), as on
|
|
27
|
+
* FAILED and a new turn), REGENERATE (replays the last user turn), APPROVE, DENY,
|
|
27
28
|
* DONE (the turn is over: `{ message, text, finishReason, usage, steps }`; a host entry
|
|
28
29
|
* 'assistant.DONE' runs after it, like 'form.DONE'). Internal: PROMPT (typing), DELTA / REPLY /
|
|
29
30
|
* FAILED (the chat driver's reply actions), ASK (a consequential call waits), RESULTS (the tool
|
|
@@ -56,10 +57,47 @@
|
|
|
56
57
|
* the host's dispose stops it. A host in an app made before the first chat() call is reported as
|
|
57
58
|
* SYG442 and runs without tools. The engine dispatches RESULTS / ASK (cause 'reply') and DONE
|
|
58
59
|
* ('next') to the host through the runtime API.
|
|
60
|
+
*
|
|
61
|
+
* PLAN-9 H-1, client tools (design: dev-plans/research/ai-compose/chat-client-tools.md, D454,
|
|
62
|
+
* D469): `clientTools: { name: { description, inputSchema } }` go in every request with the agent
|
|
63
|
+
* tools (a name in both: the client tool wins, SYG684; they need no link, so they work under
|
|
64
|
+
* SYG442 too). The app answers their calls:
|
|
65
|
+
* - a reply's client calls stay `input-available` (a Standard Schema input that fails validation
|
|
66
|
+
* is closed at once as output-error with the issues, so the model can repair it), and each fires
|
|
67
|
+
* the host entry 'assistant.TOOL_CALL' (ChatCall) once, when it opens;
|
|
68
|
+
* - 'assistant.TOOL_OUTPUT' `{ toolCallId, output }` / `{ toolCallId, errorText }` (the AI SDK's
|
|
69
|
+
* addToolOutput) answers one: only an input-available client part of the last message (checked
|
|
70
|
+
* on state, so it works after a reload); anything else does nothing (SYG685, dev);
|
|
71
|
+
* - the next request goes once the last message has no input-available and no approval-requested
|
|
72
|
+
* part: RESULTS, APPROVE / DENY, TOOL_OUTPUT, ANSWER and the cards share that one test, so
|
|
73
|
+
* whichever finishes last sends (`continue: true`; it counts as a step);
|
|
74
|
+
* - status: 'ready' while only the user's answers are outstanding (DONE waits for the end of the
|
|
75
|
+
* turn), 'streaming' while agent calls run or a card composes;
|
|
76
|
+
* - STOP (also when only client calls are open) closes them as "not answered: stopped by the
|
|
77
|
+
* user", SEND as "not answered: the user sent a new message"; REGENERATE drops them with their
|
|
78
|
+
* message; past maxSteps they are closed as not run.
|
|
79
|
+
* `openToolCalls(state)`: the input-available tool parts of the last assistant message.
|
|
80
|
+
*
|
|
81
|
+
* PLAN-9 H-2 / H-3: `ask: { catalog, backend?, instructions?, direct? }` adds the client tool
|
|
82
|
+
* `ask_user` (../compose/ask.ts, reached through `catalog._ask`): a card per call on `part.ui`,
|
|
83
|
+
* composed one at a time on the backend's sink (default compose.llm() on this sink; CARD /
|
|
84
|
+
* CARD_FAILED are its reply actions, the request key `<use>.ask:<toolCallId>`), answered by the
|
|
85
|
+
* catalog's result parts (ANSWER `{ scope, node, value }`: CHILD.select of their components, since
|
|
86
|
+
* cards render in the host's view) with the output `{ part, answer }`; CONTROL for the cards'
|
|
87
|
+
* container controls. STOP aborts a card being composed (the driver's `abort: [keys]`).
|
|
88
|
+
*
|
|
89
|
+
* Reload (design rule 12): `persist()` restores the whole slice, so a slice saved mid-request came
|
|
90
|
+
* back busy and SEND stayed locked. RESUME, once at start when the restored slice is busy (or has a
|
|
91
|
+
* card composing): status 'ready' (the partial reply kept, as on STOP), agent calls still open
|
|
92
|
+
* closed as not run, an app confirm dropped (a server approval stays answerable), and a card that
|
|
93
|
+
* was composing composed again; open client calls stay answerable.
|
|
59
94
|
*/
|
|
60
95
|
import {defineBehavior} from '../../behaviors'
|
|
61
96
|
import {ABORT} from '../../../shared'
|
|
97
|
+
import {report} from '../../diagnostics/index'
|
|
98
|
+
import xs from '../../xstreamCompat'
|
|
62
99
|
import {agentTools, hasUserText, labelsUntrusted, unlabel} from '../agent/index'
|
|
100
|
+
import {toJsonSchema, repair, isObj} from '../schema/index'
|
|
63
101
|
import {linked, engineOf, checkLinked} from '../link'
|
|
64
102
|
import {messageId} from '../messages'
|
|
65
103
|
|
|
@@ -83,6 +121,9 @@ const toolsOf = (e: Engine) => e.o.agent === false ? null : e.tools ||= agentToo
|
|
|
83
121
|
}),
|
|
84
122
|
})
|
|
85
123
|
|
|
124
|
+
// dev diagnostics (SYG684, SYG685): no-ops with diagnostics off
|
|
125
|
+
const dev = (code: string, message: string, fix: string) => { try { report(code, {component: 'chat', message, fix, severity: 'warn'}) } catch (e) { queueMicrotask(() => { throw e }) } }
|
|
126
|
+
|
|
86
127
|
// ------------------------------------------------------------------ messages
|
|
87
128
|
const isTool = (p: any) => typeof p?.type == 'string' && p.type.startsWith('tool-')
|
|
88
129
|
const openCalls = (m: any): any[] => (m?.parts || []).filter((p: any) => isTool(p) && p.state == 'input-available')
|
|
@@ -93,18 +134,31 @@ const askInfo = (p: any) => {
|
|
|
93
134
|
const name = p.type.slice(5)
|
|
94
135
|
return {tool: name, component: 'server', action: name, description: p.approval?.requestReason ?? `Run the ${name} tool`, input: p.input, approvalId: p.approval?.id}
|
|
95
136
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
137
|
+
/**
|
|
138
|
+
* a tool part with an A-1 result (`{ output }` or `{ error }`); the other side's field goes (a late
|
|
139
|
+
* result replaces the "not run" error of a stopped turn, G-626)
|
|
140
|
+
*/
|
|
141
|
+
const withResult = (p: any, r: any) => {
|
|
142
|
+
const {output: _o, errorText: _e, ...rest} = p
|
|
143
|
+
return 'error' in r ? {...rest, state: 'output-error', errorText: r.error} : {...rest, state: 'output-available', output: r.output}
|
|
144
|
+
}
|
|
145
|
+
/** H-1: the tool parts the app answers (clientTools, ask_user) */
|
|
146
|
+
type Cfg = {client: Record<string, any>; ask?: any; kit?: any}
|
|
147
|
+
const isClient = (c: Cfg, p: any) => Object.prototype.hasOwnProperty.call(c.client, p.type.slice(5))
|
|
148
|
+
/** an ask_user card that is still composing / open → cancelled */
|
|
149
|
+
const cancel = (p: any) => p.ui && (p.ui.status == 'composing' || p.ui.status == 'open') ? {...p.ui, status: 'cancelled', pending: null} : p.ui
|
|
98
150
|
/**
|
|
99
151
|
* The last message with its open tool parts closed (a turn stopped, failed or capped): a call
|
|
100
|
-
* that already ran gets its result (`ran`, G-626), the others `why
|
|
101
|
-
* waiting is denied
|
|
152
|
+
* that already ran gets its result (`ran`, G-626), the others `why` (client calls `cwhy`, their
|
|
153
|
+
* cards cancelled); a server approval still waiting is denied
|
|
102
154
|
*/
|
|
103
|
-
const closeOpen = (msgs: any[], why: string, ran?: Map<string, any
|
|
155
|
+
const closeOpen = (msgs: any[], why: string, ran?: Map<string, any>, c?: Cfg, cwhy = why) => {
|
|
104
156
|
const m = msgs[msgs.length - 1]
|
|
105
157
|
if (!openCalls(m).length && !firstAsk(m)) return msgs
|
|
106
158
|
return [...msgs.slice(0, -1), {...m, parts: m.parts.map((p: any) => !isTool(p) ? p
|
|
107
|
-
: p.state == 'input-available' ? (ran?.has(p.toolCallId) ? withResult(p, ran.get(p.toolCallId))
|
|
159
|
+
: p.state == 'input-available' ? (ran?.has(p.toolCallId) ? withResult(p, ran.get(p.toolCallId))
|
|
160
|
+
: c && isClient(c, p) ? {...p, state: 'output-error', errorText: cwhy, ...(p.ui && {ui: cancel(p)})}
|
|
161
|
+
: {...p, state: 'output-error', errorText: why})
|
|
108
162
|
: p.state == 'approval-requested' ? {...p, state: 'output-denied', approval: {...p.approval, approved: false, reason: why}}
|
|
109
163
|
: p)}]
|
|
110
164
|
}
|
|
@@ -113,61 +167,213 @@ const user = (text: string) => ({id: messageId(), role: 'user', parts: [{type: '
|
|
|
113
167
|
const busy = (s: any) => s.status == 'submitted' || s.status == 'streaming'
|
|
114
168
|
const lastUser = (msgs: any[]) => { for (let i = msgs.length - 1; i >= 0; i--) if (msgs[i].role == 'user') return i; return -1 }
|
|
115
169
|
const lastOf = (msgs: any[]) => msgs[msgs.length - 1]
|
|
170
|
+
/** the last message's parts through `f` (an assistant message; otherwise unchanged) */
|
|
171
|
+
const mapLast = (msgs: any[], f: (p: any) => any) => {
|
|
172
|
+
const m = lastOf(msgs)
|
|
173
|
+
return m?.role != 'assistant' ? msgs : [...msgs.slice(0, -1), {...m, parts: m.parts.map(f)}]
|
|
174
|
+
}
|
|
175
|
+
/** the input-available tool parts of the last assistant message (H-1: the calls the app answers, and agent calls while they run) */
|
|
176
|
+
export const openToolCalls = (s: any): any[] => { const m = s && Array.isArray(s.messages) ? lastOf(s.messages) : null; return m?.role == 'assistant' ? openCalls(m) : [] }
|
|
177
|
+
const composing = (p: any) => p.state == 'input-available' && p.ui?.status == 'composing'
|
|
116
178
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
179
|
+
/**
|
|
180
|
+
* H-1 rule 3 and 4: what the slice does after a call closed or a card changed: the next request
|
|
181
|
+
* (`send`) once nothing is open, else the status: 'streaming' while agent calls run or a card
|
|
182
|
+
* composes (or a server approval waits), 'ready' while only the user's answers are outstanding
|
|
183
|
+
*/
|
|
184
|
+
const settle = (s: any, messages: any[], c: Cfg) => {
|
|
185
|
+
const last = lastOf(messages), ask = firstAsk(last), open = openCalls(last)
|
|
186
|
+
if (!ask && !open.length) return {s: {...s, messages, status: 'submitted', pending: null}, send: true}
|
|
187
|
+
const agent = open.some(p => !isClient(c, p))
|
|
188
|
+
const working = ask || agent || open.some(composing)
|
|
189
|
+
return {s: {...s, messages, status: working ? 'streaming' : 'ready', pending: ask ? askInfo(ask) : agent && s.pending?.approvalId === undefined ? s.pending : null}}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** H-2: start the first queued card (cards compose one at a time); a card that can't start fails and the next one goes */
|
|
193
|
+
const nextCard = (c: Cfg, msgs: any[], st: any, k: string): {msgs: any[]; request?: any} => {
|
|
194
|
+
if (!c.ask || !c.ask.backend) return {msgs}
|
|
195
|
+
for (;;) {
|
|
196
|
+
const m = lastOf(msgs), parts = m?.role == 'assistant' ? m.parts : []
|
|
197
|
+
if (parts.some((p: any) => composing(p) && p.ui.pending)) return {msgs}
|
|
198
|
+
const p = parts.find(composing)
|
|
199
|
+
if (!p) return {msgs}
|
|
200
|
+
const r = c.kit.startCard(c.ask, p, st, k), np = r.part ?? r
|
|
201
|
+
msgs = mapLast(msgs, (x: any) => x === p ? np : x)
|
|
202
|
+
if (r.request !== undefined) return {msgs, request: r.request}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* one step's outcome per (slice, data, engine, host): STATE, the sinks and EFFECT of an action
|
|
208
|
+
* read the same one (`x`: the host's state and the use's key, compared by value)
|
|
209
|
+
*/
|
|
210
|
+
const memo = (f: (s: any, d: any, e?: any, x?: any) => any) => {
|
|
119
211
|
let l: any[] = []
|
|
120
|
-
return (s: any, d: any, e?:
|
|
212
|
+
return (s: any, d: any, e?: any, x?: any) => l[0] === s && l[1] === d && l[2] === e && l[3] === x?.state && l[4] === x?.k ? l[5] : (l = [s, d, e, x?.state, x?.k, f(s, d, e, x)])[5]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* H-1: a client call's Standard Schema input, checked synchronously (the lenient `{ value }`
|
|
217
|
+
* unwrap and repair, as A-1's parseInput): `{ input }` or `{ issues }`; an async schema or a plain
|
|
218
|
+
* JSON Schema is not checked here
|
|
219
|
+
*/
|
|
220
|
+
function checkInput(tool: any, input: any): {input?: any; issues?: string} {
|
|
221
|
+
const sc = tool?.inputSchema
|
|
222
|
+
if (!sc || typeof sc['~standard']?.validate != 'function') return {input}
|
|
223
|
+
const c = toJsonSchema(sc)
|
|
224
|
+
let v = c.wrapped && isObj(input) && 'value' in input ? input.value : input
|
|
225
|
+
if (c.schema) v = repair(v, c.wrapped ? c.schema.properties.value : c.schema, c.schema)
|
|
226
|
+
const r = sc['~standard'].validate(v)
|
|
227
|
+
if (!r || typeof r.then == 'function') return {input}
|
|
228
|
+
return r.issues ? {issues: r.issues.map((i: any) => (i.path?.length ? i.path.map((x: any) => isObj(x) ? x.key : x).join('.') + ': ' : '') + i.message).join('; ')} : {input: r.value}
|
|
229
|
+
}
|
|
230
|
+
/** the JSON Schema a request sends for a client tool */
|
|
231
|
+
const schemaOf = (tool: any) => {
|
|
232
|
+
const sc = tool?.inputSchema
|
|
233
|
+
if (sc && typeof sc['~standard']?.validate == 'function') return toJsonSchema(sc).schema ?? {type: 'object'}
|
|
234
|
+
return sc ?? {type: 'object'}
|
|
121
235
|
}
|
|
122
236
|
|
|
123
237
|
const textOf = (d: any) => typeof d == 'string' ? d : d && typeof d == 'object' && typeof d.text == 'string' && !('target' in d) ? d.text : undefined
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// reply
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
238
|
+
/** steps that read the use's client config; made per chat() call */
|
|
239
|
+
function steps(c: Cfg, o: any) {
|
|
240
|
+
const sendStep = memo((s, d) => {
|
|
241
|
+
const given = textOf(d), text = given ?? s.prompt
|
|
242
|
+
if (busy(s) || typeof text != 'string' || !text.trim()) return
|
|
243
|
+
return {...s, messages: [...closeOpen(s.messages, 'not run: a new message was sent', undefined, c, 'not answered: the user sent a new message'), user(text)], prompt: given === undefined ? '' : s.prompt, draft: '', draftReasoning: '', status: 'submitted', error: null, pending: null}
|
|
244
|
+
})
|
|
245
|
+
const regenStep = memo(s => {
|
|
246
|
+
const i = lastUser(s.messages)
|
|
247
|
+
if (busy(s) || i < 0) return
|
|
248
|
+
return {...s, messages: s.messages.slice(0, i + 1), draft: '', draftReasoning: '', status: 'submitted', error: null, pending: null}
|
|
249
|
+
})
|
|
250
|
+
// a reply: run its agent calls (status stays streaming), open its client calls (TOOL_CALL; cards
|
|
251
|
+
// start composing), wait for a server approval (`pending`), or the turn is over (DONE). Past
|
|
252
|
+
// maxSteps the open calls are closed as not run. A reply to a request that continued the last
|
|
253
|
+
// (assistant) message replaces it (G-628: the driver grew that message, as the AI SDK does)
|
|
254
|
+
const replyStep = memo((s, d, e, p) => {
|
|
255
|
+
const m = d?.message
|
|
256
|
+
if (!m) return
|
|
257
|
+
const max = e?.o.maxSteps ?? 8, calls = openCalls(m), over = !!e && e.steps >= max
|
|
258
|
+
const agent = calls.filter(x => !isClient(c, x)), run = agent.length > 0 && !!e && !over
|
|
259
|
+
// past maxSteps every call is closed; with no engine (SYG442) the agent calls are
|
|
260
|
+
let msg = calls.length && (over || (!e && agent.length)) ? closeOpen([m], `not run: the step limit (maxSteps ${max}) was reached`, undefined, over ? undefined : c)[0] : m
|
|
261
|
+
const opened: any[] = []
|
|
262
|
+
let closed = false
|
|
263
|
+
if (!over && calls.length > agent.length) {
|
|
264
|
+
msg = {...msg, parts: msg.parts.map((x: any) => {
|
|
265
|
+
if (!isTool(x) || x.state != 'input-available' || !isClient(c, x)) return x
|
|
266
|
+
const name = x.type.slice(5), call = {id: x.toolCallId, name, input: x.input}
|
|
267
|
+
let y = x
|
|
268
|
+
if (c.ask && name == 'ask_user') y = c.kit.openCard(c.ask, x, p?.state)
|
|
269
|
+
else {
|
|
270
|
+
const r = checkInput(c.client[name], x.input)
|
|
271
|
+
if (r.issues) y = {...x, state: 'output-error', errorText: `Invalid input: ${r.issues}`}
|
|
272
|
+
else call.input = r.input
|
|
273
|
+
}
|
|
274
|
+
if (y.state == 'input-available') opened.push(call)
|
|
275
|
+
else closed = true
|
|
276
|
+
return y
|
|
277
|
+
})}
|
|
278
|
+
}
|
|
279
|
+
const ask = !run && firstAsk(msg)
|
|
280
|
+
const prev = lastOf(s.messages)
|
|
281
|
+
let messages = prev?.role == 'assistant' ? [...s.messages.slice(0, -1), msg] : [...s.messages, msg]
|
|
282
|
+
const card = nextCard(c, messages, p?.state, p?.k)
|
|
283
|
+
messages = card.msgs
|
|
284
|
+
const last = lastOf(messages), wait = openCalls(last).filter(x => isClient(c, x))
|
|
285
|
+
const send = !run && !ask && !wait.length && closed
|
|
286
|
+
const status = send ? 'submitted' : run || ask || wait.some(composing) ? 'streaming' : 'ready'
|
|
287
|
+
return {s: {...s, messages, draft: '', draftReasoning: '', status, pending: ask ? askInfo(ask) : null}, calls: run ? agent : null, ask: !!ask, msg: last, wait: wait.length > 0, send, opened, compose: card.request}
|
|
288
|
+
})
|
|
289
|
+
// a reply's tool results: the next request (or the next server approval first). `late`: a call
|
|
290
|
+
// that was running when STOP came changed the app: its "not run" part gets the result (G-626)
|
|
291
|
+
const resultsStep = memo((s, d, e) => {
|
|
292
|
+
if (!e || !Array.isArray(d?.results) || (d.turn !== e.turn && !d.late)) return
|
|
293
|
+
const out = new Map(d.results.map((r: any) => [r.id, r]))
|
|
294
|
+
const fix = (p: any) => {
|
|
295
|
+
const r: any = isTool(p) && out.get(p.toolCallId)
|
|
296
|
+
return !r ? p : d.late ? (p.state == 'output-error' && /^not run/.test(p.errorText) ? withResult(p, r) : p) : p.state == 'input-available' ? withResult(p, r) : p
|
|
297
|
+
}
|
|
298
|
+
const msgs = s.messages.map((m: any) => m.role == 'assistant' && m.parts?.some((p: any) => out.has(p.toolCallId)) ? {...m, parts: m.parts.map(fix)} : m)
|
|
299
|
+
if (d.late) return {s: {...s, messages: msgs}}
|
|
300
|
+
return settle(s, msgs, c)
|
|
301
|
+
})
|
|
302
|
+
// APPROVE / DENY of a server approval (G-628): the part goes to approval-responded, as the AI
|
|
303
|
+
// SDK's addToolApprovalResponse does; once none waits, the conversation is sent again and the
|
|
304
|
+
// server runs (or skips) the call, continuing the same message
|
|
305
|
+
const answerStep = memo((s, ok) => {
|
|
306
|
+
const id = s.pending?.approvalId, last = lastOf(s.messages)
|
|
307
|
+
if (id === undefined || last?.role != 'assistant') return
|
|
308
|
+
return settle(s, mapLast(s.messages, (p: any) => isTool(p) && p.state == 'approval-requested' && p.approval?.id === id ? {...p, state: 'approval-responded', approval: {...p.approval, approved: ok}} : p), c)
|
|
309
|
+
})
|
|
310
|
+
/** the input-available client part `id` of the last assistant message, or undefined (SYG685) */
|
|
311
|
+
const openPart = (s: any, id: any, what: string, guard = (_p: any) => true) => {
|
|
312
|
+
const p = openToolCalls(s).find(x => x.toolCallId === id && isClient(c, x) && guard(x))
|
|
313
|
+
if (!p) dev('SYG685', `${what} for the tool call '${id}', which is not waiting for an answer (answered, closed, or not a client tool call of the last message); it is ignored`, 'Answer only the calls in openToolCalls(state); a late answer after STOP, SEND or REGENERATE is dropped on purpose')
|
|
314
|
+
return p
|
|
155
315
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
316
|
+
/** after a call closed or a card changed: the next queued card, then rule 3 */
|
|
317
|
+
const after = (s: any, msgs: any[], x: any) => { const n = nextCard(c, msgs, x?.state, x?.k); return {...settle(s, n.msgs, c), compose: n.request} }
|
|
318
|
+
// H-1 rule 5: TOOL_OUTPUT answers an open client call
|
|
319
|
+
const outputStep = memo((s, d, _e, x) => {
|
|
320
|
+
const p = d && typeof d == 'object' && openPart(s, d.toolCallId, 'TOOL_OUTPUT')
|
|
321
|
+
if (!p) return
|
|
322
|
+
const err = 'errorText' in d
|
|
323
|
+
const {output: _o, errorText: _x, ...rest} = p
|
|
324
|
+
const np = {...rest, ...(err ? {state: 'output-error', errorText: String(d.errorText)} : {state: 'output-available', output: d.output}),
|
|
325
|
+
...(p.ui && {ui: {...p.ui, status: err ? 'failed' : 'answered', pending: null}})}
|
|
326
|
+
return after(s, mapLast(s.messages, (y: any) => y === p ? np : y), x)
|
|
327
|
+
})
|
|
328
|
+
// H-2: a result part answered an open card: the call's output is `{ part, answer }`
|
|
329
|
+
const answerCard = memo((s, d, _e, x) => {
|
|
330
|
+
const p = d && typeof d == 'object' && openPart(s, d.scope, 'ANSWER', y => y.ui?.status == 'open')
|
|
331
|
+
if (!p) return
|
|
332
|
+
const part = p.ui.layout?.nodes?.[d.node]?.part ?? null
|
|
333
|
+
const np = {...p, state: 'output-available', output: {part, answer: d.value}, ui: {...p.ui, status: 'answered', answer: {node: d.node, part, value: d.value}}}
|
|
334
|
+
return after(s, mapLast(s.messages, (y: any) => y === p ? np : y), x)
|
|
335
|
+
})
|
|
336
|
+
// H-2: the backend's reply for the card in flight (another step, the card, or a failure), then the next card
|
|
337
|
+
const cardStep = (ok: boolean) => memo((s, d, _e, x) => {
|
|
338
|
+
const st = x?.state, k = x?.k
|
|
339
|
+
const card = openToolCalls(s).find((y: any) => composing(y) && y.ui.pending)
|
|
340
|
+
// the reply's request key: the chat driver's `key`, an HTTP reply's `askKey` tag (G-756, ask.ts), a failure's request
|
|
341
|
+
const key = d?.askKey ?? d?.key ?? d?.request?.key
|
|
342
|
+
// a late reply (the card was cancelled or answered, or the turn replaced) is dropped
|
|
343
|
+
if (!card || (typeof key == 'string' && key != `${k}.ask:${card.toolCallId}`)) return
|
|
344
|
+
const r = ok ? c.kit.cardReply(c.ask, card, d, st, k) : c.kit.failed(card, String(d?.error?.message ?? d?.error ?? 'the request failed'))
|
|
345
|
+
const msgs = mapLast(s.messages, (y: any) => y === card ? r.part ?? r : y)
|
|
346
|
+
return r.request !== undefined ? {...settle(s, msgs, c), compose: r.request} : after(s, msgs, x)
|
|
347
|
+
})
|
|
348
|
+
const cardOk = cardStep(true), cardFailed = cardStep(false)
|
|
349
|
+
// H-2: a container control in a card: ui[node][name] = value, on the card's part
|
|
350
|
+
const controlStep = (s: any, d: any) => {
|
|
351
|
+
if (!d || typeof d.scope != 'string') return
|
|
352
|
+
let hit = false
|
|
353
|
+
const messages = s.messages.map((m: any) => m.role != 'assistant' || !m.parts.some((x: any) => x.toolCallId === d.scope && x.ui) ? m
|
|
354
|
+
: {...m, parts: m.parts.map((x: any) => x.toolCallId !== d.scope || !x.ui ? x : (hit = true, {...x, ui: {...x.ui, ui: {...x.ui.ui, [d.key]: {...x.ui.ui?.[d.key], [d.name]: d.value}}}}))})
|
|
355
|
+
return hit ? {...s, messages} : undefined
|
|
356
|
+
}
|
|
357
|
+
// rule 12: a restored busy slice (its request is gone) is made usable again; a composing card is composed again
|
|
358
|
+
const resumeStep = memo((s, _d, _e, p) => {
|
|
359
|
+
const msgs0 = s.messages.length ? mapLast(s.messages, (x: any) => composing(x) ? {...x, ui: {...x.ui, pending: null}} : x) : s.messages
|
|
360
|
+
// agent calls still open never get their results now
|
|
361
|
+
const m = lastOf(msgs0)
|
|
362
|
+
let msgs = m?.role == 'assistant' && openCalls(m).some(x => !isClient(c, x))
|
|
363
|
+
? mapLast(msgs0, (x: any) => isTool(x) && x.state == 'input-available' && !isClient(c, x) ? {...x, state: 'output-error', errorText: 'not run: the page was reloaded'} : x) : msgs0
|
|
364
|
+
msgs = withPartial(s, msgs)
|
|
365
|
+
const card = nextCard(c, msgs, p?.state, p?.k)
|
|
366
|
+
const ask = firstAsk(lastOf(card.msgs))
|
|
367
|
+
return {s: {...s, messages: card.msgs, draft: '', draftReasoning: '', status: ask || card.request !== undefined ? 'streaming' : 'ready', pending: ask ? askInfo(ask) : null}, compose: card.request}
|
|
368
|
+
})
|
|
369
|
+
/** the partial reply (STOP, RESUME) appended to the last assistant message, or as a new one */
|
|
370
|
+
const withPartial = (s: any, msgs: any[]) => {
|
|
371
|
+
const partial = [...(s.draftReasoning ? [{type: 'reasoning', text: s.draftReasoning}] : []), ...(s.draft ? [{type: 'text', text: s.draft}] : [])]
|
|
372
|
+
const last = lastOf(msgs)
|
|
373
|
+
return !partial.length ? msgs : last?.role == 'assistant' ? [...msgs.slice(0, -1), {...last, parts: [...last.parts, {type: 'step-start'}, ...partial]}] : [...msgs, {id: messageId(), role: 'assistant', parts: partial}]
|
|
374
|
+
}
|
|
375
|
+
return {sendStep, regenStep, replyStep, resultsStep, answerStep, outputStep, answerCard, cardOk, cardFailed, controlStep, resumeStep, withPartial}
|
|
376
|
+
}
|
|
171
377
|
|
|
172
378
|
// ------------------------------------------------------------------ the request
|
|
173
379
|
/** the app-state message's id and metadata marker (G-631) */
|
|
@@ -193,15 +399,19 @@ function appState(t: any, groups: any[], labels: Map<string, any>): any {
|
|
|
193
399
|
return {id: APP_STATE, role: 'user', metadata: {sygnal: APP_STATE, untrusted}, parts: [{type: 'text', text}]}
|
|
194
400
|
}
|
|
195
401
|
|
|
402
|
+
const shadowed = new Set<string>()
|
|
196
403
|
/**
|
|
197
404
|
* The request for the conversation `messages`: tools and app state of now, the state right before
|
|
198
405
|
* the last user message; `fresh`: a new turn. When the last message is the assistant's (tool
|
|
199
|
-
* results, an approval answered) the reply continues it (`continue: true`, G-628)
|
|
406
|
+
* results, an approval answered) the reply continues it (`continue: true`, G-628). H-1: the client
|
|
407
|
+
* tools join the agent tools (a name in both: the client tool, SYG684); `host`: the host's state
|
|
408
|
+
* (ask's direct layout schema reads it)
|
|
200
409
|
*/
|
|
201
|
-
function request(e: Engine | undefined, o: any, k: string, messages: any[], fresh: boolean) {
|
|
410
|
+
function request(e: Engine | undefined, o: any, k: string, messages: any[], fresh: boolean, c?: Cfg, host?: any) {
|
|
202
411
|
const t = e && toolsOf(e)
|
|
203
412
|
if (e) {
|
|
204
|
-
|
|
413
|
+
// a new turn: a call still waiting on `until` answers now, as pending
|
|
414
|
+
if (fresh) { e.turn++; e.steps = 0; e.answer?.(false); e.tools?.cancel() }
|
|
205
415
|
e.steps++
|
|
206
416
|
}
|
|
207
417
|
const list = t ? t.list() : [], groups = t ? t.groups() : [], labels = new Map<string, any>(), bare = new Map<string, string>()
|
|
@@ -211,17 +421,26 @@ function request(e: Engine | undefined, o: any, k: string, messages: any[], fres
|
|
|
211
421
|
}
|
|
212
422
|
const state = t && appState(t, groups, labels), i = lastUser(messages)
|
|
213
423
|
const schema = (x: any) => bare.has(x.name) ? unlabel(x.inputSchema, `their labels are in ${bare.get(x.name)}_labels in the app state`) : x.inputSchema
|
|
424
|
+
const tools: any = Object.fromEntries(list.map((x: any) => [x.name, {description: x.description, inputSchema: schema(x)}]))
|
|
425
|
+
if (c) for (const n in c.client) {
|
|
426
|
+
if (n in tools && !shadowed.has(n)) { shadowed.add(n); dev('SYG684', `the client tool '${n}' has the name of an agent tool; the client tool is sent and its calls are the app's to answer`, 'Rename the client tool, or the agent declaration or action') }
|
|
427
|
+
const x = c.client[n]
|
|
428
|
+
tools[n] = c.ask && n == 'ask_user' ? c.kit.askTool(c.ask, host) : {description: x.description, inputSchema: schemaOf(x)}
|
|
429
|
+
}
|
|
214
430
|
return {
|
|
215
431
|
...o.transportOptions,
|
|
216
432
|
messages: !state ? messages : i < 0 ? [state, ...messages] : [...messages.slice(0, i), state, ...messages.slice(i)],
|
|
217
433
|
...(o.instructions && {instructions: o.instructions}),
|
|
218
|
-
...(
|
|
219
|
-
...(o.model
|
|
434
|
+
...(Object.keys(tools).length && {tools}),
|
|
435
|
+
...modelOf(o.model, host),
|
|
220
436
|
...(lastOf(messages)?.role == 'assistant' && {continue: true}),
|
|
221
437
|
key: k, delta: k + '.DELTA', ok: k + '.REPLY', error: k + '.FAILED',
|
|
222
438
|
}
|
|
223
439
|
}
|
|
224
440
|
|
|
441
|
+
/** `model`: a model id, or a function of the host's state returning one (resolved per request) */
|
|
442
|
+
const modelOf = (m: any, host: any) => { const v = typeof m == 'function' ? m(host) : m; return v ? {model: v} : {} }
|
|
443
|
+
|
|
225
444
|
/**
|
|
226
445
|
* run a reply's tool calls one at a time; RESULTS when all are done (dropped if the turn moved on).
|
|
227
446
|
* The results so far are kept on the engine (`ran`): STOP gives the calls that ran their results
|
|
@@ -243,20 +462,60 @@ async function runTools(e: Engine, calls: any[]) {
|
|
|
243
462
|
dispatch(e, 'RESULTS', {turn, results}, 'reply')
|
|
244
463
|
}
|
|
245
464
|
|
|
465
|
+
/** a click on a card's container control (catalog.render's `control()` data attributes) */
|
|
466
|
+
const controlOf = (ev: any) => {
|
|
467
|
+
const el = ev?.ownerTarget || ev?.target?.closest?.('[data-compose-control]') || ev?.target
|
|
468
|
+
const ds = el?.dataset || {}
|
|
469
|
+
if (!ds.composeControl || !ds.composeKey || ds.composeScope === undefined) return null
|
|
470
|
+
let value: any = ds.composeValue
|
|
471
|
+
try { value = JSON.parse(value) } catch (_) { /* a plain string */ }
|
|
472
|
+
return {scope: ds.composeScope, key: ds.composeKey, name: ds.composeControl, value}
|
|
473
|
+
}
|
|
474
|
+
|
|
246
475
|
/**
|
|
247
476
|
* The chat behavior (PLAN-6 L-3): `uses = { assistant: chat({ sink, form, prompt, stop, approve,
|
|
248
|
-
* deny, instructions, model, agent, maxSteps, transportOptions }) }`. Types:
|
|
477
|
+
* deny, instructions, model, agent, maxSteps, transportOptions, clientTools, ask }) }`. Types:
|
|
478
|
+
* src/ai.d.ts.
|
|
249
479
|
*/
|
|
250
480
|
export const chat = (options: any = {}): any => {
|
|
251
|
-
const {sink = 'LLM', form, prompt, stop, approve, deny, regenerate} = options
|
|
481
|
+
const {sink = 'LLM', form, prompt, stop, approve, deny, regenerate, clientTools, ask} = options
|
|
252
482
|
const on = (DOM: any, sel: any, ev = 'click', o?: any) => DOM.select(sel).events(ev, o)
|
|
253
483
|
const E = engineFor
|
|
484
|
+
// H-1 / H-2: the calls the app answers; ask_user's card machinery comes with the catalog
|
|
485
|
+
const kit = ask?.catalog?._ask
|
|
486
|
+
if (ask && !kit) throw new Error('[Sygnal] chat({ ask }): `ask.catalog` must be a catalog made by defineCatalog()')
|
|
487
|
+
const c: Cfg = {client: {...clientTools}, ...(ask && {kit, ask: kit.askConfig(ask, options, sink)})}
|
|
488
|
+
if (ask) c.client.ask_user = {}
|
|
489
|
+
const B: string | undefined = c.ask?.backend?.sink
|
|
490
|
+
const S = steps(c, options)
|
|
491
|
+
const {sendStep, regenStep, replyStep, resultsStep, answerStep} = S
|
|
492
|
+
// the sink handlers' view of the host: its state (props.state) and the use's key
|
|
493
|
+
const P = (p: any, k: string) => p && {state: p.state, k}
|
|
494
|
+
// a step's request on the chat sink (`send`), and the card request on the backend's sink
|
|
495
|
+
const out = (n: any, e: any, o: any, k: string, p: any, which: 'chat' | 'card') => {
|
|
496
|
+
if (!n) return ABORT
|
|
497
|
+
if (which == 'card') return n.compose ?? ABORT
|
|
498
|
+
return n.send ? request(e, o, k, n.s.messages, false, c, p?.state) : B === sink && n.compose !== undefined ? n.compose : ABORT
|
|
499
|
+
}
|
|
500
|
+
/** model entries for a step's chat request and its card request (one entry when they share a sink) */
|
|
501
|
+
const sinks = (step: (s: any, d: any, e: any, p: any) => any) => ({
|
|
502
|
+
[sink]: (s: any, d: any, _n: any, p: any, o: any, k: string) => { const e = E(p, k, o); return out(step(s, d, e, P(p, k)), e, o, k, p, 'chat') },
|
|
503
|
+
...(B && B !== sink && {[B]: (s: any, d: any, _n: any, p: any, o: any, k: string) => out(step(s, d, E(p, k, o), P(p, k)), undefined, o, k, p, 'card')}),
|
|
504
|
+
})
|
|
505
|
+
const stateOf = (step: (s: any, d: any, e: any, p: any) => any) => (s: any, d: any, _n: any, p: any, o: any, k: string) => step(s, d, E(p, k, o), P(p, k))?.s || ABORT
|
|
506
|
+
/** the card request in flight's key (STOP aborts it) */
|
|
507
|
+
const inflight = (s: any, k: string) => { const x = openToolCalls(s).find((p: any) => composing(p) && p.ui.pending); return x && `${k}.ask:${x.toolCallId}` }
|
|
254
508
|
const b = defineBehavior({
|
|
255
509
|
initialState: {messages: [], prompt: '', draft: '', draftReasoning: '', status: 'ready', pending: null, error: null},
|
|
256
510
|
intent: (so: any) => {
|
|
257
|
-
const {DOM} = so
|
|
511
|
+
const {DOM, CHILD} = so
|
|
258
512
|
// SYG442: an app made before the first chat() call has no layer: no tools, no loop
|
|
259
513
|
checkLinked(so, 'chat', 'it sends no tools and runs no tool calls')
|
|
514
|
+
// rule 12: a slice restored busy (persist) or with a card composing; a microtask later, so
|
|
515
|
+
// the requests it sends reach drivers that are wired after the instance is made
|
|
516
|
+
const restored = so.STATE?.stream?.take?.(1).filter((s: any) => s && (busy(s) || openToolCalls(s).some(composing)))
|
|
517
|
+
.map((s: any) => xs.fromPromise(Promise.resolve(s))).flatten()
|
|
518
|
+
const results = c.ask?.results || []
|
|
260
519
|
return {
|
|
261
520
|
...(prompt && {PROMPT: on(DOM, prompt, 'input').map((ev: any) => ev.target.value)}),
|
|
262
521
|
...(form && {SEND: on(DOM, form, 'submit', {preventDefault: true})}),
|
|
@@ -264,70 +523,92 @@ export const chat = (options: any = {}): any => {
|
|
|
264
523
|
...(approve && {APPROVE: on(DOM, approve)}),
|
|
265
524
|
...(deny && {DENY: on(DOM, deny)}),
|
|
266
525
|
...(regenerate && {REGENERATE: on(DOM, regenerate)}),
|
|
526
|
+
...(restored && {RESUME: restored}),
|
|
527
|
+
// H-2: a result part's PARENT value (compose.answer): cards render in the host's view
|
|
528
|
+
...(results.length && CHILD && {ANSWER: xs.merge(...results.map((C: any) => CHILD.select(C))).filter((v: any) => v && typeof v == 'object' && v.scope !== undefined)}),
|
|
529
|
+
...(c.ask?.controls && DOM && {CONTROL: DOM.select('[data-compose-control]').events('click').map(controlOf).filter((x: any) => !!x)}),
|
|
267
530
|
}
|
|
268
531
|
},
|
|
269
532
|
model: {
|
|
270
533
|
PROMPT: (s: any, p: any) => typeof p == 'string' && p !== s.prompt ? {...s, prompt: p} : ABORT,
|
|
271
534
|
SEND: {
|
|
272
535
|
STATE: (s: any, d: any) => sendStep(s, d) || ABORT,
|
|
273
|
-
[sink]: (s: any, d: any, _n: any, p: any, o: any, k: string) => { const n = sendStep(s, d); return n ? request(E(p, k, o), o, k, n.messages, true) : ABORT },
|
|
536
|
+
[sink]: (s: any, d: any, _n: any, p: any, o: any, k: string) => { const n = sendStep(s, d); return n ? request(E(p, k, o), o, k, n.messages, true, c, p?.state) : ABORT },
|
|
274
537
|
},
|
|
275
538
|
REGENERATE: {
|
|
276
539
|
STATE: (s: any) => regenStep(s, 0) || ABORT,
|
|
277
|
-
[sink]: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { const n = regenStep(s, 0); return n ? request(E(p, k, o), o, k, n.messages, true) : ABORT },
|
|
540
|
+
[sink]: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { const n = regenStep(s, 0); return n ? request(E(p, k, o), o, k, n.messages, true, c, p?.state) : ABORT },
|
|
278
541
|
},
|
|
279
542
|
// G-627: the reply's text and reasoning so far
|
|
280
543
|
DELTA: (s: any, d: any) => ({...s, draft: d?.text ?? s.draft, draftReasoning: d?.reasoning ?? s.draftReasoning, status: 'streaming'}),
|
|
281
544
|
REPLY: {
|
|
282
|
-
STATE: (
|
|
283
|
-
|
|
545
|
+
STATE: stateOf(replyStep),
|
|
546
|
+
...sinks(replyStep),
|
|
547
|
+
// the turn is over: DONE in the same flush (a host entry 'assistant.DONE' runs after it).
|
|
548
|
+
// H-1: each client call that opened fires TOOL_CALL; the turn waits for their answers
|
|
284
549
|
EFFECT: (s: any, d: any, next: any, p: any, o: any, k: string) => {
|
|
285
|
-
const e = E(p, k, o), r = replyStep(s, d, e)
|
|
286
|
-
if (!r
|
|
550
|
+
const e = E(p, k, o), r = replyStep(s, d, e, P(p, k))
|
|
551
|
+
if (!r) return
|
|
552
|
+
for (const call of r.opened) e ? dispatch(e, 'TOOL_CALL', call, 'next') : next('TOOL_CALL', call, 0)
|
|
553
|
+
if (r.ask) return
|
|
287
554
|
const done = {message: r.msg, text: d.text, finishReason: d.finishReason, usage: d.usage, steps: e?.steps ?? 1}
|
|
288
555
|
if (r.calls) void runTools(e!, r.calls)
|
|
556
|
+
else if (r.wait || r.send) return
|
|
289
557
|
else if (e) dispatch(e, 'DONE', done, 'next')
|
|
290
558
|
else next('DONE', done, 0)
|
|
291
559
|
},
|
|
292
560
|
},
|
|
293
561
|
RESULTS: {
|
|
294
|
-
STATE: (
|
|
295
|
-
[sink]: (s: any, d: any, _n: any, p: any, o: any, k: string) => { const e = E(p, k, o), n = resultsStep(s, d, e); return n?.send ? request(e, o, k, n.s.messages, false) : ABORT },
|
|
562
|
+
STATE: stateOf(resultsStep),
|
|
563
|
+
[sink]: (s: any, d: any, _n: any, p: any, o: any, k: string) => { const e = E(p, k, o), n = resultsStep(s, d, e); return n?.send ? request(e, o, k, n.s.messages, false, c, p?.state) : ABORT },
|
|
296
564
|
},
|
|
297
565
|
ASK: (s: any, info: any) => ({...s, pending: info}),
|
|
298
566
|
// a consequential app call (A-1's confirm) or a server approval (G-628: answerStep)
|
|
299
567
|
APPROVE: {
|
|
300
568
|
STATE: (s: any) => answerStep(s, true)?.s || (s.pending ? {...s, pending: null} : ABORT),
|
|
301
|
-
[sink]: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { const n = answerStep(s, true); return n?.send ? request(E(p, k, o), o, k, n.s.messages, false) : ABORT },
|
|
569
|
+
[sink]: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { const n = answerStep(s, true); return n?.send ? request(E(p, k, o), o, k, n.s.messages, false, c, p?.state) : ABORT },
|
|
302
570
|
EFFECT: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { if (s.pending?.approvalId === undefined) E(p, k, o)?.answer?.(true) },
|
|
303
571
|
},
|
|
304
572
|
DENY: {
|
|
305
573
|
STATE: (s: any) => answerStep(s, false)?.s || (s.pending ? {...s, pending: null} : ABORT),
|
|
306
|
-
[sink]: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { const n = answerStep(s, false); return n?.send ? request(E(p, k, o), o, k, n.s.messages, false) : ABORT },
|
|
574
|
+
[sink]: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { const n = answerStep(s, false); return n?.send ? request(E(p, k, o), o, k, n.s.messages, false, c, p?.state) : ABORT },
|
|
307
575
|
EFFECT: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { if (s.pending?.approvalId === undefined) E(p, k, o)?.answer?.(false) },
|
|
308
576
|
},
|
|
577
|
+
// H-1: the app answers a client call (rule 5)
|
|
578
|
+
TOOL_OUTPUT: {STATE: stateOf(S.outputStep), ...sinks(S.outputStep)},
|
|
579
|
+
// H-1: a client call opened (a host entry 'assistant.TOOL_CALL' runs after it)
|
|
580
|
+
TOOL_CALL: () => ABORT,
|
|
309
581
|
STOP: {
|
|
310
|
-
// the calls that ran keep their results (G-626); the partial reply is kept (its reasoning
|
|
582
|
+
// the calls that ran keep their results (G-626); the partial reply is kept (its reasoning
|
|
583
|
+
// too); H-1: open client calls are closed, a card being composed is cancelled
|
|
311
584
|
STATE: (s: any, _d: any, _n: any, p: any, o: any, k: string) => {
|
|
312
|
-
if (!busy(s)) return ABORT
|
|
313
|
-
const e = E(p, k, o), msgs = closeOpen(s.messages, 'not run: stopped by the user', e?.ran?.turn === e?.turn ? e?.ran?.out : undefined)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
585
|
+
if (!busy(s) && !openToolCalls(s).length) return ABORT
|
|
586
|
+
const e = E(p, k, o), msgs = closeOpen(s.messages, 'not run: stopped by the user', e?.ran?.turn === e?.turn ? e?.ran?.out : undefined, c, 'not answered: stopped by the user')
|
|
587
|
+
return {...s, draft: '', draftReasoning: '', status: 'ready', pending: null, messages: S.withPartial(s, msgs)}
|
|
588
|
+
},
|
|
589
|
+
[sink]: (s: any, _d: any, _n: any, _p: any, _o: any, k: string) => {
|
|
590
|
+
const keys = [...(busy(s) ? [k] : []), ...(B === sink && inflight(s, k) ? [inflight(s, k)] : [])]
|
|
591
|
+
return !keys.length ? ABORT : {abort: keys.length == 1 ? keys[0] : keys}
|
|
320
592
|
},
|
|
321
|
-
[
|
|
593
|
+
...(B && B !== sink && {[B]: (s: any, _d: any, _n: any, _p: any, _o: any, k: string) => inflight(s, k) ? {abort: inflight(s, k)} : ABORT}),
|
|
322
594
|
// the turn moves on: a waiting confirm is declined, tool results still to come are dropped
|
|
323
|
-
EFFECT: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { const e = busy(s) && E(p, k, o); if (e) { e.turn++; e.answer?.(false) } },
|
|
595
|
+
EFFECT: (s: any, _d: any, _n: any, p: any, o: any, k: string) => { const e = busy(s) && E(p, k, o); if (e) { e.turn++; e.answer?.(false); e.tools?.cancel() } },
|
|
324
596
|
},
|
|
325
597
|
FAILED: {
|
|
326
|
-
STATE: (s: any, d: any) => ({...s, messages: closeOpen(s.messages, 'not run: the request failed'), draft: '', draftReasoning: '', status: 'error', pending: null, error: d?.error?.message ?? String(d?.error ?? 'error')}),
|
|
327
|
-
EFFECT: (_s: any, _d: any, _n: any, p: any, o: any, k: string) => { const e = E(p, k, o); if (e) { e.turn++; e.answer?.(false) } },
|
|
598
|
+
STATE: (s: any, d: any) => ({...s, messages: closeOpen(s.messages, 'not run: the request failed', undefined, c), draft: '', draftReasoning: '', status: 'error', pending: null, error: d?.error?.message ?? String(d?.error ?? 'error')}),
|
|
599
|
+
EFFECT: (_s: any, _d: any, _n: any, p: any, o: any, k: string) => { const e = E(p, k, o); if (e) { e.turn++; e.answer?.(false); e.tools?.cancel() } },
|
|
328
600
|
},
|
|
329
601
|
// (REPLY already set ready: a host entry gets the finished state)
|
|
330
602
|
DONE: (s: any) => s.status == 'ready' ? ABORT : {...s, status: 'ready'},
|
|
603
|
+
// rule 12: once at start, for a slice restored busy
|
|
604
|
+
RESUME: {STATE: stateOf(S.resumeStep), ...sinks(S.resumeStep)},
|
|
605
|
+
...(c.ask && {
|
|
606
|
+
// H-2: the card backend's replies, a result part's answer, a card's container control
|
|
607
|
+
CARD: {STATE: stateOf(S.cardOk), ...sinks(S.cardOk)},
|
|
608
|
+
CARD_FAILED: {STATE: stateOf(S.cardFailed), ...sinks(S.cardFailed)},
|
|
609
|
+
ANSWER: {STATE: stateOf(S.answerCard), ...sinks(S.answerCard)},
|
|
610
|
+
CONTROL: (s: any, d: any) => S.controlStep(s, d) || ABORT,
|
|
611
|
+
}),
|
|
331
612
|
},
|
|
332
613
|
})(options)
|
|
333
614
|
// the `prompt` option is a selector, not the slice's start value (defineBehavior's option override)
|