@dudousxd/nestjs-agent-react 0.20.0 → 0.22.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
@@ -19,24 +19,58 @@ pnpm add @dudousxd/nestjs-agent-react @ai-sdk/react ai react
19
19
  import { useAgentChat, MessageList, ChatInput } from '@dudousxd/nestjs-agent-react';
20
20
 
21
21
  function Chat() {
22
- const chat = useAgentChat({
23
- baseUrl: '/agent',
24
- getHeaders: () => ({ 'x-actor-id': me.id, 'x-actor-role': me.roles.join(',') }),
25
- });
22
+ const chat = useAgentChat(); // same-origin, `/agent` — no provider needed
26
23
  return (
27
24
  <>
28
- <MessageList
29
- messages={chat.messages}
30
- status={chat.status}
31
- regeneratable
32
- onRegenerate={() => chat.regenerate()}
33
- />
25
+ <MessageList messages={chat.messages} status={chat.status} />
34
26
  <ChatInput onSubmit={(text) => chat.sendMessage({ text })} />
35
27
  </>
36
28
  );
37
29
  }
38
30
  ```
39
31
 
32
+ `useAgentChat` does the wiring itself: give it a `threadId` and it loads that thread's history and
33
+ re-attaches to a turn still streaming on it (`history: false` / `resume: false` opt out); it gates
34
+ sends on the reported quota; approvals and question sets in the transcript are actionable with no
35
+ handlers passed. On top of the AI SDK chat it returns:
36
+
37
+ | On `chat` | What it is |
38
+ |---|---|
39
+ | `transcript` | `useChatTranscript` already bound to the chat — approve/reject/answer/skip, stop, fork, regenerate, the tool catalog, and timestamps/usage read from message metadata. Override any of it with `useAgentChat({ transcript: { … } })`. |
40
+ | `composer` | `{ text, setText, files, canSend, blockedBy, submit() }` — `files` is `useAttachments` on the chat's backend; `submit()` sends the draft with the ready files attached and clears both; `blockedBy` is `'empty' \| 'busy' \| 'uploading' \| 'quota'`. |
41
+ | `models` | `{ list, providers, selected, select(id), pinToThread(id) }` — loaded the first time `list` is read. |
42
+ | `quota` / `blocked` | `useQuota`'s state, and the window blocking sends (the `blocked` option overrides it). |
43
+ | `approve` / `reject` / `answer` / `skip` | `({ toolCallId, … })` — the same object shape the transcript's handlers take. |
44
+ | `fork` / `truncateFrom` / `promote` | `({ messageId, threadId? })` / `({ threadId? })`, defaulting to this chat's thread. |
45
+ | `cancel`, `regenerate`, `getThreadId`, `backend`, `runId`, `activeRunId`, `isLoadingHistory`, `connection`, `background` | — |
46
+
47
+ Threads (list, rename, delete) are `useThreads()`; the granular hooks stay the escape hatch.
48
+
49
+ ### Configure the connection once: `<AgentProvider>`
50
+
51
+ Every hook (`useAgentChat`, `useThreads`, `useModels`, `useAgents`, `useQuota`, `useToolCatalog`,
52
+ `useMessageFeedback`, `useAttachments`) talks to the enclosing provider's backend unless handed a
53
+ `backend` of its own. Without a provider they share one same-origin client on `/agent`.
54
+
55
+ ```tsx
56
+ import { AgentProvider } from '@dudousxd/nestjs-agent-react';
57
+ import { mediaAttachments } from '@dudousxd/nestjs-agent-react/media';
58
+
59
+ <AgentProvider
60
+ baseUrl="https://api.example.com" // origin only; default '' (same origin)
61
+ path="api/agent" // AgentModule's `path` + global prefix; default 'agent'
62
+ credentials="include"
63
+ getHeaders={() => ({ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') })}
64
+ attachments={{ upload: mediaAttachments() }} // optional
65
+ genui={{ registry, catalog }} // optional — same props as <GenuiProvider>
66
+ >
67
+ <App />
68
+ </AgentProvider>
69
+ ```
70
+
71
+ `<AgentProvider backend={myBackend}>` takes any `AgentBackend` instead of the connection props.
72
+ `useAgentBackend()` returns the backend in scope, for your own calls.
73
+
40
74
  ## Bring your own UI
41
75
 
42
76
  The package is **headless by design**, in four layers, and only the top one renders anything:
@@ -58,15 +92,15 @@ tool grouping, the edit machine, or the copy flash, and it never imports `Messag
58
92
  import { useAgentChat, useChatTranscript } from '@dudousxd/nestjs-agent-react';
59
93
 
60
94
  function Chat() {
61
- const chat = useAgentChat({ baseUrl: '/agent' });
95
+ const chat = useAgentChat();
96
+ // `chat.transcript` is this, pre-wired; `useChatTranscript` directly for any AI SDK chat.
62
97
  const transcript = useChatTranscript({
63
98
  messages: chat.messages,
64
99
  status: chat.status,
65
100
  editable: true,
66
- onEditSubmit: (id, text) => chat.sendMessage({ text }),
67
- onFork: (id) => chat.forkThread(threadId, id),
101
+ onEditSubmit: ({ text }) => chat.sendMessage({ text }),
102
+ onFork: ({ messageId }) => chat.fork({ messageId }),
68
103
  onStop: () => chat.cancel(),
69
- getUsage: (message) => readUsage(message),
70
104
  });
71
105
 
72
106
  return (
@@ -134,8 +168,8 @@ catalog to the transcript and every tool call carries a `description`, and every
134
168
  `activity` grouping:
135
169
 
136
170
  ```tsx
137
- const chat = useAgentChat({ baseUrl: '/agent' });
138
- const { catalog } = useToolCatalog({ client: chat.client });
171
+ const chat = useAgentChat();
172
+ const { catalog } = useToolCatalog(); // the provider's backend; `{ backend, agent }` to override
139
173
  const transcript = useChatTranscript({ messages: chat.messages, status: chat.status, toolCatalog: catalog });
140
174
 
141
175
  // in a `tools` block:
@@ -330,11 +364,11 @@ the model, so a `/` menu and the agent's own reach cannot drift apart:
330
364
  ```tsx
331
365
  import { createSkillsSource, useAgentChat } from '@dudousxd/nestjs-agent-react';
332
366
 
333
- const chat = useAgentChat({ baseUrl: '/agent', onThreadCreated: setThreadId });
367
+ const chat = useAgentChat({ onThreadCreated: setThreadId });
334
368
  // Identity-stable so the list is read once per thread, not once per keystroke.
335
369
  const sources = useMemo(
336
- () => [createSkillsSource({ client: chat.client, getThreadId: () => threadId })],
337
- [chat.client],
370
+ () => [createSkillsSource({ backend: chat.backend, getThreadId: () => threadId })],
371
+ [chat.backend],
338
372
  );
339
373
  ```
340
374
 
@@ -420,14 +454,11 @@ import { isTextUIPart, isToolUIPart } from 'ai';
420
454
 
421
455
  function CustomChat({ threadId }: { threadId?: string }) {
422
456
  const chat = useAgentChat({
423
- baseUrl: '/agent',
424
457
  // Omit the key entirely when absent — `UseAgentChatOptions` is built with
425
458
  // `exactOptionalPropertyTypes`, so an explicit `threadId: undefined` doesn't type-check.
426
459
  ...(threadId !== undefined ? { threadId } : {}),
427
460
  agent: 'support',
428
- // Reattach to a turn still streaming when the page loaded — survives a refresh.
429
- resume: true,
430
- getHeaders: () => ({ 'x-actor-id': currentUser.id }),
461
+ // A `threadId` loads its history and re-attaches to a turn still streaming — by default.
431
462
  onThreadCreated: (newThreadId) => router.replace(`/chat/${newThreadId}`),
432
463
  // Fires once per run when the SERVER is done writing (title + terminal state persisted) —
433
464
  // the right signal to refetch a thread list/sidebar; `onFinish` only means "a turn rendered".
@@ -472,10 +503,7 @@ disables itself while busy never noticed; a double-submitted one, and StrictMode
472
503
  effect, did.
473
504
 
474
505
  `chat` is the AI SDK v7 `useChat` return value (`messages`, `status`, `sendMessage`, `stop`, …) spread
475
- together with the extras: `runId`/`activeRunId`, `connection`, `getThreadId`, `backend` (the `AgentBackend`; also
476
- under its old name `client`), thread list/CRUD
477
- (`threads`, `loadThreads`, `loadThread`, `deleteThread`, `forkThread`, `renameThread`, `promoteThread`,
478
- `truncateFromMessage`), `quota`/`loadQuota`, `cancel`, HITL `approve`/`reject` and `answer`/`skip`, and `regenerate`.
506
+ together with the extras in the table above.
479
507
  `MyToolCard`'s `part` prop above types as the exported `AnyToolUIPart` (`ToolUIPart | DynamicToolUIPart`
480
508
  — `MessageItem` uses the same union for its own `renderToolPart` callback).
481
509
 
@@ -517,7 +545,8 @@ const backend: AgentBackend = {
517
545
  setMessageFeedback: (id, input) => api.messages.feedback(id, input),
518
546
  };
519
547
 
520
- const chat = useAgentChat({ backend }); // chat.backend === backend, typed as yours
548
+ <AgentProvider backend={backend}>…</AgentProvider>; // every hook below uses it
549
+ const chat = useAgentChat({ backend }); // or per hook — chat.backend === backend, typed as yours
521
550
  ```
522
551
 
523
552
  Calling a hook method whose optional backend member is missing throws
@@ -528,15 +557,17 @@ attachments?, pageContext?, regenerate? }`; the SSE it returns and the REST shap
528
557
  **Cookie session + CSRF with the default client.** `credentials` and `getHeaders` are all it takes —
529
558
  `getHeaders` runs per request, so a rotated token is picked up:
530
559
 
531
- ```ts
560
+ ```tsx
532
561
  const readCookie = (name: string) =>
533
562
  decodeURIComponent(document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))?.[1] ?? '');
534
563
 
535
- useAgentChat({
536
- baseUrl: '/api',
537
- credentials: 'include', // 'same-origin' (the fetch default) is enough when the API is same-origin
538
- getHeaders: () => ({ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }),
539
- });
564
+ <AgentProvider
565
+ path="api/agent"
566
+ credentials="include" // 'same-origin' (the fetch default) is enough when the API is same-origin
567
+ getHeaders={() => ({ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') })}
568
+ >
569
+ <App />
570
+ </AgentProvider>;
540
571
  ```
541
572
 
542
573
  ### Reconnecting a dropped stream
@@ -554,8 +585,8 @@ the last failed attempt the turn ends with an error.
554
585
  ```tsx
555
586
  import { useMessageFeedback, useThreads } from '@dudousxd/nestjs-agent-react';
556
587
 
557
- const { threads, isLoading, rename, remove, refresh } = useThreads({ backend: chat.backend });
558
- const feedback = useMessageFeedback({ backend: chat.backend, threadId: chat.getThreadId });
588
+ const { threads, isLoading, rename, remove, refresh } = useThreads();
589
+ const feedback = useMessageFeedback({ threadId: chat.getThreadId });
559
590
 
560
591
  <button aria-pressed={feedback.feedbackOf(message)?.value === 'up'}
561
592
  onClick={() => feedback.toggle(message, 'up')}>Helpful</button>
@@ -570,14 +601,10 @@ its run persisted (the live message carries `metadata.runId`).
570
601
  ### Picking a model and an agent
571
602
 
572
603
  ```tsx
573
- import { useAgents, useModels } from '@dudousxd/nestjs-agent-react';
604
+ const chat = useAgentChat();
605
+ const { providers, selected, select } = chat.models; // loaded on first read
574
606
 
575
- const [model, setModel] = useState<string | undefined>();
576
- const chat = useAgentChat({ backend, model }); // sent as the body's `model` on every turn
577
- const { providers, find, defaultModel } = useModels({ backend: chat.backend, agent: 'support' });
578
- const { agents } = useAgents({ backend: chat.backend });
579
-
580
- <select value={model ?? defaultModel ?? ''} onChange={(e) => setModel(e.target.value)}>
607
+ <select value={selected ?? ''} onChange={(e) => select(e.target.value)}>
581
608
  {providers.map((p) => (
582
609
  <optgroup key={p.id} label={p.label}>
583
610
  {p.models.map((m) => (
@@ -590,10 +617,14 @@ const { agents } = useAgents({ backend: chat.backend });
590
617
  </select>
591
618
  ```
592
619
 
593
- `useModels` reads `GET <base>/models?agent=` (models grouped by provider, with badges and
594
- availability; `models` is the same list flattened, `find(id)` looks one up). A single send can
595
- override with `sendMessage(msg, { body: { model } })`; `chat.setThreadModel(id)` pins a model on
596
- the thread (`null` unpins), so every later turn without its own runs on it. The server refuses a
620
+ `chat.models` reads `GET <base>/models?agent=` (models grouped by provider, with badges and
621
+ availability; `list` is the same flattened) the first time `list`/`providers` is read. `select(id)`
622
+ runs the following turns on it (sent as the body's `model`); `selected` is the pick, else the
623
+ thread's pin, else the server default. `pinToThread(id)` pins it on the thread (`null` unpins) so
624
+ it survives reloads — on a chat with no thread yet, the pin lands when the first send creates one.
625
+ `useAgentChat({ model })` controls the model yourself; a single send can override it with
626
+ `sendMessage(msg, { body: { model } })`. `useModels()` / `useAgents()` are the standalone hooks
627
+ (an agent picker: `useAgents().agents`, sent as `useAgentChat({ agent })`). The server refuses a
597
628
  model its catalog does not offer as available.
598
629
 
599
630
  ### Quota
@@ -601,17 +632,19 @@ model its catalog does not offer as available.
601
632
  ```tsx
602
633
  import { QuotaBlockedError, useQuota } from '@dudousxd/nestjs-agent-react';
603
634
 
604
- const quota = useQuota({ backend }); // GET <base>/quota
605
- const chat = useAgentChat({ backend, blocked: quota.blocked });
635
+ const chat = useAgentChat(); // reads GET <base>/quota and gates sends on it
636
+ const { quota } = chat;
606
637
 
607
638
  <meter value={quota.month?.usedUsd} max={quota.month?.limitUsd} />
608
- {quota.blocked ? <p>{quota.blocked.reason}</p> : null}
639
+ {chat.blocked ? <p>{chat.blocked.reason}</p> : null}
609
640
  ```
610
641
 
611
- `useQuota` returns every window (`day`, `month`, …) with its usage and ceilings, and `blocked` when
612
- one is exhausted; it re-reads after every run a chat on the same backend settles (`pollMs` to poll
613
- as well). With `blocked` passed in, `sendMessage`/`regenerate` reject with `QuotaBlockedError`
614
- instead of starting a turn the server would refuse with `429`.
642
+ `chat.quota` (and the standalone `useQuota()`) returns every window (`day`, `month`, …) with its
643
+ usage and ceilings, and `blocked` when one is exhausted; it re-reads after every run a chat on the
644
+ same backend settles. While a window is exhausted, `sendMessage`/`regenerate` reject with
645
+ `QuotaBlockedError` (and `chat.composer.blockedBy` is `'quota'`) instead of starting a turn the server
646
+ would refuse with `429`. `useAgentChat({ blocked })` overrides the gate (`null` never blocks);
647
+ `quota: false` skips the request.
615
648
 
616
649
  ### The transport, standalone
617
650
 
@@ -623,7 +656,7 @@ import { useChat } from '@ai-sdk/react';
623
656
  import { AgentChatTransport } from '@dudousxd/nestjs-agent-react';
624
657
 
625
658
  const transport = new AgentChatTransport({
626
- baseUrl: '/agent',
659
+ path: 'agent', // the default
627
660
  getHeaders: () => ({ 'x-actor-id': currentUser.id }),
628
661
  onMeta: ({ runId, threadId }) => console.log('turn started', runId, threadId),
629
662
  });
@@ -647,16 +680,15 @@ contract — for a backend that serves these routes without this library's loop
647
680
 
648
681
  ### Loading persisted history
649
682
 
650
- A reloaded thread's `StoredMessage[]` (from `AgentClient.getThread`) needs converting to `UIMessage[]`
651
- before it can seed `useChat`'s `initialMessages`:
683
+ `useAgentChat({ threadId })` loads it for you. To seed a chat with history you already have (SSR,
684
+ a cache), convert the thread's `StoredMessage[]` and pass it as `initialMessages` — the hook then
685
+ skips its own read:
652
686
 
653
687
  ```ts
654
688
  import { storedThreadToUiMessages } from '@dudousxd/nestjs-agent-react';
655
689
 
656
- const detail = await chat.client.getThread(threadId);
657
690
  const initialMessages = storedThreadToUiMessages(detail.messages);
658
- // Feed into useAgentChat({ threadId, initialMessages, ... }) on the mount that owns this thread —
659
- // `initialMessages` is only read once, on mount.
691
+ useAgentChat({ threadId, initialMessages });
660
692
  ```
661
693
 
662
694
  `storedThreadToUiMessages` merges the store's one-row-per-model-iteration turns (a turn with tool
@@ -668,19 +700,17 @@ persisted pushed components as `data-ui` parts, so a reloaded thread shows what
668
700
 
669
701
  ### Attachments
670
702
 
671
- `useAttachments` is the composer's file tray without the tray: validation, one upload per file
672
- with progress and cancel, retry, image previews, and the handlers for a file input, a drop zone and
673
- paste.
703
+ `chat.composer.files` (or `useAttachments()` on its own) is the composer's file tray without the
704
+ tray: validation, one upload per file with progress and cancel, retry, image previews, and the
705
+ handlers for a file input, a drop zone and paste. `chat.composer.submit()` sends the ready files
706
+ with the draft and clears them.
674
707
 
675
708
  ```tsx
676
- import { messageFiles, useAttachments } from '@dudousxd/nestjs-agent-react';
677
-
678
- const files = useAttachments({
679
- backend: chat.backend, // or `upload: (file, { signal, onProgress }) => myUpload(file)`
680
- accept: 'image/*,.pdf',
681
- maxBytes: 20 * 1024 * 1024,
682
- maxFiles: 5,
709
+ const chat = useAgentChat({
710
+ // optional — or `upload: (file, { signal, onProgress }) => myUpload(file)`
711
+ composer: { accept: 'image/*,.pdf', maxBytes: 20 * 1024 * 1024, maxFiles: 5 },
683
712
  });
713
+ const { files } = chat.composer;
684
714
 
685
715
  <div {...files.dropZoneProps} data-dragging={files.isDragging}>
686
716
  <textarea onPaste={files.onPaste} />
@@ -695,13 +725,8 @@ const files = useAttachments({
695
725
  ))}
696
726
  </div>
697
727
 
698
- <button
699
- disabled={files.isUploading}
700
- onClick={async () => {
701
- await chat.sendMessage({ text }, { body: { attachments: files.refs } });
702
- files.clear();
703
- }}
704
- />
728
+ <textarea value={chat.composer.text} onChange={(e) => chat.composer.setText(e.target.value)} />
729
+ <button disabled={!chat.composer.canSend} onClick={() => chat.composer.submit()} />
705
730
  ```
706
731
 
707
732
  An item is `uploading`, `ready`, `error` (retry with `files.retry(id)`) or `rejected` (failed
@@ -713,25 +738,25 @@ flight. `messageFiles(message)` reads the files back off any message — live or
713
738
 
714
739
  With `AgentMediaAttachmentsModule` on the server (`@dudousxd/nestjs-agent/media`), one option turns
715
740
  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):
741
+ and retry, on the client's own connection (origin, path, headers, credentials):
717
742
 
718
743
  ```tsx
719
744
  import { mediaAttachments } from '@dudousxd/nestjs-agent-react/media';
720
745
 
721
- const chat = useAgentChat({ attachments: mediaAttachments() });
722
- const files = useAttachments({ backend: chat.backend });
746
+ <AgentProvider attachments={{ upload: mediaAttachments() }}>…</AgentProvider>;
747
+ const files = useAttachments(); // uploads resumably
723
748
  ```
724
749
 
725
750
  Headless; `@dudousxd/nestjs-media-client` is an optional peer only this subpath uses. Extending it:
726
751
 
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 })`.
752
+ - `mediaAttachments({ chunkSize, retries })` — tuning (the path comes from the client's `path`).
753
+ - `new AgentClient({ …connection, attachments: { upload: mediaAttachments() } })` — your own client.
754
+ - `createMediaUpload(connection)` — a bare `upload` for `useAttachments({ upload })`, or the
755
+ `uploadAttachment` member of your own `AgentBackend`.
731
756
  - Refusals throw `MediaUploadError` with the HTTP `status` (`413`, `415`, …); an aborted or failed
732
757
  upload is discarded on the server.
733
758
 
734
- Your own storage instead: `useAgentChat({ attachments: (file, { signal, onProgress }, connection) => … })`
759
+ Your own storage instead: `<AgentProvider attachments={{ upload: (file, { signal, onProgress }, connection) => … }}>`
735
760
  (an `AttachmentUploadStrategy`), or `useAttachments({ upload })`, resolving to a
736
761
  `{ mediaId, url, contentType, name }` your server's `AGENT_ATTACHMENT_STAGING` recognises.
737
762
 
@@ -1,4 +1,4 @@
1
- import { ThreadSummary, ThreadDetail, MessageAttachment, ToolCatalogEntry, SkillCatalogEntry, QuotaView, QuotaReport, ModelCatalogView, AgentCatalogEntry, MessageFeedbackValue, MessageFeedback } from '@dudousxd/nestjs-agent-core';
1
+ import { ThreadSummary, ThreadDetail, MessageAttachment, ToolCatalogEntry, SkillCatalogEntry, QuotaReport, ModelCatalogView, AgentCatalogEntry, MessageFeedbackValue, MessageFeedback } from '@dudousxd/nestjs-agent-core';
2
2
 
3
3
  /**
4
4
  * Partial update accepted by `PATCH <base>/threads/:threadId`. `defaultAgent: null` clears a
@@ -54,15 +54,17 @@ interface UploadAttachmentOptions {
54
54
  }
55
55
  /** How {@link AgentClient} reaches the server — handed to an {@link AttachmentUploadStrategy}. */
56
56
  interface AgentConnection {
57
- /** Origin + base path, trailing slash removed (`''` for same-origin). */
57
+ /** The server's origin, trailing slash removed (`''` for same-origin). */
58
58
  baseUrl: string;
59
+ /** The agent's route prefix, normalized to a leading slash (`'/agent'`), or `''`. */
60
+ path: string;
59
61
  /** The client's static + per-request headers, resolved now (auth, CSRF). */
60
62
  headers: () => Promise<Record<string, string>>;
61
63
  credentials?: RequestCredentials;
62
64
  fetch: typeof fetch;
63
65
  }
64
66
  /**
65
- * Replaces {@link AgentClient}'s own `uploadAttachment` (`POST <base>/agent/attachments`) — e.g.
67
+ * Replaces {@link AgentClient}'s own `uploadAttachment` (`POST <path>/attachments`) — e.g.
66
68
  * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media`, or your own storage. Gets the
67
69
  * client's connection so it needs no configuration of its own. Resolve with the attachment your
68
70
  * server's `AGENT_ATTACHMENT_STAGING` recognises by `mediaId`.
@@ -73,7 +75,8 @@ type AttachmentUploadStrategy = (file: File, options: UploadAttachmentOptions, c
73
75
  * hooks) and whatever serves the agent. {@link AgentClient} is the default implementation, over the
74
76
  * library's own REST routes with `fetch`. An app with its own client — a generated one, a different
75
77
  * 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 })`.
78
+ * docs/stream-protocol.md — implements this instead and passes it as `<AgentProvider backend>` (or
79
+ * per hook, `useAgentChat({ backend })`).
77
80
  *
78
81
  * The streaming, thread and cancel members are required: without them there is no chat. The rest
79
82
  * are optional; a hook that needs one the backend does not have throws
@@ -114,7 +117,6 @@ interface AgentBackend {
114
117
  uploadAttachment?(file: File, options?: UploadAttachmentOptions): Promise<MessageAttachment>;
115
118
  listTools?(agent?: string): Promise<ToolCatalogEntry[]>;
116
119
  listSkills?(threadId?: string): Promise<SkillCatalogEntry[]>;
117
- getQuotaToday?(): Promise<QuotaView>;
118
120
  /** `GET <base>/quota` — every budget window, and which one blocks sends, if any. */
119
121
  getQuota?(): Promise<QuotaReport>;
120
122
  /** `GET <base>/models?agent=` — what a model picker offers. */
@@ -149,8 +151,6 @@ declare class AgentHttpError extends Error {
149
151
  readonly path: string;
150
152
  constructor(status: number, method: string, path: string, statusText: string);
151
153
  }
152
- /** The quota-today read-model: usage, the configured limit (null → unlimited), and USD spend. */
153
- type QuotaToday = QuotaView;
154
154
  interface CancelResult {
155
155
  aborted: boolean;
156
156
  }
@@ -158,8 +158,16 @@ interface OkResult {
158
158
  ok: boolean;
159
159
  }
160
160
  interface AgentClientOptions {
161
- /** Origin + base path, e.g. `https://api.example.com`. Defaults to `''`. */
161
+ /**
162
+ * The server's origin, e.g. `https://api.example.com`. Defaults to `''` (same origin). The
163
+ * agent's route prefix is {@link AgentClientOptions.path}, not part of this.
164
+ */
162
165
  baseUrl?: string;
166
+ /**
167
+ * The agent's route prefix — `AgentModule`'s `path`, with any global prefix in front
168
+ * (`'api/agent'`). Leading/trailing slashes are optional. Defaults to `'agent'`.
169
+ */
170
+ path?: string;
163
171
  /** Static headers merged into every request. */
164
172
  headers?: Record<string, string>;
165
173
  /**
@@ -175,12 +183,15 @@ interface AgentClientOptions {
175
183
  credentials?: RequestCredentials;
176
184
  /** Injectable for tests / non-browser runtimes. */
177
185
  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;
186
+ /** Attachment uploads. */
187
+ attachments?: {
188
+ /**
189
+ * How `uploadAttachment` uploads. Omitted → `POST <path>/attachments` (multipart). Pass
190
+ * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through
191
+ * nestjs-media, or your own {@link AttachmentUploadStrategy}.
192
+ */
193
+ upload?: AttachmentUploadStrategy;
194
+ };
184
195
  }
185
196
  /**
186
197
  * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.
@@ -189,10 +200,10 @@ interface AgentClientOptions {
189
200
  declare class AgentClient implements AgentBackend {
190
201
  private readonly options;
191
202
  constructor(options?: AgentClientOptions);
192
- /** `POST /agent/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */
203
+ /** `POST <path>/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */
193
204
  openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse>;
194
205
  /**
195
- * `GET /agent/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is
206
+ * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is
196
207
  * streaming under that id (404).
197
208
  */
198
209
  resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null>;
@@ -220,25 +231,23 @@ declare class AgentClient implements AgentBackend {
220
231
  getThread(id: string): Promise<ThreadDetail>;
221
232
  deleteThread(id: string): Promise<void>;
222
233
  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. */
234
+ /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */
225
235
  updateThread(id: string, patch: ThreadPatch): Promise<OkResult>;
226
236
  /**
227
237
  * 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
238
+ * mirrors the backend's `POST <path>/attachments`. The returned {@link MessageAttachment} is
229
239
  * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.
230
240
  */
231
241
  uploadAttachment(file: File, options?: UploadAttachmentOptions): Promise<MessageAttachment>;
232
242
  private uploadWithProgress;
233
243
  promoteThread(id: string): Promise<OkResult>;
234
244
  truncateFromMessage(threadId: string, messageId: string): Promise<OkResult>;
235
- /** `GET /agent/models?agent=` — the models this caller may pick, grouped by provider. */
245
+ /** `GET <path>/models?agent=` — the models this caller may pick, grouped by provider. */
236
246
  listModels(agent?: string): Promise<ModelCatalogView>;
237
- /** `GET /agent/agents` — the registered agents, the default one flagged. */
247
+ /** `GET <path>/agents` — the registered agents, the default one flagged. */
238
248
  listAgents(): Promise<AgentCatalogEntry[]>;
239
- /** `GET /agent/quota` — the caller's budget windows and the one blocking sends, if any. */
249
+ /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */
240
250
  getQuota(): Promise<QuotaReport>;
241
- getQuotaToday(): Promise<QuotaToday>;
242
251
  cancelStream(runId: string): Promise<CancelResult>;
243
252
  /**
244
253
  * `remember` approves later calls of the same tool in the same thread; `via` names the surface
@@ -276,10 +285,13 @@ declare class AgentClient implements AgentBackend {
276
285
  /** This client's connection, for an {@link AttachmentUploadStrategy}. */
277
286
  private connection;
278
287
  private baseUrl;
288
+ private agentPath;
289
+ /** Origin + agent path: what every route hangs off. */
290
+ private root;
279
291
  private resolveHeaders;
280
292
  private credentials;
281
293
  private request;
282
294
  private handleResponse;
283
295
  }
284
296
 
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 };
297
+ export { type AgentBackend as A, type CancelResult as C, type MessageFeedbackInput as M, type ResumeStreamRequest as R, type ThreadPatch as T, type UploadAttachmentOptions as U, type AttachmentUploadStrategy as a, AgentBackendUnsupportedError as b, AgentClient as c, type AgentClientOptions as d, type AgentConnection as e, AgentHttpError as f, type ChatStreamRequest as g, type ChatStreamResponse as h, requireBackendMethod as r };