@dudousxd/nestjs-agent-react 0.7.4 → 0.8.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
@@ -111,8 +111,8 @@ function Chat() {
111
111
  | On the instance | What it is |
112
112
  |---|---|
113
113
  | `items` | The mounted window, oldest first. Each item carries `blocks`, `text`, `usage`, `timestamp`, `isStreaming`, `isLastAssistant`, and its action machines. |
114
- | `items[i].blocks` | `{ kind: 'text' \| 'reasoning' \| 'tools' \| 'sources' \| 'elicitation' }`. A `tools` block is a run of CONSECUTIVE tool parts — any other part between two calls (including a `step-start` marker) ends the run. A `reasoning` block carries `isOpen`/`toggle`, open while it streams. A `sources` block appears only under `sources: true`, an `elicitation` block only under `onAnswer` — see below. |
115
- | `items[i].blocks[n]` (`tools`) | Also carries `calls`: the same parts, each with `{ toolCallId, name, isAwaitingApproval, approve, reject, error }`. |
114
+ | `items[i].blocks` | `{ kind: 'text' \| 'reasoning' \| 'tools' \| 'sources' \| 'elicitation' \| 'ui' }`. A `ui` block is a component the server pushed (`{ id, component, props, version }`) — look `component` up in your own registry. A `tools` block is a run of CONSECUTIVE tool parts — any other part between two calls (including a `step-start` marker) ends the run. A `reasoning` block carries `isOpen`/`toggle`, open while it streams. A `sources` block appears only under `sources: true`, an `elicitation` block only under `onAnswer` — see below. |
115
+ | `items[i].blocks[n]` (`tools`) | Also carries `calls`: the same parts, each with `{ toolCallId, name, toolKind, parentId, children, approval, isAwaitingApproval, approve, reject, error }`, and `roots`: the same calls as a tree (a call nested under another by the stream's `parentId` sits in its parent's `children`). `approval` is `{ approver, expiresAt, reason }` when the runner said who has to decide, else `null`. |
116
116
  | `items[i].copy` | `{ available, copied, copy() }` — `copied` flashes for `copyResetMs` (default 1500). |
117
117
  | `items[i].edit` | `{ available, isEditing, draft, canSave, start(), cancel(), setDraft(), save(), getTextareaProps() }`. The prop-getter focuses with the caret at the end, saves on Enter, cancels on Escape. |
118
118
  | `items[i].fork` / `.regenerate` | `{ available, run() }`. Regenerate is offered on the last assistant message only. |
@@ -324,6 +324,10 @@ function CustomChat({ threadId }: { threadId?: string }) {
324
324
  onRunSettled: ({ status }) => {
325
325
  if (status === 'completed') refetchThreadList();
326
326
  },
327
+ // The server named (or renamed) the thread mid-stream — update the header now.
328
+ onTitle: (title) => setHeaderTitle(title),
329
+ // Every data part as it arrives: pushed `data-ui` components, `data-approval-requested`, …
330
+ onData: (part) => analytics.track(part.type),
327
331
  });
328
332
 
329
333
  return (
@@ -381,6 +385,20 @@ const transport = new AgentChatTransport({
381
385
  const chat = useChat({ transport });
382
386
  ```
383
387
 
388
+ Beyond text, reasoning and tool calls, the transport maps the rest of the stream vocabulary to AI SDK
389
+ data parts, which `useChat`'s `onData` (and `useAgentChat({ onData })`) sees as they arrive:
390
+
391
+ | Stream frame | Becomes |
392
+ |---|---|
393
+ | `ui` | a `data-ui` part keyed by the component id (a repeat id updates it in place) |
394
+ | `approval-requested` | a `data-approval-requested` part keyed by the call id, plus the SDK's native approval request — the tool part moves to `state: 'approval-requested'` |
395
+ | `title` / `cancelled` | transient `data-title` / `data-cancelled` (never stored on the message); `useAgentChat({ onTitle })` |
396
+ | a kind this version does not know | a `data-<kind>` part — forwarded, never dropped |
397
+
398
+ `parentId` on a tool frame rides the part's `toolMetadata` next to `toolKind`. The full wire
399
+ contract — for a backend that serves these routes without this library's loop — is
400
+ [docs/stream-protocol.md](../../docs/stream-protocol.md).
401
+
384
402
  ### Loading persisted history
385
403
 
386
404
  A reloaded thread's `StoredMessage[]` (from `AgentClient.getThread`) needs converting to `UIMessage[]`
package/dist/index.cjs CHANGED
@@ -202,7 +202,8 @@ var AgentChatTransport = class {
202
202
  * Parse the backend's SSE byte stream and re-emit it as a valid v7 UI-message chunk stream.
203
203
  * Recognized frames:
204
204
  * - `event: meta` `data: {"runId","threadId"}` → records identity
205
- * - `data: <AgentStreamEvent JSON>` → mapped to UI chunks (text / reasoning / tool)
205
+ * - `data: <AgentStreamEvent JSON>` → mapped to UI chunks (text / reasoning / tool /
206
+ * `data-*`; see {@link AgentChatTransport} docs)
206
207
  * - `event: done` `data: {}` → terminates
207
208
  * - `event: error` `data: {code,message}` → error chunk
208
209
  *
@@ -219,7 +220,10 @@ var AgentChatTransport = class {
219
220
  let stepIndex = 0;
220
221
  let textId = null;
221
222
  let reasoningId = null;
223
+ let textSeq = 0;
224
+ let reasoningSeq = 0;
222
225
  const announced = /* @__PURE__ */ new Set();
226
+ const toolMetadata = /* @__PURE__ */ new Map();
223
227
  const record = /* @__PURE__ */ __name((meta) => this.recordMeta(meta), "record");
224
228
  const endAttempt = /* @__PURE__ */ __name(() => {
225
229
  this.attemptLive = false;
@@ -241,6 +245,8 @@ var AgentChatTransport = class {
241
245
  stepIndex += 1;
242
246
  textId = null;
243
247
  reasoningId = null;
248
+ textSeq = 0;
249
+ reasoningSeq = 0;
244
250
  controller.enqueue({
245
251
  type: "start-step"
246
252
  });
@@ -250,8 +256,7 @@ var AgentChatTransport = class {
250
256
  if (!stepOpen) openStep();
251
257
  }
252
258
  __name(ensureStep, "ensureStep");
253
- function closeStep() {
254
- if (!stepOpen) return;
259
+ function closeRuns() {
255
260
  if (textId !== null) {
256
261
  controller.enqueue({
257
262
  type: "text-end",
@@ -266,12 +271,43 @@ var AgentChatTransport = class {
266
271
  });
267
272
  reasoningId = null;
268
273
  }
274
+ }
275
+ __name(closeRuns, "closeRuns");
276
+ function closeStep() {
277
+ if (!stepOpen) return;
278
+ closeRuns();
269
279
  controller.enqueue({
270
280
  type: "finish-step"
271
281
  });
272
282
  stepOpen = false;
273
283
  }
274
284
  __name(closeStep, "closeStep");
285
+ function metadataFor(id, toolKind2, parentId) {
286
+ const merged = {
287
+ ...toolMetadata.get(id)
288
+ };
289
+ if (toolKind2 !== void 0) merged.toolKind = toolKind2;
290
+ if (parentId !== void 0 && merged.parentId === void 0) merged.parentId = parentId;
291
+ if (Object.keys(merged).length === 0) return void 0;
292
+ toolMetadata.set(id, merged);
293
+ return merged;
294
+ }
295
+ __name(metadataFor, "metadataFor");
296
+ function forwardAsData(event, transient) {
297
+ ensureStarted();
298
+ const { kind, ...data } = event;
299
+ controller.enqueue({
300
+ type: `data-${kind}`,
301
+ ...typeof data.id === "string" ? {
302
+ id: data.id
303
+ } : {},
304
+ data,
305
+ ...transient ? {
306
+ transient: true
307
+ } : {}
308
+ });
309
+ }
310
+ __name(forwardAsData, "forwardAsData");
275
311
  function emit(event) {
276
312
  switch (event.kind) {
277
313
  case "step-start":
@@ -291,7 +327,8 @@ var AgentChatTransport = class {
291
327
  case "text":
292
328
  ensureStep();
293
329
  if (textId === null) {
294
- textId = `txt-${stepIndex}`;
330
+ textId = textSeq === 0 ? `txt-${stepIndex}` : `txt-${stepIndex}.${textSeq}`;
331
+ textSeq += 1;
295
332
  controller.enqueue({
296
333
  type: "text-start",
297
334
  id: textId
@@ -306,7 +343,8 @@ var AgentChatTransport = class {
306
343
  case "reasoning":
307
344
  ensureStep();
308
345
  if (reasoningId === null) {
309
- reasoningId = `rsn-${stepIndex}`;
346
+ reasoningId = reasoningSeq === 0 ? `rsn-${stepIndex}` : `rsn-${stepIndex}.${reasoningSeq}`;
347
+ reasoningSeq += 1;
310
348
  controller.enqueue({
311
349
  type: "reasoning-start",
312
350
  id: reasoningId
@@ -318,23 +356,20 @@ var AgentChatTransport = class {
318
356
  delta: event.text
319
357
  });
320
358
  break;
321
- case "tool-input-start":
359
+ case "tool-input-start": {
322
360
  ensureStep();
323
361
  announced.add(event.id);
362
+ const metadata = metadataFor(event.id, event.toolKind, event.parentId);
324
363
  controller.enqueue({
325
364
  type: "tool-input-start",
326
365
  toolCallId: event.id,
327
366
  toolName: event.name,
328
- // `toolKind` is absent on older backends — omitted (not `undefined`-valued) so
329
- // `toolMetadata` itself is only present when there's something to say, letting a
330
- // UI gate approval affordances on `kind === 'action'` without hardcoding tool names.
331
- ...event.toolKind !== void 0 ? {
332
- toolMetadata: {
333
- toolKind: event.toolKind
334
- }
367
+ ...metadata !== void 0 ? {
368
+ toolMetadata: metadata
335
369
  } : {}
336
370
  });
337
371
  break;
372
+ }
338
373
  case "tool-input-delta":
339
374
  ensureStep();
340
375
  controller.enqueue({
@@ -343,21 +378,21 @@ var AgentChatTransport = class {
343
378
  inputTextDelta: event.delta
344
379
  });
345
380
  break;
346
- case "tool-input-available":
381
+ case "tool-input-available": {
347
382
  ensureStep();
348
383
  announced.add(event.id);
384
+ const metadata = metadataFor(event.id, event.toolKind, event.parentId);
349
385
  controller.enqueue({
350
386
  type: "tool-input-available",
351
387
  toolCallId: event.id,
352
388
  toolName: event.name,
353
389
  input: event.input,
354
- ...event.toolKind !== void 0 ? {
355
- toolMetadata: {
356
- toolKind: event.toolKind
357
- }
390
+ ...metadata !== void 0 ? {
391
+ toolMetadata: metadata
358
392
  } : {}
359
393
  });
360
394
  break;
395
+ }
361
396
  case "elicitation":
362
397
  ensureStep();
363
398
  if (!announced.has(event.id)) {
@@ -398,7 +433,39 @@ var AgentChatTransport = class {
398
433
  toolCallId: event.id
399
434
  });
400
435
  break;
436
+ case "approval-requested":
437
+ ensureStep();
438
+ forwardAsData(event, false);
439
+ if (announced.has(event.id)) {
440
+ controller.enqueue({
441
+ type: "tool-approval-request",
442
+ approvalId: event.id,
443
+ toolCallId: event.id
444
+ });
445
+ }
446
+ break;
447
+ case "ui":
448
+ ensureStep();
449
+ closeRuns();
450
+ controller.enqueue({
451
+ type: "data-ui",
452
+ id: event.id,
453
+ data: {
454
+ id: event.id,
455
+ component: event.component,
456
+ props: event.props,
457
+ ...event.version !== void 0 ? {
458
+ version: event.version
459
+ } : {}
460
+ }
461
+ });
462
+ break;
463
+ case "title":
464
+ case "cancelled":
465
+ forwardAsData(event, true);
466
+ break;
401
467
  default:
468
+ forwardAsData(event, false);
402
469
  break;
403
470
  }
404
471
  }
@@ -906,15 +973,18 @@ function buildTranscriptBlocks(message, options) {
906
973
  let reasoningCounter = 0;
907
974
  let sourcesCounter = 0;
908
975
  let elicitationCounter = 0;
976
+ const approvals = readApprovals(message.parts ?? []);
909
977
  function flushTools() {
910
978
  if (toolBuffer.length === 0) {
911
979
  return;
912
980
  }
981
+ const calls = toolBuffer.map((part) => buildToolCall(part, options.approval, approvals.get(part.toolCallId) ?? null));
913
982
  blocks.push({
914
983
  kind: "tools",
915
984
  key: `${message.id}-tools-${toolCounter++}`,
916
985
  parts: toolBuffer,
917
- calls: toolBuffer.map((part) => buildToolCall(part, options.approval))
986
+ calls,
987
+ roots: nestToolCalls(calls)
918
988
  });
919
989
  toolBuffer = [];
920
990
  }
@@ -977,7 +1047,21 @@ function buildTranscriptBlocks(message, options) {
977
1047
  fileBuffer.push(part);
978
1048
  continue;
979
1049
  }
1050
+ if (part.type === APPROVAL_PART) {
1051
+ continue;
1052
+ }
980
1053
  flushAll();
1054
+ if ((0, import_ai.isDataUIPart)(part)) {
1055
+ const ui = part.type === UI_PART ? readUiComponent(part) : null;
1056
+ if (ui !== null) {
1057
+ blocks.push({
1058
+ kind: "ui",
1059
+ key: `${message.id}-ui-${ui.id}`,
1060
+ ...ui
1061
+ });
1062
+ }
1063
+ continue;
1064
+ }
981
1065
  if ((0, import_ai.isTextUIPart)(part)) {
982
1066
  blocks.push({
983
1067
  kind: "text",
@@ -1061,7 +1145,42 @@ function isAwaitingApproval(part) {
1061
1145
  return toolKind(part) === "action" && readElicitationRequest(part) === null;
1062
1146
  }
1063
1147
  __name(isAwaitingApproval, "isAwaitingApproval");
1064
- function buildToolCall(part, options) {
1148
+ function toolParentId(part) {
1149
+ const metadata = part.toolMetadata;
1150
+ return isRecord(metadata) && typeof metadata.parentId === "string" ? metadata.parentId : null;
1151
+ }
1152
+ __name(toolParentId, "toolParentId");
1153
+ function nestToolCalls(calls) {
1154
+ const byId = new Map(calls.map((call) => [
1155
+ call.toolCallId,
1156
+ call
1157
+ ]));
1158
+ const roots = [];
1159
+ for (const call of calls) {
1160
+ const parent = call.parentId !== null ? byId.get(call.parentId) : void 0;
1161
+ if (parent === void 0 || isAncestor(call, parent, byId)) {
1162
+ roots.push(call);
1163
+ continue;
1164
+ }
1165
+ parent.children.push(call);
1166
+ }
1167
+ return roots;
1168
+ }
1169
+ __name(nestToolCalls, "nestToolCalls");
1170
+ function isAncestor(call, candidate, byId) {
1171
+ const seen = /* @__PURE__ */ new Set();
1172
+ let current = candidate;
1173
+ while (current !== void 0 && !seen.has(current.toolCallId)) {
1174
+ if (current.toolCallId === call.toolCallId) {
1175
+ return true;
1176
+ }
1177
+ seen.add(current.toolCallId);
1178
+ current = current.parentId !== null ? byId.get(current.parentId) : void 0;
1179
+ }
1180
+ return false;
1181
+ }
1182
+ __name(isAncestor, "isAncestor");
1183
+ function buildToolCall(part, options, approval) {
1065
1184
  const toolCallId = part.toolCallId;
1066
1185
  const awaiting = isAwaitingApproval(part);
1067
1186
  const sending = awaiting ? options?.submitting(toolCallId) ?? null : null;
@@ -1069,6 +1188,10 @@ function buildToolCall(part, options) {
1069
1188
  part,
1070
1189
  toolCallId,
1071
1190
  name: (0, import_ai.getToolName)(part),
1191
+ toolKind: toolKind(part) ?? null,
1192
+ parentId: toolParentId(part),
1193
+ children: [],
1194
+ approval,
1072
1195
  isAwaitingApproval: awaiting,
1073
1196
  approve: {
1074
1197
  available: awaiting && options?.canApprove === true,
@@ -1199,6 +1322,44 @@ function buildElicitationBlock(key, part, request, options) {
1199
1322
  };
1200
1323
  }
1201
1324
  __name(buildElicitationBlock, "buildElicitationBlock");
1325
+ var UI_PART = "data-ui";
1326
+ var APPROVAL_PART = "data-approval-requested";
1327
+ function readApprovals(parts) {
1328
+ const out = /* @__PURE__ */ new Map();
1329
+ for (const part of parts) {
1330
+ if (part.type !== APPROVAL_PART) {
1331
+ continue;
1332
+ }
1333
+ const data = part.data;
1334
+ if (!isRecord(data) || typeof data.id !== "string" || typeof data.approver !== "string") {
1335
+ continue;
1336
+ }
1337
+ out.set(data.id, {
1338
+ approver: data.approver,
1339
+ expiresAt: typeof data.expiresAt === "string" ? data.expiresAt : null,
1340
+ reason: typeof data.reason === "string" ? data.reason : null
1341
+ });
1342
+ }
1343
+ return out;
1344
+ }
1345
+ __name(readApprovals, "readApprovals");
1346
+ function readUiComponent(part) {
1347
+ const data = part.data;
1348
+ if (!isRecord(data) || typeof data.component !== "string") {
1349
+ return null;
1350
+ }
1351
+ const id = typeof data.id === "string" ? data.id : part.id;
1352
+ if (id === void 0) {
1353
+ return null;
1354
+ }
1355
+ return {
1356
+ id,
1357
+ component: data.component,
1358
+ props: isRecord(data.props) ? data.props : {},
1359
+ version: typeof data.version === "number" ? data.version : null
1360
+ };
1361
+ }
1362
+ __name(readUiComponent, "readUiComponent");
1202
1363
  function isRecord(value) {
1203
1364
  return typeof value === "object" && value !== null;
1204
1365
  }
@@ -1892,6 +2053,14 @@ function renderBlocks(item, slots) {
1892
2053
  }
1893
2054
  if (block.kind === "tools") {
1894
2055
  nodes.push(...renderToolBlock(item, block, slots));
2056
+ continue;
2057
+ }
2058
+ if (block.kind === "ui" && slots.renderUi) {
2059
+ nodes.push(/* @__PURE__ */ import_react5.default.createElement("div", {
2060
+ key: block.key,
2061
+ "data-slot": "ui",
2062
+ "data-component": block.component
2063
+ }, slots.renderUi(block)));
1895
2064
  }
1896
2065
  }
1897
2066
  return nodes;
@@ -1999,7 +2168,7 @@ function joinClasses(...entries) {
1999
2168
  __name(joinClasses, "joinClasses");
2000
2169
 
2001
2170
  // src/components/message-list.tsx
2002
- function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, renderReasoning, renderFiles, reasoningLabel, getMeta, onFork, editable, onEditSubmit, regeneratable, onRegenerate, getUsage, getCreatedAt, emptyState, typingIndicator, followUps, onFollowUpClick, error, onRetry, classNames }) {
2171
+ function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, renderReasoning, renderFiles, renderUi, reasoningLabel, getMeta, onFork, editable, onEditSubmit, regeneratable, onRegenerate, getUsage, getCreatedAt, emptyState, typingIndicator, followUps, onFollowUpClick, error, onRetry, classNames }) {
2003
2172
  const transcript = useChatTranscript({
2004
2173
  messages,
2005
2174
  status,
@@ -2049,6 +2218,9 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
2049
2218
  ...renderFiles ? {
2050
2219
  renderFiles
2051
2220
  } : {},
2221
+ ...renderUi ? {
2222
+ renderUi
2223
+ } : {},
2052
2224
  ...reasoningLabel !== void 0 ? {
2053
2225
  reasoningLabel
2054
2226
  } : {},
@@ -2703,6 +2875,13 @@ function useAgentChat(options) {
2703
2875
  ...options.initialMessages !== void 0 ? {
2704
2876
  messages: options.initialMessages
2705
2877
  } : {},
2878
+ onData: /* @__PURE__ */ __name((part) => {
2879
+ latest.current.onData?.(part);
2880
+ if (part.type === "data-title") {
2881
+ const title = part.data?.title;
2882
+ if (typeof title === "string") latest.current.onTitle?.(title);
2883
+ }
2884
+ }, "onData"),
2706
2885
  onFinish: /* @__PURE__ */ __name(({ isError }) => {
2707
2886
  latest.current.onFinish?.();
2708
2887
  if (latest.current.background === true) {