@dudousxd/nestjs-agent-react 0.23.0 → 0.25.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
@@ -99,22 +99,87 @@ __export(src_exports, {
99
99
  useThreads: () => useThreads,
100
100
  useToolCatalog: () => useToolCatalog,
101
101
  useTranscriptItem: () => useTranscriptItem,
102
- validateAnswer: () => import_nestjs_agent_core.validateElicitationAnswer,
103
- validateAnswerValue: () => import_nestjs_agent_core.validateElicitationValue
102
+ validateAnswer: () => import_nestjs_agent_core2.validateElicitationAnswer,
103
+ validateAnswerValue: () => import_nestjs_agent_core2.validateElicitationValue
104
104
  });
105
105
  module.exports = __toCommonJS(src_exports);
106
106
 
107
+ // src/http-error.ts
108
+ function readErrorAnswer(text) {
109
+ if (text == null || text === "") return {
110
+ body: void 0,
111
+ message: void 0,
112
+ code: void 0
113
+ };
114
+ let body = text;
115
+ try {
116
+ body = JSON.parse(text);
117
+ } catch {
118
+ return {
119
+ body: text,
120
+ message: void 0,
121
+ code: void 0
122
+ };
123
+ }
124
+ if (body === null || typeof body !== "object") return {
125
+ body,
126
+ message: void 0,
127
+ code: void 0
128
+ };
129
+ const record = body;
130
+ const message = typeof record.message === "string" && record.message !== "" ? record.message : Array.isArray(record.message) && record.message.length > 0 && record.message.every((each) => typeof each === "string") ? record.message.join("; ") : void 0;
131
+ const code = typeof record.code === "string" && record.code !== "" ? record.code : void 0;
132
+ return {
133
+ body,
134
+ message,
135
+ code
136
+ };
137
+ }
138
+ __name(readErrorAnswer, "readErrorAnswer");
139
+ async function readErrorResponse(response) {
140
+ let text;
141
+ try {
142
+ text = await response.text();
143
+ } catch {
144
+ text = void 0;
145
+ }
146
+ return readErrorAnswer(text);
147
+ }
148
+ __name(readErrorResponse, "readErrorResponse");
149
+ function reportHttpError(listener, error) {
150
+ if (listener === void 0) return;
151
+ try {
152
+ listener(error);
153
+ } catch {
154
+ }
155
+ }
156
+ __name(reportHttpError, "reportHttpError");
157
+
107
158
  // src/client.ts
108
- var AgentHttpError = class extends Error {
159
+ var AgentHttpError = class _AgentHttpError extends Error {
109
160
  static {
110
161
  __name(this, "AgentHttpError");
111
162
  }
112
163
  status;
113
164
  method;
114
165
  path;
115
- constructor(status, method, path, statusText) {
116
- super(`Agent request failed: ${method} ${path} \u2192 ${status} ${statusText}`), this.status = status, this.method = method, this.path = path;
166
+ /** The answer's body: parsed JSON, else its text; `undefined` when empty. */
167
+ body;
168
+ /** The body's `code` (`quota_exceeded`, …), when the server sent one. */
169
+ code;
170
+ constructor(status, method, path, statusText, answer = {
171
+ body: void 0,
172
+ message: void 0,
173
+ code: void 0
174
+ }) {
175
+ super(answer.message ?? `Agent request failed: ${method} ${path} \u2192 ${status} ${statusText}`), this.status = status, this.method = method, this.path = path;
117
176
  this.name = "AgentHttpError";
177
+ this.body = answer.body;
178
+ this.code = answer.code;
179
+ }
180
+ /** Read a refused `response`'s body into an error. */
181
+ static async from(response, method, path) {
182
+ return new _AgentHttpError(response.status, method, path, response.statusText, await readErrorResponse(response));
118
183
  }
119
184
  };
120
185
  function normalizeAgentPath(path) {
@@ -149,7 +214,7 @@ var AgentClient = class {
149
214
  } : {}
150
215
  });
151
216
  if (!response.ok || !response.body) {
152
- throw new AgentHttpError(response.status, "POST", `${this.agentPath()}/chat`, response.statusText);
217
+ throw await this.failure(response, "POST", `${this.agentPath()}/chat`);
153
218
  }
154
219
  return streamResponse(response);
155
220
  }
@@ -176,7 +241,7 @@ var AgentClient = class {
176
241
  return null;
177
242
  }
178
243
  if (!response.ok || !response.body) {
179
- throw new AgentHttpError(response.status, "GET", `${this.agentPath()}${path}`, response.statusText);
244
+ throw await this.failure(response, "GET", `${this.agentPath()}${path}`);
180
245
  }
181
246
  return streamResponse(response);
182
247
  }
@@ -269,7 +334,9 @@ var AgentClient = class {
269
334
  };
270
335
  xhr.onload = () => {
271
336
  if (xhr.status < 200 || xhr.status >= 300) {
272
- reject(new AgentHttpError(xhr.status, "POST", `${this.agentPath()}/attachments`, xhr.statusText));
337
+ const error = new AgentHttpError(xhr.status, "POST", `${this.agentPath()}/attachments`, xhr.statusText, readErrorAnswer(xhr.responseText));
338
+ reportHttpError(this.options.onHttpError, error);
339
+ reject(error);
273
340
  return;
274
341
  }
275
342
  onProgress?.(1);
@@ -332,6 +399,7 @@ var AgentClient = class {
332
399
  * out takes the pre-picked default the request carried, resolved server-side against the request
333
400
  * the run already holds. Omit the whole object and the user has confirmed every pre-picked
334
401
  * answer — which is the point of the surface, so it is a valid submission rather than a blank.
402
+ * `via` names the surface the answer came through (the server records `'web'` when omitted).
335
403
  */
336
404
  answerToolCall(input) {
337
405
  return this.request("POST", "/tool-call/answer", input);
@@ -354,7 +422,10 @@ var AgentClient = class {
354
422
  path: this.agentPath(),
355
423
  headers: /* @__PURE__ */ __name(() => this.resolveHeaders(), "headers"),
356
424
  fetch: this.fetchImpl(),
357
- ...this.credentials()
425
+ ...this.credentials(),
426
+ ...this.options.onHttpError !== void 0 ? {
427
+ onHttpError: this.options.onHttpError
428
+ } : {}
358
429
  };
359
430
  }
360
431
  baseUrl() {
@@ -397,9 +468,15 @@ var AgentClient = class {
397
468
  });
398
469
  return this.handleResponse(response, method, path);
399
470
  }
471
+ /** The error for a refused `response`, already reported to `onHttpError`. */
472
+ async failure(response, method, path) {
473
+ const error = await AgentHttpError.from(response, method, path);
474
+ reportHttpError(this.options.onHttpError, error);
475
+ return error;
476
+ }
400
477
  async handleResponse(response, method, path) {
401
478
  if (!response.ok) {
402
- throw new AgentHttpError(response.status, method, path, response.statusText);
479
+ throw await this.failure(response, method, path);
403
480
  }
404
481
  if (response.status === 204) return void 0;
405
482
  const text = await response.text();
@@ -967,6 +1044,13 @@ var AgentChatTransport = class {
967
1044
  }
968
1045
  });
969
1046
  break;
1047
+ case "message-metadata":
1048
+ ensureStarted();
1049
+ controller.enqueue({
1050
+ type: "message-metadata",
1051
+ messageMetadata: event.metadata
1052
+ });
1053
+ break;
970
1054
  case "title":
971
1055
  case "cancelled":
972
1056
  forwardAsData(event, true);
@@ -1747,14 +1831,16 @@ __name(GenerativeUI, "GenerativeUI");
1747
1831
 
1748
1832
  // src/provider.tsx
1749
1833
  var AgentBackendContext = sharedContext("@dudousxd/nestjs-agent-react:agent-backend");
1750
- function AgentProvider({ backend, baseUrl, path, headers, getHeaders, credentials, fetch, attachments, genui, children }) {
1834
+ function AgentProvider({ backend, baseUrl, path, headers, getHeaders, credentials, fetch, onHttpError, attachments, genui, children }) {
1751
1835
  const latestHeaders = (0, import_react5.useRef)({
1752
1836
  headers,
1753
- getHeaders
1837
+ getHeaders,
1838
+ onHttpError
1754
1839
  });
1755
1840
  latestHeaders.current = {
1756
1841
  headers,
1757
- getHeaders
1842
+ getHeaders,
1843
+ onHttpError
1758
1844
  };
1759
1845
  const upload = attachments?.upload;
1760
1846
  const value = (0, import_react5.useMemo)(() => backend ?? new AgentClient({
@@ -1775,6 +1861,7 @@ function AgentProvider({ backend, baseUrl, path, headers, getHeaders, credential
1775
1861
  upload
1776
1862
  }
1777
1863
  } : {},
1864
+ onHttpError: /* @__PURE__ */ __name((error) => latestHeaders.current.onHttpError?.(error), "onHttpError"),
1778
1865
  getHeaders: /* @__PURE__ */ __name(async () => {
1779
1866
  const current = latestHeaders.current;
1780
1867
  const dynamic = await current.getHeaders?.() ?? {};
@@ -2188,6 +2275,7 @@ function useModels(options = {}) {
2188
2275
  providers,
2189
2276
  models,
2190
2277
  defaultModel: data?.default ?? null,
2278
+ locked: data?.locked ?? null,
2191
2279
  find: /* @__PURE__ */ __name((id) => id == null ? void 0 : byId.get(id), "find"),
2192
2280
  isLoading,
2193
2281
  error,
@@ -2203,6 +2291,7 @@ function useModels(options = {}) {
2203
2291
  __name(useModels, "useModels");
2204
2292
 
2205
2293
  // src/quota/use-quota.ts
2294
+ var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
2206
2295
  var import_react11 = require("react");
2207
2296
 
2208
2297
  // src/threads/threads-events.ts
@@ -2258,6 +2347,7 @@ function useQuota(options = {}) {
2258
2347
  day: windows.find((window2) => window2.period === "day"),
2259
2348
  month: windows.find((window2) => window2.period === "month"),
2260
2349
  blocked: data?.blocked ?? null,
2350
+ warning: data === void 0 || data.blocked !== void 0 ? null : data.warning ?? (0, import_nestjs_agent_core.quotaWarning)(windows) ?? null,
2261
2351
  isLoading,
2262
2352
  error,
2263
2353
  refresh
@@ -2701,11 +2791,11 @@ var import_react18 = __toESM(require("react"), 1);
2701
2791
  var import_react17 = require("react");
2702
2792
 
2703
2793
  // src/transcript/model.ts
2704
- var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
2794
+ var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
2705
2795
  var import_ai2 = require("ai");
2706
2796
 
2707
2797
  // src/elicitation/answers.ts
2708
- var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
2798
+ var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
2709
2799
  function pad(value) {
2710
2800
  return String(value).padStart(2, "0");
2711
2801
  }
@@ -3300,7 +3390,7 @@ function readElicitationRequest(part) {
3300
3390
  if (!isRecord3(candidate) || typeof candidate.id !== "string" || typeof candidate.prompt !== "string") {
3301
3391
  return null;
3302
3392
  }
3303
- const input2 = (0, import_nestjs_agent_core2.readElicitationInput)(candidate.input) ?? null;
3393
+ const input2 = (0, import_nestjs_agent_core3.readElicitationInput)(candidate.input) ?? null;
3304
3394
  const rawOptions = Array.isArray(candidate.options) ? candidate.options : [];
3305
3395
  if (rawOptions.length === 0 && (input2 === null || input2.type === "select")) {
3306
3396
  return null;
@@ -3346,7 +3436,9 @@ function readElicitationOutcome(part) {
3346
3436
  answers,
3347
3437
  skipped: output.skipped,
3348
3438
  defaulted: Array.isArray(output.defaulted) ? output.defaulted.filter((id) => typeof id === "string") : [],
3349
- summary: typeof output.summary === "string" ? output.summary : null
3439
+ summary: typeof output.summary === "string" ? output.summary : null,
3440
+ answeredBy: typeof output.answeredBy === "string" ? output.answeredBy : null,
3441
+ answeredVia: typeof output.answeredVia === "string" ? output.answeredVia : null
3350
3442
  };
3351
3443
  }
3352
3444
  __name(readElicitationOutcome, "readElicitationOutcome");
@@ -3358,7 +3450,7 @@ function buildElicitationBlock(key, part, request, options) {
3358
3450
  const picked = isPending ? options.picked(toolCallId, question.id) : void 0;
3359
3451
  const selected = picked ?? outcome?.answers[question.id] ?? question.defaults;
3360
3452
  const pick = /* @__PURE__ */ __name((values) => options.pick(toolCallId, question.id, values), "pick");
3361
- const error = isPending ? (0, import_nestjs_agent_core2.validateElicitationAnswer)({
3453
+ const error = isPending ? (0, import_nestjs_agent_core3.validateElicitationAnswer)({
3362
3454
  id: question.id,
3363
3455
  prompt: question.prompt,
3364
3456
  options: question.options.map(({ value, label }) => ({
@@ -5007,7 +5099,7 @@ function useApprovalCountdown(expiresAt, options = {}) {
5007
5099
  __name(useApprovalCountdown, "useApprovalCountdown");
5008
5100
 
5009
5101
  // src/stored-message-to-ui-message.ts
5010
- var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
5102
+ var import_nestjs_agent_core4 = require("@dudousxd/nestjs-agent-core");
5011
5103
  function isRefusal(result) {
5012
5104
  if (result.denied === true) {
5013
5105
  return true;
@@ -5020,7 +5112,7 @@ function refusalReason(result) {
5020
5112
  const { output } = result;
5021
5113
  if (output !== null && typeof output === "object" && "reason" in output) {
5022
5114
  const reason = output.reason;
5023
- return typeof reason === "string" && reason !== import_nestjs_agent_core3.DEFAULT_REFUSAL_REASON ? reason : void 0;
5115
+ return typeof reason === "string" && reason !== import_nestjs_agent_core4.DEFAULT_REFUSAL_REASON ? reason : void 0;
5024
5116
  }
5025
5117
  return void 0;
5026
5118
  }
@@ -5121,13 +5213,19 @@ function storedMessageToUiMessage(message) {
5121
5213
  for (const call of message.toolCalls ?? []) {
5122
5214
  const result = message.toolResults?.find((candidate) => candidate.id === call.id);
5123
5215
  const toolKind2 = call.kind;
5216
+ const toolMetadata = {
5217
+ ...toolKind2 !== void 0 ? {
5218
+ toolKind: toolKind2
5219
+ } : {},
5220
+ ...call.parentId !== void 0 ? {
5221
+ parentId: call.parentId
5222
+ } : {}
5223
+ };
5124
5224
  parts.push({
5125
5225
  type: `tool-${call.name}`,
5126
5226
  toolCallId: call.id,
5127
- ...toolKind2 !== void 0 ? {
5128
- toolMetadata: {
5129
- toolKind: toolKind2
5130
- }
5227
+ ...Object.keys(toolMetadata).length > 0 ? {
5228
+ toolMetadata
5131
5229
  } : {},
5132
5230
  ...result !== void 0 ? isRefusal(result) ? {
5133
5231
  // A declined action is NOT an available output. Reloading a thread used to bring one
@@ -5155,6 +5253,8 @@ function storedMessageToUiMessage(message) {
5155
5253
  parts.push(...pushedByCall.get(call.id) ?? []);
5156
5254
  }
5157
5255
  const metadata = {
5256
+ // The host's own facts first: the library's keys win on a clash.
5257
+ ...message.metadata,
5158
5258
  ...message.feedback !== void 0 ? {
5159
5259
  feedback: message.feedback
5160
5260
  } : {},
@@ -5214,6 +5314,8 @@ function mergeTurn(rows) {
5214
5314
  const feedback = lastRow.feedback;
5215
5315
  const createdAt = rows[0]?.createdAt;
5216
5316
  const metadata = {
5317
+ // Host facts of every row, a later row's winning (the turn's final state), under the library's.
5318
+ ...Object.assign({}, ...rows.map((row) => row.metadata ?? {})),
5217
5319
  ...usage !== void 0 ? {
5218
5320
  usage
5219
5321
  } : {},
@@ -5286,6 +5388,7 @@ function useAgentChat(options = {}) {
5286
5388
  const [pickedModel, setPickedModel] = (0, import_react23.useState)(void 0);
5287
5389
  const pickedModelRef = (0, import_react23.useRef)(pickedModel);
5288
5390
  pickedModelRef.current = pickedModel;
5391
+ const lockedRef = (0, import_react23.useRef)(false);
5289
5392
  const pendingPin = (0, import_react23.useRef)(void 0);
5290
5393
  const regenerateNext = (0, import_react23.useRef)(false);
5291
5394
  const currentThreadId = (0, import_react23.useCallback)(() => latest.current.threadId ?? createdThreadId.current, []);
@@ -5333,8 +5436,13 @@ function useAgentChat(options = {}) {
5333
5436
  const regenerate2 = regenerateNext.current;
5334
5437
  regenerateNext.current = false;
5335
5438
  const threadId = currentThreadId();
5336
- const model = current.model ?? pickedModelRef.current;
5439
+ const model = current.model ?? (lockedRef.current ? void 0 : pickedModelRef.current);
5337
5440
  return {
5441
+ // Read per send: a host that switches agents before the first message (a picker on a
5442
+ // new chat) sends the one picked now, not the one this chat mounted with.
5443
+ ...current.agent !== void 0 ? {
5444
+ agent: current.agent
5445
+ } : {},
5338
5446
  ...threadId !== void 0 ? {
5339
5447
  threadId
5340
5448
  } : {},
@@ -5422,6 +5530,7 @@ function useAgentChat(options = {}) {
5422
5530
  setRunId(void 0);
5423
5531
  setActiveRunId(null);
5424
5532
  setThreadModel(null);
5533
+ setPickedModel(void 0);
5425
5534
  setBackgroundRuns([]);
5426
5535
  setHistoryError(null);
5427
5536
  autoResumeRunIdRef.current = void 0;
@@ -5586,19 +5695,25 @@ function useAgentChat(options = {}) {
5586
5695
  }, [
5587
5696
  client
5588
5697
  ]);
5589
- const answer = (0, import_react23.useCallback)(async ({ toolCallId, answers }) => {
5698
+ const answer = (0, import_react23.useCallback)(async ({ toolCallId, answers, via }) => {
5590
5699
  await requireBackendMethod(client, "answerToolCall")({
5591
5700
  toolCallId,
5592
5701
  ...answers !== void 0 ? {
5593
5702
  answers
5703
+ } : {},
5704
+ ...via !== void 0 ? {
5705
+ via
5594
5706
  } : {}
5595
5707
  });
5596
5708
  }, [
5597
5709
  client
5598
5710
  ]);
5599
- const skip = (0, import_react23.useCallback)(async ({ toolCallId }) => {
5711
+ const skip = (0, import_react23.useCallback)(async ({ toolCallId, via }) => {
5600
5712
  await requireBackendMethod(client, "skipToolCall")({
5601
- toolCallId
5713
+ toolCallId,
5714
+ ...via !== void 0 ? {
5715
+ via
5716
+ } : {}
5602
5717
  });
5603
5718
  }, [
5604
5719
  client
@@ -5678,10 +5793,14 @@ function useAgentChat(options = {}) {
5678
5793
  modelsWantedRef.current = true;
5679
5794
  queueMicrotask(() => setModelsWanted(true));
5680
5795
  }, []);
5681
- const selectModel = (0, import_react23.useCallback)((id) => setPickedModel(id), []);
5796
+ lockedRef.current = catalog.locked !== null;
5797
+ const selectModel = (0, import_react23.useCallback)((id) => {
5798
+ if (!lockedRef.current) setPickedModel(id);
5799
+ }, []);
5682
5800
  const pinToThread = (0, import_react23.useCallback)(async (model) => {
5683
5801
  const threadId = currentThreadId();
5684
5802
  setThreadModel(model);
5803
+ setPickedModel(void 0);
5685
5804
  if (threadId === void 0) {
5686
5805
  pendingPin.current = {
5687
5806
  model
@@ -5707,7 +5826,16 @@ function useAgentChat(options = {}) {
5707
5826
  wantModels();
5708
5827
  return catalog.providers;
5709
5828
  },
5710
- selected: options.model ?? pickedModel ?? threadModel ?? catalog.defaultModel ?? null,
5829
+ selected: catalog.locked?.model ?? options.model ?? pickedModel ?? threadModel ?? catalog.defaultModel ?? null,
5830
+ pinned: threadModel,
5831
+ get defaultModel() {
5832
+ wantModels();
5833
+ return catalog.defaultModel;
5834
+ },
5835
+ get locked() {
5836
+ wantModels();
5837
+ return catalog.locked;
5838
+ },
5711
5839
  select: selectModel,
5712
5840
  pinToThread,
5713
5841
  isLoading: modelsWanted && catalog.isLoading,
@@ -5735,10 +5863,26 @@ function useAgentChat(options = {}) {
5735
5863
  const current = composerRef.current;
5736
5864
  if (current.blockedBy !== null) return;
5737
5865
  const refs = current.files.refs;
5866
+ const files2 = current.files.items.flatMap((item) => item.status === "ready" && item.attachment !== void 0 ? [
5867
+ {
5868
+ type: "file",
5869
+ mediaType: item.attachment.contentType,
5870
+ filename: item.attachment.name,
5871
+ url: item.attachment.url,
5872
+ providerMetadata: {
5873
+ agent: {
5874
+ mediaId: item.attachment.mediaId
5875
+ }
5876
+ }
5877
+ }
5878
+ ] : []);
5738
5879
  const draft = current.text.trim();
5739
5880
  setText("");
5740
5881
  current.files.clear();
5741
- await sendMessage({
5882
+ await sendMessage(files2.length > 0 ? {
5883
+ text: draft,
5884
+ files: files2
5885
+ } : {
5742
5886
  text: draft
5743
5887
  }, refs.length > 0 ? {
5744
5888
  body: {