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