sygnal 6.1.1 → 6.2.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/astro/server.cjs.js.map +1 -1
  3. package/dist/astro/server.mjs.map +1 -1
  4. package/dist/diagnostics.cjs.js +10 -3
  5. package/dist/diagnostics.cjs.js.map +1 -1
  6. package/dist/diagnostics.esm.js +10 -3
  7. package/dist/diagnostics.esm.js.map +1 -1
  8. package/dist/guide/adapters.md +2 -1
  9. package/dist/guide/agent.md +112 -6
  10. package/dist/guide/ai-chat.md +60 -16
  11. package/dist/guide/ai-decisions.md +1 -1
  12. package/dist/guide/behaviors.md +1 -1
  13. package/dist/guide/error-boundaries.md +2 -1
  14. package/dist/guide/forms.md +1 -1
  15. package/dist/guide/http.md +4 -4
  16. package/dist/guide/mcp-apps.md +4 -2
  17. package/dist/guide/persistence.md +1 -1
  18. package/dist/guide/recipes/ai-form-fill.md +1 -1
  19. package/dist/guide/recipes/ai-summarize.md +1 -1
  20. package/dist/guide/recipes/ai-support-inbox.md +1 -1
  21. package/dist/guide/resources.md +11 -10
  22. package/dist/guide/ui/combobox.md +1 -1
  23. package/dist/guide/undo.md +1 -1
  24. package/dist/guide/web-components.md +1 -0
  25. package/dist/guide/webmcp.md +1 -2
  26. package/dist/index.cjs.js +155 -31
  27. package/dist/index.cjs.js.map +1 -1
  28. package/dist/index.d.ts +42 -6
  29. package/dist/index.esm.js +155 -31
  30. package/dist/index.esm.js.map +1 -1
  31. package/dist/sygnal.min.js +1 -1
  32. package/dist/sygnal.min.js.map +1 -1
  33. package/dist/vike/config/package.json +1 -1
  34. package/llms.txt +2 -2
  35. package/package.json +1 -1
  36. package/src/ai.d.ts +41 -5
  37. package/src/extra/ai/agent/index.ts +111 -26
  38. package/src/extra/ai/chat/behavior.ts +14 -6
  39. package/src/extra/ai/webmcp.ts +5 -3
  40. package/src/extra/diagnostics/codes.ts +10 -3
  41. package/src/index.d.ts +1 -1
package/dist/index.cjs.js CHANGED
@@ -744,6 +744,9 @@ const DEV_CODE_SEVERITY = {
744
744
  SYG240: 'error',
745
745
  SYG241: 'error',
746
746
  SYG243: 'warn',
747
+ SYG246: 'error',
748
+ SYG247: 'warn',
749
+ SYG248: 'warn',
747
750
  // PLAN-6 A-2 (WebMCP, reported with the severity passed)
748
751
  SYG242: 'warn',
749
752
  SYG244: 'warn',
@@ -12688,7 +12691,8 @@ async function parseInput(schema, args, options = {}) {
12688
12691
  * `t.tools()` / `t.callTool()` / `t.agentContext()` (A-4) all use it.
12689
12692
  *
12690
12693
  * Comp.agent = { name, description?, read?, label?, untrusted?,
12691
- * actions: { ACTION: { description, input?, consequential?, when?, idempotent? } } }
12694
+ * actions: { ACTION: { description, input?, consequential?, when?, idempotent?, until?, timeout? } },
12695
+ * queries: { find: { description, input?, when?, run: (state, input) => result } } }
12692
12696
  *
12693
12697
  * It reaches the app only through the runtime API (`root`, `get`, `dispatch`, `flushed`,
12694
12698
  * `addHooks`) and one hook layer, so the core pays 0 bytes:
@@ -12707,12 +12711,24 @@ async function parseInput(schema, args, options = {}) {
12707
12711
  * outcome measured by `wrapHandler` on that action only: success when a STATE handler changed
12708
12712
  * the state structurally, a sink got a value or an EFFECT ran; `abort(reason)` refuses with the
12709
12713
  * reason; otherwise "changed nothing" (`idempotent: true`: `{ ok: true, unchanged: true }`, D256);
12714
+ * - `until`: an action whose result arrives later (a `resources` entry, a driver reply) answers
12715
+ * once it is in state: `until: 'search'` (the resource is neither loading nor refreshing) or
12716
+ * `until: (state) => boolean`, checked after the action's flush and after every flush that
12717
+ * changed something; `{ ok: true, pending: true, note, state }` after `timeout` (default 10 s) or
12718
+ * `cancel()` (the chat behavior's STOP / new turn); `removed: true` when the instance went away.
12719
+ * Calls run one at a time, so a waiting call delays the next one;
12720
+ * - queries: read-only tools (`readOnlyHint`) that take an input, for data too large for `read`
12721
+ * or answers computed from the state: `run(state, input)` is a pure, synchronous function of the
12722
+ * instance's state; a call validates the input as for actions, checks `when`, and returns
12723
+ * `{ ok: true, result }` without dispatching anything (no confirm, no flush);
12710
12724
  * - `when` and `read`: cached by state identity per instance (reducers are immutable);
12711
12725
  * - HMR: given the `run()` result, it re-binds to `app.__runtime` after the root's dispose.
12712
12726
  * Diagnostics (dev, once each): SYG240 an input with no JSON Schema form (the tool is left out;
12713
- * `list({ all: true })` shows it with `error`), SYG241 `read` / `when` / `label` threw, SYG243 a
12714
- * lossy input schema, SYG440 two declarations with one name, SYG441 Collection items with no or
12715
- * duplicate keys.
12727
+ * `list({ all: true })` shows it with `error`), SYG241 `read` / `when` / `until` / `label` / a query threw,
12728
+ * SYG243 a lossy input schema, SYG246 a query whose `run` is not a function or returned a promise,
12729
+ * SYG247 a query with the tool name of an action (not offered), SYG248 `until` names no resource of
12730
+ * the component (the call answers at once), SYG440 two declarations with one
12731
+ * name, SYG441 Collection items with no or duplicate keys.
12716
12732
  */
12717
12733
  const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/\W+/g, '_').toLowerCase();
12718
12734
  const keyOf$1 = (iv) => iv.state?.id;
@@ -12725,9 +12741,9 @@ const dev$2 = (code, decl, what, component, message) => {
12725
12741
  if (s.has(code + what))
12726
12742
  return;
12727
12743
  s.add(code + what);
12728
- // severities are passed (DEV_CODE_SEVERITY, kept out of the main bundle's table): 240 / 241 error, 243 / 440 / 441 warn
12744
+ // severities are passed (DEV_CODE_SEVERITY, kept out of the main bundle's table): 240 / 241 / 246 error, 243 / 247 / 248 / 440 / 441 warn
12729
12745
  try {
12730
- report(code, { component, message, severity: code < 'SYG242' ? 'error' : 'warn' });
12746
+ report(code, { component, message, severity: code < 'SYG242' || code == 'SYG246' ? 'error' : 'warn' });
12731
12747
  }
12732
12748
  catch (e) {
12733
12749
  queueMicrotask(() => { throw e; });
@@ -12826,6 +12842,17 @@ function agentTools(target, options = {}) {
12826
12842
  const owners = new Map();
12827
12843
  const listeners = new Set();
12828
12844
  let last, scheduled = false;
12845
+ // `until`: calls waiting for their action's result to land in state, checked after each flush that changed something
12846
+ const waiters = new Set(), cancels = new Set();
12847
+ let waking = false;
12848
+ const wake = () => {
12849
+ if (!waiters.size || waking)
12850
+ return;
12851
+ waking = true;
12852
+ queueMicrotask(() => api.flushed().then(() => { waking = false; for (const w of [...waiters])
12853
+ w(); }));
12854
+ };
12855
+ const changed = () => { dirty(); wake(); };
12829
12856
  const dirty = () => {
12830
12857
  ver++;
12831
12858
  if (!listeners.size || scheduled || stopped)
@@ -12836,10 +12863,10 @@ function agentTools(target, options = {}) {
12836
12863
  };
12837
12864
  const hooks = {
12838
12865
  onCreate: dirty,
12839
- onPatch: dirty,
12840
- onReducer: dirty,
12866
+ onPatch: changed,
12867
+ onReducer: changed,
12841
12868
  onDispose: (iv) => {
12842
- dirty();
12869
+ changed();
12843
12870
  // HMR: run()'s hmr() disposes this app, starts its successor and swaps __runtime
12844
12871
  if (iv.isRoot && isApp)
12845
12872
  queueMicrotask(rebind);
@@ -13009,24 +13036,39 @@ function agentTools(target, options = {}) {
13009
13036
  const d = e.decl;
13010
13037
  if (d.read)
13011
13038
  recs.set(e.name + '_read', { e, tool: { name: e.name + '_read', description: `Read ${e.name}` + (d.description ? `: ${d.description}` : ''), inputSchema: { type: 'object', properties: {} }, annotations: { readOnlyHint: true, ...(d.untrusted && { untrustedContentHint: true }) } } });
13012
- for (const action in d.actions) {
13013
- const a = d.actions[action], name = e.name + '_' + snake(action), comp = e.ivs[0].name;
13039
+ // the actions, then the queries (read-only, `run(state, input)`): one tool each
13040
+ const defs = [];
13041
+ for (const k in d.actions)
13042
+ defs.push([k, d.actions[k], false]);
13043
+ for (const k in d.queries)
13044
+ defs.push([k, d.queries[k], true]);
13045
+ for (const [action, a, isQuery] of defs) {
13046
+ const name = e.name + '_' + snake(action), comp = e.ivs[0].name, where = `agent.${isQuery ? 'queries' : 'actions'}.${action}`;
13047
+ if (isQuery && recs.has(name)) {
13048
+ dev$2('SYG247', d, action, comp, `${where} has the tool name ${name} of an action; the query is not offered (rename it)`);
13049
+ continue;
13050
+ }
13051
+ if (isQuery && typeof a?.run != 'function') {
13052
+ dev$2('SYG246', d, action, comp, `${where}.run is not a function; the query is not offered`);
13053
+ continue;
13054
+ }
13014
13055
  const c = a.input ? toJsonSchema(a.input) : undefined;
13015
13056
  if (c?.schema)
13016
13057
  enumsOf(c.schema, e.enums);
13017
- const tool = { name, description: a.description, inputSchema: c?.schema || { type: 'object', properties: {} }, annotations: a.consequential ? { consequentialHint: true } : {} };
13018
- const rec = { e, action, a, tool };
13058
+ const tool = { name, description: a.description, inputSchema: c?.schema || { type: 'object', properties: {} },
13059
+ annotations: isQuery ? { readOnlyHint: true, ...(d.untrusted && { untrustedContentHint: true }) } : a.consequential ? { consequentialHint: true } : {} };
13060
+ const rec = isQuery ? { e, query: action, a, tool } : { e, action, a, tool };
13019
13061
  recs.set(name, rec);
13020
13062
  if (e.item)
13021
- seen.set(name, { e: { ...e, ivs: [] }, action, a, tool });
13063
+ seen.set(name, { ...rec, e: { ...e, ivs: [] } });
13022
13064
  if (c?.error) {
13023
13065
  tool.error = `SYG240 ${c.error}`;
13024
13066
  rec.off = 1;
13025
- dev$2('SYG240', d, action, comp, `agent.actions.${action}.input: ${c.error}; the tool is not offered`);
13067
+ dev$2('SYG240', d, action, comp, `${where}.input: ${c.error}; the tool is not offered`);
13026
13068
  continue;
13027
13069
  }
13028
13070
  if (c?.lossy)
13029
- dev$2('SYG243', d, action, comp, `agent.actions.${action}.input: the model doesn't see ${c.lossy}`);
13071
+ dev$2('SYG243', d, action, comp, `${where}.input: the model doesn't see ${c.lossy}`);
13030
13072
  const live = e.ivs.filter(iv => whenOk(iv, action, a.when));
13031
13073
  if (!live.length) {
13032
13074
  rec.off = 1;
@@ -13124,8 +13166,8 @@ function agentTools(target, options = {}) {
13124
13166
  }
13125
13167
  if (r.tool.error)
13126
13168
  return { ok: false, error: r.tool.error };
13127
- const { e, a, action } = r;
13128
- if (!action)
13169
+ const { e, a, action, query } = r;
13170
+ if (!action && !query)
13129
13171
  return { ok: true, state: context()[e.name] };
13130
13172
  const key = r.key || 'id', view = e.view;
13131
13173
  let raw = args ?? {}, k;
@@ -13150,22 +13192,25 @@ function agentTools(target, options = {}) {
13150
13192
  let iv = resolve();
13151
13193
  if (!iv)
13152
13194
  return e.item ? missing(e, view, k) : { ok: false, error: `${e.name} is not on the page` };
13153
- if (!whenOk(iv, action, a.when))
13195
+ if (query)
13196
+ return ask(name, iv, query, a, data);
13197
+ const type = action;
13198
+ if (!whenOk(iv, type, a.when))
13154
13199
  return { ok: false, error: `${name} is not available now`, state: resultState(iv) };
13155
13200
  if (a.consequential) {
13156
13201
  const c = 'confirm' in o ? o.confirm : options.confirm;
13157
- const info = { tool: name, component: iv.name, action, description: a.description, input: data, ...(e.item && { key: k, label: labelOf(iv) }) };
13202
+ const info = { tool: name, component: iv.name, action: type, description: a.description, input: data, ...(e.item && { key: k, label: labelOf(iv) }) };
13158
13203
  if (!(typeof c == 'function' ? await c(info) : c === true))
13159
13204
  return { ok: false, error: 'the user declined' };
13160
13205
  // the app may have changed while the user decided: resolve and check again
13161
13206
  iv = resolve();
13162
13207
  if (!iv)
13163
13208
  return e.item ? { ...missing(e, view, k), error: `the ${e.name} with id ${JSON.stringify(k)} is gone` } : { ok: false, error: `${e.name} is not on the page any more` };
13164
- if (!whenOk(iv, action, a.when))
13209
+ if (!whenOk(iv, type, a.when))
13165
13210
  return { ok: false, error: `${name} is not available any more`, state: resultState(iv) };
13166
13211
  }
13167
- const rec = pending = { id: iv.id, type: action, data, ran: false, out: [] };
13168
- api.dispatch(iv.id, action, data, 'agent');
13212
+ const rec = pending = { id: iv.id, type, data, ran: false, out: [] };
13213
+ api.dispatch(iv.id, type, data, 'agent');
13169
13214
  await api.flushed();
13170
13215
  if (pending === rec)
13171
13216
  pending = null;
@@ -13182,12 +13227,76 @@ function agentTools(target, options = {}) {
13182
13227
  const gone = iv.disposed;
13183
13228
  const state = gone ? resultState(api.get(iv.parentId)) : resultState(iv);
13184
13229
  if (out.some(x => x.kind == 'changed' || x.kind == 'sent' || x.kind == 'effect'))
13185
- return gone ? { ok: true, removed: true, state } : { ok: true, state };
13230
+ return gone ? { ok: true, removed: true, state } : a.until ? settle(name, iv, type, a) : { ok: true, state };
13186
13231
  if (refused)
13187
13232
  return { ok: false, error: `${action} was refused: ${refused.reason}`, state };
13188
13233
  return a.idempotent ? { ok: true, unchanged: true, state } : { ok: false, error: `${action} changed nothing (already so, or the input matched nothing)`, state };
13189
13234
  }
13235
+ /** `until` as a predicate: a resource name is done when that resource is neither loading nor refreshing */
13236
+ const doneTest = (iv, action, u) => {
13237
+ if (typeof u == 'function')
13238
+ return u;
13239
+ if (typeof u != 'string')
13240
+ return;
13241
+ const res = iv.def.view.resources;
13242
+ if (!res || !(u in res)) {
13243
+ dev$2('SYG248', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until names '${u}', which is not one of ${iv.name}'s resources${res ? ` (${Object.keys(res).join(', ')})` : ''}; the call answers at once`);
13244
+ return;
13245
+ }
13246
+ return (s) => { const r = s?.[u]; return !r || (r.status != 'loading' && !r.refreshing); };
13247
+ };
13248
+ /**
13249
+ * `until`: answer once the action's result is in state (after this flush or a later one), or
13250
+ * with `pending` on `timeout` / `cancel()`; `removed` when the instance went away meanwhile
13251
+ */
13252
+ function settle(name, iv, action, a) {
13253
+ const test = doneTest(iv, action, a.until);
13254
+ const holds = () => {
13255
+ if (!test)
13256
+ return true;
13257
+ try {
13258
+ return !!test(iv.state);
13259
+ }
13260
+ catch (err) {
13261
+ dev$2('SYG241', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until threw: ${err?.message ?? err}`);
13262
+ return true;
13263
+ }
13264
+ };
13265
+ const result = (extra) => ({ ok: true, ...extra, state: iv.disposed ? resultState(api.get(iv.parentId)) : resultState(iv) });
13266
+ if (holds())
13267
+ return result();
13268
+ return new Promise(resolve => {
13269
+ const finish = (extra) => { waiters.delete(check); cancels.delete(cancel); clearTimeout(timer); resolve(result(extra)); };
13270
+ const cancel = () => finish({ pending: true, note: `${name} is still in progress: its result will be in the app state later` });
13271
+ const check = () => stopped ? cancel() : iv.disposed ? finish({ removed: true }) : holds() && finish();
13272
+ const timer = setTimeout(cancel, a.timeout ?? 10000);
13273
+ waiters.add(check);
13274
+ cancels.add(cancel);
13275
+ });
13276
+ }
13277
+ /** a query call: `run(state, input)` on the instance's state now; nothing is dispatched */
13278
+ function ask(name, iv, query, q, data) {
13279
+ if (!whenOk(iv, query, q.when))
13280
+ return { ok: false, error: `${name} is not available now` };
13281
+ let result;
13282
+ try {
13283
+ result = q.run(iv.state, data);
13284
+ }
13285
+ catch (err) {
13286
+ const m = err?.message ?? err;
13287
+ dev$2('SYG241', declOf(iv), 'query' + query, iv.name, `agent.queries.${query}.run threw: ${m}`);
13288
+ return { ok: false, error: `${name} failed: ${m}` };
13289
+ }
13290
+ if (result && typeof result.then == 'function') {
13291
+ dev$2('SYG246', declOf(iv), 'promise' + query, iv.name, `agent.queries.${query}.run returned a promise: a query is a pure, synchronous function of the state`);
13292
+ return { ok: false, error: `${name} is not available: it returned a promise` };
13293
+ }
13294
+ return { ok: true, result };
13295
+ }
13190
13296
  return {
13297
+ /** answer every call waiting on `until` now, as `pending` (a chat turn stopped or replaced) */
13298
+ cancel() { for (const c of [...cancels])
13299
+ c(); },
13191
13300
  /** the offered tools; `{ all: true }` adds declared tools that can't be offered (`error`, SYG240) */
13192
13301
  list,
13193
13302
  /** run a tool as an agent: resolves with the result after the action's flush */
@@ -13199,7 +13308,8 @@ function agentTools(target, options = {}) {
13199
13308
  dirty();
13200
13309
  return () => { listeners.delete(fn); };
13201
13310
  },
13202
- stop() { stopped = true; off?.(); listeners.clear(); },
13311
+ stop() { stopped = true; off?.(); listeners.clear(); for (const c of [...cancels])
13312
+ c(); },
13203
13313
  /** internal (A-2, WebMCP; chat; the command bar): the offered tools grouped by declaration, with `untrusted` as declared and an item declaration's live labels (id → `agent.label`) */
13204
13314
  groups() {
13205
13315
  const rs = [...build().values()];
@@ -17722,15 +17832,15 @@ function experimentalExposeWebMcp(app, options = {}) {
17722
17832
  o[k] = params(s[k], tool, path);
17723
17833
  return o;
17724
17834
  };
17725
- /** a result within 1,500 characters of JSON: `state` and long fields are cut first */
17835
+ /** a result within 1,500 characters of JSON: `state`, a query's `result` and long fields are cut first */
17726
17836
  const fit = (r, tool) => {
17727
17837
  if (!r || typeof r != 'object')
17728
17838
  r = { ok: false, error: 'no result' };
17729
17839
  let s = JSON.stringify(r);
17730
17840
  if (s.length <= OUT)
17731
17841
  return r;
17732
- dev('SYG242', tool + 'out', `a result of ${tool} is ${s.length} characters of JSON, over the budget of ${OUT}; its state is cut (make the agent.read projection smaller)`);
17733
- const { state, issues, keys, ...rest } = r;
17842
+ dev('SYG242', tool + 'out', `a result of ${tool} is ${s.length} characters of JSON, over the budget of ${OUT}; its ${'result' in r ? 'result' : 'state'} is cut (make ${'result' in r ? "the query's result" : 'the agent.read projection'} smaller)`);
17843
+ const { state, result, issues, keys, ...rest } = r;
17734
17844
  const o = { ...rest, truncated: true };
17735
17845
  if (typeof o.error == 'string')
17736
17846
  o.error = cut(o.error, 400);
@@ -17739,10 +17849,13 @@ function experimentalExposeWebMcp(app, options = {}) {
17739
17849
  for (let room = OUT; room > 20; room = room / 2 | 0) {
17740
17850
  if (state !== undefined)
17741
17851
  o.state = cut(JSON.stringify(state), room);
17852
+ if (result !== undefined)
17853
+ o.result = cut(JSON.stringify(result), room);
17742
17854
  if (JSON.stringify(o).length <= OUT)
17743
17855
  return o;
17744
17856
  }
17745
17857
  delete o.state;
17858
+ delete o.result;
17746
17859
  delete o.issues;
17747
17860
  return o;
17748
17861
  };
@@ -18039,7 +18152,8 @@ function checkLinked(sources, name, without) {
18039
18152
  *
18040
18153
  * Actions ('assistant.X'): SEND (the form's submit: the prompt; or a string as data), STOP (aborts
18041
18154
  * the stream, keeps the partial text and reasoning; calls that already ran keep their results,
18042
- * G-626), REGENERATE (replays the last user turn), APPROVE, DENY,
18155
+ * G-626; a call waiting on an action's `until` answers as pending: the tool set's cancel(), as on
18156
+ * FAILED and a new turn), REGENERATE (replays the last user turn), APPROVE, DENY,
18043
18157
  * DONE (the turn is over: `{ message, text, finishReason, usage, steps }`; a host entry
18044
18158
  * 'assistant.DONE' runs after it, like 'form.DONE'). Internal: PROMPT (typing), DELTA / REPLY /
18045
18159
  * FAILED (the chat driver's reply actions), ASK (a consequential call waits), RESULTS (the tool
@@ -18099,8 +18213,14 @@ const askInfo = (p) => {
18099
18213
  const name = p.type.slice(5);
18100
18214
  return { tool: name, component: 'server', action: name, description: p.approval?.requestReason ?? `Run the ${name} tool`, input: p.input, approvalId: p.approval?.id };
18101
18215
  };
18102
- /** a tool part with an A-1 result (`{ output }` or `{ error }`) */
18103
- const withResult = (p, r) => 'error' in r ? { ...p, state: 'output-error', errorText: r.error } : { ...p, state: 'output-available', output: r.output };
18216
+ /**
18217
+ * a tool part with an A-1 result (`{ output }` or `{ error }`); the other side's field goes (a late
18218
+ * result replaces the "not run" error of a stopped turn, G-626)
18219
+ */
18220
+ const withResult = (p, r) => {
18221
+ const { output: _o, errorText: _e, ...rest } = p;
18222
+ return 'error' in r ? { ...rest, state: 'output-error', errorText: r.error } : { ...rest, state: 'output-available', output: r.output };
18223
+ };
18104
18224
  /**
18105
18225
  * The last message with its open tool parts closed (a turn stopped, failed or capped): a call
18106
18226
  * that already ran gets its result (`ran`, G-626), the others `why`; a server approval still
@@ -18215,10 +18335,12 @@ function appState(t, groups, labels) {
18215
18335
  function request(e, o, k, messages, fresh) {
18216
18336
  const t = e && toolsOf$1(e);
18217
18337
  if (e) {
18338
+ // a new turn: a call still waiting on `until` answers now, as pending
18218
18339
  if (fresh) {
18219
18340
  e.turn++;
18220
18341
  e.steps = 0;
18221
18342
  e.answer?.(false);
18343
+ e.tools?.cancel();
18222
18344
  }
18223
18345
  e.steps++;
18224
18346
  }
@@ -18354,6 +18476,7 @@ const chat = (options = {}) => {
18354
18476
  EFFECT: (s, _d, _n, p, o, k) => { const e = busy$1(s) && E(p, k, o); if (e) {
18355
18477
  e.turn++;
18356
18478
  e.answer?.(false);
18479
+ e.tools?.cancel();
18357
18480
  } },
18358
18481
  },
18359
18482
  FAILED: {
@@ -18361,6 +18484,7 @@ const chat = (options = {}) => {
18361
18484
  EFFECT: (_s, _d, _n, p, o, k) => { const e = E(p, k, o); if (e) {
18362
18485
  e.turn++;
18363
18486
  e.answer?.(false);
18487
+ e.tools?.cancel();
18364
18488
  } },
18365
18489
  },
18366
18490
  // (REPLY already set ready: a host entry gets the finished state)