gemi 0.59.0 → 0.61.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 (237) hide show
  1. package/dist/ai/Agent.d.ts +533 -25
  2. package/dist/ai/Agent.d.ts.map +1 -1
  3. package/dist/ai/Agent.test-d.d.ts +2 -0
  4. package/dist/ai/Agent.test-d.d.ts.map +1 -0
  5. package/dist/ai/AgentController.d.ts +275 -6
  6. package/dist/ai/AgentController.d.ts.map +1 -1
  7. package/dist/ai/AgentProvider.d.ts +255 -0
  8. package/dist/ai/AgentProvider.d.ts.map +1 -0
  9. package/dist/ai/Schema.d.ts +125 -0
  10. package/dist/ai/Schema.d.ts.map +1 -0
  11. package/dist/ai/Schema.test-d.d.ts +2 -0
  12. package/dist/ai/Schema.test-d.d.ts.map +1 -0
  13. package/dist/ai/client/index.d.ts +24 -0
  14. package/dist/ai/client/index.d.ts.map +1 -0
  15. package/dist/ai/client/index.js +1053 -0
  16. package/dist/ai/client/index.js.map +1 -0
  17. package/dist/ai/client/reducer.d.ts +127 -0
  18. package/dist/ai/client/reducer.d.ts.map +1 -0
  19. package/dist/ai/client/reducer.test-d.d.ts +2 -0
  20. package/dist/ai/client/reducer.test-d.d.ts.map +1 -0
  21. package/dist/ai/client/sse.d.ts +54 -0
  22. package/dist/ai/client/sse.d.ts.map +1 -0
  23. package/dist/ai/example.d.ts +129 -0
  24. package/dist/ai/example.d.ts.map +1 -0
  25. package/dist/ai/index.d.ts +35 -0
  26. package/dist/ai/index.d.ts.map +1 -0
  27. package/dist/ai/index.js +20 -0
  28. package/dist/ai/index.js.map +23 -0
  29. package/dist/ai/live/harness.d.ts +137 -0
  30. package/dist/ai/live/harness.d.ts.map +1 -0
  31. package/dist/ai/providers/call.d.ts +42 -0
  32. package/dist/ai/providers/call.d.ts.map +1 -0
  33. package/dist/ai/providers/capabilities.d.ts +50 -0
  34. package/dist/ai/providers/capabilities.d.ts.map +1 -0
  35. package/dist/ai/providers/errors.d.ts +37 -0
  36. package/dist/ai/providers/errors.d.ts.map +1 -0
  37. package/dist/ai/providers/fakeProvider.d.ts +35 -0
  38. package/dist/ai/providers/fakeProvider.d.ts.map +1 -0
  39. package/dist/ai/providers/http.d.ts +46 -0
  40. package/dist/ai/providers/http.d.ts.map +1 -0
  41. package/dist/ai/providers/request.d.ts +69 -0
  42. package/dist/ai/providers/request.d.ts.map +1 -0
  43. package/dist/ai/providers/stream.d.ts +42 -0
  44. package/dist/ai/providers/stream.d.ts.map +1 -0
  45. package/dist/ai/signing.d.ts +195 -0
  46. package/dist/ai/signing.d.ts.map +1 -0
  47. package/dist/ai/store/LiveRuns.d.ts +142 -0
  48. package/dist/ai/store/LiveRuns.d.ts.map +1 -0
  49. package/dist/ai/store/MemoryAgentStore.d.ts +61 -0
  50. package/dist/ai/store/MemoryAgentStore.d.ts.map +1 -0
  51. package/dist/ai/store/index.d.ts +4 -0
  52. package/dist/ai/store/index.d.ts.map +1 -0
  53. package/dist/ai/store/sse.d.ts +58 -0
  54. package/dist/ai/store/sse.d.ts.map +1 -0
  55. package/dist/ai/store/stubAgentRun.d.ts +56 -0
  56. package/dist/ai/store/stubAgentRun.d.ts.map +1 -0
  57. package/dist/ai/types.d.ts +443 -0
  58. package/dist/ai/types.d.ts.map +1 -0
  59. package/dist/ai/useChat.d.ts +253 -7
  60. package/dist/ai/useChat.d.ts.map +1 -1
  61. package/dist/bin/gemi.js +500 -16
  62. package/dist/bin/gemi.js.map +10 -5
  63. package/dist/chunk-1aqzcgfr.js +5 -0
  64. package/dist/chunk-1aqzcgfr.js.map +10 -0
  65. package/dist/{chunk-get4mkx8.js → chunk-1b7e9rj7.js} +2 -2
  66. package/dist/{chunk-get4mkx8.js.map → chunk-1b7e9rj7.js.map} +1 -1
  67. package/dist/{chunk-gfma8e03.js → chunk-3aemqfdr.js} +2 -2
  68. package/dist/{chunk-gfma8e03.js.map → chunk-3aemqfdr.js.map} +1 -1
  69. package/dist/{chunk-j06g4sqc.js → chunk-528n3vgy.js} +2 -2
  70. package/dist/{chunk-j06g4sqc.js.map → chunk-528n3vgy.js.map} +1 -1
  71. package/dist/{chunk-2khdxyjb.js → chunk-57a0nqfj.js} +1 -1
  72. package/dist/chunk-57a0nqfj.js.map +10 -0
  73. package/dist/{chunk-c40n5r4v.js → chunk-5mhcwnyd.js} +2 -2
  74. package/dist/{chunk-c40n5r4v.js.map → chunk-5mhcwnyd.js.map} +1 -1
  75. package/dist/{chunk-gwchvzdp.js → chunk-71pk1mxx.js} +2 -2
  76. package/dist/{chunk-gwchvzdp.js.map → chunk-71pk1mxx.js.map} +1 -1
  77. package/dist/{chunk-spbgpndn.js → chunk-7t1hjs9f.js} +2 -2
  78. package/dist/{chunk-spbgpndn.js.map → chunk-7t1hjs9f.js.map} +1 -1
  79. package/dist/{chunk-9gsdcjt7.js → chunk-7xvaace2.js} +3 -3
  80. package/dist/{chunk-9gsdcjt7.js.map → chunk-7xvaace2.js.map} +1 -1
  81. package/dist/{chunk-fxy42w6n.js → chunk-8ag0da2s.js} +2 -2
  82. package/dist/{chunk-fxy42w6n.js.map → chunk-8ag0da2s.js.map} +1 -1
  83. package/dist/chunk-8r8epsef.js +5 -0
  84. package/dist/chunk-8r8epsef.js.map +11 -0
  85. package/dist/chunk-91cj3nxk.js +6 -0
  86. package/dist/{chunk-txhcx69q.js.map → chunk-91cj3nxk.js.map} +2 -2
  87. package/dist/{chunk-0fm6jh9b.js → chunk-9nmvm20t.js} +2 -2
  88. package/dist/{chunk-0fm6jh9b.js.map → chunk-9nmvm20t.js.map} +1 -1
  89. package/dist/{chunk-98a3k7bp.js → chunk-9xpa7dpy.js} +2 -2
  90. package/dist/{chunk-98a3k7bp.js.map → chunk-9xpa7dpy.js.map} +1 -1
  91. package/dist/{chunk-qva4841r.js → chunk-a1exbqcq.js} +3 -3
  92. package/dist/{chunk-qva4841r.js.map → chunk-a1exbqcq.js.map} +1 -1
  93. package/dist/{chunk-cw9y6k15.js → chunk-bb19bwg6.js} +2 -2
  94. package/dist/{chunk-cw9y6k15.js.map → chunk-bb19bwg6.js.map} +1 -1
  95. package/dist/{chunk-3gvjn3q4.js → chunk-cf7bvd12.js} +1 -1
  96. package/dist/{chunk-rkbv3df7.js → chunk-djp2xeqe.js} +2 -2
  97. package/dist/{chunk-rkbv3df7.js.map → chunk-djp2xeqe.js.map} +1 -1
  98. package/dist/chunk-ds44bqr9.js +4 -0
  99. package/dist/{chunk-4mcyyh1v.js.map → chunk-ds44bqr9.js.map} +4 -9
  100. package/dist/{chunk-wzvs3sym.js → chunk-exndjhza.js} +3 -3
  101. package/dist/{chunk-wzvs3sym.js.map → chunk-exndjhza.js.map} +1 -1
  102. package/dist/{chunk-f6dd4gd8.js → chunk-f233yzxf.js} +2 -2
  103. package/dist/{chunk-f6dd4gd8.js.map → chunk-f233yzxf.js.map} +1 -1
  104. package/dist/chunk-fz5g2z6h.js +4 -0
  105. package/dist/{chunk-fbvvqf9b.js.map → chunk-fz5g2z6h.js.map} +2 -2
  106. package/dist/{chunk-vj9538yn.js → chunk-gcszdwcb.js} +2 -2
  107. package/dist/{chunk-vj9538yn.js.map → chunk-gcszdwcb.js.map} +1 -1
  108. package/dist/{chunk-pkjq9833.js → chunk-htesx7ym.js} +4 -4
  109. package/dist/{chunk-pkjq9833.js.map → chunk-htesx7ym.js.map} +1 -1
  110. package/dist/chunk-hyxmmj9b.js +5 -0
  111. package/dist/{chunk-n412aa9s.js.map → chunk-hyxmmj9b.js.map} +2 -2
  112. package/dist/{chunk-stq96kya.js → chunk-k2sjvt0c.js} +2 -2
  113. package/dist/{chunk-stq96kya.js.map → chunk-k2sjvt0c.js.map} +1 -1
  114. package/dist/{chunk-zhbrkpb3.js → chunk-k75phgj4.js} +4 -4
  115. package/dist/{chunk-zhbrkpb3.js.map → chunk-k75phgj4.js.map} +1 -1
  116. package/dist/{chunk-y64j80v9.js → chunk-m0tp7zjp.js} +2 -2
  117. package/dist/{chunk-y64j80v9.js.map → chunk-m0tp7zjp.js.map} +1 -1
  118. package/dist/{chunk-06j6rsew.js → chunk-m45j7p1y.js} +2 -2
  119. package/dist/{chunk-06j6rsew.js.map → chunk-m45j7p1y.js.map} +1 -1
  120. package/dist/chunk-mca9wsvs.js +5 -0
  121. package/dist/{chunk-z2tcxwyr.js.map → chunk-mca9wsvs.js.map} +3 -4
  122. package/dist/{chunk-tey1xayb.js → chunk-ms13evzp.js} +2 -2
  123. package/dist/{chunk-tey1xayb.js.map → chunk-ms13evzp.js.map} +1 -1
  124. package/dist/{chunk-bn1v4sfs.js → chunk-r962ae93.js} +2 -2
  125. package/dist/{chunk-bn1v4sfs.js.map → chunk-r962ae93.js.map} +1 -1
  126. package/dist/{chunk-23h0dmx2.js → chunk-s41ees18.js} +2 -2
  127. package/dist/{chunk-23h0dmx2.js.map → chunk-s41ees18.js.map} +1 -1
  128. package/dist/chunk-snb68dgr.js +4 -0
  129. package/dist/{chunk-hwhw98hc.js.map → chunk-snb68dgr.js.map} +1 -1
  130. package/dist/chunk-sz051605.js +5 -0
  131. package/dist/chunk-sz051605.js.map +14 -0
  132. package/dist/{chunk-2cwcfwg3.js → chunk-tr3cbx8k.js} +2 -2
  133. package/dist/{chunk-2cwcfwg3.js.map → chunk-tr3cbx8k.js.map} +2 -2
  134. package/dist/{chunk-dgasxgsm.js → chunk-vqcswg7h.js} +2 -2
  135. package/dist/{chunk-dgasxgsm.js.map → chunk-vqcswg7h.js.map} +2 -2
  136. package/dist/{chunk-zqsfanvk.js → chunk-wpb1xpdp.js} +2 -2
  137. package/dist/{chunk-zqsfanvk.js.map → chunk-wpb1xpdp.js.map} +1 -1
  138. package/dist/{chunk-z1e55w67.js → chunk-ybqss0jy.js} +2 -2
  139. package/dist/{chunk-z1e55w67.js.map → chunk-ybqss0jy.js.map} +1 -1
  140. package/dist/{chunk-cejf873g.js → chunk-yk5wqmyh.js} +2 -2
  141. package/dist/{chunk-cejf873g.js.map → chunk-yk5wqmyh.js.map} +1 -1
  142. package/dist/{chunk-8kj3zrm9.js → chunk-ywntv8yw.js} +4 -4
  143. package/dist/{chunk-8kj3zrm9.js.map → chunk-ywntv8yw.js.map} +1 -1
  144. package/dist/chunks/{ThemeProvider-ByU4BQdL.js → ThemeProvider-BZ2SsSZ3.js} +60 -39
  145. package/dist/chunks/ThemeProvider-BZ2SsSZ3.js.map +1 -0
  146. package/dist/chunks/useParams-BN3XXfmG.js +20 -0
  147. package/dist/chunks/useParams-BN3XXfmG.js.map +1 -0
  148. package/dist/client/index.js +3 -3
  149. package/dist/client/index.js.map +1 -1
  150. package/dist/client/useDictionary.d.ts.map +1 -1
  151. package/dist/config/index.d.ts +2 -0
  152. package/dist/config/index.d.ts.map +1 -1
  153. package/dist/config/index.js +2 -2
  154. package/dist/config/index.js.map +3 -3
  155. package/dist/database/index.js +1 -1
  156. package/dist/facades/index.js +2 -2
  157. package/dist/facades/index.js.map +1 -1
  158. package/dist/http/ApiRouter.d.ts +30 -2
  159. package/dist/http/ApiRouter.d.ts.map +1 -1
  160. package/dist/http/index.js +2 -2
  161. package/dist/http/index.js.map +1 -1
  162. package/dist/i18n/defineDictionary.d.ts +7 -4
  163. package/dist/i18n/defineDictionary.d.ts.map +1 -1
  164. package/dist/i18n/dictionaryRegistry.d.ts +38 -14
  165. package/dist/i18n/dictionaryRegistry.d.ts.map +1 -1
  166. package/dist/i18n/dictionaryRuntime.js +1 -1
  167. package/dist/i18n/index.js +2 -2
  168. package/dist/i18n/index.js.map +2 -2
  169. package/dist/kernel/index.js +2 -2
  170. package/dist/kernel/index.js.map +2 -2
  171. package/dist/orm/index.js +2 -2
  172. package/dist/orm/index.js.map +2 -2
  173. package/dist/server/index.js +1 -1
  174. package/dist/services/index.js +2 -2
  175. package/dist/services/index.js.map +2 -2
  176. package/dist/testing/index.js +2 -1
  177. package/dist/testing/index.js.map +1 -1
  178. package/package.json +5 -2
  179. package/skills/gemi-react-best-practices/SKILL.md +231 -0
  180. package/skills/gemi-react-best-practices/rules/_sections.md +56 -0
  181. package/skills/gemi-react-best-practices/rules/_template.md +28 -0
  182. package/skills/gemi-react-best-practices/rules/bundle-deep-imports.md +48 -0
  183. package/skills/gemi-react-best-practices/rules/bundle-mount-gate-heavy-panels.md +63 -0
  184. package/skills/gemi-react-best-practices/rules/client-form-vs-mutation-hooks.md +57 -0
  185. package/skills/gemi-react-best-practices/rules/client-loading-error-exports.md +54 -0
  186. package/skills/gemi-react-best-practices/rules/client-no-effect-data-flow.md +68 -0
  187. package/skills/gemi-react-best-practices/rules/client-typed-links.md +51 -0
  188. package/skills/gemi-react-best-practices/rules/controller-authorize-every-tenant-read.md +65 -0
  189. package/skills/gemi-react-best-practices/rules/controller-parse-request-at-the-boundary.md +54 -0
  190. package/skills/gemi-react-best-practices/rules/controller-redirect-facade-throws.md +68 -0
  191. package/skills/gemi-react-best-practices/rules/controller-request-schema.md +61 -0
  192. package/skills/gemi-react-best-practices/rules/controller-throw-framework-errors.md +57 -0
  193. package/skills/gemi-react-best-practices/rules/i18n-define-dictionary-inline.md +58 -0
  194. package/skills/gemi-react-best-practices/rules/orm-analytics-connection.md +53 -0
  195. package/skills/gemi-react-best-practices/rules/orm-include-not-n-plus-one.md +56 -0
  196. package/skills/gemi-react-best-practices/rules/orm-paginate-helper.md +69 -0
  197. package/skills/gemi-react-best-practices/rules/orm-plain-rows-by-default.md +55 -0
  198. package/skills/gemi-react-best-practices/rules/orm-select-narrow.md +58 -0
  199. package/skills/gemi-react-best-practices/rules/orm-transaction-no-io.md +54 -0
  200. package/skills/gemi-react-best-practices/rules/orm-transaction-sequential.md +64 -0
  201. package/skills/gemi-react-best-practices/rules/payload-dont-overprefetch.md +54 -0
  202. package/skills/gemi-react-best-practices/rules/payload-instant-vs-prefetch.md +58 -0
  203. package/skills/gemi-react-best-practices/rules/payload-minimal-view-props.md +51 -0
  204. package/skills/gemi-react-best-practices/rules/payload-parallel-controller-work.md +56 -0
  205. package/skills/gemi-react-best-practices/rules/payload-prefetch-late-queries.md +58 -0
  206. package/skills/gemi-react-best-practices/rules/payload-prefetch-mirrors-usequery.md +52 -0
  207. package/skills/gemi-react-best-practices/rules/query-debounce-search-variant.md +52 -0
  208. package/skills/gemi-react-best-practices/rules/query-keep-previous-data.md +40 -0
  209. package/skills/gemi-react-best-practices/rules/query-lazy-vs-mount-gate.md +54 -0
  210. package/skills/gemi-react-best-practices/rules/query-mutate-over-refetch.md +55 -0
  211. package/skills/gemi-react-best-practices/rules/query-no-hand-rolled-fetch.md +60 -0
  212. package/skills/gemi-react-best-practices/rules/query-revalidate-on-focus.md +44 -0
  213. package/skills/gemi-react-best-practices/rules/query-share-cache-key.md +51 -0
  214. package/skills/gemi-react-best-practices/rules/query-suspense-default.md +52 -0
  215. package/skills/gemi-react-best-practices/rules/routing-cache-policy-constants.md +53 -0
  216. package/skills/gemi-react-best-practices/rules/routing-middleware-dsl.md +60 -0
  217. package/skills/gemi-react-best-practices/rules/routing-resource-routes.md +59 -0
  218. package/skills/gemi-react-best-practices/rules/routing-routers-are-classes.md +55 -0
  219. package/skills/gemi-react-best-practices/rules/service-lazy-not-module-scope.md +63 -0
  220. package/skills/gemi-react-best-practices/rules/service-queue-is-in-memory.md +52 -0
  221. package/skills/gemi-react-best-practices/rules/service-static-token-and-name.md +52 -0
  222. package/skills/gemi-react-best-practices/rules/structure-discovered-vs-registered.md +71 -0
  223. package/skills/gemi-react-best-practices/rules/structure-do-not-reinvent-the-framework.md +58 -0
  224. package/skills/gemi-react-best-practices/rules/testing-assert-behaviour-over-markup.md +54 -0
  225. package/skills/gemi-react-best-practices/rules/testing-match-the-suite.md +57 -0
  226. package/skills/gemi-react-best-practices/rules/testing-page-seeds-real-inputs.md +65 -0
  227. package/dist/chunk-2khdxyjb.js.map +0 -10
  228. package/dist/chunk-4mcyyh1v.js +0 -4
  229. package/dist/chunk-fbvvqf9b.js +0 -4
  230. package/dist/chunk-hwhw98hc.js +0 -4
  231. package/dist/chunk-n412aa9s.js +0 -5
  232. package/dist/chunk-q0y0j3ne.js +0 -5
  233. package/dist/chunk-q0y0j3ne.js.map +0 -11
  234. package/dist/chunk-txhcx69q.js +0 -6
  235. package/dist/chunk-z2tcxwyr.js +0 -5
  236. package/dist/chunks/ThemeProvider-ByU4BQdL.js.map +0 -1
  237. /package/dist/{chunk-3gvjn3q4.js.map → chunk-cf7bvd12.js.map} +0 -0
@@ -0,0 +1,1053 @@
1
+ import { t as useParams } from "../../chunks/useParams-BN3XXfmG.js";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ //#region ai/client/reducer.ts
4
+ function initialChatState(init = {}) {
5
+ return {
6
+ messages: init.messages ? [...init.messages] : [],
7
+ pending: init.pending ? [...init.pending] : [],
8
+ error: null,
9
+ threadId: init.threadId,
10
+ seq: init.seq ?? -1,
11
+ cursorRunId: init.cursorRunId,
12
+ runMessageIds: [],
13
+ loadedTools: []
14
+ };
15
+ }
16
+ /**
17
+ * Apply one frame.
18
+ *
19
+ * `now` is an argument rather than a `Date.now()` call so the reducer stays a
20
+ * pure function of its inputs — it is only ever read when a frame arrives for a
21
+ * message the list has not seen, which is precisely the mid-run-attach path a
22
+ * test needs to pin down.
23
+ */
24
+ function applyFrame(state, frame, now = (/* @__PURE__ */ new Date()).toISOString()) {
25
+ if (!(frame.event.type === "run-start" && frame.event.runId !== state.cursorRunId) && frame.seq <= state.seq) return state;
26
+ return {
27
+ ...reduce(state, frame.event, now),
28
+ seq: frame.seq
29
+ };
30
+ }
31
+ function reduce(state, event, now) {
32
+ switch (event.type) {
33
+ case "run-start": return {
34
+ ...state,
35
+ runId: event.runId,
36
+ cursorRunId: event.runId,
37
+ threadId: event.threadId ?? state.threadId,
38
+ runMessageIds: [],
39
+ loadedTools: [],
40
+ finishReason: void 0
41
+ };
42
+ case "message-start": return withMessage(state, event.messageId, now, (message) => ({
43
+ ...message,
44
+ role: event.role
45
+ }));
46
+ case "text-delta":
47
+ if (isFinished(state, event.messageId)) return state;
48
+ return withMessage(state, event.messageId, now, (message) => ({
49
+ ...message,
50
+ content: appendText(message.content, event.delta)
51
+ }));
52
+ case "reasoning-delta":
53
+ if (isFinished(state, event.messageId)) return state;
54
+ return withMessage(state, event.messageId, now, (message) => ({
55
+ ...message,
56
+ content: appendReasoning(message.content, event.delta, event.id)
57
+ }));
58
+ case "output-delta": return withMessage(state, event.messageId, now, (message) => ({
59
+ ...message,
60
+ content: upsert(message.content, {
61
+ type: "output",
62
+ value: event.snapshot,
63
+ partial: true
64
+ }, (part) => part.type === "output")
65
+ }));
66
+ case "tool-call": return withMessage(state, event.messageId, now, (message) => {
67
+ const index = message.content.findIndex((part) => part.type === "tool-call" && part.toolCallId === event.part.toolCallId);
68
+ const existing = index === -1 ? void 0 : message.content[index];
69
+ const progress = event.part.progress ?? existing?.progress;
70
+ const nested = event.part.nested ?? existing?.nested;
71
+ const part = {
72
+ ...event.part,
73
+ ...progress ? { progress } : {},
74
+ ...nested ? { nested } : {}
75
+ };
76
+ const content = message.content.slice();
77
+ if (index === -1) content.push(part);
78
+ else content[index] = part;
79
+ return {
80
+ ...message,
81
+ content
82
+ };
83
+ });
84
+ case "tool-search": {
85
+ const loaded = event.loaded.filter((name) => !state.loadedTools.includes(name));
86
+ if (loaded.length === 0) return state;
87
+ return {
88
+ ...state,
89
+ loadedTools: [...state.loadedTools, ...loaded]
90
+ };
91
+ }
92
+ case "tool-progress": return withToolCall(state, event.toolCallId, (part) => ({
93
+ ...part,
94
+ progress: [...part.progress ?? [], event.data]
95
+ }));
96
+ case "nested-event": {
97
+ const nestedEvent = event;
98
+ return withToolCall(state, event.toolCallId, (part) => {
99
+ const runs = part.nested ?? [];
100
+ const index = runs.findIndex((run) => run.runId === nestedEvent.runId);
101
+ const next = applyNested(runs[index] ?? {
102
+ runId: nestedEvent.runId,
103
+ agent: nestedEvent.agent,
104
+ messages: []
105
+ }, nestedEvent, now);
106
+ const nested = index === -1 ? [...runs, next] : runs.map((r, i) => i === index ? next : r);
107
+ return {
108
+ ...part,
109
+ nested
110
+ };
111
+ });
112
+ }
113
+ case "tool-result": {
114
+ const next = withMessage(state, event.messageId, now, (message) => ({
115
+ ...message,
116
+ content: upsert(message.content, event.part, (part) => part.type === "tool-result" && part.toolCallId === event.part.toolCallId)
117
+ }));
118
+ return {
119
+ ...next,
120
+ pending: next.pending.filter((call) => call.toolCallId !== event.part.toolCallId)
121
+ };
122
+ }
123
+ case "awaiting-input": return {
124
+ ...state,
125
+ runId: event.runId,
126
+ pending: [...event.pending]
127
+ };
128
+ case "message-end": return withMessage(state, event.messageId, now, (message) => ({
129
+ ...message,
130
+ finishReason: event.finishReason,
131
+ content: message.content.map((part) => part.type === "output" && part.partial ? {
132
+ ...part,
133
+ partial: false
134
+ } : part)
135
+ }));
136
+ case "usage": {
137
+ const index = lastIndexWhere(state.messages, (message) => message.role === "assistant");
138
+ if (index === -1) return state;
139
+ const messages = state.messages.slice();
140
+ messages[index] = {
141
+ ...messages[index],
142
+ usage: event.usage
143
+ };
144
+ return {
145
+ ...state,
146
+ messages
147
+ };
148
+ }
149
+ case "error": return {
150
+ ...state,
151
+ error: event.error,
152
+ pending: []
153
+ };
154
+ case "run-end": return {
155
+ ...state,
156
+ runId: void 0,
157
+ finishReason: event.finishReason,
158
+ messages: finishUnended(state, event.finishReason)
159
+ };
160
+ default: return state;
161
+ }
162
+ }
163
+ /**
164
+ * The interrupted turn, kept.
165
+ *
166
+ * `stop()` calls this before the server has said anything, because the point of
167
+ * stopping is that the UI reacts now. Whatever the assistant had produced stays
168
+ * in the list marked `aborted` — dropping it would lose text the user already
169
+ * read, and would leave the transcript unable to say it was cut short.
170
+ *
171
+ * `pending` is untouched: a question the conversation is holding survives a
172
+ * stopped run, and clearing it would silently discard the answer the user still
173
+ * owes.
174
+ */
175
+ function markAborted(state) {
176
+ return {
177
+ ...state,
178
+ runId: void 0,
179
+ finishReason: "aborted",
180
+ messages: finishUnended(state, "aborted")
181
+ };
182
+ }
183
+ /**
184
+ * The messages this run wrote, finished off with `reason` if they never ended —
185
+ * and, inside them, the sub-runs this run left mid-sentence.
186
+ *
187
+ * The nesting descent is not an extra: a `NestedRun` renders with the component
188
+ * that renders `messages`, so a nested message with no `finishReason` draws a
189
+ * live cursor inside a block that will never receive another frame. The parent
190
+ * ending is the last word anything will say about it, and for `markAborted`
191
+ * there is no other word available at all — `stop()` marks the transcript
192
+ * optimistically, before the server has answered, so no forwarded nested
193
+ * `run-end` can arrive in time.
194
+ *
195
+ * It recurses through `closeRun`, which builds a `ChatState` around the sub-run
196
+ * and calls back here, so a sub-sub-run is closed by the same three lines that
197
+ * close the top level — the same trick `applyNested` plays on the way in.
198
+ */
199
+ function finishUnended(state, reason) {
200
+ return state.messages.map((message) => message.role === "assistant" && state.runMessageIds.includes(message.id) ? closeMessage(message, reason) : message);
201
+ }
202
+ /**
203
+ * One message of this run, and the sub-runs hanging off its tool calls.
204
+ *
205
+ * The message's own finish reason is only filled in when it is missing, but the
206
+ * descent happens either way: the message a run parks on is *finished* while the
207
+ * sub-run underneath it is still live, and that is the case where the block
208
+ * stuck streaming is easiest to hit.
209
+ */
210
+ function closeMessage(message, reason) {
211
+ const content = message.content.map((part) => part.type === "tool-call" && part.nested?.some((run) => run.finishReason === void 0) ? {
212
+ ...part,
213
+ nested: part.nested.map((run) => run.finishReason === void 0 ? closeRun(run, reason) : run)
214
+ } : part);
215
+ if (message.finishReason === void 0) return {
216
+ ...message,
217
+ finishReason: reason,
218
+ content
219
+ };
220
+ return content.some((part, index) => part !== message.content[index]) ? {
221
+ ...message,
222
+ content
223
+ } : message;
224
+ }
225
+ /** A sub-run the parent outlived, stamped with the parent's reason, recursively. */
226
+ function closeRun(run, reason) {
227
+ const sub = {
228
+ messages: run.messages,
229
+ pending: [],
230
+ error: null,
231
+ seq: -1,
232
+ runMessageIds: run.messages.map((message) => message.id),
233
+ loadedTools: []
234
+ };
235
+ return {
236
+ ...run,
237
+ finishReason: reason,
238
+ messages: finishUnended(sub, reason)
239
+ };
240
+ }
241
+ /**
242
+ * Run one update over the tool-call part with this id, wherever it lives.
243
+ *
244
+ * `tool-progress` and `nested-event` are the only events that name a tool call
245
+ * without naming a message, so the message has to be found rather than told —
246
+ * a scan from the newest end, because the call being worked on is the one that
247
+ * just arrived.
248
+ *
249
+ * **A frame for a tool call this transcript does not have is dropped.** That is
250
+ * the mid-run `/attach` case the whole file is written for, and it is the one
251
+ * place the usual answer — create it on the spot — is the wrong one: a
252
+ * `ToolCallPart` needs a tool `name` and an `input`, and neither is on these
253
+ * events. Inventing them would put a tool call the model never made into the
254
+ * transcript, under a name that is not a key of the agent's tools, which breaks
255
+ * the narrowing that is the reason the shapes are carried into the browser at
256
+ * all. So the progress is lost and the transcript stays true, rather than the
257
+ * other way round.
258
+ *
259
+ * In practice a client that has the cursor has the part: `useChat` documents
260
+ * `cursor` and `initialMessages` as things to persist together, and a client
261
+ * that persisted one without the other is already the case that doubles text.
262
+ * Parking the orphans until their tool call shows up was considered and is not
263
+ * worth it — the call is *earlier* in the stream, never later, so nothing would
264
+ * ever drain the parking lot.
265
+ */
266
+ function withToolCall(state, toolCallId, update) {
267
+ for (let i = state.messages.length - 1; i >= 0; i--) {
268
+ const message = state.messages[i];
269
+ const index = message.content.findIndex((part) => part.type === "tool-call" && part.toolCallId === toolCallId);
270
+ if (index === -1) continue;
271
+ if (message.finishReason !== void 0 && !isReenterable(state, message, toolCallId)) return state;
272
+ const content = message.content.slice();
273
+ content[index] = update(content[index]);
274
+ const messages = state.messages.slice();
275
+ messages[i] = {
276
+ ...message,
277
+ content
278
+ };
279
+ const runMessageIds = state.runMessageIds.includes(message.id) ? state.runMessageIds : [...state.runMessageIds, message.id];
280
+ return {
281
+ ...state,
282
+ runMessageIds,
283
+ messages
284
+ };
285
+ }
286
+ return state;
287
+ }
288
+ /**
289
+ * One sub-run event, applied to the sub-run's own transcript.
290
+ *
291
+ * The sub-state is built and thrown away per event rather than kept, because
292
+ * there is nowhere to keep it: `NestedRun` is a wire type and holds a
293
+ * transcript, not a reducer state. Everything the inner `reduce` needs is
294
+ * derivable from it.
295
+ *
296
+ * - `seq` is `-1` and this calls `reduce`, not `applyFrame`. A nested event
297
+ * has no sequence of its own; it is numbered in the *parent's* stream, and
298
+ * the parent's cursor has already refused the duplicates.
299
+ * - `runMessageIds` is every message in the transcript, which for a nested
300
+ * run is exactly right and needs none of the parent's care. The parent
301
+ * scopes it because its list mixes runs; a `NestedRun` is keyed by `runId`
302
+ * and only ever written by events carrying that id, so every message in it
303
+ * belongs to the run that is ending.
304
+ * - `pending` is dropped. An escalated question reaches the user on the
305
+ * *parent's* `awaiting-input`, carrying the path that says which tool to
306
+ * re-enter; the copy inside the sub-run's own stream carries no path, so
307
+ * surfacing it would show the question twice and make one of them
308
+ * unanswerable.
309
+ */
310
+ function applyNested(run, event, now) {
311
+ const next = reduce({
312
+ messages: run.messages,
313
+ pending: [],
314
+ error: null,
315
+ seq: -1,
316
+ runMessageIds: run.messages.map((message) => message.id),
317
+ loadedTools: [],
318
+ finishReason: run.finishReason
319
+ }, event.event, now);
320
+ return {
321
+ ...run,
322
+ ...event.label !== void 0 ? { label: event.label } : {},
323
+ messages: next.messages,
324
+ ...next.finishReason !== void 0 ? { finishReason: next.finishReason } : {},
325
+ ...event.event.type === "usage" ? { usage: event.event.usage } : {}
326
+ };
327
+ }
328
+ /**
329
+ * Whether a finished message's tool call can still be written to.
330
+ *
331
+ * Exactly one message can say yes: the one a run parked on, holding a call
332
+ * nobody has answered yet. That is the resume turn — the tool is re-entered
333
+ * from the top and streams progress and sub-run events against a message that
334
+ * closed on the previous turn.
335
+ *
336
+ * "Open" is the server's own test, `Agent.openCalls`: a tool call with no
337
+ * `tool-result` for it anywhere in the transcript. The result is the thing that
338
+ * says the re-entry finished, and it always arrives *after* the frames this
339
+ * guards — `attachToHistory` emits it once `executeTool` returns.
340
+ */
341
+ function isReenterable(state, message, toolCallId) {
342
+ if (message.finishReason !== "awaiting-input") return false;
343
+ return !state.messages.some((candidate) => candidate.content.some((part) => part.type === "tool-result" && part.toolCallId === toolCallId));
344
+ }
345
+ function isFinished(state, messageId) {
346
+ const message = state.messages.find((candidate) => candidate.id === messageId);
347
+ return message !== void 0 && message.finishReason !== void 0;
348
+ }
349
+ function lastIndexWhere(items, match) {
350
+ for (let i = items.length - 1; i >= 0; i--) if (match(items[i])) return i;
351
+ return -1;
352
+ }
353
+ function withMessage(state, messageId, now, update) {
354
+ const index = state.messages.findIndex((message) => message.id === messageId);
355
+ const runMessageIds = state.runMessageIds.includes(messageId) ? state.runMessageIds : [...state.runMessageIds, messageId];
356
+ if (index === -1) {
357
+ const blank = {
358
+ id: messageId,
359
+ role: "assistant",
360
+ content: [],
361
+ createdAt: now
362
+ };
363
+ return {
364
+ ...state,
365
+ runMessageIds,
366
+ messages: [...state.messages, update(blank)]
367
+ };
368
+ }
369
+ const messages = state.messages.slice();
370
+ messages[index] = update(messages[index]);
371
+ return {
372
+ ...state,
373
+ runMessageIds,
374
+ messages
375
+ };
376
+ }
377
+ function appendText(content, delta) {
378
+ const last = content[content.length - 1];
379
+ if (last && last.type === "text") return [...content.slice(0, -1), {
380
+ type: "text",
381
+ text: last.text + delta
382
+ }];
383
+ return [...content, {
384
+ type: "text",
385
+ text: delta
386
+ }];
387
+ }
388
+ /**
389
+ * Mirrors `appendReasoning` in Agent.ts, including its merge rule: deltas join
390
+ * the last part only when the reasoning-item id matches, so a step that
391
+ * produced two items stays two parts instead of flattening into one under the
392
+ * wrong id. The two implementations have to agree, because a stateless client
393
+ * posts this content straight back and the provider matches on that id.
394
+ */
395
+ function appendReasoning(content, delta, id) {
396
+ const last = content[content.length - 1];
397
+ if (last && last.type === "reasoning" && last.id === id) return [...content.slice(0, -1), {
398
+ ...last,
399
+ text: (last.text ?? "") + delta
400
+ }];
401
+ return [...content, id ? {
402
+ type: "reasoning",
403
+ id,
404
+ text: delta
405
+ } : {
406
+ type: "reasoning",
407
+ text: delta
408
+ }];
409
+ }
410
+ function upsert(content, part, match) {
411
+ const index = content.findIndex(match);
412
+ if (index === -1) return [...content, part];
413
+ const next = content.slice();
414
+ next[index] = part;
415
+ return next;
416
+ }
417
+ //#endregion
418
+ //#region ai/client/sse.ts
419
+ /**
420
+ * Bytes to frames.
421
+ *
422
+ * Split out of the hook because none of it is React: it is a byte-level parser,
423
+ * and the interesting cases — a chunk boundary landing inside a `data:` line, a
424
+ * multi-byte character split across two reads, a stream that dies mid-event —
425
+ * are all reachable from a test with a string and no DOM.
426
+ *
427
+ * Deliberately not `EventSource`. The agent routes are POSTs carrying a turn,
428
+ * `EventSource` can only GET, and reattachment wants the cursor in the request
429
+ * body next to the thread id rather than in a header the browser owns.
430
+ */
431
+ /**
432
+ * Feed it chunks, get frames. Holds whatever is left over between calls, which
433
+ * is the whole point: `push()` is called once per `reader.read()` and those
434
+ * boundaries have nothing to do with event boundaries.
435
+ */
436
+ var SSEFrameDecoder = class {
437
+ buffer = "";
438
+ textDecoder = new TextDecoder();
439
+ /**
440
+ * A `\r` at the very end of a chunk is ambiguous — the next chunk may open
441
+ * with the `\n` that completes a CRLF, or with the next line. Holding it back
442
+ * for one chunk is what keeps a boundary there from being read as a blank
443
+ * line and cutting an event in half.
444
+ */
445
+ pendingCR = false;
446
+ lastSeq = -1;
447
+ /** The cursor to resume from: the highest `id:` seen. */
448
+ get cursor() {
449
+ return this.lastSeq;
450
+ }
451
+ push(chunk) {
452
+ this.append(typeof chunk === "string" ? chunk : this.textDecoder.decode(chunk, { stream: true }));
453
+ return this.drain();
454
+ }
455
+ /**
456
+ * End of stream. Anything still buffered is an event with no terminating
457
+ * blank line, which the SSE spec discards and so does this — a truncated tail
458
+ * is usually truncated JSON, and half a `text-delta` appended to a message is
459
+ * worse than a missing one.
460
+ */
461
+ flush() {
462
+ this.append(this.textDecoder.decode());
463
+ const frames = this.drain();
464
+ this.buffer = "";
465
+ this.pendingCR = false;
466
+ return frames;
467
+ }
468
+ /** Normalises every line ending to `\n` so the rest of the parser only has to
469
+ * know about one. */
470
+ append(text) {
471
+ if (text === "") return;
472
+ let rest = text;
473
+ if (this.pendingCR) {
474
+ this.buffer += "\n";
475
+ this.pendingCR = false;
476
+ if (rest.startsWith("\n")) rest = rest.slice(1);
477
+ }
478
+ if (rest.endsWith("\r")) {
479
+ rest = rest.slice(0, -1);
480
+ this.pendingCR = true;
481
+ }
482
+ this.buffer += rest.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
483
+ }
484
+ drain() {
485
+ const frames = [];
486
+ let boundary = this.buffer.indexOf("\n\n");
487
+ while (boundary !== -1) {
488
+ const block = this.buffer.slice(0, boundary);
489
+ this.buffer = this.buffer.slice(boundary + 2);
490
+ const frame = this.parse(block);
491
+ if (frame) frames.push(frame);
492
+ boundary = this.buffer.indexOf("\n\n");
493
+ }
494
+ return frames;
495
+ }
496
+ parse(block) {
497
+ let id;
498
+ const data = [];
499
+ for (const line of block.split("\n")) {
500
+ if (line === "" || line.startsWith(":")) continue;
501
+ const colon = line.indexOf(":");
502
+ const field = colon === -1 ? line : line.slice(0, colon);
503
+ let value = colon === -1 ? "" : line.slice(colon + 1);
504
+ if (value.startsWith(" ")) value = value.slice(1);
505
+ if (field === "data") data.push(value);
506
+ else if (field === "id") id = value;
507
+ }
508
+ if (data.length === 0) return null;
509
+ let event;
510
+ try {
511
+ event = JSON.parse(data.join("\n"));
512
+ } catch {
513
+ return null;
514
+ }
515
+ const parsed = id === void 0 || id === "" ? NaN : Number(id);
516
+ const seq = Number.isFinite(parsed) ? parsed : this.lastSeq + 1;
517
+ this.lastSeq = seq;
518
+ return {
519
+ seq,
520
+ event
521
+ };
522
+ }
523
+ };
524
+ /**
525
+ * The same decoder over a `Response.body`.
526
+ *
527
+ * Cancels the reader when the caller stops consuming — a `break` out of the
528
+ * `for await` is how the hook drops a stream on unmount, and without the cancel
529
+ * the connection stays open behind it.
530
+ */
531
+ async function* decodeSSE(stream) {
532
+ if (!stream) return;
533
+ const reader = stream.getReader();
534
+ const decoder = new SSEFrameDecoder();
535
+ try {
536
+ while (true) {
537
+ const { done, value } = await reader.read();
538
+ if (done) {
539
+ yield* decoder.flush();
540
+ return;
541
+ }
542
+ if (value) yield* decoder.push(value);
543
+ }
544
+ } finally {
545
+ try {
546
+ await reader.cancel();
547
+ } catch {}
548
+ }
549
+ }
550
+ //#endregion
551
+ //#region ai/useChat.tsx
552
+ var localIdCounter = 0;
553
+ /** Ids the client mints: messages it authored, and the correlation id it names a
554
+ * run by before the server has. Server-assigned ids are what keep a reattached
555
+ * stream from duplicating a message, so the two must not collide — hence the
556
+ * prefix, which also tells a server log which end invented an id. */
557
+ function localId() {
558
+ return `local_${typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${localIdCounter++}`}`;
559
+ }
560
+ /** The same substitution `useMutation` does, so a parameterised agent path
561
+ * behaves like every other route in the package. */
562
+ function applyRouteParams(url, params) {
563
+ let out = url;
564
+ for (const [key, value] of Object.entries(params)) out = out.replace(`:${key}?`, value).replace(`:${key}`, value);
565
+ return out;
566
+ }
567
+ function isAbort(error) {
568
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
569
+ }
570
+ async function httpError(response) {
571
+ let message = response.statusText || `Request failed with status ${response.status}`;
572
+ let code = response.status === 429 ? "rate_limited" : "unknown";
573
+ try {
574
+ const data = await response.json();
575
+ message = data?.error?.message ?? data?.message ?? message;
576
+ if (data?.error?.code === "thread_not_found") code = "thread_not_found";
577
+ } catch {}
578
+ return {
579
+ code,
580
+ message,
581
+ retryable: response.status === 429 || response.status >= 500
582
+ };
583
+ }
584
+ function turnFrom(message) {
585
+ let text = "";
586
+ const files = [];
587
+ for (const part of message.content) {
588
+ if (part.type === "text") text += part.text;
589
+ if (part.type === "file") files.push({
590
+ fileId: part.fileId,
591
+ name: part.name,
592
+ mimeType: part.mimeType
593
+ });
594
+ }
595
+ return {
596
+ ...text ? { text } : {},
597
+ ...files.length > 0 ? { files } : {}
598
+ };
599
+ }
600
+ /**
601
+ * The transcript as the server needs it, without the progress logs.
602
+ *
603
+ * In stateless mode `send` posts the whole history on *every* turn, so anything
604
+ * that accumulates on a message is paid for again on each one. `progress` is
605
+ * the only part of the transcript that grows without a bound the model imposes:
606
+ * a generator tool yielding per chunk writes an entry per chunk, and a tool that
607
+ * yielded five thousand times would put five thousand objects in the body of
608
+ * turn 2, turn 3, and every turn after, forever.
609
+ *
610
+ * Nothing server-side reads it. It is not translated into a provider message,
611
+ * `openCalls` matches on `toolCallId`, and the resume path replays a sub-agent
612
+ * from `nested` — which is why `nested` is kept here, recursed into rather than
613
+ * dropped, while `progress` is not. The client's own copy is untouched: this
614
+ * shapes the request body only, so a UI still renders every yield it received.
615
+ */
616
+ function forWire(messages) {
617
+ return messages.map((message) => {
618
+ if (!message.content.some((part) => part.type === "tool-call" && (part.progress || part.nested))) return message;
619
+ return {
620
+ ...message,
621
+ content: message.content.map((part) => {
622
+ if (part.type !== "tool-call") return part;
623
+ const { progress: _progress, ...rest } = part;
624
+ return rest.nested ? {
625
+ ...rest,
626
+ nested: rest.nested.map((run) => ({
627
+ ...run,
628
+ messages: forWire(run.messages)
629
+ }))
630
+ } : rest;
631
+ })
632
+ };
633
+ });
634
+ }
635
+ /**
636
+ * The path is the agent's route, exactly as mounted:
637
+ *
638
+ * const { messages, sendMessage } = useChat("/chat")
639
+ *
640
+ * and a tool part inside `messages` narrows on `name`, giving `input` and
641
+ * `output` their real types — which is the whole reason the tool tuple is
642
+ * carried through `Agent`, `AgentRoute` and `RPC` instead of being erased at the
643
+ * first boundary.
644
+ */
645
+ function useChat(path, params = {}) {
646
+ const { threadId: initialThreadId, initialMessages, cursor: initialCursor, attach: attachOnMount = true, body: extraBody, headers, onFinish, onError, onAwaitingInput, onAttachMiss } = params;
647
+ const routeParams = useParams();
648
+ const base = useMemo(() => `/api${applyRouteParams(String(path).replace(/^[A-Z]+:/, ""), routeParams)}`, [path, routeParams]);
649
+ /**
650
+ * The state lives in a ref and is mirrored into `useState` for rendering.
651
+ *
652
+ * A stream applies frames faster than React commits, and the reducer needs
653
+ * the result of the previous frame, not the value captured when the loop
654
+ * started. The ref is the source of truth; `setState` is the notification.
655
+ *
656
+ * `any` inside, exact at the boundary: the wire carries erased tool shapes and
657
+ * the route's real ones are re-asserted once, in the returned object.
658
+ */
659
+ const stateRef = useRef(null);
660
+ if (stateRef.current === null) stateRef.current = initialChatState({
661
+ messages: initialMessages,
662
+ threadId: initialThreadId,
663
+ seq: initialCursor?.seq,
664
+ cursorRunId: initialCursor?.runId
665
+ });
666
+ const [state, setState] = useState(stateRef.current);
667
+ const [phase, setPhase] = useState("idle");
668
+ const mountedRef = useRef(true);
669
+ const abortRef = useRef(null);
670
+ const handlers = useRef({
671
+ onFinish,
672
+ onError,
673
+ onAwaitingInput,
674
+ onAttachMiss
675
+ });
676
+ handlers.current = {
677
+ onFinish,
678
+ onError,
679
+ onAwaitingInput,
680
+ onAttachMiss
681
+ };
682
+ const requestRef = useRef({
683
+ base,
684
+ headers,
685
+ extraBody
686
+ });
687
+ requestRef.current = {
688
+ base,
689
+ headers,
690
+ extraBody
691
+ };
692
+ useEffect(() => {
693
+ mountedRef.current = true;
694
+ return () => {
695
+ mountedRef.current = false;
696
+ abortRef.current?.controller.abort();
697
+ abortRef.current = null;
698
+ };
699
+ }, []);
700
+ const commit = useCallback((next) => {
701
+ stateRef.current = next;
702
+ if (mountedRef.current) setState(next);
703
+ }, []);
704
+ const setPhaseSafe = useCallback((next) => {
705
+ if (mountedRef.current) setPhase(next);
706
+ }, []);
707
+ /**
708
+ * Report a failure without touching the conversation.
709
+ *
710
+ * `pending` deliberately survives. Most of what fails here has nothing to do
711
+ * with the question the conversation is holding — an upload that 500s, an
712
+ * answer aimed at a call this client never had — and the pending calls carry
713
+ * the only signatures that can answer them, held nowhere else. Dropping them
714
+ * because an unrelated request failed would leave the user unable to approve
715
+ * anything at all, with a fresh turn (which the server reads as refusing
716
+ * everything) the only way out.
717
+ *
718
+ * The failures that really do end the turn clear `pending` where they happen:
719
+ * `send` clears it as the turn goes out, and the stream's own `error` event
720
+ * clears it in the reducer.
721
+ */
722
+ const fail = useCallback((error) => {
723
+ commit({
724
+ ...stateRef.current,
725
+ error
726
+ });
727
+ handlers.current.onError?.(error);
728
+ }, [commit]);
729
+ const post = useCallback((url, payload, signal) => {
730
+ const { headers: extraHeaders } = requestRef.current;
731
+ return fetch(url, {
732
+ method: "POST",
733
+ headers: {
734
+ "Content-Type": "application/json",
735
+ ...extraHeaders
736
+ },
737
+ body: JSON.stringify(payload),
738
+ signal
739
+ });
740
+ }, []);
741
+ /**
742
+ * The server call that ends a run. Shared by `stop()` and by a `send` that
743
+ * supersedes one; what each does with a failure is its own, because a stop
744
+ * that did not land is a run still working through its tool loop and still
745
+ * billing, behind a transcript that says it was cut — and whose problem that
746
+ * is depends on who asked.
747
+ */
748
+ const postStop = useCallback(async (body, report) => {
749
+ const { base: url } = requestRef.current;
750
+ try {
751
+ const response = await post(`${url}/stop`, body);
752
+ if (!response.ok) report(await httpError(response));
753
+ } catch (error) {
754
+ if (isAbort(error)) return;
755
+ report({
756
+ code: "unknown",
757
+ message: error instanceof Error ? error.message : String(error),
758
+ retryable: true
759
+ });
760
+ }
761
+ }, [post]);
762
+ const consume = useCallback(async (response, signal) => {
763
+ for await (const frame of decodeSSE(response.body)) {
764
+ if (!mountedRef.current || signal.aborted) return;
765
+ setPhaseSafe("streaming");
766
+ const previous = stateRef.current;
767
+ const next = applyFrame(previous, frame);
768
+ if (next === previous) continue;
769
+ commit(next);
770
+ const event = frame.event;
771
+ if (event.type === "message-end") {
772
+ const before = previous.messages.find((m) => m.id === event.messageId);
773
+ const message = next.messages.find((m) => m.id === event.messageId);
774
+ if (message && before?.finishReason === void 0) handlers.current.onFinish?.(message);
775
+ } else if (event.type === "awaiting-input") handlers.current.onAwaitingInput?.(event.pending);
776
+ else if (event.type === "error") handlers.current.onError?.(event.error);
777
+ }
778
+ }, [commit, setPhaseSafe]);
779
+ const send = useCallback(async (turn) => {
780
+ const { base: url, extraBody: body } = requestRef.current;
781
+ const superseded = abortRef.current;
782
+ superseded?.controller.abort();
783
+ const clientRunId = localId();
784
+ const controller = new AbortController();
785
+ abortRef.current = {
786
+ controller,
787
+ clientRunId
788
+ };
789
+ if (superseded) {
790
+ const { runId, threadId } = stateRef.current;
791
+ const body = {
792
+ ...runId ? { runId } : {},
793
+ ...superseded.clientRunId ? { clientRunId: superseded.clientRunId } : {}
794
+ };
795
+ if (Object.keys(body).length > 0) postStop(body, threadId ? () => {} : (error) => handlers.current.onError?.(error));
796
+ }
797
+ const previous = superseded ? markAborted(stateRef.current) : stateRef.current;
798
+ const history = previous.messages;
799
+ const authored = turn.text || turn.files?.length ? [{
800
+ id: localId(),
801
+ role: "user",
802
+ content: [...turn.text ? [{
803
+ type: "text",
804
+ text: turn.text
805
+ }] : [], ...(turn.files ?? []).map((file) => ({
806
+ type: "file",
807
+ ...file
808
+ }))],
809
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
810
+ }] : [];
811
+ commit({
812
+ ...previous,
813
+ messages: [...history, ...authored],
814
+ error: null,
815
+ pending: []
816
+ });
817
+ setPhaseSafe("submitted");
818
+ try {
819
+ const payload = {
820
+ turn,
821
+ clientRunId,
822
+ ...stateRef.current.threadId ? { threadId: stateRef.current.threadId } : { messages: forWire(history) },
823
+ ...body
824
+ };
825
+ const response = await post(url, payload, controller.signal);
826
+ if (!response.ok) {
827
+ fail(await httpError(response));
828
+ return;
829
+ }
830
+ await consume(response, controller.signal);
831
+ } catch (error) {
832
+ if (!isAbort(error)) fail({
833
+ code: "unknown",
834
+ message: error instanceof Error ? error.message : String(error),
835
+ retryable: true
836
+ });
837
+ } finally {
838
+ if (abortRef.current?.controller === controller) {
839
+ abortRef.current = null;
840
+ setPhaseSafe("idle");
841
+ }
842
+ }
843
+ }, [
844
+ commit,
845
+ consume,
846
+ fail,
847
+ post,
848
+ postStop,
849
+ setPhaseSafe
850
+ ]);
851
+ const sendMessage = useCallback((turn) => send(typeof turn === "string" ? { text: turn } : turn), [send]);
852
+ /**
853
+ * Answers queued within one tick become one turn.
854
+ *
855
+ * This is not an optimisation. A turn that leaves a pending call unanswered
856
+ * denies it, so sending three approvals as three turns would have the first
857
+ * one refuse the other two — and the natural UI code, a loop over `pending`
858
+ * calling `approve` for each, is exactly that shape. Coalescing on the
859
+ * microtask is what makes the declared per-call signature safe to use the way
860
+ * it reads.
861
+ */
862
+ const queued = useRef(null);
863
+ const queueResult = useCallback((result) => {
864
+ if (queued.current) {
865
+ queued.current.results.push(result);
866
+ return queued.current.flushed;
867
+ }
868
+ const results = [result];
869
+ const flushed = Promise.resolve().then(() => {
870
+ queued.current = null;
871
+ return send({ toolResults: results });
872
+ });
873
+ queued.current = {
874
+ results,
875
+ flushed
876
+ };
877
+ return flushed;
878
+ }, [send]);
879
+ /**
880
+ * The half of a `ClientToolResult` the app never writes: the id, the
881
+ * signature, and the path.
882
+ *
883
+ * All three are carried by the hook and handed back untouched, which is what
884
+ * makes a sub-agent's question answerable with the same two lines as a
885
+ * top-level one. `path` is absent for a call the parent's own tools made, so
886
+ * an app cannot tell from its own code where the question came from — the only
887
+ * difference is that `pending[i].path` is there to render if it wants to say
888
+ * which agent is asking.
889
+ */
890
+ const resultFor = useCallback((toolCallId, build) => {
891
+ const matches = stateRef.current.pending.filter((candidate) => candidate.toolCallId === toolCallId);
892
+ const call = matches[0];
893
+ if (!call) {
894
+ fail({
895
+ code: "invalid_tool_result",
896
+ message: `No pending tool call ${toolCallId}`,
897
+ toolCallId,
898
+ retryable: false
899
+ });
900
+ return null;
901
+ }
902
+ if (matches.length > 1) {
903
+ fail({
904
+ code: "invalid_tool_result",
905
+ message: `Ambiguous tool call ${toolCallId}: ${matches.length} pending calls share it`,
906
+ toolCallId,
907
+ retryable: false
908
+ });
909
+ return null;
910
+ }
911
+ return build({
912
+ toolCallId,
913
+ signature: call.signature,
914
+ ...call.path ? { path: call.path } : {}
915
+ });
916
+ }, [fail]);
917
+ const approve = useCallback(async (toolCallId, approved, reason) => {
918
+ const result = resultFor(toolCallId, (base) => ({
919
+ ...base,
920
+ approve: approved,
921
+ ...reason ? { reason } : {}
922
+ }));
923
+ if (result) await queueResult(result);
924
+ }, [queueResult, resultFor]);
925
+ const answer = useCallback(async (toolCallId, output) => {
926
+ const result = resultFor(toolCallId, (base) => ({
927
+ ...base,
928
+ output
929
+ }));
930
+ if (result) await queueResult(result);
931
+ }, [queueResult, resultFor]);
932
+ const stop = useCallback(async () => {
933
+ const { runId, threadId } = stateRef.current;
934
+ const inFlight = abortRef.current;
935
+ inFlight?.controller.abort();
936
+ abortRef.current = null;
937
+ commit(markAborted(stateRef.current));
938
+ setPhaseSafe("idle");
939
+ if (!inFlight && !runId) return;
940
+ const body = {
941
+ ...runId ? { runId } : {},
942
+ ...threadId ? { threadId } : {},
943
+ ...inFlight?.clientRunId ? { clientRunId: inFlight.clientRunId } : {}
944
+ };
945
+ await postStop(body, fail);
946
+ }, [
947
+ commit,
948
+ fail,
949
+ postStop,
950
+ setPhaseSafe
951
+ ]);
952
+ const regenerate = useCallback(async () => {
953
+ const messages = stateRef.current.messages;
954
+ let assistantIndex = -1;
955
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i].role === "assistant") {
956
+ assistantIndex = i;
957
+ break;
958
+ }
959
+ if (assistantIndex === -1) return;
960
+ let userIndex = -1;
961
+ for (let i = assistantIndex - 1; i >= 0; i--) if (messages[i].role === "user") {
962
+ userIndex = i;
963
+ break;
964
+ }
965
+ if (userIndex === -1) return;
966
+ const turn = turnFrom(messages[userIndex]);
967
+ commit({
968
+ ...stateRef.current,
969
+ messages: messages.slice(0, userIndex),
970
+ pending: [],
971
+ error: null
972
+ });
973
+ await send(turn);
974
+ }, [commit, send]);
975
+ const setMessages = useCallback((messages) => {
976
+ commit({
977
+ ...stateRef.current,
978
+ messages
979
+ });
980
+ }, [commit]);
981
+ const uploadFile = useCallback(async (file) => {
982
+ const { base: url, headers: extraHeaders } = requestRef.current;
983
+ const form = new FormData();
984
+ form.append("file", file);
985
+ const response = await fetch(`${url}/files`, {
986
+ method: "POST",
987
+ headers: { ...extraHeaders },
988
+ body: form
989
+ });
990
+ if (!response.ok) {
991
+ const error = await httpError(response);
992
+ fail(error);
993
+ throw new Error(error.message);
994
+ }
995
+ const data = await response.json();
996
+ return {
997
+ fileId: data.fileId,
998
+ name: data.name ?? file.name,
999
+ mimeType: data.mimeType ?? file.type
1000
+ };
1001
+ }, [fail]);
1002
+ useEffect(() => {
1003
+ if (attachOnMount === false || !initialThreadId) return;
1004
+ const controller = new AbortController();
1005
+ abortRef.current = { controller };
1006
+ (async () => {
1007
+ try {
1008
+ const body = {
1009
+ threadId: initialThreadId,
1010
+ cursor: stateRef.current.seq,
1011
+ ...stateRef.current.cursorRunId ? { runId: stateRef.current.cursorRunId } : {}
1012
+ };
1013
+ const response = await post(`${requestRef.current.base}/attach`, body, controller.signal);
1014
+ if (!response.ok || response.status === 204 || !response.body) {
1015
+ handlers.current.onAttachMiss?.({ threadId: initialThreadId });
1016
+ return;
1017
+ }
1018
+ await consume(response, controller.signal);
1019
+ } catch {} finally {
1020
+ if (abortRef.current?.controller === controller) {
1021
+ abortRef.current = null;
1022
+ setPhaseSafe("idle");
1023
+ }
1024
+ }
1025
+ })();
1026
+ return () => controller.abort();
1027
+ }, []);
1028
+ const status = state.pending.length > 0 ? "awaiting-input" : phase !== "idle" ? phase : state.error ? "error" : "idle";
1029
+ return {
1030
+ messages: state.messages,
1031
+ status,
1032
+ error: state.error,
1033
+ threadId: state.threadId,
1034
+ runId: state.runId,
1035
+ cursor: {
1036
+ runId: state.cursorRunId,
1037
+ seq: state.seq
1038
+ },
1039
+ sendMessage,
1040
+ stop,
1041
+ regenerate,
1042
+ setMessages,
1043
+ pending: state.pending,
1044
+ loadedTools: state.loadedTools,
1045
+ approve,
1046
+ answer,
1047
+ attach: uploadFile
1048
+ };
1049
+ }
1050
+ //#endregion
1051
+ export { useChat };
1052
+
1053
+ //# sourceMappingURL=index.js.map