@dudousxd/nestjs-agent-react 0.19.0 → 0.20.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/README.md CHANGED
@@ -709,6 +709,32 @@ An item is `uploading`, `ready`, `error` (retry with `files.retry(id)`) or `reje
709
709
  flight. `messageFiles(message)` reads the files back off any message — live or replayed — with a
710
710
  `kind` (`image`, `pdf`, `text`, …), the extension and, for replayed ones, the stored `mediaId`.
711
711
 
712
+ #### Resumable uploads on nestjs-media (`@dudousxd/nestjs-agent-react/media`)
713
+
714
+ With `AgentMediaAttachmentsModule` on the server (`@dudousxd/nestjs-agent/media`), one option turns
715
+ it on — uploads go in chunks through nestjs-media's tus endpoint, with progress, abort (`remove`)
716
+ and retry, on the chat client's own connection (base url, headers, credentials):
717
+
718
+ ```tsx
719
+ import { mediaAttachments } from '@dudousxd/nestjs-agent-react/media';
720
+
721
+ const chat = useAgentChat({ attachments: mediaAttachments() });
722
+ const files = useAttachments({ backend: chat.backend });
723
+ ```
724
+
725
+ Headless; `@dudousxd/nestjs-media-client` is an optional peer only this subpath uses. Extending it:
726
+
727
+ - `mediaAttachments({ path: '/api/agent', chunkSize, retries })` — a prefixed API, tuning.
728
+ - `new AgentClient({ …connection, attachments: mediaAttachments() })` — your own client instance.
729
+ - `withMediaUploads(backend, connection)` — any other `AgentBackend`; `createMediaUpload(connection)`
730
+ — a bare `upload` for `useAttachments({ upload })`.
731
+ - Refusals throw `MediaUploadError` with the HTTP `status` (`413`, `415`, …); an aborted or failed
732
+ upload is discarded on the server.
733
+
734
+ Your own storage instead: `useAgentChat({ attachments: (file, { signal, onProgress }, connection) => … })`
735
+ (an `AttachmentUploadStrategy`), or `useAttachments({ upload })`, resolving to a
736
+ `{ mediaId, url, contentType, name }` your server's `AGENT_ATTACHMENT_STAGING` recognises.
737
+
712
738
  The rest of `AgentClient` works outside `useAgentChat` too — e.g. a standalone approvals inbox:
713
739
 
714
740
  ```ts
@@ -0,0 +1,285 @@
1
+ import { ThreadSummary, ThreadDetail, MessageAttachment, ToolCatalogEntry, SkillCatalogEntry, QuotaView, QuotaReport, ModelCatalogView, AgentCatalogEntry, MessageFeedbackValue, MessageFeedback } from '@dudousxd/nestjs-agent-core';
2
+
3
+ /**
4
+ * Partial update accepted by `PATCH <base>/threads/:threadId`. `defaultAgent: null` clears a
5
+ * previously-set default back to the module's own default; omitting it leaves the thread's
6
+ * current default untouched.
7
+ */
8
+ interface ThreadPatch {
9
+ title?: string;
10
+ defaultAgent?: string | null;
11
+ /** Pin a catalog model on the thread; `null` unpins it. */
12
+ model?: string | null;
13
+ }
14
+ /** Starting a turn: the body `POST <base>/chat` takes (see docs/stream-protocol.md). */
15
+ interface ChatStreamRequest {
16
+ /** `{ message, threadId?, agent?, model?, attachments?, pageContext?, regenerate?, … }`. */
17
+ body: Record<string, unknown>;
18
+ /** Per-request headers the AI SDK was handed for this send. */
19
+ headers?: Record<string, string>;
20
+ /** Aborted when the user stops the turn. */
21
+ signal?: AbortSignal;
22
+ }
23
+ /** Attaching to a run that is already streaming: `GET <base>/chat/:runId/stream?after=<seq>`. */
24
+ interface ResumeStreamRequest {
25
+ runId: string;
26
+ /**
27
+ * The sequence number (SSE `id:`) of the last frame the client already has. Omitted → replay
28
+ * from the first frame. A backend that cannot skip may ignore it: the transport also drops frames
29
+ * at or below it.
30
+ */
31
+ after?: number;
32
+ headers?: Record<string, string>;
33
+ signal?: AbortSignal;
34
+ }
35
+ /**
36
+ * An open chat stream: the raw `text/event-stream` bytes in the stream-protocol framing, plus the
37
+ * identity a backend may learn before the first frame (the `X-Agent-Run-Id` / `X-Agent-Thread-Id`
38
+ * headers). The body's own `event: meta` frame supplies them otherwise.
39
+ */
40
+ interface ChatStreamResponse {
41
+ body: ReadableStream<Uint8Array>;
42
+ runId?: string;
43
+ threadId?: string;
44
+ }
45
+ /** What `POST <base>/messages/:id/feedback` takes. `value: null` clears the rating. */
46
+ interface MessageFeedbackInput {
47
+ value: MessageFeedbackValue | null;
48
+ comment?: string;
49
+ }
50
+ interface UploadAttachmentOptions {
51
+ signal?: AbortSignal;
52
+ /** Called with 0..1 as the upload progresses, when the backend can observe it. */
53
+ onProgress?: (fraction: number) => void;
54
+ }
55
+ /** How {@link AgentClient} reaches the server — handed to an {@link AttachmentUploadStrategy}. */
56
+ interface AgentConnection {
57
+ /** Origin + base path, trailing slash removed (`''` for same-origin). */
58
+ baseUrl: string;
59
+ /** The client's static + per-request headers, resolved now (auth, CSRF). */
60
+ headers: () => Promise<Record<string, string>>;
61
+ credentials?: RequestCredentials;
62
+ fetch: typeof fetch;
63
+ }
64
+ /**
65
+ * Replaces {@link AgentClient}'s own `uploadAttachment` (`POST <base>/agent/attachments`) — e.g.
66
+ * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media`, or your own storage. Gets the
67
+ * client's connection so it needs no configuration of its own. Resolve with the attachment your
68
+ * server's `AGENT_ATTACHMENT_STAGING` recognises by `mediaId`.
69
+ */
70
+ type AttachmentUploadStrategy = (file: File, options: UploadAttachmentOptions, connection: AgentConnection) => Promise<MessageAttachment>;
71
+ /**
72
+ * Everything the React layer asks of a server — the seam between `useAgentChat` (and the other
73
+ * hooks) and whatever serves the agent. {@link AgentClient} is the default implementation, over the
74
+ * library's own REST routes with `fetch`. An app with its own client — a generated one, a different
75
+ * auth scheme (cookie session + CSRF header), a backend that is not this library at all but speaks
76
+ * docs/stream-protocol.md — implements this instead and passes it as `useAgentChat({ backend })`.
77
+ *
78
+ * The streaming, thread and cancel members are required: without them there is no chat. The rest
79
+ * are optional; a hook that needs one the backend does not have throws
80
+ * {@link AgentBackendUnsupportedError} when it is called, so a backend only implements what its
81
+ * server supports.
82
+ */
83
+ interface AgentBackend {
84
+ /** Start a turn and return its SSE stream. Throw on a non-2xx answer. */
85
+ openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse>;
86
+ /** Attach to a streaming run. Resolve `null` when nothing is streaming under that id (HTTP 404). */
87
+ resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null>;
88
+ /** Hard-stop a run server-side. */
89
+ cancelStream(runId: string): Promise<unknown>;
90
+ listThreads(): Promise<ThreadSummary[]>;
91
+ getThread(id: string): Promise<ThreadDetail>;
92
+ updateThread(id: string, patch: ThreadPatch): Promise<unknown>;
93
+ deleteThread(id: string): Promise<unknown>;
94
+ forkFromMessage?(threadId: string, messageId: string): Promise<ThreadSummary>;
95
+ promoteThread?(id: string): Promise<unknown>;
96
+ truncateFromMessage?(threadId: string, messageId: string): Promise<unknown>;
97
+ approveToolCall?(input: {
98
+ toolCallId: string;
99
+ remember?: boolean;
100
+ via?: string;
101
+ }): Promise<unknown>;
102
+ rejectToolCall?(input: {
103
+ toolCallId: string;
104
+ reason?: string;
105
+ via?: string;
106
+ }): Promise<unknown>;
107
+ answerToolCall?(input: {
108
+ toolCallId: string;
109
+ answers?: Record<string, string[]>;
110
+ }): Promise<unknown>;
111
+ skipToolCall?(input: {
112
+ toolCallId: string;
113
+ }): Promise<unknown>;
114
+ uploadAttachment?(file: File, options?: UploadAttachmentOptions): Promise<MessageAttachment>;
115
+ listTools?(agent?: string): Promise<ToolCatalogEntry[]>;
116
+ listSkills?(threadId?: string): Promise<SkillCatalogEntry[]>;
117
+ getQuotaToday?(): Promise<QuotaView>;
118
+ /** `GET <base>/quota` — every budget window, and which one blocks sends, if any. */
119
+ getQuota?(): Promise<QuotaReport>;
120
+ /** `GET <base>/models?agent=` — what a model picker offers. */
121
+ listModels?(agent?: string): Promise<ModelCatalogView>;
122
+ /** `GET <base>/agents` — what an agent picker offers. */
123
+ listAgents?(): Promise<AgentCatalogEntry[]>;
124
+ setMessageFeedback?(messageId: string, input: MessageFeedbackInput): Promise<{
125
+ feedback: MessageFeedback | null;
126
+ }>;
127
+ }
128
+ /** An optional {@link AgentBackend} member the bound backend does not implement was called. */
129
+ declare class AgentBackendUnsupportedError extends Error {
130
+ readonly method: string;
131
+ constructor(method: string);
132
+ }
133
+ type OptionalMethod = {
134
+ [K in keyof AgentBackend]-?: undefined extends AgentBackend[K] ? K : never;
135
+ }[keyof AgentBackend];
136
+ /**
137
+ * The optional backend method `name`, bound — or a throw naming it. For hook code that has to call
138
+ * something a backend may not offer.
139
+ */
140
+ declare function requireBackendMethod<K extends OptionalMethod>(backend: AgentBackend, name: K): NonNullable<AgentBackend[K]>;
141
+
142
+ /**
143
+ * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can
144
+ * branch (e.g. 403 → "not your thread", 429 → quota) instead of string-matching a generic Error.
145
+ */
146
+ declare class AgentHttpError extends Error {
147
+ readonly status: number;
148
+ readonly method: string;
149
+ readonly path: string;
150
+ constructor(status: number, method: string, path: string, statusText: string);
151
+ }
152
+ /** The quota-today read-model: usage, the configured limit (null → unlimited), and USD spend. */
153
+ type QuotaToday = QuotaView;
154
+ interface CancelResult {
155
+ aborted: boolean;
156
+ }
157
+ interface OkResult {
158
+ ok: boolean;
159
+ }
160
+ interface AgentClientOptions {
161
+ /** Origin + base path, e.g. `https://api.example.com`. Defaults to `''`. */
162
+ baseUrl?: string;
163
+ /** Static headers merged into every request. */
164
+ headers?: Record<string, string>;
165
+ /**
166
+ * Resolved per request — for short-lived bearer tokens, or a CSRF header read from a cookie
167
+ * (`{ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }`), which has to be read at request time because
168
+ * the server may rotate it.
169
+ */
170
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
171
+ /**
172
+ * Forwarded to fetch so cookie auth works. Same-origin requests send cookies by default; set
173
+ * `'include'` when the API lives on another origin (and have it answer with credentialed CORS).
174
+ */
175
+ credentials?: RequestCredentials;
176
+ /** Injectable for tests / non-browser runtimes. */
177
+ fetch?: typeof fetch;
178
+ /**
179
+ * How `uploadAttachment` uploads. Omitted → `POST <base>/agent/attachments` (multipart). Pass
180
+ * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through
181
+ * nestjs-media, or your own {@link AttachmentUploadStrategy}.
182
+ */
183
+ attachments?: AttachmentUploadStrategy;
184
+ }
185
+ /**
186
+ * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.
187
+ * Used by `useAgentChat`, but standalone-usable (vanilla fetch, no React).
188
+ */
189
+ declare class AgentClient implements AgentBackend {
190
+ private readonly options;
191
+ constructor(options?: AgentClientOptions);
192
+ /** `POST /agent/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */
193
+ openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse>;
194
+ /**
195
+ * `GET /agent/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is
196
+ * streaming under that id (404).
197
+ */
198
+ resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null>;
199
+ /**
200
+ * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers
201
+ * the stored rating.
202
+ */
203
+ setMessageFeedback(messageId: string, input: MessageFeedbackInput): Promise<{
204
+ feedback: MessageFeedback | null;
205
+ }>;
206
+ listThreads(): Promise<ThreadSummary[]>;
207
+ /**
208
+ * The skills this caller can invoke right now, scope-resolved — the same list, built by the same
209
+ * call, that the model is offered, so what a user can type after a `/` and what the agent can
210
+ * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to
211
+ * one conversation; omitted, the server reads it as a brand-new thread.
212
+ */
213
+ listSkills(threadId?: string): Promise<SkillCatalogEntry[]>;
214
+ /**
215
+ * The tools this caller can reach through `agent` (the default agent when omitted), each with the
216
+ * server-declared `presentation` a chat narrates it by — the same list the model is offered.
217
+ * Prefer {@link useToolCatalog}, which fetches it once and shares it.
218
+ */
219
+ listTools(agent?: string): Promise<ToolCatalogEntry[]>;
220
+ getThread(id: string): Promise<ThreadDetail>;
221
+ deleteThread(id: string): Promise<void>;
222
+ forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary>;
223
+ renameThread(id: string, title: string): Promise<OkResult>;
224
+ /** General `PATCH /agent/threads/:threadId` — title and/or the thread's pinned default agent. */
225
+ updateThread(id: string, patch: ThreadPatch): Promise<OkResult>;
226
+ /**
227
+ * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —
228
+ * mirrors the backend's `POST /agent/attachments`. The returned {@link MessageAttachment} is
229
+ * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.
230
+ */
231
+ uploadAttachment(file: File, options?: UploadAttachmentOptions): Promise<MessageAttachment>;
232
+ private uploadWithProgress;
233
+ promoteThread(id: string): Promise<OkResult>;
234
+ truncateFromMessage(threadId: string, messageId: string): Promise<OkResult>;
235
+ /** `GET /agent/models?agent=` — the models this caller may pick, grouped by provider. */
236
+ listModels(agent?: string): Promise<ModelCatalogView>;
237
+ /** `GET /agent/agents` — the registered agents, the default one flagged. */
238
+ listAgents(): Promise<AgentCatalogEntry[]>;
239
+ /** `GET /agent/quota` — the caller's budget windows and the one blocking sends, if any. */
240
+ getQuota(): Promise<QuotaReport>;
241
+ getQuotaToday(): Promise<QuotaToday>;
242
+ cancelStream(runId: string): Promise<CancelResult>;
243
+ /**
244
+ * `remember` approves later calls of the same tool in the same thread; `via` names the surface
245
+ * the decision came through (the server records `'web'` when omitted).
246
+ */
247
+ approveToolCall(input: {
248
+ toolCallId: string;
249
+ remember?: boolean;
250
+ via?: string;
251
+ }): Promise<void>;
252
+ rejectToolCall(input: {
253
+ toolCallId: string;
254
+ reason?: string;
255
+ via?: string;
256
+ }): Promise<void>;
257
+ /**
258
+ * Settle a parked question set. `answers` is questionId → chosen option values; a question left
259
+ * out takes the pre-picked default the request carried, resolved server-side against the request
260
+ * the run already holds. Omit the whole object and the user has confirmed every pre-picked
261
+ * answer — which is the point of the surface, so it is a valid submission rather than a blank.
262
+ */
263
+ answerToolCall(input: {
264
+ toolCallId: string;
265
+ answers?: Record<string, string[]>;
266
+ }): Promise<void>;
267
+ /**
268
+ * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same
269
+ * values a confirmation would, and persists differently on purpose — only one of them is
270
+ * evidence the user chose them.
271
+ */
272
+ skipToolCall(input: {
273
+ toolCallId: string;
274
+ }): Promise<void>;
275
+ private fetchImpl;
276
+ /** This client's connection, for an {@link AttachmentUploadStrategy}. */
277
+ private connection;
278
+ private baseUrl;
279
+ private resolveHeaders;
280
+ private credentials;
281
+ private request;
282
+ private handleResponse;
283
+ }
284
+
285
+ export { type AgentBackend as A, type CancelResult as C, type MessageFeedbackInput as M, type QuotaToday as Q, type ResumeStreamRequest as R, type ThreadPatch as T, type UploadAttachmentOptions as U, AgentClient as a, type AttachmentUploadStrategy as b, AgentBackendUnsupportedError as c, type AgentClientOptions as d, type AgentConnection as e, AgentHttpError as f, type ChatStreamRequest as g, type ChatStreamResponse as h, requireBackendMethod as r };
@@ -0,0 +1,285 @@
1
+ import { ThreadSummary, ThreadDetail, MessageAttachment, ToolCatalogEntry, SkillCatalogEntry, QuotaView, QuotaReport, ModelCatalogView, AgentCatalogEntry, MessageFeedbackValue, MessageFeedback } from '@dudousxd/nestjs-agent-core';
2
+
3
+ /**
4
+ * Partial update accepted by `PATCH <base>/threads/:threadId`. `defaultAgent: null` clears a
5
+ * previously-set default back to the module's own default; omitting it leaves the thread's
6
+ * current default untouched.
7
+ */
8
+ interface ThreadPatch {
9
+ title?: string;
10
+ defaultAgent?: string | null;
11
+ /** Pin a catalog model on the thread; `null` unpins it. */
12
+ model?: string | null;
13
+ }
14
+ /** Starting a turn: the body `POST <base>/chat` takes (see docs/stream-protocol.md). */
15
+ interface ChatStreamRequest {
16
+ /** `{ message, threadId?, agent?, model?, attachments?, pageContext?, regenerate?, … }`. */
17
+ body: Record<string, unknown>;
18
+ /** Per-request headers the AI SDK was handed for this send. */
19
+ headers?: Record<string, string>;
20
+ /** Aborted when the user stops the turn. */
21
+ signal?: AbortSignal;
22
+ }
23
+ /** Attaching to a run that is already streaming: `GET <base>/chat/:runId/stream?after=<seq>`. */
24
+ interface ResumeStreamRequest {
25
+ runId: string;
26
+ /**
27
+ * The sequence number (SSE `id:`) of the last frame the client already has. Omitted → replay
28
+ * from the first frame. A backend that cannot skip may ignore it: the transport also drops frames
29
+ * at or below it.
30
+ */
31
+ after?: number;
32
+ headers?: Record<string, string>;
33
+ signal?: AbortSignal;
34
+ }
35
+ /**
36
+ * An open chat stream: the raw `text/event-stream` bytes in the stream-protocol framing, plus the
37
+ * identity a backend may learn before the first frame (the `X-Agent-Run-Id` / `X-Agent-Thread-Id`
38
+ * headers). The body's own `event: meta` frame supplies them otherwise.
39
+ */
40
+ interface ChatStreamResponse {
41
+ body: ReadableStream<Uint8Array>;
42
+ runId?: string;
43
+ threadId?: string;
44
+ }
45
+ /** What `POST <base>/messages/:id/feedback` takes. `value: null` clears the rating. */
46
+ interface MessageFeedbackInput {
47
+ value: MessageFeedbackValue | null;
48
+ comment?: string;
49
+ }
50
+ interface UploadAttachmentOptions {
51
+ signal?: AbortSignal;
52
+ /** Called with 0..1 as the upload progresses, when the backend can observe it. */
53
+ onProgress?: (fraction: number) => void;
54
+ }
55
+ /** How {@link AgentClient} reaches the server — handed to an {@link AttachmentUploadStrategy}. */
56
+ interface AgentConnection {
57
+ /** Origin + base path, trailing slash removed (`''` for same-origin). */
58
+ baseUrl: string;
59
+ /** The client's static + per-request headers, resolved now (auth, CSRF). */
60
+ headers: () => Promise<Record<string, string>>;
61
+ credentials?: RequestCredentials;
62
+ fetch: typeof fetch;
63
+ }
64
+ /**
65
+ * Replaces {@link AgentClient}'s own `uploadAttachment` (`POST <base>/agent/attachments`) — e.g.
66
+ * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media`, or your own storage. Gets the
67
+ * client's connection so it needs no configuration of its own. Resolve with the attachment your
68
+ * server's `AGENT_ATTACHMENT_STAGING` recognises by `mediaId`.
69
+ */
70
+ type AttachmentUploadStrategy = (file: File, options: UploadAttachmentOptions, connection: AgentConnection) => Promise<MessageAttachment>;
71
+ /**
72
+ * Everything the React layer asks of a server — the seam between `useAgentChat` (and the other
73
+ * hooks) and whatever serves the agent. {@link AgentClient} is the default implementation, over the
74
+ * library's own REST routes with `fetch`. An app with its own client — a generated one, a different
75
+ * auth scheme (cookie session + CSRF header), a backend that is not this library at all but speaks
76
+ * docs/stream-protocol.md — implements this instead and passes it as `useAgentChat({ backend })`.
77
+ *
78
+ * The streaming, thread and cancel members are required: without them there is no chat. The rest
79
+ * are optional; a hook that needs one the backend does not have throws
80
+ * {@link AgentBackendUnsupportedError} when it is called, so a backend only implements what its
81
+ * server supports.
82
+ */
83
+ interface AgentBackend {
84
+ /** Start a turn and return its SSE stream. Throw on a non-2xx answer. */
85
+ openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse>;
86
+ /** Attach to a streaming run. Resolve `null` when nothing is streaming under that id (HTTP 404). */
87
+ resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null>;
88
+ /** Hard-stop a run server-side. */
89
+ cancelStream(runId: string): Promise<unknown>;
90
+ listThreads(): Promise<ThreadSummary[]>;
91
+ getThread(id: string): Promise<ThreadDetail>;
92
+ updateThread(id: string, patch: ThreadPatch): Promise<unknown>;
93
+ deleteThread(id: string): Promise<unknown>;
94
+ forkFromMessage?(threadId: string, messageId: string): Promise<ThreadSummary>;
95
+ promoteThread?(id: string): Promise<unknown>;
96
+ truncateFromMessage?(threadId: string, messageId: string): Promise<unknown>;
97
+ approveToolCall?(input: {
98
+ toolCallId: string;
99
+ remember?: boolean;
100
+ via?: string;
101
+ }): Promise<unknown>;
102
+ rejectToolCall?(input: {
103
+ toolCallId: string;
104
+ reason?: string;
105
+ via?: string;
106
+ }): Promise<unknown>;
107
+ answerToolCall?(input: {
108
+ toolCallId: string;
109
+ answers?: Record<string, string[]>;
110
+ }): Promise<unknown>;
111
+ skipToolCall?(input: {
112
+ toolCallId: string;
113
+ }): Promise<unknown>;
114
+ uploadAttachment?(file: File, options?: UploadAttachmentOptions): Promise<MessageAttachment>;
115
+ listTools?(agent?: string): Promise<ToolCatalogEntry[]>;
116
+ listSkills?(threadId?: string): Promise<SkillCatalogEntry[]>;
117
+ getQuotaToday?(): Promise<QuotaView>;
118
+ /** `GET <base>/quota` — every budget window, and which one blocks sends, if any. */
119
+ getQuota?(): Promise<QuotaReport>;
120
+ /** `GET <base>/models?agent=` — what a model picker offers. */
121
+ listModels?(agent?: string): Promise<ModelCatalogView>;
122
+ /** `GET <base>/agents` — what an agent picker offers. */
123
+ listAgents?(): Promise<AgentCatalogEntry[]>;
124
+ setMessageFeedback?(messageId: string, input: MessageFeedbackInput): Promise<{
125
+ feedback: MessageFeedback | null;
126
+ }>;
127
+ }
128
+ /** An optional {@link AgentBackend} member the bound backend does not implement was called. */
129
+ declare class AgentBackendUnsupportedError extends Error {
130
+ readonly method: string;
131
+ constructor(method: string);
132
+ }
133
+ type OptionalMethod = {
134
+ [K in keyof AgentBackend]-?: undefined extends AgentBackend[K] ? K : never;
135
+ }[keyof AgentBackend];
136
+ /**
137
+ * The optional backend method `name`, bound — or a throw naming it. For hook code that has to call
138
+ * something a backend may not offer.
139
+ */
140
+ declare function requireBackendMethod<K extends OptionalMethod>(backend: AgentBackend, name: K): NonNullable<AgentBackend[K]>;
141
+
142
+ /**
143
+ * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can
144
+ * branch (e.g. 403 → "not your thread", 429 → quota) instead of string-matching a generic Error.
145
+ */
146
+ declare class AgentHttpError extends Error {
147
+ readonly status: number;
148
+ readonly method: string;
149
+ readonly path: string;
150
+ constructor(status: number, method: string, path: string, statusText: string);
151
+ }
152
+ /** The quota-today read-model: usage, the configured limit (null → unlimited), and USD spend. */
153
+ type QuotaToday = QuotaView;
154
+ interface CancelResult {
155
+ aborted: boolean;
156
+ }
157
+ interface OkResult {
158
+ ok: boolean;
159
+ }
160
+ interface AgentClientOptions {
161
+ /** Origin + base path, e.g. `https://api.example.com`. Defaults to `''`. */
162
+ baseUrl?: string;
163
+ /** Static headers merged into every request. */
164
+ headers?: Record<string, string>;
165
+ /**
166
+ * Resolved per request — for short-lived bearer tokens, or a CSRF header read from a cookie
167
+ * (`{ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }`), which has to be read at request time because
168
+ * the server may rotate it.
169
+ */
170
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
171
+ /**
172
+ * Forwarded to fetch so cookie auth works. Same-origin requests send cookies by default; set
173
+ * `'include'` when the API lives on another origin (and have it answer with credentialed CORS).
174
+ */
175
+ credentials?: RequestCredentials;
176
+ /** Injectable for tests / non-browser runtimes. */
177
+ fetch?: typeof fetch;
178
+ /**
179
+ * How `uploadAttachment` uploads. Omitted → `POST <base>/agent/attachments` (multipart). Pass
180
+ * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through
181
+ * nestjs-media, or your own {@link AttachmentUploadStrategy}.
182
+ */
183
+ attachments?: AttachmentUploadStrategy;
184
+ }
185
+ /**
186
+ * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.
187
+ * Used by `useAgentChat`, but standalone-usable (vanilla fetch, no React).
188
+ */
189
+ declare class AgentClient implements AgentBackend {
190
+ private readonly options;
191
+ constructor(options?: AgentClientOptions);
192
+ /** `POST /agent/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */
193
+ openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse>;
194
+ /**
195
+ * `GET /agent/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is
196
+ * streaming under that id (404).
197
+ */
198
+ resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null>;
199
+ /**
200
+ * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers
201
+ * the stored rating.
202
+ */
203
+ setMessageFeedback(messageId: string, input: MessageFeedbackInput): Promise<{
204
+ feedback: MessageFeedback | null;
205
+ }>;
206
+ listThreads(): Promise<ThreadSummary[]>;
207
+ /**
208
+ * The skills this caller can invoke right now, scope-resolved — the same list, built by the same
209
+ * call, that the model is offered, so what a user can type after a `/` and what the agent can
210
+ * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to
211
+ * one conversation; omitted, the server reads it as a brand-new thread.
212
+ */
213
+ listSkills(threadId?: string): Promise<SkillCatalogEntry[]>;
214
+ /**
215
+ * The tools this caller can reach through `agent` (the default agent when omitted), each with the
216
+ * server-declared `presentation` a chat narrates it by — the same list the model is offered.
217
+ * Prefer {@link useToolCatalog}, which fetches it once and shares it.
218
+ */
219
+ listTools(agent?: string): Promise<ToolCatalogEntry[]>;
220
+ getThread(id: string): Promise<ThreadDetail>;
221
+ deleteThread(id: string): Promise<void>;
222
+ forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary>;
223
+ renameThread(id: string, title: string): Promise<OkResult>;
224
+ /** General `PATCH /agent/threads/:threadId` — title and/or the thread's pinned default agent. */
225
+ updateThread(id: string, patch: ThreadPatch): Promise<OkResult>;
226
+ /**
227
+ * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —
228
+ * mirrors the backend's `POST /agent/attachments`. The returned {@link MessageAttachment} is
229
+ * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.
230
+ */
231
+ uploadAttachment(file: File, options?: UploadAttachmentOptions): Promise<MessageAttachment>;
232
+ private uploadWithProgress;
233
+ promoteThread(id: string): Promise<OkResult>;
234
+ truncateFromMessage(threadId: string, messageId: string): Promise<OkResult>;
235
+ /** `GET /agent/models?agent=` — the models this caller may pick, grouped by provider. */
236
+ listModels(agent?: string): Promise<ModelCatalogView>;
237
+ /** `GET /agent/agents` — the registered agents, the default one flagged. */
238
+ listAgents(): Promise<AgentCatalogEntry[]>;
239
+ /** `GET /agent/quota` — the caller's budget windows and the one blocking sends, if any. */
240
+ getQuota(): Promise<QuotaReport>;
241
+ getQuotaToday(): Promise<QuotaToday>;
242
+ cancelStream(runId: string): Promise<CancelResult>;
243
+ /**
244
+ * `remember` approves later calls of the same tool in the same thread; `via` names the surface
245
+ * the decision came through (the server records `'web'` when omitted).
246
+ */
247
+ approveToolCall(input: {
248
+ toolCallId: string;
249
+ remember?: boolean;
250
+ via?: string;
251
+ }): Promise<void>;
252
+ rejectToolCall(input: {
253
+ toolCallId: string;
254
+ reason?: string;
255
+ via?: string;
256
+ }): Promise<void>;
257
+ /**
258
+ * Settle a parked question set. `answers` is questionId → chosen option values; a question left
259
+ * out takes the pre-picked default the request carried, resolved server-side against the request
260
+ * the run already holds. Omit the whole object and the user has confirmed every pre-picked
261
+ * answer — which is the point of the surface, so it is a valid submission rather than a blank.
262
+ */
263
+ answerToolCall(input: {
264
+ toolCallId: string;
265
+ answers?: Record<string, string[]>;
266
+ }): Promise<void>;
267
+ /**
268
+ * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same
269
+ * values a confirmation would, and persists differently on purpose — only one of them is
270
+ * evidence the user chose them.
271
+ */
272
+ skipToolCall(input: {
273
+ toolCallId: string;
274
+ }): Promise<void>;
275
+ private fetchImpl;
276
+ /** This client's connection, for an {@link AttachmentUploadStrategy}. */
277
+ private connection;
278
+ private baseUrl;
279
+ private resolveHeaders;
280
+ private credentials;
281
+ private request;
282
+ private handleResponse;
283
+ }
284
+
285
+ export { type AgentBackend as A, type CancelResult as C, type MessageFeedbackInput as M, type QuotaToday as Q, type ResumeStreamRequest as R, type ThreadPatch as T, type UploadAttachmentOptions as U, AgentClient as a, type AttachmentUploadStrategy as b, AgentBackendUnsupportedError as c, type AgentClientOptions as d, type AgentConnection as e, AgentHttpError as f, type ChatStreamRequest as g, type ChatStreamResponse as h, requireBackendMethod as r };
package/dist/index.cjs CHANGED
@@ -225,6 +225,9 @@ var AgentClient = class {
225
225
  * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.
226
226
  */
227
227
  async uploadAttachment(file, options = {}) {
228
+ if (this.options.attachments !== void 0) {
229
+ return this.options.attachments(file, options, this.connection());
230
+ }
228
231
  if (options.onProgress !== void 0 && this.options.fetch === void 0 && typeof XMLHttpRequest !== "undefined") {
229
232
  return this.uploadWithProgress(file, options);
230
233
  }
@@ -339,6 +342,15 @@ var AgentClient = class {
339
342
  fetchImpl() {
340
343
  return this.options.fetch ?? globalThis.fetch;
341
344
  }
345
+ /** This client's connection, for an {@link AttachmentUploadStrategy}. */
346
+ connection() {
347
+ return {
348
+ baseUrl: this.baseUrl(),
349
+ headers: /* @__PURE__ */ __name(() => this.resolveHeaders(), "headers"),
350
+ fetch: this.fetchImpl(),
351
+ ...this.credentials()
352
+ };
353
+ }
342
354
  baseUrl() {
343
355
  return (this.options.baseUrl ?? "").replace(/\/$/, "");
344
356
  }
@@ -4537,6 +4549,9 @@ function useAgentChat(options) {
4537
4549
  ...options.fetch !== void 0 ? {
4538
4550
  fetch: options.fetch
4539
4551
  } : {},
4552
+ ...options.attachments !== void 0 ? {
4553
+ attachments: options.attachments
4554
+ } : {},
4540
4555
  getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders")
4541
4556
  });
4542
4557
  }, []);