@dudousxd/nestjs-agent-react 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -101,18 +101,26 @@ var AgentChatTransport = class {
101
101
  this.options.onMeta?.(meta);
102
102
  }
103
103
  /**
104
- * Parse the backend's SSE byte stream and synthesize a valid v7
105
- * UI-message chunk stream. Recognized backend frames:
106
- * - `event: meta` `data: {"runId","threadId"}` → records identity
107
- * - `data: {"delta":"..."}` → `text-delta`
108
- * - `event: done` `data: {}` → terminates
104
+ * Parse the backend's SSE byte stream and re-emit it as a valid v7 UI-message chunk stream.
105
+ * Recognized frames:
106
+ * - `event: meta` `data: {"runId","threadId"}` → records identity
107
+ * - `data: <AgentStreamEvent JSON>` → mapped to UI chunks (text / reasoning / tool)
108
+ * - `event: done` `data: {}` → terminates
109
+ * - `event: error` `data: {code,message}` → error chunk
110
+ *
111
+ * A run is ONE UI message with N steps. Each `step-start`/`step-finish` pair brackets a model
112
+ * call plus its tool execution; text and reasoning open lazily and close at the step boundary, so
113
+ * tool-call cards (input streaming → output) render live between the prose.
109
114
  */
110
115
  toChunkStream(source) {
111
116
  const reader = source.getReader();
112
117
  const decoder = new TextDecoder();
113
- const textId = `txt-${Math.random().toString(36).slice(2)}`;
114
118
  let buffer = "";
115
119
  let started = false;
120
+ let stepOpen = false;
121
+ let stepIndex = 0;
122
+ let textId = null;
123
+ let reasoningId = null;
116
124
  const record = /* @__PURE__ */ __name((meta) => this.recordMeta(meta), "record");
117
125
  return new ReadableStream({
118
126
  async pull(controller) {
@@ -122,24 +130,154 @@ var AgentChatTransport = class {
122
130
  controller.enqueue({
123
131
  type: "start"
124
132
  });
125
- controller.enqueue({
126
- type: "start-step"
127
- });
128
- controller.enqueue({
129
- type: "text-start",
130
- id: textId
131
- });
132
133
  }
133
134
  __name(ensureStarted, "ensureStarted");
134
- function finish() {
135
+ function openStep() {
135
136
  ensureStarted();
137
+ if (stepOpen) closeStep();
138
+ stepOpen = true;
139
+ stepIndex += 1;
140
+ textId = null;
141
+ reasoningId = null;
136
142
  controller.enqueue({
137
- type: "text-end",
138
- id: textId
143
+ type: "start-step"
139
144
  });
145
+ }
146
+ __name(openStep, "openStep");
147
+ function ensureStep() {
148
+ if (!stepOpen) openStep();
149
+ }
150
+ __name(ensureStep, "ensureStep");
151
+ function closeStep() {
152
+ if (!stepOpen) return;
153
+ if (textId !== null) {
154
+ controller.enqueue({
155
+ type: "text-end",
156
+ id: textId
157
+ });
158
+ textId = null;
159
+ }
160
+ if (reasoningId !== null) {
161
+ controller.enqueue({
162
+ type: "reasoning-end",
163
+ id: reasoningId
164
+ });
165
+ reasoningId = null;
166
+ }
140
167
  controller.enqueue({
141
168
  type: "finish-step"
142
169
  });
170
+ stepOpen = false;
171
+ }
172
+ __name(closeStep, "closeStep");
173
+ function emit(event) {
174
+ switch (event.kind) {
175
+ case "step-start":
176
+ openStep();
177
+ break;
178
+ case "step-finish":
179
+ closeStep();
180
+ if (event.costUsd !== void 0) {
181
+ controller.enqueue({
182
+ type: "message-metadata",
183
+ messageMetadata: {
184
+ costUsd: event.costUsd
185
+ }
186
+ });
187
+ }
188
+ break;
189
+ case "text":
190
+ ensureStep();
191
+ if (textId === null) {
192
+ textId = `txt-${stepIndex}`;
193
+ controller.enqueue({
194
+ type: "text-start",
195
+ id: textId
196
+ });
197
+ }
198
+ controller.enqueue({
199
+ type: "text-delta",
200
+ id: textId,
201
+ delta: event.text
202
+ });
203
+ break;
204
+ case "reasoning":
205
+ ensureStep();
206
+ if (reasoningId === null) {
207
+ reasoningId = `rsn-${stepIndex}`;
208
+ controller.enqueue({
209
+ type: "reasoning-start",
210
+ id: reasoningId
211
+ });
212
+ }
213
+ controller.enqueue({
214
+ type: "reasoning-delta",
215
+ id: reasoningId,
216
+ delta: event.text
217
+ });
218
+ break;
219
+ case "tool-input-start":
220
+ ensureStep();
221
+ controller.enqueue({
222
+ type: "tool-input-start",
223
+ toolCallId: event.id,
224
+ toolName: event.name,
225
+ // `toolKind` is absent on older backends — omitted (not `undefined`-valued) so
226
+ // `toolMetadata` itself is only present when there's something to say, letting a
227
+ // UI gate approval affordances on `kind === 'action'` without hardcoding tool names.
228
+ ...event.toolKind !== void 0 ? {
229
+ toolMetadata: {
230
+ toolKind: event.toolKind
231
+ }
232
+ } : {}
233
+ });
234
+ break;
235
+ case "tool-input-delta":
236
+ ensureStep();
237
+ controller.enqueue({
238
+ type: "tool-input-delta",
239
+ toolCallId: event.id,
240
+ inputTextDelta: event.delta
241
+ });
242
+ break;
243
+ case "tool-input-available":
244
+ ensureStep();
245
+ controller.enqueue({
246
+ type: "tool-input-available",
247
+ toolCallId: event.id,
248
+ toolName: event.name,
249
+ input: event.input,
250
+ ...event.toolKind !== void 0 ? {
251
+ toolMetadata: {
252
+ toolKind: event.toolKind
253
+ }
254
+ } : {}
255
+ });
256
+ break;
257
+ case "tool-output":
258
+ ensureStep();
259
+ controller.enqueue({
260
+ type: "tool-output-available",
261
+ toolCallId: event.id,
262
+ output: event.output
263
+ });
264
+ break;
265
+ case "tool-output-error":
266
+ ensureStep();
267
+ controller.enqueue({
268
+ type: "tool-output-error",
269
+ toolCallId: event.id,
270
+ errorText: event.error
271
+ });
272
+ break;
273
+ default:
274
+ break;
275
+ }
276
+ }
277
+ __name(emit, "emit");
278
+ function finish() {
279
+ ensureStarted();
280
+ closeStep();
143
281
  controller.enqueue({
144
282
  type: "finish"
145
283
  });
@@ -178,15 +316,8 @@ var AgentChatTransport = class {
178
316
  const meta = parseMeta(frame.data);
179
317
  if (meta) record(meta);
180
318
  } else if (frame.data) {
181
- const delta = parseDelta(frame.data);
182
- if (delta) {
183
- ensureStarted();
184
- controller.enqueue({
185
- type: "text-delta",
186
- id: textId,
187
- delta
188
- });
189
- }
319
+ const event = parseEvent(frame.data);
320
+ if (event) emit(event);
190
321
  }
191
322
  separator = buffer.indexOf("\n\n");
192
323
  }
@@ -248,17 +379,17 @@ function parseErrorText(data) {
248
379
  return "Agent run failed";
249
380
  }
250
381
  __name(parseErrorText, "parseErrorText");
251
- function parseDelta(data) {
382
+ function parseEvent(data) {
252
383
  try {
253
384
  const parsed = JSON.parse(data);
254
- if (parsed !== null && typeof parsed === "object" && "delta" in parsed && typeof parsed.delta === "string") {
255
- return parsed.delta;
385
+ if (parsed !== null && typeof parsed === "object" && "kind" in parsed) {
386
+ return parsed;
256
387
  }
257
388
  } catch {
258
389
  }
259
390
  return null;
260
391
  }
261
- __name(parseDelta, "parseDelta");
392
+ __name(parseEvent, "parseEvent");
262
393
  function extractText(message) {
263
394
  let text = "";
264
395
  for (const part of message.parts ?? []) {
@@ -302,10 +433,39 @@ var AgentClient = class {
302
433
  return this.request("POST", `/agent/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`);
303
434
  }
304
435
  renameThread(id, title) {
305
- return this.request("PATCH", `/agent/threads/${encodeURIComponent(id)}`, {
436
+ return this.updateThread(id, {
306
437
  title
307
438
  });
308
439
  }
440
+ /** General `PATCH /agent/threads/:threadId` — title and/or the thread's pinned default agent. */
441
+ updateThread(id, patch) {
442
+ return this.request("PATCH", `/agent/threads/${encodeURIComponent(id)}`, patch);
443
+ }
444
+ /**
445
+ * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —
446
+ * mirrors the backend's `POST /agent/attachments`. The returned {@link MessageAttachment} is
447
+ * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.
448
+ */
449
+ async uploadAttachment(file) {
450
+ const fetchImpl = this.options.fetch ?? globalThis.fetch;
451
+ const baseUrl = (this.options.baseUrl ?? "").replace(/\/$/, "");
452
+ const dynamic = await this.options.getHeaders?.() ?? {};
453
+ const formData = new FormData();
454
+ formData.append("file", file);
455
+ const response = await fetchImpl(`${baseUrl}/agent/attachments`, {
456
+ method: "POST",
457
+ headers: {
458
+ accept: "application/json",
459
+ ...this.options.headers,
460
+ ...dynamic
461
+ },
462
+ body: formData,
463
+ ...this.options.credentials !== void 0 ? {
464
+ credentials: this.options.credentials
465
+ } : {}
466
+ });
467
+ return this.handleResponse(response, "POST", "/agent/attachments");
468
+ }
309
469
  promoteThread(id) {
310
470
  return this.request("POST", `/agent/threads/${encodeURIComponent(id)}/promote`);
311
471
  }
@@ -345,6 +505,9 @@ var AgentClient = class {
345
505
  credentials: this.options.credentials
346
506
  } : {}
347
507
  });
508
+ return this.handleResponse(response, method, path);
509
+ }
510
+ async handleResponse(response, method, path) {
348
511
  if (!response.ok) {
349
512
  throw new AgentHttpError(response.status, method, path, response.statusText);
350
513
  }
@@ -355,16 +518,66 @@ var AgentClient = class {
355
518
  }
356
519
  };
357
520
 
521
+ // src/stored-message-to-ui-message.ts
522
+ function storedMessageToUiMessage(message) {
523
+ const parts = [];
524
+ if (message.content) {
525
+ parts.push({
526
+ type: "text",
527
+ text: message.content
528
+ });
529
+ }
530
+ for (const attachment of message.attachments ?? []) {
531
+ parts.push({
532
+ type: "file",
533
+ mediaType: attachment.contentType,
534
+ filename: attachment.name,
535
+ url: attachment.url
536
+ });
537
+ }
538
+ for (const call of message.toolCalls ?? []) {
539
+ const result = message.toolResults?.find((candidate) => candidate.id === call.id);
540
+ const toolKind = call.kind;
541
+ parts.push({
542
+ type: `tool-${call.name}`,
543
+ toolCallId: call.id,
544
+ ...toolKind !== void 0 ? {
545
+ toolMetadata: {
546
+ toolKind
547
+ }
548
+ } : {},
549
+ ...result !== void 0 ? {
550
+ state: "output-available",
551
+ input: call.input,
552
+ output: result.output
553
+ } : {
554
+ state: "input-available",
555
+ input: call.input
556
+ }
557
+ });
558
+ }
559
+ return {
560
+ id: message.id,
561
+ role: message.role,
562
+ parts
563
+ };
564
+ }
565
+ __name(storedMessageToUiMessage, "storedMessageToUiMessage");
566
+
358
567
  // src/use-agent-chat.ts
359
568
  import { useChat } from "@ai-sdk/react";
360
- import { useCallback, useMemo, useRef, useState } from "react";
569
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
361
570
  function useAgentChat(options) {
362
571
  const latest = useRef(options);
363
572
  latest.current = options;
364
573
  const [runId, setRunId] = useState(options.resumeRunId);
365
574
  const runIdRef = useRef(runId);
366
575
  runIdRef.current = runId;
576
+ const autoResumeRunIdRef = useRef(void 0);
577
+ const [autoResume, setAutoResume] = useState(false);
578
+ const [activeRunId, setActiveRunId] = useState(null);
367
579
  const regenerateNext = useRef(false);
580
+ const createdThreadId = useRef(void 0);
368
581
  const client = useMemo(() => {
369
582
  if (options.client) return options.client;
370
583
  return new AgentClient({
@@ -380,9 +593,33 @@ function useAgentChat(options) {
380
593
  getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders")
381
594
  });
382
595
  }, []);
596
+ useEffect(() => {
597
+ if (!options.resume || options.threadId === void 0) return;
598
+ let cancelled = false;
599
+ const threadId = options.threadId;
600
+ void client.getThread(threadId).then((thread) => {
601
+ if (cancelled) return;
602
+ setActiveRunId(thread.activeRunId ?? null);
603
+ if (thread.activeRunId != null) {
604
+ autoResumeRunIdRef.current = thread.activeRunId;
605
+ setAutoResume(true);
606
+ }
607
+ });
608
+ return () => {
609
+ cancelled = true;
610
+ };
611
+ }, [
612
+ client,
613
+ options.resume,
614
+ options.threadId
615
+ ]);
383
616
  const transport = useMemo(() => {
384
617
  function onMeta(meta) {
385
618
  setRunId(meta.runId);
619
+ if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
620
+ createdThreadId.current = meta.threadId;
621
+ latest.current.onThreadCreated?.(meta.threadId);
622
+ }
386
623
  }
387
624
  __name(onMeta, "onMeta");
388
625
  return new AgentChatTransport({
@@ -404,9 +641,10 @@ function useAgentChat(options) {
404
641
  const pageContext = current.getPageContext?.() ?? null;
405
642
  const regenerate2 = regenerateNext.current;
406
643
  regenerateNext.current = false;
644
+ const threadId = current.threadId ?? createdThreadId.current;
407
645
  return {
408
- ...current.threadId !== void 0 ? {
409
- threadId: current.threadId
646
+ ...threadId !== void 0 ? {
647
+ threadId
410
648
  } : {},
411
649
  ...pageContext ? {
412
650
  pageContext
@@ -416,13 +654,13 @@ function useAgentChat(options) {
416
654
  } : {}
417
655
  };
418
656
  }, "getBody"),
419
- getResumeRunId: /* @__PURE__ */ __name(() => latest.current.resumeRunId, "getResumeRunId"),
657
+ getResumeRunId: /* @__PURE__ */ __name(() => latest.current.resumeRunId ?? autoResumeRunIdRef.current, "getResumeRunId"),
420
658
  onMeta
421
659
  });
422
660
  }, []);
423
661
  const chat = useChat({
424
662
  transport,
425
- resume: options.resumeRunId !== void 0,
663
+ resume: options.resumeRunId !== void 0 || autoResume,
426
664
  ...options.threadId !== void 0 ? {
427
665
  id: options.threadId
428
666
  } : {},
@@ -489,10 +727,10 @@ function useAgentChat(options) {
489
727
  ]);
490
728
  const cancel = useCallback(async () => {
491
729
  chatRef.current.stop();
492
- const activeRunId = runIdRef.current;
493
- if (activeRunId) {
730
+ const activeRunId2 = runIdRef.current;
731
+ if (activeRunId2) {
494
732
  try {
495
- await client.cancelStream(activeRunId);
733
+ await client.cancelStream(activeRunId2);
496
734
  } catch {
497
735
  }
498
736
  }
@@ -524,6 +762,8 @@ function useAgentChat(options) {
524
762
  ...chat,
525
763
  addToolResult,
526
764
  runId,
765
+ /** The `resume`-fetched thread's active run id, or `null` once resolved with none. */
766
+ activeRunId,
527
767
  client,
528
768
  threads,
529
769
  loadThreads,
@@ -552,10 +792,10 @@ async function mergeHeaders(options) {
552
792
  __name(mergeHeaders, "mergeHeaders");
553
793
 
554
794
  // src/components/chat-input.tsx
555
- import React, { useEffect as useEffect2, useState as useState3 } from "react";
795
+ import React, { useEffect as useEffect3, useState as useState3 } from "react";
556
796
 
557
797
  // src/components/use-speech-recognition.ts
558
- import { useCallback as useCallback2, useEffect, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
798
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
559
799
  function getRecognitionCtor() {
560
800
  if (typeof window === "undefined") return null;
561
801
  const scope = window;
@@ -571,7 +811,7 @@ function useSpeechRecognition({ lang = "en-US", onFinalTranscript, onInterimTran
571
811
  const interimRef = useRef2(onInterimTranscript);
572
812
  finalRef.current = onFinalTranscript;
573
813
  interimRef.current = onInterimTranscript;
574
- useEffect(() => {
814
+ useEffect2(() => {
575
815
  if (!ctor) return;
576
816
  const recognition = new ctor();
577
817
  recognition.lang = lang;
@@ -651,7 +891,7 @@ function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = t
651
891
  }, "onFinalTranscript"),
652
892
  onInterimTranscript: /* @__PURE__ */ __name((text) => setInterim(text), "onInterimTranscript")
653
893
  });
654
- useEffect2(() => {
894
+ useEffect3(() => {
655
895
  if (!speech.isListening) setInterim("");
656
896
  }, [
657
897
  speech.isListening
@@ -710,7 +950,7 @@ __name(ChatInput, "ChatInput");
710
950
 
711
951
  // src/components/message-item.tsx
712
952
  import { isTextUIPart, isToolUIPart } from "ai";
713
- import React2, { useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
953
+ import React2, { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
714
954
  function MessageItem({ message, renderToolPart, renderToolGroup, renderText, classNames, isStreaming = false, editable, onEditSubmit, regeneratable, onRegenerate, onFork, usage, createdAt }) {
715
955
  const isUser = message.role === "user";
716
956
  const parts = message.parts ?? [];
@@ -814,7 +1054,7 @@ function EditableUserBubble({ message, onSubmit, renderText, classNames, actions
814
1054
  const [isEditing, setIsEditing] = useState4(false);
815
1055
  const [draft, setDraft] = useState4(initialText);
816
1056
  const textareaRef = useRef3(null);
817
- useEffect3(() => {
1057
+ useEffect4(() => {
818
1058
  if (isEditing && textareaRef.current) {
819
1059
  const el = textareaRef.current;
820
1060
  el.focus();
@@ -1132,6 +1372,7 @@ export {
1132
1372
  formatRelativeTime,
1133
1373
  loadStoredSpeechLang,
1134
1374
  persistSpeechLang,
1375
+ storedMessageToUiMessage,
1135
1376
  useAgentChat,
1136
1377
  useSpeechRecognition
1137
1378
  };