@dudousxd/nestjs-agent-react 0.27.0 → 0.29.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
@@ -37,8 +37,8 @@ handlers passed. On top of the AI SDK chat it returns:
37
37
  | On `chat` | What it is |
38
38
  |---|---|
39
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 — while a turn runs it QUEUES it (see *Typing ahead*); `blockedBy` is `'empty' \| 'busy' \| 'uploading' \| 'quota'` (`'busy'` only with `whileRunning: 'block'`). |
41
- | `queue` | `{ items, paused, isSupported, add(text, { attachments?, mode? }), remove(id), edit(id, text), move(id, index), clear(), resume(), error }` — messages sent mid-turn, waiting server-side for the running turn to settle. |
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 — while a turn runs it QUEUES it (see *Typing ahead*), and `submit({ mode: 'interrupt' \| 'queue' })` picks what happens mid-turn for that one send; `blockedBy` is `'empty' \| 'busy' \| 'uploading' \| 'quota'` (`'busy'` only with `whileRunning: 'block'`). |
41
+ | `queue` | `{ items, paused, isSupported, add(text, { attachments?, mode? }), remove(id), interrupt(id), edit(id, text), move(id, index), clear(), resume(), error }` — messages sent mid-turn, waiting server-side for the running turn to settle. |
42
42
  | `models` | `{ list, providers, selected, pinned, locked, select(id), pinToThread(id) }` — loaded the first time `list` is read. |
43
43
  | `quota` / `blocked` | `useQuota`'s state, and the window blocking sends (the `blocked` option overrides it). |
44
44
  | `approve` / `reject` / `answer` / `skip` | `({ toolCallId, … })` — the same object shape the transcript's handlers take. |
@@ -305,6 +305,46 @@ A refused settlement (403 "not your thread" or "not your approval", 410 "expired
305
305
  `call.error` with the affordance still live, rather than escaping as an unhandled rejection. Render
306
306
  it — a button that silently does nothing is indistinguishable from a broken one.
307
307
 
308
+ ### When a run fails
309
+
310
+ The server closes a failed run's stream with `event: error` + `{ code, message }`. `chat.error`
311
+ carries the message; `chat.runError` carries the whole frame — `{ code, message, runId? }`, `null`
312
+ again once the next attempt starts — so the app words each failure itself. The library renders
313
+ nothing for it:
314
+
315
+ ```tsx
316
+ import { isRunNotActiveError } from '@dudousxd/nestjs-agent-react';
317
+
318
+ const FRIENDLY: Record<string, string> = {
319
+ replay_diverged: 'This answer was interrupted by an update. Send your message again.',
320
+ model_no_output: 'The model returned nothing. Try again.',
321
+ run_failed: 'Something went wrong on our side. Try again.',
322
+ };
323
+
324
+ {chat.runError && <p role="alert">{FRIENDLY[chat.runError.code ?? ''] ?? chat.runError.message}</p>}
325
+ ```
326
+
327
+ `AGENT_RUN_ERROR_CODES` lists the codes this library's loop sends (`quota_exceeded`,
328
+ `output_rejected`, `structured_output_invalid`, `replay_diverged`, `model_no_output`, `run_failed`).
329
+ In production the `message` of the last three is one generic sentence — the error itself stays in
330
+ the server's log.
331
+
332
+ A failed turn leaves its thread usable: the next send starts a new turn. What it cannot do is
333
+ answer a card the dead turn left on screen. `approve` / `reject` / `answer` / `skip` on one reject
334
+ with an `AgentHttpError` whose `status` is `409` and `code` is `run_not_active`:
335
+
336
+ ```tsx
337
+ try {
338
+ await chat.approve({ toolCallId });
339
+ } catch (error) {
340
+ if (isRunNotActiveError(error)) showStale('This request expired with its turn — send it again.');
341
+ else throw error;
342
+ }
343
+ ```
344
+
345
+ On the transcript model the same refusal is `call.errorCode === 'run_not_active'` (and
346
+ `elicitation.errorCode`) next to `call.error`, the server's message.
347
+
308
348
  ### Completing as you type
309
349
 
310
350
  `useComposerAutocomplete` is the state machine behind a `/`-style menu in the composer. It is
@@ -607,8 +647,21 @@ const chat = useAgentChat({ threadId });
607
647
  - `useAgentChat({ whileRunning })`: `'queue'` (default), `'interrupt'` (cancel the running turn and
608
648
  run this next), or `'block'` (refuse, `composer.blockedBy === 'busy'` — the old behaviour, also
609
649
  what a backend without `enqueueMessage` gets).
650
+ - One send can answer differently from the chat's `whileRunning`: `composer.submit({ mode })` and
651
+ `sendMessage(message, { mode })` take `'queue'` or `'interrupt'` — a "send now" button next to a
652
+ plain send that queues. The composer clears its own draft and files either way; with nothing
653
+ running, `mode` means nothing and the message is simply sent. It also lifts `'block'` for that
654
+ send.
610
655
  - `chat.queue.edit(id, text)`, `move(id, index)`, `remove(id)`, `clear()` change what is waiting;
611
656
  `add(text, { attachments, mode })` queues from your own code.
657
+ - `chat.queue.interrupt(id)` runs a message that is already waiting NOW: it moves to the head as an
658
+ interrupt and the running turn is cancelled for it, in one server call (the message keeps its id
659
+ and never leaves the queue — do not `remove` and `add` it again). With nothing running it starts
660
+ at once.
661
+ - A waiting message's files have the shape a sent message's do: `chat.queue.items[n].files` is
662
+ `MessageFile[]` — what `messageFiles(message)` gives — and `chat.transcript.queued[n].files` is the
663
+ same plus `isImage`, so one file renderer draws both. `attachmentFile(attachment)` makes one from
664
+ an uploaded `MessageAttachment`.
612
665
  - The queue pauses behind a failed turn (`run_failed`), a Stop (`cancelled`) or an exhausted quota
613
666
  (`quota_exceeded`); `chat.queue.paused` says which, `resume()` lifts it. A queue left waiting with
614
667
  nothing running is started when the thread loads.
@@ -168,6 +168,16 @@ interface AgentBackend {
168
168
  updateQueuedMessage?(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState>;
169
169
  /** `DELETE <base>/queue/:messageId`. */
170
170
  removeQueuedMessage?(messageId: string): Promise<ChatQueueState>;
171
+ /**
172
+ * `POST <base>/queue/:messageId/interrupt` — run a waiting message now: it moves to the head as an
173
+ * interrupt and the running turn is cancelled for it. `interrupting` is the run that was
174
+ * cancelled; `runId` is set instead when nothing was running and the message started at once.
175
+ * What `chat.queue.interrupt(id)` calls.
176
+ */
177
+ interruptQueuedMessage?(messageId: string): Promise<ChatQueueState & {
178
+ runId?: string;
179
+ interrupting?: string;
180
+ }>;
171
181
  /** `DELETE <base>/threads/:id/queue`. */
172
182
  clearQueue?(threadId: string): Promise<ChatQueueState>;
173
183
  /** `POST <base>/threads/:id/queue/resume` — `runId` when the head started. */
@@ -313,6 +323,14 @@ declare class AgentClient implements AgentBackend {
313
323
  updateQueuedMessage(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState>;
314
324
  /** `DELETE <path>/queue/:messageId`. */
315
325
  removeQueuedMessage(messageId: string): Promise<ChatQueueState>;
326
+ /**
327
+ * `POST <path>/queue/:messageId/interrupt` — run a waiting message now, cancelling the running
328
+ * turn for it. `runId` when nothing was running and it started; else `interrupting`.
329
+ */
330
+ interruptQueuedMessage(messageId: string): Promise<ChatQueueState & {
331
+ runId?: string;
332
+ interrupting?: string;
333
+ }>;
316
334
  /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */
317
335
  clearQueue(threadId: string): Promise<ChatQueueState>;
318
336
  /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */
@@ -418,4 +436,4 @@ declare class AgentClient implements AgentBackend {
418
436
  private handleResponse;
419
437
  }
420
438
 
421
- export { type AgentBackend as A, type CancelResult as C, type ErrorAnswer as E, type HttpErrorListener as H, type MessageFeedbackInput as M, type QueuedMessageUpdate 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 AgentRequestError as g, type ChatStreamRequest as h, type ChatStreamResponse as i, type QueuedSendResult as j, requireBackendMethod as r };
439
+ export { type AgentRequestError as A, type CancelResult as C, type ErrorAnswer as E, type HttpErrorListener as H, type MessageFeedbackInput as M, type QueuedMessageUpdate as Q, type ResumeStreamRequest as R, type ThreadPatch as T, type UploadAttachmentOptions as U, type AgentBackend as a, type AttachmentUploadStrategy as b, AgentBackendUnsupportedError as c, AgentClient as d, type AgentClientOptions as e, type AgentConnection as f, AgentHttpError as g, type ChatStreamRequest as h, type ChatStreamResponse as i, type QueuedSendResult as j, requireBackendMethod as r };
@@ -168,6 +168,16 @@ interface AgentBackend {
168
168
  updateQueuedMessage?(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState>;
169
169
  /** `DELETE <base>/queue/:messageId`. */
170
170
  removeQueuedMessage?(messageId: string): Promise<ChatQueueState>;
171
+ /**
172
+ * `POST <base>/queue/:messageId/interrupt` — run a waiting message now: it moves to the head as an
173
+ * interrupt and the running turn is cancelled for it. `interrupting` is the run that was
174
+ * cancelled; `runId` is set instead when nothing was running and the message started at once.
175
+ * What `chat.queue.interrupt(id)` calls.
176
+ */
177
+ interruptQueuedMessage?(messageId: string): Promise<ChatQueueState & {
178
+ runId?: string;
179
+ interrupting?: string;
180
+ }>;
171
181
  /** `DELETE <base>/threads/:id/queue`. */
172
182
  clearQueue?(threadId: string): Promise<ChatQueueState>;
173
183
  /** `POST <base>/threads/:id/queue/resume` — `runId` when the head started. */
@@ -313,6 +323,14 @@ declare class AgentClient implements AgentBackend {
313
323
  updateQueuedMessage(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState>;
314
324
  /** `DELETE <path>/queue/:messageId`. */
315
325
  removeQueuedMessage(messageId: string): Promise<ChatQueueState>;
326
+ /**
327
+ * `POST <path>/queue/:messageId/interrupt` — run a waiting message now, cancelling the running
328
+ * turn for it. `runId` when nothing was running and it started; else `interrupting`.
329
+ */
330
+ interruptQueuedMessage(messageId: string): Promise<ChatQueueState & {
331
+ runId?: string;
332
+ interrupting?: string;
333
+ }>;
316
334
  /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */
317
335
  clearQueue(threadId: string): Promise<ChatQueueState>;
318
336
  /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */
@@ -418,4 +436,4 @@ declare class AgentClient implements AgentBackend {
418
436
  private handleResponse;
419
437
  }
420
438
 
421
- export { type AgentBackend as A, type CancelResult as C, type ErrorAnswer as E, type HttpErrorListener as H, type MessageFeedbackInput as M, type QueuedMessageUpdate 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 AgentRequestError as g, type ChatStreamRequest as h, type ChatStreamResponse as i, type QueuedSendResult as j, requireBackendMethod as r };
439
+ export { type AgentRequestError as A, type CancelResult as C, type ErrorAnswer as E, type HttpErrorListener as H, type MessageFeedbackInput as M, type QueuedMessageUpdate as Q, type ResumeStreamRequest as R, type ThreadPatch as T, type UploadAttachmentOptions as U, type AgentBackend as a, type AttachmentUploadStrategy as b, AgentBackendUnsupportedError as c, AgentClient as d, type AgentClientOptions as e, type AgentConnection as f, AgentHttpError as g, type ChatStreamRequest as h, type ChatStreamResponse as i, type QueuedSendResult as j, requireBackendMethod as r };
@@ -329,6 +329,12 @@ interface TranscriptToolCall {
329
329
  reject: TranscriptSettleState;
330
330
  /** A failed decision — the call is still parked, so the affordance stays live. */
331
331
  error: string | null;
332
+ /**
333
+ * The server's machine-readable reason for {@link error}, when it gave one. `run_not_active`
334
+ * means the turn that asked has ended: nothing is waiting for the decision, and pressing again
335
+ * will be refused again — word it yourself and let the person send the message again.
336
+ */
337
+ errorCode: string | null;
332
338
  }
333
339
  /**
334
340
  * A run of CONSECUTIVE tool parts, grouped so a UI can collapse "5 tools ran" into one affordance.
@@ -452,6 +458,8 @@ interface TranscriptElicitationBlock {
452
458
  outcome: TranscriptElicitationOutcome | null;
453
459
  /** A failed submission — the run is still parked, so the form stays live. */
454
460
  error: string | null;
461
+ /** The server's machine-readable reason for {@link error} (`run_not_active`, …), when it gave one. */
462
+ errorCode: string | null;
455
463
  answer: TranscriptSettleState;
456
464
  skip: TranscriptSettleState;
457
465
  }
@@ -501,6 +509,8 @@ interface ElicitationBlockOptions {
501
509
  /** Which decision this call is sending, or `null` for none. */
502
510
  submitting: (toolCallId: string) => SettleAction | null;
503
511
  errorOf: (toolCallId: string) => string | null;
512
+ /** The `code` of the error `errorOf` reports, when the server sent one. */
513
+ errorCodeOf?: (toolCallId: string) => string | null;
504
514
  }
505
515
  /** Where an approval decision is sent, and what the last one did. */
506
516
  interface ApprovalBlockOptions {
@@ -511,6 +521,8 @@ interface ApprovalBlockOptions {
511
521
  /** Which decision this call is sending, or `null` for none. */
512
522
  submitting: (toolCallId: string) => SettleAction | null;
513
523
  errorOf: (toolCallId: string) => string | null;
524
+ /** The `code` of the error `errorOf` reports, when the server sent one. */
525
+ errorCodeOf?: (toolCallId: string) => string | null;
514
526
  }
515
527
  interface BuildBlocksOptions {
516
528
  /** Disclosure lookup for a reasoning run; `isStreaming` is the fallback when untouched. */
@@ -329,6 +329,12 @@ interface TranscriptToolCall {
329
329
  reject: TranscriptSettleState;
330
330
  /** A failed decision — the call is still parked, so the affordance stays live. */
331
331
  error: string | null;
332
+ /**
333
+ * The server's machine-readable reason for {@link error}, when it gave one. `run_not_active`
334
+ * means the turn that asked has ended: nothing is waiting for the decision, and pressing again
335
+ * will be refused again — word it yourself and let the person send the message again.
336
+ */
337
+ errorCode: string | null;
332
338
  }
333
339
  /**
334
340
  * A run of CONSECUTIVE tool parts, grouped so a UI can collapse "5 tools ran" into one affordance.
@@ -452,6 +458,8 @@ interface TranscriptElicitationBlock {
452
458
  outcome: TranscriptElicitationOutcome | null;
453
459
  /** A failed submission — the run is still parked, so the form stays live. */
454
460
  error: string | null;
461
+ /** The server's machine-readable reason for {@link error} (`run_not_active`, …), when it gave one. */
462
+ errorCode: string | null;
455
463
  answer: TranscriptSettleState;
456
464
  skip: TranscriptSettleState;
457
465
  }
@@ -501,6 +509,8 @@ interface ElicitationBlockOptions {
501
509
  /** Which decision this call is sending, or `null` for none. */
502
510
  submitting: (toolCallId: string) => SettleAction | null;
503
511
  errorOf: (toolCallId: string) => string | null;
512
+ /** The `code` of the error `errorOf` reports, when the server sent one. */
513
+ errorCodeOf?: (toolCallId: string) => string | null;
504
514
  }
505
515
  /** Where an approval decision is sent, and what the last one did. */
506
516
  interface ApprovalBlockOptions {
@@ -511,6 +521,8 @@ interface ApprovalBlockOptions {
511
521
  /** Which decision this call is sending, or `null` for none. */
512
522
  submitting: (toolCallId: string) => SettleAction | null;
513
523
  errorOf: (toolCallId: string) => string | null;
524
+ /** The `code` of the error `errorOf` reports, when the server sent one. */
525
+ errorCodeOf?: (toolCallId: string) => string | null;
514
526
  }
515
527
  interface BuildBlocksOptions {
516
528
  /** Disclosure lookup for a reasoning run; `isStreaming` is the fallback when untouched. */