@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.cjs CHANGED
@@ -42,6 +42,7 @@ __export(src_exports, {
42
42
  formatRelativeTime: () => formatRelativeTime,
43
43
  loadStoredSpeechLang: () => loadStoredSpeechLang,
44
44
  persistSpeechLang: () => persistSpeechLang,
45
+ storedMessageToUiMessage: () => storedMessageToUiMessage,
45
46
  useAgentChat: () => useAgentChat,
46
47
  useSpeechRecognition: () => useSpeechRecognition
47
48
  });
@@ -147,18 +148,26 @@ var AgentChatTransport = class {
147
148
  this.options.onMeta?.(meta);
148
149
  }
149
150
  /**
150
- * Parse the backend's SSE byte stream and synthesize a valid v7
151
- * UI-message chunk stream. Recognized backend frames:
152
- * - `event: meta` `data: {"runId","threadId"}` → records identity
153
- * - `data: {"delta":"..."}` → `text-delta`
154
- * - `event: done` `data: {}` → terminates
151
+ * Parse the backend's SSE byte stream and re-emit it as a valid v7 UI-message chunk stream.
152
+ * Recognized frames:
153
+ * - `event: meta` `data: {"runId","threadId"}` → records identity
154
+ * - `data: <AgentStreamEvent JSON>` → mapped to UI chunks (text / reasoning / tool)
155
+ * - `event: done` `data: {}` → terminates
156
+ * - `event: error` `data: {code,message}` → error chunk
157
+ *
158
+ * A run is ONE UI message with N steps. Each `step-start`/`step-finish` pair brackets a model
159
+ * call plus its tool execution; text and reasoning open lazily and close at the step boundary, so
160
+ * tool-call cards (input streaming → output) render live between the prose.
155
161
  */
156
162
  toChunkStream(source) {
157
163
  const reader = source.getReader();
158
164
  const decoder = new TextDecoder();
159
- const textId = `txt-${Math.random().toString(36).slice(2)}`;
160
165
  let buffer = "";
161
166
  let started = false;
167
+ let stepOpen = false;
168
+ let stepIndex = 0;
169
+ let textId = null;
170
+ let reasoningId = null;
162
171
  const record = /* @__PURE__ */ __name((meta) => this.recordMeta(meta), "record");
163
172
  return new ReadableStream({
164
173
  async pull(controller) {
@@ -168,24 +177,154 @@ var AgentChatTransport = class {
168
177
  controller.enqueue({
169
178
  type: "start"
170
179
  });
171
- controller.enqueue({
172
- type: "start-step"
173
- });
174
- controller.enqueue({
175
- type: "text-start",
176
- id: textId
177
- });
178
180
  }
179
181
  __name(ensureStarted, "ensureStarted");
180
- function finish() {
182
+ function openStep() {
181
183
  ensureStarted();
184
+ if (stepOpen) closeStep();
185
+ stepOpen = true;
186
+ stepIndex += 1;
187
+ textId = null;
188
+ reasoningId = null;
182
189
  controller.enqueue({
183
- type: "text-end",
184
- id: textId
190
+ type: "start-step"
185
191
  });
192
+ }
193
+ __name(openStep, "openStep");
194
+ function ensureStep() {
195
+ if (!stepOpen) openStep();
196
+ }
197
+ __name(ensureStep, "ensureStep");
198
+ function closeStep() {
199
+ if (!stepOpen) return;
200
+ if (textId !== null) {
201
+ controller.enqueue({
202
+ type: "text-end",
203
+ id: textId
204
+ });
205
+ textId = null;
206
+ }
207
+ if (reasoningId !== null) {
208
+ controller.enqueue({
209
+ type: "reasoning-end",
210
+ id: reasoningId
211
+ });
212
+ reasoningId = null;
213
+ }
186
214
  controller.enqueue({
187
215
  type: "finish-step"
188
216
  });
217
+ stepOpen = false;
218
+ }
219
+ __name(closeStep, "closeStep");
220
+ function emit(event) {
221
+ switch (event.kind) {
222
+ case "step-start":
223
+ openStep();
224
+ break;
225
+ case "step-finish":
226
+ closeStep();
227
+ if (event.costUsd !== void 0) {
228
+ controller.enqueue({
229
+ type: "message-metadata",
230
+ messageMetadata: {
231
+ costUsd: event.costUsd
232
+ }
233
+ });
234
+ }
235
+ break;
236
+ case "text":
237
+ ensureStep();
238
+ if (textId === null) {
239
+ textId = `txt-${stepIndex}`;
240
+ controller.enqueue({
241
+ type: "text-start",
242
+ id: textId
243
+ });
244
+ }
245
+ controller.enqueue({
246
+ type: "text-delta",
247
+ id: textId,
248
+ delta: event.text
249
+ });
250
+ break;
251
+ case "reasoning":
252
+ ensureStep();
253
+ if (reasoningId === null) {
254
+ reasoningId = `rsn-${stepIndex}`;
255
+ controller.enqueue({
256
+ type: "reasoning-start",
257
+ id: reasoningId
258
+ });
259
+ }
260
+ controller.enqueue({
261
+ type: "reasoning-delta",
262
+ id: reasoningId,
263
+ delta: event.text
264
+ });
265
+ break;
266
+ case "tool-input-start":
267
+ ensureStep();
268
+ controller.enqueue({
269
+ type: "tool-input-start",
270
+ toolCallId: event.id,
271
+ toolName: event.name,
272
+ // `toolKind` is absent on older backends — omitted (not `undefined`-valued) so
273
+ // `toolMetadata` itself is only present when there's something to say, letting a
274
+ // UI gate approval affordances on `kind === 'action'` without hardcoding tool names.
275
+ ...event.toolKind !== void 0 ? {
276
+ toolMetadata: {
277
+ toolKind: event.toolKind
278
+ }
279
+ } : {}
280
+ });
281
+ break;
282
+ case "tool-input-delta":
283
+ ensureStep();
284
+ controller.enqueue({
285
+ type: "tool-input-delta",
286
+ toolCallId: event.id,
287
+ inputTextDelta: event.delta
288
+ });
289
+ break;
290
+ case "tool-input-available":
291
+ ensureStep();
292
+ controller.enqueue({
293
+ type: "tool-input-available",
294
+ toolCallId: event.id,
295
+ toolName: event.name,
296
+ input: event.input,
297
+ ...event.toolKind !== void 0 ? {
298
+ toolMetadata: {
299
+ toolKind: event.toolKind
300
+ }
301
+ } : {}
302
+ });
303
+ break;
304
+ case "tool-output":
305
+ ensureStep();
306
+ controller.enqueue({
307
+ type: "tool-output-available",
308
+ toolCallId: event.id,
309
+ output: event.output
310
+ });
311
+ break;
312
+ case "tool-output-error":
313
+ ensureStep();
314
+ controller.enqueue({
315
+ type: "tool-output-error",
316
+ toolCallId: event.id,
317
+ errorText: event.error
318
+ });
319
+ break;
320
+ default:
321
+ break;
322
+ }
323
+ }
324
+ __name(emit, "emit");
325
+ function finish() {
326
+ ensureStarted();
327
+ closeStep();
189
328
  controller.enqueue({
190
329
  type: "finish"
191
330
  });
@@ -224,15 +363,8 @@ var AgentChatTransport = class {
224
363
  const meta = parseMeta(frame.data);
225
364
  if (meta) record(meta);
226
365
  } else if (frame.data) {
227
- const delta = parseDelta(frame.data);
228
- if (delta) {
229
- ensureStarted();
230
- controller.enqueue({
231
- type: "text-delta",
232
- id: textId,
233
- delta
234
- });
235
- }
366
+ const event = parseEvent(frame.data);
367
+ if (event) emit(event);
236
368
  }
237
369
  separator = buffer.indexOf("\n\n");
238
370
  }
@@ -294,17 +426,17 @@ function parseErrorText(data) {
294
426
  return "Agent run failed";
295
427
  }
296
428
  __name(parseErrorText, "parseErrorText");
297
- function parseDelta(data) {
429
+ function parseEvent(data) {
298
430
  try {
299
431
  const parsed = JSON.parse(data);
300
- if (parsed !== null && typeof parsed === "object" && "delta" in parsed && typeof parsed.delta === "string") {
301
- return parsed.delta;
432
+ if (parsed !== null && typeof parsed === "object" && "kind" in parsed) {
433
+ return parsed;
302
434
  }
303
435
  } catch {
304
436
  }
305
437
  return null;
306
438
  }
307
- __name(parseDelta, "parseDelta");
439
+ __name(parseEvent, "parseEvent");
308
440
  function extractText(message) {
309
441
  let text = "";
310
442
  for (const part of message.parts ?? []) {
@@ -348,10 +480,39 @@ var AgentClient = class {
348
480
  return this.request("POST", `/agent/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`);
349
481
  }
350
482
  renameThread(id, title) {
351
- return this.request("PATCH", `/agent/threads/${encodeURIComponent(id)}`, {
483
+ return this.updateThread(id, {
352
484
  title
353
485
  });
354
486
  }
487
+ /** General `PATCH /agent/threads/:threadId` — title and/or the thread's pinned default agent. */
488
+ updateThread(id, patch) {
489
+ return this.request("PATCH", `/agent/threads/${encodeURIComponent(id)}`, patch);
490
+ }
491
+ /**
492
+ * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —
493
+ * mirrors the backend's `POST /agent/attachments`. The returned {@link MessageAttachment} is
494
+ * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.
495
+ */
496
+ async uploadAttachment(file) {
497
+ const fetchImpl = this.options.fetch ?? globalThis.fetch;
498
+ const baseUrl = (this.options.baseUrl ?? "").replace(/\/$/, "");
499
+ const dynamic = await this.options.getHeaders?.() ?? {};
500
+ const formData = new FormData();
501
+ formData.append("file", file);
502
+ const response = await fetchImpl(`${baseUrl}/agent/attachments`, {
503
+ method: "POST",
504
+ headers: {
505
+ accept: "application/json",
506
+ ...this.options.headers,
507
+ ...dynamic
508
+ },
509
+ body: formData,
510
+ ...this.options.credentials !== void 0 ? {
511
+ credentials: this.options.credentials
512
+ } : {}
513
+ });
514
+ return this.handleResponse(response, "POST", "/agent/attachments");
515
+ }
355
516
  promoteThread(id) {
356
517
  return this.request("POST", `/agent/threads/${encodeURIComponent(id)}/promote`);
357
518
  }
@@ -391,6 +552,9 @@ var AgentClient = class {
391
552
  credentials: this.options.credentials
392
553
  } : {}
393
554
  });
555
+ return this.handleResponse(response, method, path);
556
+ }
557
+ async handleResponse(response, method, path) {
394
558
  if (!response.ok) {
395
559
  throw new AgentHttpError(response.status, method, path, response.statusText);
396
560
  }
@@ -401,6 +565,52 @@ var AgentClient = class {
401
565
  }
402
566
  };
403
567
 
568
+ // src/stored-message-to-ui-message.ts
569
+ function storedMessageToUiMessage(message) {
570
+ const parts = [];
571
+ if (message.content) {
572
+ parts.push({
573
+ type: "text",
574
+ text: message.content
575
+ });
576
+ }
577
+ for (const attachment of message.attachments ?? []) {
578
+ parts.push({
579
+ type: "file",
580
+ mediaType: attachment.contentType,
581
+ filename: attachment.name,
582
+ url: attachment.url
583
+ });
584
+ }
585
+ for (const call of message.toolCalls ?? []) {
586
+ const result = message.toolResults?.find((candidate) => candidate.id === call.id);
587
+ const toolKind = call.kind;
588
+ parts.push({
589
+ type: `tool-${call.name}`,
590
+ toolCallId: call.id,
591
+ ...toolKind !== void 0 ? {
592
+ toolMetadata: {
593
+ toolKind
594
+ }
595
+ } : {},
596
+ ...result !== void 0 ? {
597
+ state: "output-available",
598
+ input: call.input,
599
+ output: result.output
600
+ } : {
601
+ state: "input-available",
602
+ input: call.input
603
+ }
604
+ });
605
+ }
606
+ return {
607
+ id: message.id,
608
+ role: message.role,
609
+ parts
610
+ };
611
+ }
612
+ __name(storedMessageToUiMessage, "storedMessageToUiMessage");
613
+
404
614
  // src/use-agent-chat.ts
405
615
  var import_react = require("@ai-sdk/react");
406
616
  var import_react2 = require("react");
@@ -410,7 +620,11 @@ function useAgentChat(options) {
410
620
  const [runId, setRunId] = (0, import_react2.useState)(options.resumeRunId);
411
621
  const runIdRef = (0, import_react2.useRef)(runId);
412
622
  runIdRef.current = runId;
623
+ const autoResumeRunIdRef = (0, import_react2.useRef)(void 0);
624
+ const [autoResume, setAutoResume] = (0, import_react2.useState)(false);
625
+ const [activeRunId, setActiveRunId] = (0, import_react2.useState)(null);
413
626
  const regenerateNext = (0, import_react2.useRef)(false);
627
+ const createdThreadId = (0, import_react2.useRef)(void 0);
414
628
  const client = (0, import_react2.useMemo)(() => {
415
629
  if (options.client) return options.client;
416
630
  return new AgentClient({
@@ -426,9 +640,33 @@ function useAgentChat(options) {
426
640
  getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders")
427
641
  });
428
642
  }, []);
643
+ (0, import_react2.useEffect)(() => {
644
+ if (!options.resume || options.threadId === void 0) return;
645
+ let cancelled = false;
646
+ const threadId = options.threadId;
647
+ void client.getThread(threadId).then((thread) => {
648
+ if (cancelled) return;
649
+ setActiveRunId(thread.activeRunId ?? null);
650
+ if (thread.activeRunId != null) {
651
+ autoResumeRunIdRef.current = thread.activeRunId;
652
+ setAutoResume(true);
653
+ }
654
+ });
655
+ return () => {
656
+ cancelled = true;
657
+ };
658
+ }, [
659
+ client,
660
+ options.resume,
661
+ options.threadId
662
+ ]);
429
663
  const transport = (0, import_react2.useMemo)(() => {
430
664
  function onMeta(meta) {
431
665
  setRunId(meta.runId);
666
+ if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
667
+ createdThreadId.current = meta.threadId;
668
+ latest.current.onThreadCreated?.(meta.threadId);
669
+ }
432
670
  }
433
671
  __name(onMeta, "onMeta");
434
672
  return new AgentChatTransport({
@@ -450,9 +688,10 @@ function useAgentChat(options) {
450
688
  const pageContext = current.getPageContext?.() ?? null;
451
689
  const regenerate2 = regenerateNext.current;
452
690
  regenerateNext.current = false;
691
+ const threadId = current.threadId ?? createdThreadId.current;
453
692
  return {
454
- ...current.threadId !== void 0 ? {
455
- threadId: current.threadId
693
+ ...threadId !== void 0 ? {
694
+ threadId
456
695
  } : {},
457
696
  ...pageContext ? {
458
697
  pageContext
@@ -462,13 +701,13 @@ function useAgentChat(options) {
462
701
  } : {}
463
702
  };
464
703
  }, "getBody"),
465
- getResumeRunId: /* @__PURE__ */ __name(() => latest.current.resumeRunId, "getResumeRunId"),
704
+ getResumeRunId: /* @__PURE__ */ __name(() => latest.current.resumeRunId ?? autoResumeRunIdRef.current, "getResumeRunId"),
466
705
  onMeta
467
706
  });
468
707
  }, []);
469
708
  const chat = (0, import_react.useChat)({
470
709
  transport,
471
- resume: options.resumeRunId !== void 0,
710
+ resume: options.resumeRunId !== void 0 || autoResume,
472
711
  ...options.threadId !== void 0 ? {
473
712
  id: options.threadId
474
713
  } : {},
@@ -535,10 +774,10 @@ function useAgentChat(options) {
535
774
  ]);
536
775
  const cancel = (0, import_react2.useCallback)(async () => {
537
776
  chatRef.current.stop();
538
- const activeRunId = runIdRef.current;
539
- if (activeRunId) {
777
+ const activeRunId2 = runIdRef.current;
778
+ if (activeRunId2) {
540
779
  try {
541
- await client.cancelStream(activeRunId);
780
+ await client.cancelStream(activeRunId2);
542
781
  } catch {
543
782
  }
544
783
  }
@@ -570,6 +809,8 @@ function useAgentChat(options) {
570
809
  ...chat,
571
810
  addToolResult,
572
811
  runId,
812
+ /** The `resume`-fetched thread's active run id, or `null` once resolved with none. */
813
+ activeRunId,
573
814
  client,
574
815
  threads,
575
816
  loadThreads,
@@ -1179,6 +1420,7 @@ __name(persistSpeechLang, "persistSpeechLang");
1179
1420
  formatRelativeTime,
1180
1421
  loadStoredSpeechLang,
1181
1422
  persistSpeechLang,
1423
+ storedMessageToUiMessage,
1182
1424
  useAgentChat,
1183
1425
  useSpeechRecognition
1184
1426
  });