sygnal 6.2.0 → 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 +61 -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 +164 -3
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +164 -3
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/agent.md +7 -1
- package/dist/guide/ai-cards.md +319 -0
- package/dist/guide/ai-chat.md +4 -0
- package/dist/guide/ai-compose.md +881 -0
- package/dist/guide/ai-decisions.md +16 -0
- package/dist/guide/http.md +1 -0
- package/dist/index.cjs.js +5695 -347
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +638 -8
- package/dist/index.esm.js +5685 -347
- 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 +6 -4
- package/package.json +2 -1
- package/src/ai.d.ts +642 -7
- 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/chat/behavior.ts +354 -81
- 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/diagnostics/checks/next.ts +86 -2
- package/src/extra/diagnostics/codes.ts +54 -0
- 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 +41 -0
- 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
|
@@ -57,10 +57,47 @@
|
|
|
57
57
|
* the host's dispose stops it. A host in an app made before the first chat() call is reported as
|
|
58
58
|
* SYG442 and runs without tools. The engine dispatches RESULTS / ASK (cause 'reply') and DONE
|
|
59
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.
|
|
60
94
|
*/
|
|
61
95
|
import {defineBehavior} from '../../behaviors'
|
|
62
96
|
import {ABORT} from '../../../shared'
|
|
97
|
+
import {report} from '../../diagnostics/index'
|
|
98
|
+
import xs from '../../xstreamCompat'
|
|
63
99
|
import {agentTools, hasUserText, labelsUntrusted, unlabel} from '../agent/index'
|
|
100
|
+
import {toJsonSchema, repair, isObj} from '../schema/index'
|
|
64
101
|
import {linked, engineOf, checkLinked} from '../link'
|
|
65
102
|
import {messageId} from '../messages'
|
|
66
103
|
|
|
@@ -84,6 +121,9 @@ const toolsOf = (e: Engine) => e.o.agent === false ? null : e.tools ||= agentToo
|
|
|
84
121
|
}),
|
|
85
122
|
})
|
|
86
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
|
+
|
|
87
127
|
// ------------------------------------------------------------------ messages
|
|
88
128
|
const isTool = (p: any) => typeof p?.type == 'string' && p.type.startsWith('tool-')
|
|
89
129
|
const openCalls = (m: any): any[] => (m?.parts || []).filter((p: any) => isTool(p) && p.state == 'input-available')
|
|
@@ -102,16 +142,23 @@ const withResult = (p: any, r: any) => {
|
|
|
102
142
|
const {output: _o, errorText: _e, ...rest} = p
|
|
103
143
|
return 'error' in r ? {...rest, state: 'output-error', errorText: r.error} : {...rest, state: 'output-available', output: r.output}
|
|
104
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
|
|
105
150
|
/**
|
|
106
151
|
* The last message with its open tool parts closed (a turn stopped, failed or capped): a call
|
|
107
|
-
* that already ran gets its result (`ran`, G-626), the others `why
|
|
108
|
-
* 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
|
|
109
154
|
*/
|
|
110
|
-
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) => {
|
|
111
156
|
const m = msgs[msgs.length - 1]
|
|
112
157
|
if (!openCalls(m).length && !firstAsk(m)) return msgs
|
|
113
158
|
return [...msgs.slice(0, -1), {...m, parts: m.parts.map((p: any) => !isTool(p) ? p
|
|
114
|
-
: 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})
|
|
115
162
|
: p.state == 'approval-requested' ? {...p, state: 'output-denied', approval: {...p.approval, approved: false, reason: why}}
|
|
116
163
|
: p)}]
|
|
117
164
|
}
|
|
@@ -120,61 +167,213 @@ const user = (text: string) => ({id: messageId(), role: 'user', parts: [{type: '
|
|
|
120
167
|
const busy = (s: any) => s.status == 'submitted' || s.status == 'streaming'
|
|
121
168
|
const lastUser = (msgs: any[]) => { for (let i = msgs.length - 1; i >= 0; i--) if (msgs[i].role == 'user') return i; return -1 }
|
|
122
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'
|
|
178
|
+
|
|
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
|
+
}
|
|
123
191
|
|
|
124
|
-
/**
|
|
125
|
-
const
|
|
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) => {
|
|
126
211
|
let l: any[] = []
|
|
127
|
-
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'}
|
|
128
235
|
}
|
|
129
236
|
|
|
130
237
|
const textOf = (d: any) => typeof d == 'string' ? d : d && typeof d == 'object' && typeof d.text == 'string' && !('target' in d) ? d.text : undefined
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// reply
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
162
315
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}
|
|
178
377
|
|
|
179
378
|
// ------------------------------------------------------------------ the request
|
|
180
379
|
/** the app-state message's id and metadata marker (G-631) */
|
|
@@ -200,12 +399,15 @@ function appState(t: any, groups: any[], labels: Map<string, any>): any {
|
|
|
200
399
|
return {id: APP_STATE, role: 'user', metadata: {sygnal: APP_STATE, untrusted}, parts: [{type: 'text', text}]}
|
|
201
400
|
}
|
|
202
401
|
|
|
402
|
+
const shadowed = new Set<string>()
|
|
203
403
|
/**
|
|
204
404
|
* The request for the conversation `messages`: tools and app state of now, the state right before
|
|
205
405
|
* the last user message; `fresh`: a new turn. When the last message is the assistant's (tool
|
|
206
|
-
* 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)
|
|
207
409
|
*/
|
|
208
|
-
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) {
|
|
209
411
|
const t = e && toolsOf(e)
|
|
210
412
|
if (e) {
|
|
211
413
|
// a new turn: a call still waiting on `until` answers now, as pending
|
|
@@ -219,17 +421,26 @@ function request(e: Engine | undefined, o: any, k: string, messages: any[], fres
|
|
|
219
421
|
}
|
|
220
422
|
const state = t && appState(t, groups, labels), i = lastUser(messages)
|
|
221
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
|
+
}
|
|
222
430
|
return {
|
|
223
431
|
...o.transportOptions,
|
|
224
432
|
messages: !state ? messages : i < 0 ? [state, ...messages] : [...messages.slice(0, i), state, ...messages.slice(i)],
|
|
225
433
|
...(o.instructions && {instructions: o.instructions}),
|
|
226
|
-
...(
|
|
227
|
-
...(o.model
|
|
434
|
+
...(Object.keys(tools).length && {tools}),
|
|
435
|
+
...modelOf(o.model, host),
|
|
228
436
|
...(lastOf(messages)?.role == 'assistant' && {continue: true}),
|
|
229
437
|
key: k, delta: k + '.DELTA', ok: k + '.REPLY', error: k + '.FAILED',
|
|
230
438
|
}
|
|
231
439
|
}
|
|
232
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
|
+
|
|
233
444
|
/**
|
|
234
445
|
* run a reply's tool calls one at a time; RESULTS when all are done (dropped if the turn moved on).
|
|
235
446
|
* The results so far are kept on the engine (`ran`): STOP gives the calls that ran their results
|
|
@@ -251,20 +462,60 @@ async function runTools(e: Engine, calls: any[]) {
|
|
|
251
462
|
dispatch(e, 'RESULTS', {turn, results}, 'reply')
|
|
252
463
|
}
|
|
253
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
|
+
|
|
254
475
|
/**
|
|
255
476
|
* The chat behavior (PLAN-6 L-3): `uses = { assistant: chat({ sink, form, prompt, stop, approve,
|
|
256
|
-
* deny, instructions, model, agent, maxSteps, transportOptions }) }`. Types:
|
|
477
|
+
* deny, instructions, model, agent, maxSteps, transportOptions, clientTools, ask }) }`. Types:
|
|
478
|
+
* src/ai.d.ts.
|
|
257
479
|
*/
|
|
258
480
|
export const chat = (options: any = {}): any => {
|
|
259
|
-
const {sink = 'LLM', form, prompt, stop, approve, deny, regenerate} = options
|
|
481
|
+
const {sink = 'LLM', form, prompt, stop, approve, deny, regenerate, clientTools, ask} = options
|
|
260
482
|
const on = (DOM: any, sel: any, ev = 'click', o?: any) => DOM.select(sel).events(ev, o)
|
|
261
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}` }
|
|
262
508
|
const b = defineBehavior({
|
|
263
509
|
initialState: {messages: [], prompt: '', draft: '', draftReasoning: '', status: 'ready', pending: null, error: null},
|
|
264
510
|
intent: (so: any) => {
|
|
265
|
-
const {DOM} = so
|
|
511
|
+
const {DOM, CHILD} = so
|
|
266
512
|
// SYG442: an app made before the first chat() call has no layer: no tools, no loop
|
|
267
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 || []
|
|
268
519
|
return {
|
|
269
520
|
...(prompt && {PROMPT: on(DOM, prompt, 'input').map((ev: any) => ev.target.value)}),
|
|
270
521
|
...(form && {SEND: on(DOM, form, 'submit', {preventDefault: true})}),
|
|
@@ -272,70 +523,92 @@ export const chat = (options: any = {}): any => {
|
|
|
272
523
|
...(approve && {APPROVE: on(DOM, approve)}),
|
|
273
524
|
...(deny && {DENY: on(DOM, deny)}),
|
|
274
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)}),
|
|
275
530
|
}
|
|
276
531
|
},
|
|
277
532
|
model: {
|
|
278
533
|
PROMPT: (s: any, p: any) => typeof p == 'string' && p !== s.prompt ? {...s, prompt: p} : ABORT,
|
|
279
534
|
SEND: {
|
|
280
535
|
STATE: (s: any, d: any) => sendStep(s, d) || ABORT,
|
|
281
|
-
[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 },
|
|
282
537
|
},
|
|
283
538
|
REGENERATE: {
|
|
284
539
|
STATE: (s: any) => regenStep(s, 0) || ABORT,
|
|
285
|
-
[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 },
|
|
286
541
|
},
|
|
287
542
|
// G-627: the reply's text and reasoning so far
|
|
288
543
|
DELTA: (s: any, d: any) => ({...s, draft: d?.text ?? s.draft, draftReasoning: d?.reasoning ?? s.draftReasoning, status: 'streaming'}),
|
|
289
544
|
REPLY: {
|
|
290
|
-
STATE: (
|
|
291
|
-
|
|
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
|
|
292
549
|
EFFECT: (s: any, d: any, next: any, p: any, o: any, k: string) => {
|
|
293
|
-
const e = E(p, k, o), r = replyStep(s, d, e)
|
|
294
|
-
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
|
|
295
554
|
const done = {message: r.msg, text: d.text, finishReason: d.finishReason, usage: d.usage, steps: e?.steps ?? 1}
|
|
296
555
|
if (r.calls) void runTools(e!, r.calls)
|
|
556
|
+
else if (r.wait || r.send) return
|
|
297
557
|
else if (e) dispatch(e, 'DONE', done, 'next')
|
|
298
558
|
else next('DONE', done, 0)
|
|
299
559
|
},
|
|
300
560
|
},
|
|
301
561
|
RESULTS: {
|
|
302
|
-
STATE: (
|
|
303
|
-
[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 },
|
|
304
564
|
},
|
|
305
565
|
ASK: (s: any, info: any) => ({...s, pending: info}),
|
|
306
566
|
// a consequential app call (A-1's confirm) or a server approval (G-628: answerStep)
|
|
307
567
|
APPROVE: {
|
|
308
568
|
STATE: (s: any) => answerStep(s, true)?.s || (s.pending ? {...s, pending: null} : ABORT),
|
|
309
|
-
[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 },
|
|
310
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) },
|
|
311
571
|
},
|
|
312
572
|
DENY: {
|
|
313
573
|
STATE: (s: any) => answerStep(s, false)?.s || (s.pending ? {...s, pending: null} : ABORT),
|
|
314
|
-
[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 },
|
|
315
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) },
|
|
316
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,
|
|
317
581
|
STOP: {
|
|
318
|
-
// 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
|
|
319
584
|
STATE: (s: any, _d: any, _n: any, p: any, o: any, k: string) => {
|
|
320
|
-
if (!busy(s)) return ABORT
|
|
321
|
-
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)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
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}
|
|
328
592
|
},
|
|
329
|
-
[
|
|
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}),
|
|
330
594
|
// the turn moves on: a waiting confirm is declined, tool results still to come are dropped
|
|
331
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() } },
|
|
332
596
|
},
|
|
333
597
|
FAILED: {
|
|
334
|
-
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')}),
|
|
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')}),
|
|
335
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() } },
|
|
336
600
|
},
|
|
337
601
|
// (REPLY already set ready: a host entry gets the finished state)
|
|
338
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
|
+
}),
|
|
339
612
|
},
|
|
340
613
|
})(options)
|
|
341
614
|
// the `prompt` option is a selector, not the slice's start value (defineBehavior's option override)
|