@dudousxd/nestjs-agent-react 0.21.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
@@ -22,18 +22,30 @@ function Chat() {
22
22
  const chat = useAgentChat(); // same-origin, `/agent` — no provider needed
23
23
  return (
24
24
  <>
25
- <MessageList
26
- messages={chat.messages}
27
- status={chat.status}
28
- regeneratable
29
- onRegenerate={() => chat.regenerate()}
30
- />
25
+ <MessageList messages={chat.messages} status={chat.status} />
31
26
  <ChatInput onSubmit={(text) => chat.sendMessage({ text })} />
32
27
  </>
33
28
  );
34
29
  }
35
30
  ```
36
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
+
37
49
  ### Configure the connection once: `<AgentProvider>`
38
50
 
39
51
  Every hook (`useAgentChat`, `useThreads`, `useModels`, `useAgents`, `useQuota`, `useToolCatalog`,
@@ -81,14 +93,14 @@ import { useAgentChat, useChatTranscript } from '@dudousxd/nestjs-agent-react';
81
93
 
82
94
  function Chat() {
83
95
  const chat = useAgentChat();
96
+ // `chat.transcript` is this, pre-wired; `useChatTranscript` directly for any AI SDK chat.
84
97
  const transcript = useChatTranscript({
85
98
  messages: chat.messages,
86
99
  status: chat.status,
87
100
  editable: true,
88
- onEditSubmit: (id, text) => chat.sendMessage({ text }),
89
- onFork: (id) => chat.forkThread(threadId, id),
101
+ onEditSubmit: ({ text }) => chat.sendMessage({ text }),
102
+ onFork: ({ messageId }) => chat.fork({ messageId }),
90
103
  onStop: () => chat.cancel(),
91
- getUsage: (message) => readUsage(message),
92
104
  });
93
105
 
94
106
  return (
@@ -446,8 +458,7 @@ function CustomChat({ threadId }: { threadId?: string }) {
446
458
  // `exactOptionalPropertyTypes`, so an explicit `threadId: undefined` doesn't type-check.
447
459
  ...(threadId !== undefined ? { threadId } : {}),
448
460
  agent: 'support',
449
- // Reattach to a turn still streaming when the page loaded — survives a refresh.
450
- resume: true,
461
+ // A `threadId` loads its history and re-attaches to a turn still streaming — by default.
451
462
  onThreadCreated: (newThreadId) => router.replace(`/chat/${newThreadId}`),
452
463
  // Fires once per run when the SERVER is done writing (title + terminal state persisted) —
453
464
  // the right signal to refetch a thread list/sidebar; `onFinish` only means "a turn rendered".
@@ -492,10 +503,7 @@ disables itself while busy never noticed; a double-submitted one, and StrictMode
492
503
  effect, did.
493
504
 
494
505
  `chat` is the AI SDK v7 `useChat` return value (`messages`, `status`, `sendMessage`, `stop`, …) spread
495
- together with the extras: `runId`/`activeRunId`, `connection`, `getThreadId`, `backend` (the `AgentBackend`; also
496
- under its old name `client`), thread list/CRUD
497
- (`threads`, `loadThreads`, `loadThread`, `deleteThread`, `forkThread`, `renameThread`, `promoteThread`,
498
- `truncateFromMessage`), `quota`/`loadQuota`, `cancel`, HITL `approve`/`reject` and `answer`/`skip`, and `regenerate`.
506
+ together with the extras in the table above.
499
507
  `MyToolCard`'s `part` prop above types as the exported `AnyToolUIPart` (`ToolUIPart | DynamicToolUIPart`
500
508
  — `MessageItem` uses the same union for its own `renderToolPart` callback).
501
509
 
@@ -593,14 +601,10 @@ its run persisted (the live message carries `metadata.runId`).
593
601
  ### Picking a model and an agent
594
602
 
595
603
  ```tsx
596
- import { useAgents, useModels } from '@dudousxd/nestjs-agent-react';
597
-
598
- const [model, setModel] = useState<string | undefined>();
599
- const chat = useAgentChat({ model }); // sent as the body's `model` on every turn
600
- const { providers, find, defaultModel } = useModels({ agent: 'support' });
601
- const { agents } = useAgents();
604
+ const chat = useAgentChat();
605
+ const { providers, selected, select } = chat.models; // loaded on first read
602
606
 
603
- <select value={model ?? defaultModel ?? ''} onChange={(e) => setModel(e.target.value)}>
607
+ <select value={selected ?? ''} onChange={(e) => select(e.target.value)}>
604
608
  {providers.map((p) => (
605
609
  <optgroup key={p.id} label={p.label}>
606
610
  {p.models.map((m) => (
@@ -613,10 +617,14 @@ const { agents } = useAgents();
613
617
  </select>
614
618
  ```
615
619
 
616
- `useModels` reads `GET <base>/models?agent=` (models grouped by provider, with badges and
617
- availability; `models` is the same list flattened, `find(id)` looks one up). A single send can
618
- override with `sendMessage(msg, { body: { model } })`; `chat.setThreadModel(id)` pins a model on
619
- 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
620
628
  model its catalog does not offer as available.
621
629
 
622
630
  ### Quota
@@ -624,17 +632,19 @@ model its catalog does not offer as available.
624
632
  ```tsx
625
633
  import { QuotaBlockedError, useQuota } from '@dudousxd/nestjs-agent-react';
626
634
 
627
- const quota = useQuota(); // GET <base>/quota
628
- const chat = useAgentChat({ blocked: quota.blocked });
635
+ const chat = useAgentChat(); // reads GET <base>/quota and gates sends on it
636
+ const { quota } = chat;
629
637
 
630
638
  <meter value={quota.month?.usedUsd} max={quota.month?.limitUsd} />
631
- {quota.blocked ? <p>{quota.blocked.reason}</p> : null}
639
+ {chat.blocked ? <p>{chat.blocked.reason}</p> : null}
632
640
  ```
633
641
 
634
- `useQuota` returns every window (`day`, `month`, …) with its usage and ceilings, and `blocked` when
635
- one is exhausted; it re-reads after every run a chat on the same backend settles (`pollMs` to poll
636
- as well). With `blocked` passed in, `sendMessage`/`regenerate` reject with `QuotaBlockedError`
637
- 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.
638
648
 
639
649
  ### The transport, standalone
640
650
 
@@ -670,16 +680,15 @@ contract — for a backend that serves these routes without this library's loop
670
680
 
671
681
  ### Loading persisted history
672
682
 
673
- A reloaded thread's `StoredMessage[]` (from `AgentClient.getThread`) needs converting to `UIMessage[]`
674
- 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:
675
686
 
676
687
  ```ts
677
688
  import { storedThreadToUiMessages } from '@dudousxd/nestjs-agent-react';
678
689
 
679
- const detail = await chat.backend.getThread(threadId);
680
690
  const initialMessages = storedThreadToUiMessages(detail.messages);
681
- // Feed into useAgentChat({ threadId, initialMessages, ... }) on the mount that owns this thread —
682
- // `initialMessages` is only read once, on mount.
691
+ useAgentChat({ threadId, initialMessages });
683
692
  ```
684
693
 
685
694
  `storedThreadToUiMessages` merges the store's one-row-per-model-iteration turns (a turn with tool
@@ -691,19 +700,17 @@ persisted pushed components as `data-ui` parts, so a reloaded thread shows what
691
700
 
692
701
  ### Attachments
693
702
 
694
- `useAttachments` is the composer's file tray without the tray: validation, one upload per file
695
- with progress and cancel, retry, image previews, and the handlers for a file input, a drop zone and
696
- 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.
697
707
 
698
708
  ```tsx
699
- import { messageFiles, useAttachments } from '@dudousxd/nestjs-agent-react';
700
-
701
- const files = useAttachments({
702
- // uploads through the provider's backend; or `upload: (file, { signal, onProgress }) => myUpload(file)`
703
- accept: 'image/*,.pdf',
704
- maxBytes: 20 * 1024 * 1024,
705
- 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 },
706
712
  });
713
+ const { files } = chat.composer;
707
714
 
708
715
  <div {...files.dropZoneProps} data-dragging={files.isDragging}>
709
716
  <textarea onPaste={files.onPaste} />
@@ -718,13 +725,8 @@ const files = useAttachments({
718
725
  ))}
719
726
  </div>
720
727
 
721
- <button
722
- disabled={files.isUploading}
723
- onClick={async () => {
724
- await chat.sendMessage({ text }, { body: { attachments: files.refs } });
725
- files.clear();
726
- }}
727
- />
728
+ <textarea value={chat.composer.text} onChange={(e) => chat.composer.setText(e.target.value)} />
729
+ <button disabled={!chat.composer.canSend} onClick={() => chat.composer.submit()} />
728
730
  ```
729
731
 
730
732
  An item is `uploading`, `ready`, `error` (retry with `files.retry(id)`) or `rejected` (failed
@@ -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
@@ -117,7 +117,6 @@ interface AgentBackend {
117
117
  uploadAttachment?(file: File, options?: UploadAttachmentOptions): Promise<MessageAttachment>;
118
118
  listTools?(agent?: string): Promise<ToolCatalogEntry[]>;
119
119
  listSkills?(threadId?: string): Promise<SkillCatalogEntry[]>;
120
- getQuotaToday?(): Promise<QuotaView>;
121
120
  /** `GET <base>/quota` — every budget window, and which one blocks sends, if any. */
122
121
  getQuota?(): Promise<QuotaReport>;
123
122
  /** `GET <base>/models?agent=` — what a model picker offers. */
@@ -152,8 +151,6 @@ declare class AgentHttpError extends Error {
152
151
  readonly path: string;
153
152
  constructor(status: number, method: string, path: string, statusText: string);
154
153
  }
155
- /** The quota-today read-model: usage, the configured limit (null → unlimited), and USD spend. */
156
- type QuotaToday = QuotaView;
157
154
  interface CancelResult {
158
155
  aborted: boolean;
159
156
  }
@@ -234,7 +231,6 @@ declare class AgentClient implements AgentBackend {
234
231
  getThread(id: string): Promise<ThreadDetail>;
235
232
  deleteThread(id: string): Promise<void>;
236
233
  forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary>;
237
- renameThread(id: string, title: string): Promise<OkResult>;
238
234
  /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */
239
235
  updateThread(id: string, patch: ThreadPatch): Promise<OkResult>;
240
236
  /**
@@ -252,7 +248,6 @@ declare class AgentClient implements AgentBackend {
252
248
  listAgents(): Promise<AgentCatalogEntry[]>;
253
249
  /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */
254
250
  getQuota(): Promise<QuotaReport>;
255
- getQuotaToday(): Promise<QuotaToday>;
256
251
  cancelStream(runId: string): Promise<CancelResult>;
257
252
  /**
258
253
  * `remember` approves later calls of the same tool in the same thread; `via` names the surface
@@ -299,4 +294,4 @@ declare class AgentClient implements AgentBackend {
299
294
  private handleResponse;
300
295
  }
301
296
 
302
- 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, 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 };
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 };
@@ -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
@@ -117,7 +117,6 @@ interface AgentBackend {
117
117
  uploadAttachment?(file: File, options?: UploadAttachmentOptions): Promise<MessageAttachment>;
118
118
  listTools?(agent?: string): Promise<ToolCatalogEntry[]>;
119
119
  listSkills?(threadId?: string): Promise<SkillCatalogEntry[]>;
120
- getQuotaToday?(): Promise<QuotaView>;
121
120
  /** `GET <base>/quota` — every budget window, and which one blocks sends, if any. */
122
121
  getQuota?(): Promise<QuotaReport>;
123
122
  /** `GET <base>/models?agent=` — what a model picker offers. */
@@ -152,8 +151,6 @@ declare class AgentHttpError extends Error {
152
151
  readonly path: string;
153
152
  constructor(status: number, method: string, path: string, statusText: string);
154
153
  }
155
- /** The quota-today read-model: usage, the configured limit (null → unlimited), and USD spend. */
156
- type QuotaToday = QuotaView;
157
154
  interface CancelResult {
158
155
  aborted: boolean;
159
156
  }
@@ -234,7 +231,6 @@ declare class AgentClient implements AgentBackend {
234
231
  getThread(id: string): Promise<ThreadDetail>;
235
232
  deleteThread(id: string): Promise<void>;
236
233
  forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary>;
237
- renameThread(id: string, title: string): Promise<OkResult>;
238
234
  /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */
239
235
  updateThread(id: string, patch: ThreadPatch): Promise<OkResult>;
240
236
  /**
@@ -252,7 +248,6 @@ declare class AgentClient implements AgentBackend {
252
248
  listAgents(): Promise<AgentCatalogEntry[]>;
253
249
  /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */
254
250
  getQuota(): Promise<QuotaReport>;
255
- getQuotaToday(): Promise<QuotaToday>;
256
251
  cancelStream(runId: string): Promise<CancelResult>;
257
252
  /**
258
253
  * `remember` approves later calls of the same tool in the same thread; `via` names the surface
@@ -299,4 +294,4 @@ declare class AgentClient implements AgentBackend {
299
294
  private handleResponse;
300
295
  }
301
296
 
302
- 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, 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 };
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 };