@deuz-sdk/react 1.7.1 → 1.9.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/dist/index.d.ts CHANGED
@@ -1,11 +1,28 @@
1
- import { Message, ToolApprovalRequest, ToolApprovalResponse, DeepPartial } from '@deuz-sdk/core';
1
+ import { Message, ToolApprovalRequest, Usage, FinishReason, ToolApprovalResponse, ImagePart, DeepPartial } from '@deuz-sdk/core';
2
2
  export { Message, ToolApprovalRequest, ToolApprovalResponse } from '@deuz-sdk/core';
3
- import { UIMessage, AssistantTurnState } from '@deuz-sdk/core/chat';
4
- export { AssistantTurnState, ChatHistory, UIMessage, UIToolCall } from '@deuz-sdk/core/chat';
3
+ import { UIMessage, ChatHistory, UIToolCall, AssistantTurnState, ChatInput } from '@deuz-sdk/core/chat';
4
+ export { AssistantTurnState, ChatHistory, ChatInput, UIMessage, UIMessagePart, UIToolCall } from '@deuz-sdk/core/chat';
5
5
  import * as react from 'react';
6
6
  import { ReactNode } from 'react';
7
7
  export { DeuzUIPart } from '@deuz-sdk/core/ui';
8
8
 
9
+ /**
10
+ * Picked files → canonical parts for a {@link ChatInput}. A thin, null-tolerant
11
+ * wrapper over core's `filesToImageParts`: the bytes work (Web APIs only, no
12
+ * `FileReader`, no `Buffer`) lives THERE and is deliberately not reimplemented
13
+ * here. `ImagePart` is the single carrier for all binary media, so images and
14
+ * PDFs both ride this call.
15
+ *
16
+ * ```tsx
17
+ * <input type="file" multiple onChange={async (e) => {
18
+ * const parts = await partsFromFiles(e.target.files);
19
+ * await sendMessage({ text: 'what is in these?', parts });
20
+ * }} />
21
+ * ```
22
+ */
23
+ declare function partsFromFiles(files: FileList | Iterable<Blob & {
24
+ type?: string;
25
+ }> | null | undefined): Promise<ImagePart[]>;
9
26
  /** Live cumulative USD cost, from the wire's `cost` parts (last one wins). */
10
27
  interface UseChatCost {
11
28
  costUsd: number;
@@ -17,6 +34,21 @@ interface UseChatBudgetExceeded {
17
34
  limit: number;
18
35
  value: number;
19
36
  }
37
+ /**
38
+ * Where the resume cursor lives ACROSS a page reload.
39
+ *
40
+ * An adapter rather than a hardcoded `localStorage`: core's own
41
+ * `connectDeuzStream` frames the cursor as the caller's choice, and hardcoding
42
+ * web storage would break SSR (no `window`), React Native (no `localStorage`)
43
+ * and privacy modes that throw on write. Both methods may throw — the hook
44
+ * swallows it, because losing a cursor must never kill a live stream.
45
+ */
46
+ interface UseChatCursorStore {
47
+ /** The persisted cursor, or `undefined` when there is nothing to resume. */
48
+ load(): string | undefined;
49
+ /** Persist the cursor. Called on every advance. */
50
+ save(id: string): void;
51
+ }
20
52
  interface UseChatResumeOptions {
21
53
  /**
22
54
  * Resume endpoint (see core's `resumeDeuzStreamResponse`): a GET URL or a
@@ -28,59 +60,207 @@ interface UseChatResumeOptions {
28
60
  }) => Response | Promise<Response>);
29
61
  /** Cursor to resume from (e.g. persisted across a page reload). */
30
62
  lastEventId?: string;
63
+ /**
64
+ * Attempt `reconnect()` ONCE per mounted hook — the canonical scenario
65
+ * resumability exists for (the user refreshes mid-generation) with zero app
66
+ * wiring. Fires exactly once under React 18/19 StrictMode's deliberate
67
+ * mount → unmount → mount double-invoke.
68
+ *
69
+ * An AUTOMATIC attempt that fails is NOT an error state and does NOT reach
70
+ * `onError`: the user did not ask for it, and every cold load of a chat app
71
+ * whose resume endpoint answers 404 (nothing in flight) would otherwise paint
72
+ * a permanent error. A caught-up endpoint (immediate `[DONE]`) is a silent
73
+ * no-op — no bubble, no state change. Call `reconnect()` yourself when you
74
+ * want the failure surfaced.
75
+ */
76
+ auto?: boolean;
77
+ /** Cursor persistence adapter (see {@link UseChatCursorStore}). */
78
+ cursor?: UseChatCursorStore;
31
79
  }
32
80
  interface UseChatOptions {
33
81
  /** Endpoint serving `toDeuzStreamResponse` output. */
34
82
  api: string;
35
- /** Seed canonical history (e.g. restored from a ChatStore) — rendered via `uiFromMessages`. */
83
+ /**
84
+ * Seed canonical history (e.g. restored from a ChatStore) — rendered via
85
+ * `uiFromMessages`.
86
+ *
87
+ * READ ONCE, at mount, like any `useState` initial value. It is deliberately
88
+ * NOT re-adopted when the prop changes: apps pass an inline array literal, so
89
+ * a new identity arrives on EVERY render and adopting it would reset the
90
+ * transcript continuously — and mid-stream it would clobber the turn being
91
+ * folded. Switching chats or hydrating later is an explicit action: call
92
+ * `setHistory` (both views) or `setMessages` (re-derives canonical).
93
+ */
36
94
  initialMessages?: Message[];
37
95
  headers?: Record<string, string>;
38
96
  /** Extra fields merged into every request body. */
39
97
  body?: Record<string, unknown>;
40
98
  /** Chat identity — merged into every request body (server-side ChatStore persistence). */
41
99
  chatId?: string;
42
- /** Enables `reconnect()` against a resume endpoint (wire v2, `connectDeuzStream`). */
100
+ /** Enables `reconnect()` / `resume.auto` against a resume endpoint (wire v2). */
43
101
  resume?: UseChatResumeOptions;
44
102
  /** Id source for UI messages/turns. Default: `crypto.randomUUID` (with a fallback). */
45
103
  generateId?: () => string;
104
+ /**
105
+ * Coalesce React commits to at most one per `throttleMs` (trailing edge).
106
+ * Default `0` — commit on every wire part, exactly as 1.8 did.
107
+ *
108
+ * A fast model emits hundreds of text deltas per second; with markdown
109
+ * rendering over a long transcript that visibly janks. The terminal frame is
110
+ * ALWAYS flushed (stream end, error, abort, an external tool result), so the
111
+ * final text can never be lost to coalescing. A plain `setTimeout` is fine
112
+ * here: unlike core's `src/**`, this package is not edge-lint-constrained and
113
+ * has no `deps.clock` seam.
114
+ */
115
+ throttleMs?: number;
46
116
  /**
47
117
  * Client-tool executor: called for every streamed tool call the SERVER did
48
118
  * not execute. The return value is appended as its tool_result and the chat
49
119
  * auto-continues; a throw self-heals as an is_error result.
120
+ *
121
+ * OMIT it to drive client tools from outside the hook instead: the
122
+ * round-trip PARKS, the calls appear in `pendingToolCalls`, and
123
+ * `addToolResult` answers them.
50
124
  */
51
125
  onToolCall?: (call: {
52
126
  toolCallId: string;
53
127
  toolName: string;
54
128
  input: unknown;
55
129
  }) => Promise<unknown> | unknown;
130
+ /**
131
+ * Fired for every `data-{name}` part as it arrives, BEFORE reconciliation —
132
+ * so a caller sees each intermediate write of an addressable `(name, id)`
133
+ * entry, which `dataParts` collapses to the last one. A throw is swallowed
134
+ * (G2: a consumer callback is not a transport error).
135
+ */
136
+ onData?: (data: {
137
+ name: string;
138
+ id?: string;
139
+ payload: unknown;
140
+ }) => void;
56
141
  onError?: (error: Error) => void;
142
+ /**
143
+ * How a NON-2xx response from `api` is reported (default `'error-part'`: the
144
+ * failure becomes `status: 'error'` instead of an empty assistant bubble).
145
+ * Forwarded verbatim to core's `readDeuzStream`.
146
+ */
147
+ onHttpError?: 'error-part' | 'ignore';
57
148
  /** Injectable for tests / custom transports. Defaults to global fetch. */
58
149
  fetch?: typeof fetch;
59
150
  }
60
151
  interface UseChatResult {
152
+ /** The render view. Each message carries its ordered `parts` (1.9) when it has any. */
61
153
  messages: UIMessage[];
154
+ /**
155
+ * BOTH views in one object (`{ ui, canonical }`). Only the canonical one is
156
+ * POSTable and only it survives a round-trip through a provider, so an app
157
+ * that persists a chat should persist `history.canonical`. Identity is stable
158
+ * until either view changes.
159
+ */
160
+ history: ChatHistory;
62
161
  status: 'idle' | 'streaming' | 'error';
63
162
  error: Error | undefined;
64
163
  /** Gated tool calls awaiting verdicts — the chat is PAUSED while non-empty. */
65
164
  pendingApprovals: ToolApprovalRequest[];
165
+ /**
166
+ * Client tool calls the round-trip is PARKED on (no `onToolCall` executor).
167
+ * Answer each with `addToolResult`; the chat continues once all are answered.
168
+ */
169
+ pendingToolCalls: UIToolCall[];
66
170
  /** Live cumulative cost from the wire's `cost` parts (last one wins). */
67
171
  cost?: UseChatCost;
68
172
  /** Set when the server's budget guardrail tripped this turn. */
69
173
  budgetExceeded?: UseChatBudgetExceeded;
70
- /** This turn's app-defined `data-{name}` parts, in arrival order. */
174
+ /**
175
+ * This turn's app-defined `data-{name}` parts. Entries written with an `id`
176
+ * are RECONCILED by `(name, id)` — one live status widget stays one entry, at
177
+ * its original position — while id-less entries stay append-only (1.7/1.8).
178
+ */
71
179
  dataParts: Array<{
72
180
  name: string;
181
+ id?: string;
73
182
  payload: unknown;
74
183
  }>;
75
184
  /** This turn's RAG citations. */
76
185
  citations: AssistantTurnState['citations'];
77
- sendMessage: (text: string) => Promise<void>;
186
+ /** Latest live plan snapshot for this turn (autonomous runs, 1.8). */
187
+ plan?: AssistantTurnState['plan'];
188
+ /** This turn's live activity feed ("Computer" view, 1.8). */
189
+ activity: AssistantTurnState['activity'];
190
+ /** This turn's `verifyStep` verdicts, in arrival order (1.9). */
191
+ verifications?: AssistantTurnState['verifications'];
192
+ /**
193
+ * Non-fatal execution notices for this turn, in arrival order (1.9): a
194
+ * sampling param the wire could not carry, a clamped ceiling, an unknown model
195
+ * slug served from the conservative fallback row. Render them as a dismissible
196
+ * banner — the run itself succeeded.
197
+ */
198
+ warnings?: AssistantTurnState['warnings'];
199
+ /**
200
+ * `false-finish` rejections for this turn (1.9): the server's guard decided
201
+ * the work was not actually done. A trailing entry with `willRetry: false`
202
+ * means the guard's budget ran out, so the answer on screen is the one it
203
+ * stopped on rather than one it was happy with.
204
+ */
205
+ falseFinishes?: AssistantTurnState['falseFinishes'];
206
+ /**
207
+ * Live sub-agent (`agentTool`) frames for this turn, one per `agentPath`
208
+ * (1.9). Each holds the child's OWN turn — its text, ordered `parts` and tool
209
+ * cards — so a delegated run is renderable instead of invisible, and
210
+ * `afterPart` says where in the parent's `parts` it belongs. Nothing here is
211
+ * mixed into the parent's `content`: a sub-agent's words are its own.
212
+ */
213
+ subAgents?: AssistantTurnState['subAgents'];
214
+ /** Per-step usage/finish from `step-finish` parts, in arrival order (1.9). */
215
+ steps?: AssistantTurnState['steps'];
216
+ /** Terminal token usage for this turn, from the wire's `finish` part (1.9). */
217
+ usage?: Usage;
218
+ /** Terminal finish reason (1.9) — `'length'` means the answer was truncated. */
219
+ finishReason?: FinishReason;
220
+ /**
221
+ * Send one user turn: plain text (1.8-identical, including the canonical
222
+ * `content` string) or text plus canonical parts — images/PDFs from
223
+ * {@link partsFromFiles}, `imagePart`/`filePart`, anything the wires accept.
224
+ */
225
+ sendMessage: (input: ChatInput) => Promise<void>;
78
226
  /** Abort the in-flight stream (not an error). */
79
227
  stop: () => void;
80
228
  /** Drop the trailing assistant/tool turns (core `dropTrailingAssistant`) and re-run. */
81
229
  regenerate: () => Promise<void>;
82
- /** Cut history before `messageId` (core `branchBeforeUserMessage`) and send `text`. */
83
- editAndResend: (messageId: string, text: string) => Promise<void>;
230
+ /** Cut history before `messageId` (core `branchBeforeUserMessage`) and send `input`. */
231
+ editAndResend: (messageId: string, input: ChatInput) => Promise<void>;
232
+ /**
233
+ * Replace BOTH views in one commit — the honest primitive, because Deuz keeps
234
+ * two of them and only the canonical one is POSTable. Use it to switch chats,
235
+ * hydrate after mount, or delete/edit a rendered message while keeping a
236
+ * system prompt and provider round-trip data that the UI view cannot carry.
237
+ *
238
+ * Pending gates (approvals, parked client tool calls) are DROPPED: they were
239
+ * anchored to the transcript you just replaced.
240
+ */
241
+ setHistory: (update: ChatHistory | ((prev: ChatHistory) => ChatHistory)) => void;
242
+ /**
243
+ * Sugar over `setHistory`: re-derives the canonical view with core's
244
+ * `canonicalFromUI`. That inverse projection is LOSSY — system messages,
245
+ * `Message.providerMetadata` and UI-only state do not survive it, and a
246
+ * message without ordered `parts` loses its attachments entirely — so drive
247
+ * `setHistory` directly when your history has any of those.
248
+ */
249
+ setMessages: (update: UIMessage[] | ((prev: UIMessage[]) => UIMessage[])) => void;
250
+ /**
251
+ * Answer ONE parked client tool call from outside the hook. Feeds the exact
252
+ * path `onToolCall`'s return value takes (core `clientToolResultMessage`), so
253
+ * the chat auto-continues once every call in `pendingToolCalls` is answered.
254
+ * A result for an id that is not parked is ignored — an orphan `tool_result`
255
+ * would 400 the next request.
256
+ */
257
+ addToolResult: (result: {
258
+ toolCallId: string;
259
+ output: unknown;
260
+ isError?: boolean;
261
+ }) => Promise<void>;
262
+ /** Dismiss the error: `status: 'error'` → `'idle'`. Leaves a live stream alone. */
263
+ clearError: () => void;
84
264
  /**
85
265
  * Record one verdict (the request's signed `token` is auto-preserved). Once
86
266
  * EVERY pending approval has a verdict, the chat auto-resumes with
@@ -99,6 +279,17 @@ interface UseObjectOptions {
99
279
  /** Endpoint serving `toDeuzObjectStreamResponse` output. */
100
280
  api: string;
101
281
  headers?: Record<string, string>;
282
+ /**
283
+ * How a NON-2xx response from `api` is reported (default `'error-part'`,
284
+ * which lands in `error`). Forwarded verbatim to core's `readDeuzStream`.
285
+ */
286
+ onHttpError?: 'error-part' | 'ignore';
287
+ /**
288
+ * Coalesce partials to at most one React commit per `throttleMs` (trailing
289
+ * edge). Default `0` — commit every partial, exactly as 1.8 did. The final
290
+ * partial is ALWAYS flushed, so the completed object is never lost.
291
+ */
292
+ throttleMs?: number;
102
293
  /** Injectable for tests / custom transports. Defaults to global fetch. */
103
294
  fetch?: typeof fetch;
104
295
  }
@@ -139,4 +330,4 @@ interface CostBadgeProps {
139
330
  /** Live cost readout: `$X.XXXX`, plus ` (saved $Y.YYYY)` when caching saved money. */
140
331
  declare function CostBadge({ cost, format }: CostBadgeProps): react.JSX.Element | null;
141
332
 
142
- export { CostBadge, type CostBadgeProps, ToolApprovalCard, type ToolApprovalCardProps, type UseChatBudgetExceeded, type UseChatCost, type UseChatOptions, type UseChatResult, type UseChatResumeOptions, type UseObjectOptions, type UseObjectResult, useChat, useObject };
333
+ export { CostBadge, type CostBadgeProps, ToolApprovalCard, type ToolApprovalCardProps, type UseChatBudgetExceeded, type UseChatCost, type UseChatCursorStore, type UseChatOptions, type UseChatResult, type UseChatResumeOptions, type UseObjectOptions, type UseObjectResult, partsFromFiles, useChat, useObject };