react-observer-agent 0.2.0 → 0.3.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
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  // src/tools/registerTool.ts
2
4
  function registerTool(name, handler, options) {
3
5
  return {
@@ -5,6 +7,7 @@ function registerTool(name, handler, options) {
5
7
  handler,
6
8
  description: options?.description,
7
9
  parameters: options?.parameters,
10
+ schema: options?.schema,
8
11
  confirm: options?.confirm ?? false
9
12
  };
10
13
  }
@@ -28,69 +31,6 @@ function validateToolNames(tools) {
28
31
  }
29
32
  }
30
33
 
31
- // src/provider/AIAgentProvider.tsx
32
- import { createContext, useCallback, useEffect, useMemo, useRef, useState } from "react";
33
-
34
- // src/state/resolveState.ts
35
- function resolveState(state) {
36
- const resolved = typeof state === "function" ? state() : state;
37
- return resolved;
38
- }
39
-
40
- // src/state/createStateSnapshot.ts
41
- function stripNonSerializable(obj, debug = false) {
42
- const result = {};
43
- for (const [key, value] of Object.entries(obj)) {
44
- try {
45
- JSON.stringify(value);
46
- result[key] = value;
47
- } catch {
48
- if (debug) {
49
- console.warn(
50
- `[react-observer-agent] Non-serializable value stripped from state key "${key}"`
51
- );
52
- }
53
- }
54
- }
55
- return result;
56
- }
57
- function isSerializableValue(value) {
58
- if (value === null || value === void 0) return true;
59
- if (typeof value === "function" || typeof value === "symbol") return false;
60
- if (typeof value === "bigint") return false;
61
- return true;
62
- }
63
- function createStateSnapshot(state, canAccess, debug = false) {
64
- const resolved = resolveState(state);
65
- const filtered = {};
66
- for (const key of canAccess) {
67
- if (key in resolved) {
68
- const value = resolved[key];
69
- if (!isSerializableValue(value)) {
70
- if (debug) {
71
- console.warn(
72
- `[react-observer-agent] Non-serializable value stripped from state key "${key}"`
73
- );
74
- }
75
- continue;
76
- }
77
- filtered[key] = value;
78
- }
79
- }
80
- return stripNonSerializable(filtered, debug);
81
- }
82
-
83
- // src/permissions/filterTools.ts
84
- function filterTools(tools, canExecute) {
85
- const allowed = new Set(canExecute);
86
- return tools.filter((tool) => allowed.has(tool.name));
87
- }
88
-
89
- // src/permissions/validateToolCall.ts
90
- function validateToolCall(name, canExecute) {
91
- return canExecute.includes(name);
92
- }
93
-
94
34
  // src/tools/validateArgs.ts
95
35
  function validateArgs(args, schema) {
96
36
  const errors = [];
@@ -171,14 +111,230 @@ function isPlainObject(value) {
171
111
  return typeof value === "object" && value !== null && !Array.isArray(value);
172
112
  }
173
113
 
114
+ // src/utils/describeError.ts
115
+ function describeError(error, fallback = "Unknown error") {
116
+ try {
117
+ return describeUnsafely(error, fallback);
118
+ } catch {
119
+ return fallback;
120
+ }
121
+ }
122
+ function describeUnsafely(error, fallback) {
123
+ if (error instanceof Error) {
124
+ return error.message || error.name || fallback;
125
+ }
126
+ if (typeof error === "string") {
127
+ return error || fallback;
128
+ }
129
+ if (error === null || error === void 0) {
130
+ return fallback;
131
+ }
132
+ if (typeof error === "object") {
133
+ const message = error.message;
134
+ if (typeof message === "string" && message !== "") {
135
+ return message;
136
+ }
137
+ try {
138
+ const serialized = JSON.stringify(error);
139
+ if (typeof serialized === "string" && serialized !== "{}") {
140
+ return serialized;
141
+ }
142
+ } catch {
143
+ return fallback;
144
+ }
145
+ return fallback;
146
+ }
147
+ return String(error);
148
+ }
149
+
150
+ // src/tools/validateToolArgs.ts
151
+ async function validateToolArgs(tool, args) {
152
+ if (tool.schema) {
153
+ let result;
154
+ try {
155
+ result = await tool.schema["~standard"].validate(args);
156
+ } catch (error) {
157
+ return { valid: false, errors: [describeError(error)] };
158
+ }
159
+ if (result.issues) {
160
+ return { valid: false, errors: result.issues.map(formatIssue) };
161
+ }
162
+ return { valid: true, value: result.value };
163
+ }
164
+ if (tool.parameters) {
165
+ const validation = validateArgs(args, tool.parameters);
166
+ if (!validation.valid) {
167
+ return { valid: false, errors: validation.errors };
168
+ }
169
+ }
170
+ return { valid: true, value: args };
171
+ }
172
+ function formatIssue(issue) {
173
+ const path = (issue.path ?? []).map(
174
+ (segment) => String(
175
+ typeof segment === "object" && segment !== null ? segment.key : segment
176
+ )
177
+ ).join(".");
178
+ return path === "" ? issue.message : `${path}: ${issue.message}`;
179
+ }
180
+
181
+ // src/provider/AIAgentProvider.tsx
182
+ import {
183
+ createContext,
184
+ useCallback,
185
+ useEffect,
186
+ useMemo,
187
+ useRef,
188
+ useState
189
+ } from "react";
190
+
191
+ // src/adapters/AdapterError.ts
192
+ var AdapterError = class extends Error {
193
+ constructor(message, options) {
194
+ super(message);
195
+ // Declared, not just assigned in the constructor, so the emitted typings
196
+ // publish the literal type the spec promises instead of `string`.
197
+ this.name = "AdapterError";
198
+ this.status = options?.status;
199
+ this.body = options?.body;
200
+ if (options?.cause !== void 0) {
201
+ this.cause = options.cause;
202
+ }
203
+ }
204
+ };
205
+
206
+ // src/state/resolveState.ts
207
+ function resolveState(state) {
208
+ const resolved = typeof state === "function" ? state() : state;
209
+ return resolved;
210
+ }
211
+
212
+ // src/state/createStateSnapshot.ts
213
+ function stripNonSerializable(obj, debug = false) {
214
+ const result = {};
215
+ for (const [key, value] of Object.entries(obj)) {
216
+ try {
217
+ JSON.stringify(value);
218
+ result[key] = value;
219
+ } catch {
220
+ if (debug) {
221
+ console.warn(
222
+ `[react-observer-agent] Non-serializable value stripped from state key "${key}"`
223
+ );
224
+ }
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+ function isSerializableValue(value) {
230
+ if (value === null || value === void 0) return true;
231
+ if (typeof value === "function" || typeof value === "symbol") return false;
232
+ if (typeof value === "bigint") return false;
233
+ return true;
234
+ }
235
+ function applyByteLimit(snapshot, maxBytes, debug) {
236
+ const limited = {};
237
+ for (const [key, value] of Object.entries(snapshot)) {
238
+ const json = JSON.stringify(value);
239
+ if (json === void 0 || json.length <= maxBytes) {
240
+ limited[key] = value;
241
+ continue;
242
+ }
243
+ if (debug) {
244
+ console.warn(
245
+ `[react-observer-agent] State key "${key}" is ${json.length} bytes, over the ${maxBytes} byte limit, and was truncated`
246
+ );
247
+ }
248
+ limited[key] = {
249
+ __truncated: true,
250
+ limit: maxBytes,
251
+ bytes: json.length,
252
+ preview: json.slice(0, maxBytes)
253
+ };
254
+ }
255
+ return limited;
256
+ }
257
+ function createStateSnapshot(state, canAccess, debug = false, maxBytes) {
258
+ const resolved = resolveState(state);
259
+ const filtered = {};
260
+ for (const key of canAccess) {
261
+ if (key in resolved) {
262
+ const value = resolved[key];
263
+ if (!isSerializableValue(value)) {
264
+ if (debug) {
265
+ console.warn(
266
+ `[react-observer-agent] Non-serializable value stripped from state key "${key}"`
267
+ );
268
+ }
269
+ continue;
270
+ }
271
+ filtered[key] = value;
272
+ }
273
+ }
274
+ const snapshot = stripNonSerializable(filtered, debug);
275
+ return maxBytes === void 0 ? snapshot : applyByteLimit(snapshot, maxBytes, debug);
276
+ }
277
+
278
+ // src/permissions/filterTools.ts
279
+ function filterTools(tools, canExecute) {
280
+ const allowed = new Set(canExecute);
281
+ return tools.filter((tool) => allowed.has(tool.name));
282
+ }
283
+
284
+ // src/permissions/validateToolCall.ts
285
+ function validateToolCall(name, canExecute) {
286
+ return canExecute.includes(name);
287
+ }
288
+
289
+ // src/utils/abortRace.ts
290
+ var never = () => new Promise(() => {
291
+ });
292
+ function abortRace(signal) {
293
+ if (!signal) return { promise: never(), release: () => {
294
+ } };
295
+ let release = () => {
296
+ };
297
+ const promise = new Promise((_resolve, reject) => {
298
+ const rejectAborted = () => reject(abortReason(signal));
299
+ if (signal.aborted) {
300
+ rejectAborted();
301
+ return;
302
+ }
303
+ signal.addEventListener("abort", rejectAborted, { once: true });
304
+ release = () => signal.removeEventListener("abort", rejectAborted);
305
+ });
306
+ return { promise, release };
307
+ }
308
+ function abortReason(signal) {
309
+ const { reason } = signal;
310
+ if (reason instanceof Error && reason.name === "AbortError") return reason;
311
+ const error = new Error("Interaction aborted");
312
+ error.name = "AbortError";
313
+ return error;
314
+ }
315
+
174
316
  // src/provider/executeAgentLoop.ts
175
317
  var DEFAULT_MAX_TURNS = 5;
318
+ var ABORTED_TOOL_RESULT = "Tool execution cancelled: interaction aborted";
176
319
  var READ_STATE_TOOL_NAME = "__readState";
177
320
  var EMPTY_OBJECT_SCHEMA = { type: "object", properties: {} };
321
+ var READ_STATE_SCHEMA = {
322
+ type: "object",
323
+ properties: {
324
+ keys: {
325
+ type: "array",
326
+ items: { type: "string" },
327
+ description: "State keys to read"
328
+ }
329
+ },
330
+ required: ["keys"]
331
+ };
178
332
  var UsageTotal = class {
179
333
  constructor() {
180
334
  this.promptTokens = 0;
181
335
  this.completionTokens = 0;
336
+ this.cacheReadTokens = 0;
337
+ this.cacheWriteTokens = 0;
182
338
  this.reported = false;
183
339
  }
184
340
  add(usage) {
@@ -186,14 +342,20 @@ var UsageTotal = class {
186
342
  this.reported = true;
187
343
  this.promptTokens += usage.promptTokens ?? 0;
188
344
  this.completionTokens += usage.completionTokens ?? 0;
345
+ this.cacheReadTokens += usage.cacheReadTokens ?? 0;
346
+ this.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
189
347
  }
190
348
  /** Undefined when no adapter response carried usage, rather than a false zero. */
191
349
  total() {
192
350
  if (!this.reported) return void 0;
193
- return {
351
+ const total = {
194
352
  promptTokens: this.promptTokens,
195
353
  completionTokens: this.completionTokens
196
354
  };
355
+ if (this.cacheReadTokens > 0) total.cacheReadTokens = this.cacheReadTokens;
356
+ if (this.cacheWriteTokens > 0)
357
+ total.cacheWriteTokens = this.cacheWriteTokens;
358
+ return total;
197
359
  }
198
360
  };
199
361
  function isAbortError(error) {
@@ -219,17 +381,7 @@ function buildReadStateToolDef() {
219
381
  return {
220
382
  name: READ_STATE_TOOL_NAME,
221
383
  description: "Read specific keys from the application state. Only request keys you need.",
222
- parameters: {
223
- type: "object",
224
- properties: {
225
- keys: {
226
- type: "array",
227
- items: { type: "string" },
228
- description: "State keys to read"
229
- }
230
- },
231
- required: ["keys"]
232
- }
384
+ parameters: READ_STATE_SCHEMA
233
385
  };
234
386
  }
235
387
  async function executeAgentLoop(message, ctx) {
@@ -241,7 +393,10 @@ async function executeAgentLoop(message, ctx) {
241
393
  permissions.stateDescriptions
242
394
  );
243
395
  if (debug) {
244
- console.log("[react-observer-agent] State manifest:", stateManifest.map((m) => m.key));
396
+ console.log(
397
+ "[react-observer-agent] State manifest:",
398
+ stateManifest.map((m) => m.key)
399
+ );
245
400
  }
246
401
  const allowedTools = filterTools(tools, permissions.canExecute);
247
402
  const llmTools = [];
@@ -264,7 +419,10 @@ async function executeAgentLoop(message, ctx) {
264
419
  llmTools.push(buildReadStateToolDef());
265
420
  }
266
421
  if (debug) {
267
- console.log("[react-observer-agent] Available tools:", llmTools.map((t) => t.name));
422
+ console.log(
423
+ "[react-observer-agent] Available tools:",
424
+ llmTools.map((t) => t.name)
425
+ );
268
426
  }
269
427
  const toolMap = new Map(allowedTools.map((t) => [t.name, t]));
270
428
  const messages = [
@@ -277,7 +435,28 @@ async function executeAgentLoop(message, ctx) {
277
435
  const usage = new UsageTotal();
278
436
  let turns = 0;
279
437
  let finalMessage = "";
438
+ let finalProviderData;
439
+ let stopError;
280
440
  let completed = false;
441
+ const emit = (event) => {
442
+ options?.onEvent?.(event);
443
+ };
444
+ const record = (result) => {
445
+ allToolCalls.push(result);
446
+ options?.onToolCall?.({
447
+ toolName: result.toolName,
448
+ args: result.args,
449
+ result: result.result,
450
+ status: result.status
451
+ });
452
+ emit({
453
+ type: "tool_end",
454
+ toolName: result.toolName,
455
+ args: result.args,
456
+ result: result.result,
457
+ status: result.status
458
+ });
459
+ };
281
460
  const abortedResult = () => ({
282
461
  response: {
283
462
  message: "",
@@ -287,6 +466,23 @@ async function executeAgentLoop(message, ctx) {
287
466
  },
288
467
  messages
289
468
  });
469
+ const cancelForAbort = (toolName, args, toolCallId) => {
470
+ record({
471
+ toolName,
472
+ args,
473
+ result: ABORTED_TOOL_RESULT,
474
+ status: "cancelled"
475
+ });
476
+ messages.push({
477
+ role: "tool",
478
+ content: JSON.stringify({
479
+ status: "cancelled",
480
+ reason: "Interaction aborted"
481
+ }),
482
+ toolCallId
483
+ });
484
+ return abortedResult();
485
+ };
290
486
  while (turns < maxTurns) {
291
487
  if (signal?.aborted) return abortedResult();
292
488
  turns++;
@@ -310,14 +506,21 @@ async function executeAgentLoop(message, ctx) {
310
506
  hasSystemPrompt: !!modelRequest.systemPrompt
311
507
  });
312
508
  }
509
+ emit({ type: "turn_start", turn: turns, maxTurns });
313
510
  let modelResponse;
511
+ const modelAbort = abortRace(signal);
314
512
  try {
315
- modelResponse = await model.sendMessage(modelRequest);
513
+ modelResponse = await Promise.race([
514
+ model.sendMessage(modelRequest),
515
+ modelAbort.promise
516
+ ]);
316
517
  } catch (error) {
317
518
  if (isAbortError(error) || signal?.aborted) {
318
519
  return abortedResult();
319
520
  }
320
521
  throw error;
522
+ } finally {
523
+ modelAbort.release();
321
524
  }
322
525
  usage.add(modelResponse.usage);
323
526
  if (signal?.aborted) return abortedResult();
@@ -329,26 +532,73 @@ async function executeAgentLoop(message, ctx) {
329
532
  }
330
533
  if (!modelResponse.toolCalls || modelResponse.toolCalls.length === 0) {
331
534
  finalMessage = modelResponse.content ?? "";
535
+ finalProviderData = modelResponse.providerData;
536
+ if (modelResponse.stopReason === "max_tokens") {
537
+ stopError = {
538
+ message: "Model output was cut off by the max tokens limit",
539
+ code: "TRUNCATED"
540
+ };
541
+ } else if (modelResponse.stopReason === "refusal") {
542
+ stopError = {
543
+ message: "Model declined to answer",
544
+ code: "REFUSED"
545
+ };
546
+ }
332
547
  completed = true;
333
548
  break;
334
549
  }
335
550
  messages.push({
336
551
  role: "assistant",
337
552
  content: modelResponse.content ?? "",
338
- toolCalls: modelResponse.toolCalls
553
+ toolCalls: modelResponse.toolCalls,
554
+ providerData: modelResponse.providerData
339
555
  });
340
556
  for (const llmCall of modelResponse.toolCalls) {
341
557
  if (signal?.aborted) return abortedResult();
342
558
  if (llmCall.name === READ_STATE_TOOL_NAME) {
559
+ const readValidation = validateArgs(
560
+ llmCall.arguments,
561
+ READ_STATE_SCHEMA
562
+ );
563
+ if (!readValidation.valid) {
564
+ const errorMessage = `Invalid arguments for ${READ_STATE_TOOL_NAME}: ${readValidation.errors.join("; ")}`;
565
+ if (debug) {
566
+ console.warn(`[react-observer-agent] ${errorMessage}`);
567
+ }
568
+ messages.push({
569
+ role: "tool",
570
+ content: JSON.stringify({ error: errorMessage }),
571
+ toolCallId: llmCall.id,
572
+ isError: true
573
+ });
574
+ continue;
575
+ }
343
576
  const args = llmCall.arguments;
344
- const requestedKeys = args?.keys ?? [];
345
- const allowedKeys = requestedKeys.filter((k) => permissions.canAccess.includes(k));
346
- const snapshot = createStateSnapshot(state, allowedKeys, debug);
577
+ const requestedKeys = (args?.keys ?? []).filter(
578
+ (k) => typeof k === "string"
579
+ );
580
+ const allowedKeys = requestedKeys.filter(
581
+ (k) => permissions.canAccess.includes(k)
582
+ );
583
+ const snapshot = createStateSnapshot(
584
+ state,
585
+ allowedKeys,
586
+ debug,
587
+ options?.maxStateBytes
588
+ );
347
589
  if (debug) {
348
- console.log("[react-observer-agent] readState requested:", requestedKeys);
590
+ console.log(
591
+ "[react-observer-agent] readState requested:",
592
+ requestedKeys
593
+ );
349
594
  console.log("[react-observer-agent] readState allowed:", allowedKeys);
350
595
  console.log("[react-observer-agent] readState result:", snapshot);
351
596
  }
597
+ emit({
598
+ type: "state_read",
599
+ requested: requestedKeys,
600
+ keys: allowedKeys
601
+ });
352
602
  messages.push({
353
603
  role: "tool",
354
604
  content: JSON.stringify(snapshot),
@@ -356,6 +606,11 @@ async function executeAgentLoop(message, ctx) {
356
606
  });
357
607
  continue;
358
608
  }
609
+ emit({
610
+ type: "tool_start",
611
+ toolName: llmCall.name,
612
+ args: llmCall.arguments
613
+ });
359
614
  if (!validateToolCall(llmCall.name, permissions.canExecute)) {
360
615
  const deniedResult = {
361
616
  toolName: llmCall.name,
@@ -363,17 +618,12 @@ async function executeAgentLoop(message, ctx) {
363
618
  result: `Tool "${llmCall.name}" is not permitted`,
364
619
  status: "denied"
365
620
  };
366
- allToolCalls.push(deniedResult);
367
- options?.onToolCall?.({
368
- toolName: llmCall.name,
369
- args: llmCall.arguments,
370
- result: deniedResult.result,
371
- status: "denied"
372
- });
621
+ record(deniedResult);
373
622
  messages.push({
374
623
  role: "tool",
375
624
  content: JSON.stringify({ error: deniedResult.result }),
376
- toolCallId: llmCall.id
625
+ toolCallId: llmCall.id,
626
+ isError: true
377
627
  });
378
628
  continue;
379
629
  }
@@ -385,41 +635,38 @@ async function executeAgentLoop(message, ctx) {
385
635
  result: `Tool "${llmCall.name}" not found`,
386
636
  status: "denied"
387
637
  };
388
- allToolCalls.push(deniedResult);
638
+ record(deniedResult);
389
639
  messages.push({
390
640
  role: "tool",
391
641
  content: JSON.stringify({ error: deniedResult.result }),
392
- toolCallId: llmCall.id
642
+ toolCallId: llmCall.id,
643
+ isError: true
393
644
  });
394
645
  continue;
395
646
  }
396
- if (toolDef.parameters) {
397
- const validation = validateArgs(llmCall.arguments, toolDef.parameters);
398
- if (!validation.valid) {
399
- const errorMessage = `Invalid arguments for tool "${llmCall.name}": ${validation.errors.join("; ")}`;
400
- if (debug) {
401
- console.warn(`[react-observer-agent] ${errorMessage}`);
402
- }
403
- const invalidResult = {
404
- toolName: llmCall.name,
405
- args: llmCall.arguments,
406
- result: errorMessage,
407
- status: "error"
408
- };
409
- allToolCalls.push(invalidResult);
410
- options?.onToolCall?.({
411
- toolName: llmCall.name,
412
- args: llmCall.arguments,
413
- result: errorMessage,
414
- status: "error"
415
- });
416
- messages.push({
417
- role: "tool",
418
- content: JSON.stringify({ error: errorMessage }),
419
- toolCallId: llmCall.id
420
- });
421
- continue;
647
+ const validation = await validateToolArgs(toolDef, llmCall.arguments);
648
+ if (!validation.valid) {
649
+ const errorMessage = `Invalid arguments for tool "${llmCall.name}": ${validation.errors.join("; ")}`;
650
+ if (debug) {
651
+ console.warn(`[react-observer-agent] ${errorMessage}`);
422
652
  }
653
+ record({
654
+ toolName: llmCall.name,
655
+ args: llmCall.arguments,
656
+ result: errorMessage,
657
+ status: "error"
658
+ });
659
+ messages.push({
660
+ role: "tool",
661
+ content: JSON.stringify({ error: errorMessage }),
662
+ toolCallId: llmCall.id,
663
+ isError: true
664
+ });
665
+ continue;
666
+ }
667
+ const value = validation.value;
668
+ if (signal?.aborted) {
669
+ return cancelForAbort(llmCall.name, value, llmCall.id);
423
670
  }
424
671
  if (toolDef.confirm) {
425
672
  if (!options?.onConfirm) {
@@ -428,98 +675,133 @@ async function executeAgentLoop(message, ctx) {
428
675
  `[react-observer-agent] Tool "${llmCall.name}" requires confirmation but no onConfirm handler provided. Skipping.`
429
676
  );
430
677
  }
431
- const cancelledResult = {
678
+ record({
432
679
  toolName: llmCall.name,
433
- args: llmCall.arguments,
680
+ args: value,
434
681
  result: "Tool execution cancelled: no confirmation handler provided",
435
682
  status: "cancelled"
436
- };
437
- allToolCalls.push(cancelledResult);
438
- options?.onToolCall?.({
439
- toolName: llmCall.name,
440
- args: llmCall.arguments,
441
- result: cancelledResult.result,
442
- status: "cancelled"
443
683
  });
444
684
  messages.push({
445
685
  role: "tool",
446
- content: JSON.stringify({ status: "cancelled", reason: "No confirmation handler" }),
686
+ content: JSON.stringify({
687
+ status: "cancelled",
688
+ reason: "No confirmation handler"
689
+ }),
447
690
  toolCallId: llmCall.id
448
691
  });
449
692
  continue;
450
693
  }
451
- const confirmed = await options.onConfirm({
452
- toolName: llmCall.name,
453
- args: llmCall.arguments,
454
- description: toolDef.description
455
- });
694
+ let confirmed;
695
+ const confirmAbort = abortRace(signal);
696
+ try {
697
+ confirmed = await Promise.race([
698
+ options.onConfirm({
699
+ toolName: llmCall.name,
700
+ args: value,
701
+ description: toolDef.description,
702
+ signal
703
+ }),
704
+ confirmAbort.promise
705
+ ]);
706
+ } catch (error) {
707
+ if (isAbortError(error) || signal?.aborted) {
708
+ return cancelForAbort(llmCall.name, value, llmCall.id);
709
+ }
710
+ const errorMessage = describeError(error);
711
+ record({
712
+ toolName: llmCall.name,
713
+ args: value,
714
+ result: errorMessage,
715
+ status: "error"
716
+ });
717
+ messages.push({
718
+ role: "tool",
719
+ content: JSON.stringify({ error: errorMessage }),
720
+ toolCallId: llmCall.id,
721
+ isError: true
722
+ });
723
+ continue;
724
+ } finally {
725
+ confirmAbort.release();
726
+ }
727
+ if (signal?.aborted) {
728
+ return cancelForAbort(llmCall.name, value, llmCall.id);
729
+ }
456
730
  if (!confirmed) {
457
- const cancelledResult = {
731
+ record({
458
732
  toolName: llmCall.name,
459
- args: llmCall.arguments,
733
+ args: value,
460
734
  result: "Tool execution cancelled by user",
461
735
  status: "cancelled"
462
- };
463
- allToolCalls.push(cancelledResult);
464
- options?.onToolCall?.({
465
- toolName: llmCall.name,
466
- args: llmCall.arguments,
467
- result: cancelledResult.result,
468
- status: "cancelled"
469
736
  });
470
737
  messages.push({
471
738
  role: "tool",
472
- content: JSON.stringify({ status: "cancelled", reason: "User denied" }),
739
+ content: JSON.stringify({
740
+ status: "cancelled",
741
+ reason: "User denied"
742
+ }),
473
743
  toolCallId: llmCall.id
474
744
  });
475
745
  continue;
476
746
  }
477
747
  }
748
+ let outcome;
749
+ const handlerAbort = abortRace(signal);
478
750
  try {
479
- const result = await toolDef.handler(llmCall.arguments);
480
- const status = toolDef.confirm ? "confirmed" : "success";
481
- if (debug) {
482
- console.log(`[react-observer-agent] Tool "${llmCall.name}" executed:`, { status, result });
751
+ const result = await Promise.race([
752
+ toolDef.handler(value, { signal }),
753
+ handlerAbort.promise
754
+ ]);
755
+ let content;
756
+ try {
757
+ content = JSON.stringify({ result });
758
+ } catch (error) {
759
+ throw new Error(
760
+ `Tool result is not serializable: ${describeError(error)}`
761
+ );
483
762
  }
484
- const toolResult = {
485
- toolName: llmCall.name,
486
- args: llmCall.arguments,
487
- result,
488
- status
489
- };
490
- allToolCalls.push(toolResult);
491
- options?.onToolCall?.({
492
- toolName: llmCall.name,
493
- args: llmCall.arguments,
494
- result,
495
- status
496
- });
497
- messages.push({
498
- role: "tool",
499
- content: JSON.stringify({ result }),
500
- toolCallId: llmCall.id
501
- });
763
+ outcome = { kind: "success", result, content };
502
764
  } catch (error) {
503
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
504
- const errorResult = {
765
+ outcome = isAbortError(error) && signal?.aborted ? { kind: "cancelled" } : { kind: "error", message: describeError(error) };
766
+ } finally {
767
+ handlerAbort.release();
768
+ }
769
+ if (outcome.kind === "cancelled") {
770
+ return cancelForAbort(llmCall.name, value, llmCall.id);
771
+ }
772
+ if (outcome.kind === "error") {
773
+ record({
505
774
  toolName: llmCall.name,
506
- args: llmCall.arguments,
507
- result: errorMessage,
508
- status: "error"
509
- };
510
- allToolCalls.push(errorResult);
511
- options?.onToolCall?.({
512
- toolName: llmCall.name,
513
- args: llmCall.arguments,
514
- result: errorMessage,
775
+ args: value,
776
+ result: outcome.message,
515
777
  status: "error"
516
778
  });
517
779
  messages.push({
518
780
  role: "tool",
519
- content: JSON.stringify({ error: errorMessage }),
520
- toolCallId: llmCall.id
781
+ content: JSON.stringify({ error: outcome.message }),
782
+ toolCallId: llmCall.id,
783
+ isError: true
784
+ });
785
+ continue;
786
+ }
787
+ const status = toolDef.confirm ? "confirmed" : "success";
788
+ if (debug) {
789
+ console.log(`[react-observer-agent] Tool "${llmCall.name}" executed:`, {
790
+ status,
791
+ result: outcome.result
521
792
  });
522
793
  }
794
+ record({
795
+ toolName: llmCall.name,
796
+ args: value,
797
+ result: outcome.result,
798
+ status
799
+ });
800
+ messages.push({
801
+ role: "tool",
802
+ content: outcome.content,
803
+ toolCallId: llmCall.id
804
+ });
523
805
  }
524
806
  }
525
807
  if (!completed) {
@@ -539,20 +821,60 @@ async function executeAgentLoop(message, ctx) {
539
821
  messages
540
822
  };
541
823
  }
542
- messages.push({ role: "assistant", content: finalMessage });
824
+ if (finalMessage !== "") {
825
+ messages.push({
826
+ role: "assistant",
827
+ content: finalMessage,
828
+ providerData: finalProviderData
829
+ });
830
+ }
543
831
  return {
544
832
  response: {
545
833
  message: finalMessage,
546
834
  toolCalls: allToolCalls,
835
+ error: stopError,
547
836
  usage: usage.total()
548
837
  },
549
838
  messages
550
839
  };
551
840
  }
552
841
 
842
+ // src/utils/linkSignals.ts
843
+ function linkSignals(...sources) {
844
+ const controller = new AbortController();
845
+ const listeners = [];
846
+ const release = () => {
847
+ for (const { source, listener } of listeners) {
848
+ source.removeEventListener("abort", listener);
849
+ }
850
+ listeners.length = 0;
851
+ };
852
+ for (const source of sources) {
853
+ if (!source) continue;
854
+ if (source.aborted) {
855
+ abortWith(controller, source);
856
+ release();
857
+ break;
858
+ }
859
+ const listener = () => abortWith(controller, source);
860
+ source.addEventListener("abort", listener, { once: true });
861
+ listeners.push({ source, listener });
862
+ }
863
+ return { signal: controller.signal, release };
864
+ }
865
+ function abortWith(controller, source) {
866
+ if (source.reason === void 0) {
867
+ controller.abort();
868
+ return;
869
+ }
870
+ controller.abort(source.reason);
871
+ }
872
+
553
873
  // src/provider/AIAgentProvider.tsx
554
874
  import { jsx } from "react/jsx-runtime";
555
875
  var AgentContextValue = createContext(null);
876
+ var noop = () => {
877
+ };
556
878
  function AIAgentProvider({
557
879
  model,
558
880
  state,
@@ -578,61 +900,120 @@ function AIAgentProvider({
578
900
  validateToolNames(tools);
579
901
  }, [tools]);
580
902
  const transcriptRef = useRef([]);
903
+ const queueRef = useRef(Promise.resolve());
904
+ const pendingRef = useRef(0);
905
+ const generationRef = useRef(0);
906
+ const unmountRef = useRef(null);
907
+ const unmountController = useCallback(() => {
908
+ unmountRef.current ?? (unmountRef.current = new AbortController());
909
+ return unmountRef.current;
910
+ }, []);
911
+ useEffect(() => {
912
+ if (unmountRef.current?.signal.aborted) unmountRef.current = null;
913
+ const controller = unmountController();
914
+ return () => controller.abort();
915
+ }, []);
581
916
  const clearHistory = useCallback(() => {
917
+ generationRef.current++;
582
918
  setHistory([]);
583
919
  setLastResponse(null);
584
920
  transcriptRef.current = [];
585
921
  }, []);
586
- const send = useCallback(async (message, sendOptions) => {
587
- setIsProcessing(true);
588
- const userEntry = {
589
- role: "user",
590
- content: message,
591
- timestamp: Date.now()
592
- };
593
- setHistory((prev) => [...prev, userEntry]);
594
- try {
595
- const { response, messages } = await executeAgentLoop(message, {
596
- model: modelRef.current,
597
- state: stateRef.current,
598
- tools: toolsRef.current,
599
- permissions: permissionsRef.current,
600
- options: optionsRef.current,
601
- conversationHistory: transcriptRef.current,
602
- signal: sendOptions?.signal
603
- });
604
- if (response.error?.code !== "ABORTED") {
605
- transcriptRef.current = messages;
606
- }
607
- const assistantEntry = {
608
- role: "assistant",
609
- content: response.message,
610
- toolCalls: response.toolCalls,
922
+ const runInteraction = useCallback(
923
+ async (message, sendOptions) => {
924
+ const generation = generationRef.current;
925
+ const options2 = optionsRef.current;
926
+ const abort = linkSignals(
927
+ sendOptions?.signal,
928
+ unmountController().signal
929
+ );
930
+ const userEntry = {
931
+ role: "user",
932
+ content: message,
611
933
  timestamp: Date.now()
612
934
  };
613
- setHistory((prev) => [...prev, assistantEntry]);
614
- setLastResponse(response);
935
+ setHistory((prev) => [...prev, userEntry]);
936
+ let response;
937
+ try {
938
+ const loop = await executeAgentLoop(message, {
939
+ model: modelRef.current,
940
+ state: stateRef.current,
941
+ tools: toolsRef.current,
942
+ permissions: permissionsRef.current,
943
+ options: options2,
944
+ conversationHistory: transcriptRef.current,
945
+ signal: abort.signal
946
+ });
947
+ response = loop.response;
948
+ if (generation === generationRef.current) {
949
+ if (response.error?.code !== "ABORTED") {
950
+ transcriptRef.current = loop.messages;
951
+ }
952
+ const assistantEntry = {
953
+ role: "assistant",
954
+ content: response.message,
955
+ toolCalls: response.toolCalls,
956
+ timestamp: Date.now()
957
+ };
958
+ if (response.error) {
959
+ assistantEntry.error = response.error;
960
+ }
961
+ setHistory((prev) => [...prev, assistantEntry]);
962
+ setLastResponse(response);
963
+ }
964
+ } catch (error) {
965
+ const agentError = error instanceof AdapterError ? {
966
+ message: error.message,
967
+ code: "ADAPTER_ERROR",
968
+ status: error.status,
969
+ cause: error
970
+ } : {
971
+ message: describeError(error),
972
+ cause: error
973
+ };
974
+ response = {
975
+ message: "",
976
+ toolCalls: [],
977
+ error: agentError
978
+ };
979
+ if (generation === generationRef.current) {
980
+ setHistory((prev) => [
981
+ ...prev,
982
+ {
983
+ role: "assistant",
984
+ content: "",
985
+ toolCalls: [],
986
+ error: agentError,
987
+ timestamp: Date.now()
988
+ }
989
+ ]);
990
+ setLastResponse(response);
991
+ }
992
+ } finally {
993
+ abort.release();
994
+ }
615
995
  if (response.error && response.error.code !== "ABORTED") {
616
- optionsRef.current?.onError?.(response.error);
996
+ options2?.onError?.(response.error);
617
997
  }
618
998
  return response;
619
- } catch (error) {
620
- const agentError = {
621
- message: error instanceof Error ? error.message : "Unknown error",
622
- cause: error
623
- };
624
- const errorResponse = {
625
- message: "",
626
- toolCalls: [],
627
- error: agentError
628
- };
629
- setLastResponse(errorResponse);
630
- optionsRef.current?.onError?.(agentError);
631
- return errorResponse;
632
- } finally {
633
- setIsProcessing(false);
634
- }
635
- }, []);
999
+ },
1000
+ [unmountController]
1001
+ );
1002
+ const send = useCallback(
1003
+ (message, sendOptions) => {
1004
+ pendingRef.current++;
1005
+ setIsProcessing(pendingRef.current > 0);
1006
+ const interaction = queueRef.current.then(
1007
+ () => runInteraction(message, sendOptions)
1008
+ );
1009
+ queueRef.current = interaction.then(noop, noop);
1010
+ return interaction.finally(() => {
1011
+ pendingRef.current--;
1012
+ setIsProcessing(pendingRef.current > 0);
1013
+ });
1014
+ },
1015
+ [runInteraction]
1016
+ );
636
1017
  const contextValue = useMemo(
637
1018
  () => ({
638
1019
  send,
@@ -681,7 +1062,7 @@ function openAIAdapter(config) {
681
1062
  }
682
1063
  const baseURL = config.baseURL ? config.baseURL.replace(/\/+$/, "") : OPENAI_BASE_URL;
683
1064
  const model = config.model ?? DEFAULT_MODEL;
684
- const temperature = config.temperature ?? DEFAULT_TEMPERATURE;
1065
+ const temperature = config.temperature === null ? void 0 : config.temperature ?? DEFAULT_TEMPERATURE;
685
1066
  return {
686
1067
  async sendMessage(request) {
687
1068
  const headers = {
@@ -691,10 +1072,11 @@ function openAIAdapter(config) {
691
1072
  if (config.apiKey) {
692
1073
  headers["Authorization"] = `Bearer ${config.apiKey}`;
693
1074
  }
1075
+ const conversation = request.messages.filter(isSendable).map(formatMessage);
694
1076
  const messages = request.systemPrompt ? [
695
1077
  { role: "system", content: request.systemPrompt },
696
- ...request.messages.map(formatMessage)
697
- ] : request.messages.map(formatMessage);
1078
+ ...conversation
1079
+ ] : conversation;
698
1080
  const tools = request.tools.length > 0 ? request.tools.map((t) => ({
699
1081
  type: "function",
700
1082
  function: {
@@ -705,9 +1087,11 @@ function openAIAdapter(config) {
705
1087
  })) : void 0;
706
1088
  const body = {
707
1089
  model,
708
- messages,
709
- temperature
1090
+ messages
710
1091
  };
1092
+ if (temperature !== void 0) {
1093
+ body.temperature = temperature;
1094
+ }
711
1095
  if (tools) {
712
1096
  body.tools = tools;
713
1097
  }
@@ -722,26 +1106,34 @@ function openAIAdapter(config) {
722
1106
  });
723
1107
  } catch (error) {
724
1108
  if (error instanceof Error && error.name === "AbortError") throw error;
725
- throw new Error(
726
- `Network error calling OpenAI API: ${error instanceof Error ? error.message : "Unknown error"}`
1109
+ throw new AdapterError(
1110
+ `Network error calling OpenAI API: ${describeError(error)}`,
1111
+ { cause: error }
727
1112
  );
728
1113
  }
729
1114
  if (!res.ok) {
730
1115
  const text = await res.text().catch(() => "");
731
- throw new Error(
732
- `OpenAI API error (${res.status}): ${text || res.statusText}`
1116
+ throw new AdapterError(
1117
+ `OpenAI API error (${res.status}): ${text || res.statusText}`,
1118
+ { status: res.status, body: text }
733
1119
  );
734
1120
  }
735
1121
  let data;
736
1122
  try {
737
1123
  data = await res.json();
738
- } catch {
739
- throw new Error("Failed to parse OpenAI API response as JSON");
1124
+ } catch (error) {
1125
+ throw new AdapterError("Failed to parse OpenAI API response as JSON", {
1126
+ cause: error
1127
+ });
740
1128
  }
741
1129
  return parseResponse(data);
742
1130
  }
743
1131
  };
744
1132
  }
1133
+ function isSendable(msg) {
1134
+ if (msg.role !== "assistant") return true;
1135
+ return msg.content !== "" || (msg.toolCalls?.length ?? 0) > 0;
1136
+ }
745
1137
  function formatMessage(msg) {
746
1138
  const formatted = {
747
1139
  role: msg.role,
@@ -769,11 +1161,13 @@ function parseResponse(data) {
769
1161
  const obj = data;
770
1162
  const choices = obj.choices;
771
1163
  if (!choices || choices.length === 0) {
772
- throw new Error("Malformed OpenAI response: no choices returned");
1164
+ throw new AdapterError("Malformed OpenAI response: no choices returned");
773
1165
  }
774
1166
  const message = choices[0].message;
775
1167
  if (!message) {
776
- throw new Error("Malformed OpenAI response: no message in first choice");
1168
+ throw new AdapterError(
1169
+ "Malformed OpenAI response: no message in first choice"
1170
+ );
777
1171
  }
778
1172
  const content = message.content ?? null;
779
1173
  const toolCalls = message.tool_calls;
@@ -785,12 +1179,36 @@ function parseResponse(data) {
785
1179
  name: tc.function.name,
786
1180
  arguments: safeParseJSON(tc.function.arguments)
787
1181
  })),
788
- usage: usage ? {
789
- promptTokens: usage.prompt_tokens,
790
- completionTokens: usage.completion_tokens
791
- } : void 0
1182
+ usage: usage ? mapUsage(usage) : void 0,
1183
+ stopReason: mapStopReason(choices[0].finish_reason)
792
1184
  };
793
1185
  }
1186
+ function mapUsage(usage) {
1187
+ const cached = usage.prompt_tokens_details?.cached_tokens;
1188
+ const mapped = {
1189
+ promptTokens: usage.prompt_tokens,
1190
+ completionTokens: usage.completion_tokens
1191
+ };
1192
+ if (typeof cached === "number") {
1193
+ mapped.cacheReadTokens = cached;
1194
+ }
1195
+ return mapped;
1196
+ }
1197
+ function mapStopReason(finishReason) {
1198
+ switch (finishReason) {
1199
+ case "stop":
1200
+ return "end";
1201
+ case "tool_calls":
1202
+ case "function_call":
1203
+ return "tool_use";
1204
+ case "length":
1205
+ return "max_tokens";
1206
+ case "content_filter":
1207
+ return "refusal";
1208
+ default:
1209
+ return "other";
1210
+ }
1211
+ }
794
1212
  function safeParseJSON(str) {
795
1213
  try {
796
1214
  return JSON.parse(str);
@@ -813,6 +1231,7 @@ function claudeAdapter(config) {
813
1231
  const baseURL = config.baseURL ? config.baseURL.replace(/\/+$/, "") : ANTHROPIC_BASE_URL;
814
1232
  const model = config.model ?? DEFAULT_MODEL2;
815
1233
  const maxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
1234
+ const cache = config.cache !== false;
816
1235
  return {
817
1236
  async sendMessage(request) {
818
1237
  const headers = {
@@ -829,7 +1248,13 @@ function claudeAdapter(config) {
829
1248
  messages: toAnthropicMessages(request.messages)
830
1249
  };
831
1250
  if (request.systemPrompt) {
832
- body.system = request.systemPrompt;
1251
+ body.system = cache ? [
1252
+ {
1253
+ type: "text",
1254
+ text: request.systemPrompt,
1255
+ cache_control: { type: "ephemeral" }
1256
+ }
1257
+ ] : request.systemPrompt;
833
1258
  }
834
1259
  if (request.tools.length > 0) {
835
1260
  body.tools = request.tools.map((t) => ({
@@ -849,21 +1274,26 @@ function claudeAdapter(config) {
849
1274
  });
850
1275
  } catch (error) {
851
1276
  if (error instanceof Error && error.name === "AbortError") throw error;
852
- throw new Error(
853
- `Network error calling Anthropic API: ${error instanceof Error ? error.message : "Unknown error"}`
1277
+ throw new AdapterError(
1278
+ `Network error calling Anthropic API: ${describeError(error)}`,
1279
+ { cause: error }
854
1280
  );
855
1281
  }
856
1282
  if (!res.ok) {
857
1283
  const text = await res.text().catch(() => "");
858
- throw new Error(
859
- `Anthropic API error (${res.status}): ${text || res.statusText}`
1284
+ throw new AdapterError(
1285
+ `Anthropic API error (${res.status}): ${text || res.statusText}`,
1286
+ { status: res.status, body: text }
860
1287
  );
861
1288
  }
862
1289
  let data;
863
1290
  try {
864
1291
  data = await res.json();
865
- } catch {
866
- throw new Error("Failed to parse Anthropic API response as JSON");
1292
+ } catch (error) {
1293
+ throw new AdapterError(
1294
+ "Failed to parse Anthropic API response as JSON",
1295
+ { cause: error }
1296
+ );
867
1297
  }
868
1298
  return parseResponse2(data);
869
1299
  }
@@ -880,40 +1310,60 @@ function toAnthropicMessages(messages) {
880
1310
  };
881
1311
  for (const message of messages) {
882
1312
  if (message.role === "tool") {
883
- pendingToolResults.push({
1313
+ const block = {
884
1314
  type: "tool_result",
885
1315
  tool_use_id: message.toolCallId ?? "",
886
1316
  content: message.content
887
- });
1317
+ };
1318
+ if (message.isError) {
1319
+ block.is_error = true;
1320
+ }
1321
+ pendingToolResults.push(block);
888
1322
  continue;
889
1323
  }
890
- flushToolResults();
891
- if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
892
- const blocks = [];
893
- if (message.content) {
894
- blocks.push({ type: "text", text: message.content });
895
- }
896
- for (const call of message.toolCalls) {
897
- blocks.push({
898
- type: "tool_use",
899
- id: call.id,
900
- name: call.name,
901
- input: isPlainObject2(call.arguments) ? call.arguments : {}
902
- });
903
- }
904
- result.push({ role: "assistant", content: blocks });
1324
+ if (message.role === "assistant") {
1325
+ const content = toAssistantContent(message);
1326
+ if (content === null) continue;
1327
+ flushToolResults();
1328
+ result.push({ role: "assistant", content });
905
1329
  continue;
906
1330
  }
1331
+ flushToolResults();
907
1332
  result.push({ role: message.role, content: message.content });
908
1333
  }
909
1334
  flushToolResults();
910
1335
  return result;
911
1336
  }
1337
+ function toAssistantContent(message) {
1338
+ const replay = message.providerData;
1339
+ if (Array.isArray(replay) && replay.length > 0) {
1340
+ return replay;
1341
+ }
1342
+ const toolCalls = message.toolCalls ?? [];
1343
+ if (toolCalls.length > 0) {
1344
+ const blocks = [];
1345
+ if (message.content) {
1346
+ blocks.push({ type: "text", text: message.content });
1347
+ }
1348
+ for (const call of toolCalls) {
1349
+ blocks.push({
1350
+ type: "tool_use",
1351
+ id: call.id,
1352
+ name: call.name,
1353
+ input: isPlainObject2(call.arguments) ? call.arguments : {}
1354
+ });
1355
+ }
1356
+ return blocks;
1357
+ }
1358
+ return message.content ? message.content : null;
1359
+ }
912
1360
  function parseResponse2(data) {
913
1361
  const obj = data;
914
1362
  const blocks = obj.content;
915
1363
  if (!Array.isArray(blocks)) {
916
- throw new Error("Malformed Anthropic response: no content blocks returned");
1364
+ throw new AdapterError(
1365
+ "Malformed Anthropic response: no content blocks returned"
1366
+ );
917
1367
  }
918
1368
  const textParts = [];
919
1369
  const toolCalls = [];
@@ -932,23 +1382,55 @@ function parseResponse2(data) {
932
1382
  return {
933
1383
  content: textParts.length > 0 ? textParts.join("") : null,
934
1384
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
935
- usage: usage ? {
936
- promptTokens: usage.input_tokens,
937
- completionTokens: usage.output_tokens
938
- } : void 0
1385
+ usage: usage ? toTokenUsage(usage) : void 0,
1386
+ stopReason: toStopReason(obj.stop_reason),
1387
+ providerData: blocks
1388
+ };
1389
+ }
1390
+ function toTokenUsage(usage) {
1391
+ const cacheRead = usage.cache_read_input_tokens;
1392
+ const cacheWrite = usage.cache_creation_input_tokens;
1393
+ const result = {
1394
+ // Anthropic reports the cached tokens outside input_tokens, so the total
1395
+ // input for the call is the three fields added together.
1396
+ promptTokens: usage.input_tokens + (cacheRead ?? 0) + (cacheWrite ?? 0),
1397
+ completionTokens: usage.output_tokens
939
1398
  };
1399
+ if (typeof cacheRead === "number") {
1400
+ result.cacheReadTokens = cacheRead;
1401
+ }
1402
+ if (typeof cacheWrite === "number") {
1403
+ result.cacheWriteTokens = cacheWrite;
1404
+ }
1405
+ return result;
1406
+ }
1407
+ function toStopReason(value) {
1408
+ switch (value) {
1409
+ case "end_turn":
1410
+ return "end";
1411
+ case "tool_use":
1412
+ return "tool_use";
1413
+ case "max_tokens":
1414
+ return "max_tokens";
1415
+ case "refusal":
1416
+ return "refusal";
1417
+ default:
1418
+ return "other";
1419
+ }
940
1420
  }
941
1421
  function isPlainObject2(value) {
942
1422
  return typeof value === "object" && value !== null && !Array.isArray(value);
943
1423
  }
944
1424
  export {
945
1425
  AIAgentProvider,
1426
+ AdapterError,
946
1427
  claudeAdapter,
947
1428
  filterState,
948
1429
  filterTools,
949
1430
  openAIAdapter,
950
1431
  registerTool,
951
1432
  useAgent,
1433
+ validateToolArgs,
952
1434
  validateToolCall,
953
1435
  validateToolNames
954
1436
  };