react-observer-agent 0.2.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 ADDED
@@ -0,0 +1,990 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AIAgentProvider: () => AIAgentProvider,
24
+ claudeAdapter: () => claudeAdapter,
25
+ filterState: () => filterState,
26
+ filterTools: () => filterTools,
27
+ openAIAdapter: () => openAIAdapter,
28
+ registerTool: () => registerTool,
29
+ useAgent: () => useAgent,
30
+ validateToolCall: () => validateToolCall,
31
+ validateToolNames: () => validateToolNames
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/tools/registerTool.ts
36
+ function registerTool(name, handler, options) {
37
+ return {
38
+ name,
39
+ handler,
40
+ description: options?.description,
41
+ parameters: options?.parameters,
42
+ confirm: options?.confirm ?? false
43
+ };
44
+ }
45
+
46
+ // src/tools/validateToolNames.ts
47
+ var RESERVED_PREFIX = "__";
48
+ function validateToolNames(tools) {
49
+ const seen = /* @__PURE__ */ new Set();
50
+ for (const tool of tools) {
51
+ if (tool.name.startsWith(RESERVED_PREFIX)) {
52
+ throw new Error(
53
+ `Tool name "${tool.name}" uses the reserved "${RESERVED_PREFIX}" prefix. Names beginning with "${RESERVED_PREFIX}" are reserved for internal tools.`
54
+ );
55
+ }
56
+ if (seen.has(tool.name)) {
57
+ throw new Error(
58
+ `Duplicate tool name "${tool.name}". All tools passed to a single provider must have unique names.`
59
+ );
60
+ }
61
+ seen.add(tool.name);
62
+ }
63
+ }
64
+
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
+ // src/tools/validateArgs.ts
129
+ function validateArgs(args, schema) {
130
+ const errors = [];
131
+ validateValue(args, schema, "", errors);
132
+ return { valid: errors.length === 0, errors };
133
+ }
134
+ function describeType(value) {
135
+ if (value === null) return "null";
136
+ if (value === void 0) return "undefined";
137
+ if (Array.isArray(value)) return "array";
138
+ if (typeof value === "number") {
139
+ return Number.isInteger(value) ? "integer" : "number";
140
+ }
141
+ return typeof value;
142
+ }
143
+ function matchesType(value, expected) {
144
+ const actual = describeType(value);
145
+ if (expected === "number") return actual === "number" || actual === "integer";
146
+ return actual === expected;
147
+ }
148
+ function sameValue(a, b) {
149
+ if (a === b) return true;
150
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
151
+ return false;
152
+ }
153
+ return JSON.stringify(a) === JSON.stringify(b);
154
+ }
155
+ function label(path) {
156
+ return path === "" ? "value" : path;
157
+ }
158
+ function validateValue(value, schema, path, errors) {
159
+ if (!schema || typeof schema !== "object") return;
160
+ const expectedType = schema.type;
161
+ if (typeof expectedType === "string" && !matchesType(value, expectedType)) {
162
+ errors.push(
163
+ `${label(path)} should be ${expectedType}, got ${describeType(value)}`
164
+ );
165
+ return;
166
+ }
167
+ if (Array.isArray(schema.enum) && !schema.enum.some((option) => sameValue(option, value))) {
168
+ errors.push(
169
+ `${label(path)} should be one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
170
+ );
171
+ }
172
+ if (isPlainObject(value)) {
173
+ validateObject(value, schema, path, errors);
174
+ }
175
+ if (Array.isArray(value) && isPlainObject(schema.items)) {
176
+ const itemSchema = schema.items;
177
+ value.forEach((item, index) => {
178
+ validateValue(item, itemSchema, `${label(path)}[${index}]`, errors);
179
+ });
180
+ }
181
+ }
182
+ function validateObject(value, schema, path, errors) {
183
+ if (Array.isArray(schema.required)) {
184
+ for (const key of schema.required) {
185
+ if (typeof key === "string" && value[key] === void 0) {
186
+ errors.push(`${path === "" ? "" : `${path}.`}${key} is required`);
187
+ }
188
+ }
189
+ }
190
+ if (isPlainObject(schema.properties)) {
191
+ const properties = schema.properties;
192
+ for (const [key, propertySchema] of Object.entries(properties)) {
193
+ if (value[key] !== void 0 && isPlainObject(propertySchema)) {
194
+ validateValue(
195
+ value[key],
196
+ propertySchema,
197
+ path === "" ? key : `${path}.${key}`,
198
+ errors
199
+ );
200
+ }
201
+ }
202
+ }
203
+ }
204
+ function isPlainObject(value) {
205
+ return typeof value === "object" && value !== null && !Array.isArray(value);
206
+ }
207
+
208
+ // src/provider/executeAgentLoop.ts
209
+ var DEFAULT_MAX_TURNS = 5;
210
+ var READ_STATE_TOOL_NAME = "__readState";
211
+ var EMPTY_OBJECT_SCHEMA = { type: "object", properties: {} };
212
+ var UsageTotal = class {
213
+ constructor() {
214
+ this.promptTokens = 0;
215
+ this.completionTokens = 0;
216
+ this.reported = false;
217
+ }
218
+ add(usage) {
219
+ if (!usage) return;
220
+ this.reported = true;
221
+ this.promptTokens += usage.promptTokens ?? 0;
222
+ this.completionTokens += usage.completionTokens ?? 0;
223
+ }
224
+ /** Undefined when no adapter response carried usage, rather than a false zero. */
225
+ total() {
226
+ if (!this.reported) return void 0;
227
+ return {
228
+ promptTokens: this.promptTokens,
229
+ completionTokens: this.completionTokens
230
+ };
231
+ }
232
+ };
233
+ function isAbortError(error) {
234
+ return error instanceof Error && error.name === "AbortError";
235
+ }
236
+ function buildStateManifest(canAccess, descriptions) {
237
+ return canAccess.map((key) => ({
238
+ key,
239
+ description: descriptions?.[key] ?? key
240
+ }));
241
+ }
242
+ function buildStateManifestPrompt(manifest) {
243
+ if (manifest.length === 0) return "";
244
+ const lines = manifest.map((m) => `- ${m.key}: ${m.description}`);
245
+ return [
246
+ "Available application state (use the __readState tool to access specific keys when needed):",
247
+ ...lines,
248
+ "",
249
+ "Only request state keys relevant to the user's question. Do not read all keys at once unless necessary."
250
+ ].join("\n");
251
+ }
252
+ function buildReadStateToolDef() {
253
+ return {
254
+ name: READ_STATE_TOOL_NAME,
255
+ 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
+ }
267
+ };
268
+ }
269
+ async function executeAgentLoop(message, ctx) {
270
+ const { model, state, tools, permissions, options, signal } = ctx;
271
+ const debug = options?.debug ?? false;
272
+ const maxTurns = options?.maxTurns ?? DEFAULT_MAX_TURNS;
273
+ const stateManifest = buildStateManifest(
274
+ permissions.canAccess,
275
+ permissions.stateDescriptions
276
+ );
277
+ if (debug) {
278
+ console.log("[react-observer-agent] State manifest:", stateManifest.map((m) => m.key));
279
+ }
280
+ const allowedTools = filterTools(tools, permissions.canExecute);
281
+ const llmTools = [];
282
+ for (const tool of allowedTools) {
283
+ if (!tool.description) {
284
+ if (debug) {
285
+ console.warn(
286
+ `[react-observer-agent] Tool "${tool.name}" has no description and is hidden from the LLM.`
287
+ );
288
+ }
289
+ continue;
290
+ }
291
+ llmTools.push({
292
+ name: tool.name,
293
+ description: tool.description,
294
+ parameters: tool.parameters ?? EMPTY_OBJECT_SCHEMA
295
+ });
296
+ }
297
+ if (stateManifest.length > 0) {
298
+ llmTools.push(buildReadStateToolDef());
299
+ }
300
+ if (debug) {
301
+ console.log("[react-observer-agent] Available tools:", llmTools.map((t) => t.name));
302
+ }
303
+ const toolMap = new Map(allowedTools.map((t) => [t.name, t]));
304
+ const messages = [
305
+ ...ctx.conversationHistory,
306
+ { role: "user", content: message }
307
+ ];
308
+ const manifestPrompt = buildStateManifestPrompt(stateManifest);
309
+ const systemPrompt = [options?.systemPrompt, manifestPrompt].filter(Boolean).join("\n\n") || void 0;
310
+ const allToolCalls = [];
311
+ const usage = new UsageTotal();
312
+ let turns = 0;
313
+ let finalMessage = "";
314
+ let completed = false;
315
+ const abortedResult = () => ({
316
+ response: {
317
+ message: "",
318
+ toolCalls: allToolCalls,
319
+ error: { message: "Interaction aborted", code: "ABORTED" },
320
+ usage: usage.total()
321
+ },
322
+ messages
323
+ });
324
+ while (turns < maxTurns) {
325
+ if (signal?.aborted) return abortedResult();
326
+ turns++;
327
+ if (debug) {
328
+ console.log(`[react-observer-agent] Turn ${turns}/${maxTurns}`);
329
+ }
330
+ const modelRequest = {
331
+ // A snapshot, since the loop keeps appending to `messages` after this
332
+ // call and an adapter that reads it asynchronously would see the churn.
333
+ messages: [...messages],
334
+ tools: llmTools,
335
+ state: {},
336
+ systemPrompt,
337
+ stateManifest,
338
+ signal
339
+ };
340
+ if (debug) {
341
+ console.log("[react-observer-agent] LLM request:", {
342
+ messageCount: modelRequest.messages.length,
343
+ toolCount: modelRequest.tools.length,
344
+ hasSystemPrompt: !!modelRequest.systemPrompt
345
+ });
346
+ }
347
+ let modelResponse;
348
+ try {
349
+ modelResponse = await model.sendMessage(modelRequest);
350
+ } catch (error) {
351
+ if (isAbortError(error) || signal?.aborted) {
352
+ return abortedResult();
353
+ }
354
+ throw error;
355
+ }
356
+ usage.add(modelResponse.usage);
357
+ if (signal?.aborted) return abortedResult();
358
+ if (debug) {
359
+ console.log("[react-observer-agent] LLM response:", {
360
+ content: modelResponse.content?.slice(0, 200),
361
+ toolCalls: modelResponse.toolCalls?.map((tc) => tc.name)
362
+ });
363
+ }
364
+ if (!modelResponse.toolCalls || modelResponse.toolCalls.length === 0) {
365
+ finalMessage = modelResponse.content ?? "";
366
+ completed = true;
367
+ break;
368
+ }
369
+ messages.push({
370
+ role: "assistant",
371
+ content: modelResponse.content ?? "",
372
+ toolCalls: modelResponse.toolCalls
373
+ });
374
+ for (const llmCall of modelResponse.toolCalls) {
375
+ if (signal?.aborted) return abortedResult();
376
+ if (llmCall.name === READ_STATE_TOOL_NAME) {
377
+ 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);
381
+ if (debug) {
382
+ console.log("[react-observer-agent] readState requested:", requestedKeys);
383
+ console.log("[react-observer-agent] readState allowed:", allowedKeys);
384
+ console.log("[react-observer-agent] readState result:", snapshot);
385
+ }
386
+ messages.push({
387
+ role: "tool",
388
+ content: JSON.stringify(snapshot),
389
+ toolCallId: llmCall.id
390
+ });
391
+ continue;
392
+ }
393
+ if (!validateToolCall(llmCall.name, permissions.canExecute)) {
394
+ const deniedResult = {
395
+ toolName: llmCall.name,
396
+ args: llmCall.arguments,
397
+ result: `Tool "${llmCall.name}" is not permitted`,
398
+ status: "denied"
399
+ };
400
+ allToolCalls.push(deniedResult);
401
+ options?.onToolCall?.({
402
+ toolName: llmCall.name,
403
+ args: llmCall.arguments,
404
+ result: deniedResult.result,
405
+ status: "denied"
406
+ });
407
+ messages.push({
408
+ role: "tool",
409
+ content: JSON.stringify({ error: deniedResult.result }),
410
+ toolCallId: llmCall.id
411
+ });
412
+ continue;
413
+ }
414
+ const toolDef = toolMap.get(llmCall.name);
415
+ if (!toolDef) {
416
+ const deniedResult = {
417
+ toolName: llmCall.name,
418
+ args: llmCall.arguments,
419
+ result: `Tool "${llmCall.name}" not found`,
420
+ status: "denied"
421
+ };
422
+ allToolCalls.push(deniedResult);
423
+ messages.push({
424
+ role: "tool",
425
+ content: JSON.stringify({ error: deniedResult.result }),
426
+ toolCallId: llmCall.id
427
+ });
428
+ continue;
429
+ }
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;
456
+ }
457
+ }
458
+ if (toolDef.confirm) {
459
+ if (!options?.onConfirm) {
460
+ if (debug) {
461
+ console.warn(
462
+ `[react-observer-agent] Tool "${llmCall.name}" requires confirmation but no onConfirm handler provided. Skipping.`
463
+ );
464
+ }
465
+ const cancelledResult = {
466
+ toolName: llmCall.name,
467
+ args: llmCall.arguments,
468
+ result: "Tool execution cancelled: no confirmation handler provided",
469
+ 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
+ });
478
+ messages.push({
479
+ role: "tool",
480
+ content: JSON.stringify({ status: "cancelled", reason: "No confirmation handler" }),
481
+ toolCallId: llmCall.id
482
+ });
483
+ continue;
484
+ }
485
+ const confirmed = await options.onConfirm({
486
+ toolName: llmCall.name,
487
+ args: llmCall.arguments,
488
+ description: toolDef.description
489
+ });
490
+ if (!confirmed) {
491
+ const cancelledResult = {
492
+ toolName: llmCall.name,
493
+ args: llmCall.arguments,
494
+ result: "Tool execution cancelled by user",
495
+ 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
+ });
504
+ messages.push({
505
+ role: "tool",
506
+ content: JSON.stringify({ status: "cancelled", reason: "User denied" }),
507
+ toolCallId: llmCall.id
508
+ });
509
+ continue;
510
+ }
511
+ }
512
+ 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 });
517
+ }
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
+ });
536
+ } 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?.({
546
+ toolName: llmCall.name,
547
+ args: llmCall.arguments,
548
+ result: errorMessage,
549
+ status: "error"
550
+ });
551
+ messages.push({
552
+ role: "tool",
553
+ content: JSON.stringify({ error: errorMessage }),
554
+ toolCallId: llmCall.id
555
+ });
556
+ }
557
+ }
558
+ }
559
+ if (!completed) {
560
+ if (debug) {
561
+ console.warn(`[react-observer-agent] Max turns (${maxTurns}) reached`);
562
+ }
563
+ return {
564
+ response: {
565
+ message: "",
566
+ toolCalls: allToolCalls,
567
+ error: {
568
+ message: `Agent did not produce a final response within ${maxTurns} turns`,
569
+ code: "MAX_TURNS"
570
+ },
571
+ usage: usage.total()
572
+ },
573
+ messages
574
+ };
575
+ }
576
+ messages.push({ role: "assistant", content: finalMessage });
577
+ return {
578
+ response: {
579
+ message: finalMessage,
580
+ toolCalls: allToolCalls,
581
+ usage: usage.total()
582
+ },
583
+ messages
584
+ };
585
+ }
586
+
587
+ // src/provider/AIAgentProvider.tsx
588
+ var import_jsx_runtime = require("react/jsx-runtime");
589
+ var AgentContextValue = (0, import_react.createContext)(null);
590
+ function AIAgentProvider({
591
+ model,
592
+ state,
593
+ tools,
594
+ permissions,
595
+ options,
596
+ children
597
+ }) {
598
+ const [isProcessing, setIsProcessing] = (0, import_react.useState)(false);
599
+ const [history, setHistory] = (0, import_react.useState)([]);
600
+ const [lastResponse, setLastResponse] = (0, import_react.useState)(null);
601
+ const modelRef = (0, import_react.useRef)(model);
602
+ const stateRef = (0, import_react.useRef)(state);
603
+ const toolsRef = (0, import_react.useRef)(tools);
604
+ const permissionsRef = (0, import_react.useRef)(permissions);
605
+ const optionsRef = (0, import_react.useRef)(options);
606
+ modelRef.current = model;
607
+ stateRef.current = state;
608
+ toolsRef.current = tools;
609
+ permissionsRef.current = permissions;
610
+ optionsRef.current = options;
611
+ (0, import_react.useEffect)(() => {
612
+ validateToolNames(tools);
613
+ }, [tools]);
614
+ const transcriptRef = (0, import_react.useRef)([]);
615
+ const clearHistory = (0, import_react.useCallback)(() => {
616
+ setHistory([]);
617
+ setLastResponse(null);
618
+ transcriptRef.current = [];
619
+ }, []);
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,
645
+ timestamp: Date.now()
646
+ };
647
+ setHistory((prev) => [...prev, assistantEntry]);
648
+ setLastResponse(response);
649
+ if (response.error && response.error.code !== "ABORTED") {
650
+ optionsRef.current?.onError?.(response.error);
651
+ }
652
+ 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
+ }, []);
670
+ const contextValue = (0, import_react.useMemo)(
671
+ () => ({
672
+ send,
673
+ isProcessing,
674
+ history,
675
+ clearHistory,
676
+ lastResponse
677
+ }),
678
+ [send, isProcessing, history, clearHistory, lastResponse]
679
+ );
680
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AgentContextValue.Provider, { value: contextValue, children });
681
+ }
682
+
683
+ // src/provider/useAgent.ts
684
+ var import_react2 = require("react");
685
+ function useAgent() {
686
+ const context = (0, import_react2.useContext)(AgentContextValue);
687
+ if (!context) {
688
+ throw new Error(
689
+ "useAgent() must be used within an <AIAgentProvider>. Wrap your component tree with <AIAgentProvider> to use this hook."
690
+ );
691
+ }
692
+ return context;
693
+ }
694
+
695
+ // src/permissions/filterState.ts
696
+ function filterState(state, canAccess) {
697
+ const filtered = {};
698
+ for (const key of canAccess) {
699
+ if (key in state) {
700
+ filtered[key] = state[key];
701
+ }
702
+ }
703
+ return filtered;
704
+ }
705
+
706
+ // src/adapters/openai.ts
707
+ var DEFAULT_MODEL = "gpt-4o";
708
+ var DEFAULT_TEMPERATURE = 0.2;
709
+ var OPENAI_BASE_URL = "https://api.openai.com/v1";
710
+ function openAIAdapter(config) {
711
+ if (!config.apiKey && !config.baseURL) {
712
+ throw new Error(
713
+ 'openAIAdapter requires either "apiKey" or "baseURL". Provide an API key for direct access, or a baseURL to route through your backend proxy.'
714
+ );
715
+ }
716
+ const baseURL = config.baseURL ? config.baseURL.replace(/\/+$/, "") : OPENAI_BASE_URL;
717
+ const model = config.model ?? DEFAULT_MODEL;
718
+ const temperature = config.temperature ?? DEFAULT_TEMPERATURE;
719
+ return {
720
+ async sendMessage(request) {
721
+ const headers = {
722
+ "Content-Type": "application/json",
723
+ ...config.headers
724
+ };
725
+ if (config.apiKey) {
726
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
727
+ }
728
+ const messages = request.systemPrompt ? [
729
+ { role: "system", content: request.systemPrompt },
730
+ ...request.messages.map(formatMessage)
731
+ ] : request.messages.map(formatMessage);
732
+ const tools = request.tools.length > 0 ? request.tools.map((t) => ({
733
+ type: "function",
734
+ function: {
735
+ name: t.name,
736
+ description: t.description,
737
+ parameters: t.parameters
738
+ }
739
+ })) : void 0;
740
+ const body = {
741
+ model,
742
+ messages,
743
+ temperature
744
+ };
745
+ if (tools) {
746
+ body.tools = tools;
747
+ }
748
+ const url = baseURL.includes("/chat/completions") ? baseURL : `${baseURL}/chat/completions`;
749
+ let res;
750
+ try {
751
+ res = await fetch(url, {
752
+ method: "POST",
753
+ headers,
754
+ body: JSON.stringify(body),
755
+ signal: request.signal
756
+ });
757
+ } catch (error) {
758
+ 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"}`
761
+ );
762
+ }
763
+ if (!res.ok) {
764
+ const text = await res.text().catch(() => "");
765
+ throw new Error(
766
+ `OpenAI API error (${res.status}): ${text || res.statusText}`
767
+ );
768
+ }
769
+ let data;
770
+ try {
771
+ data = await res.json();
772
+ } catch {
773
+ throw new Error("Failed to parse OpenAI API response as JSON");
774
+ }
775
+ return parseResponse(data);
776
+ }
777
+ };
778
+ }
779
+ function formatMessage(msg) {
780
+ const formatted = {
781
+ role: msg.role,
782
+ content: msg.content
783
+ };
784
+ if (msg.toolCallId) {
785
+ formatted.tool_call_id = msg.toolCallId;
786
+ }
787
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
788
+ formatted.tool_calls = msg.toolCalls.map((tc) => ({
789
+ id: tc.id,
790
+ type: "function",
791
+ function: {
792
+ name: tc.name,
793
+ arguments: typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments ?? {})
794
+ }
795
+ }));
796
+ if (msg.content === "") {
797
+ formatted.content = null;
798
+ }
799
+ }
800
+ return formatted;
801
+ }
802
+ function parseResponse(data) {
803
+ const obj = data;
804
+ const choices = obj.choices;
805
+ if (!choices || choices.length === 0) {
806
+ throw new Error("Malformed OpenAI response: no choices returned");
807
+ }
808
+ const message = choices[0].message;
809
+ if (!message) {
810
+ throw new Error("Malformed OpenAI response: no message in first choice");
811
+ }
812
+ const content = message.content ?? null;
813
+ const toolCalls = message.tool_calls;
814
+ const usage = obj.usage;
815
+ return {
816
+ content,
817
+ toolCalls: toolCalls?.map((tc) => ({
818
+ id: tc.id,
819
+ name: tc.function.name,
820
+ arguments: safeParseJSON(tc.function.arguments)
821
+ })),
822
+ usage: usage ? {
823
+ promptTokens: usage.prompt_tokens,
824
+ completionTokens: usage.completion_tokens
825
+ } : void 0
826
+ };
827
+ }
828
+ function safeParseJSON(str) {
829
+ try {
830
+ return JSON.parse(str);
831
+ } catch {
832
+ return str;
833
+ }
834
+ }
835
+
836
+ // src/adapters/claude.ts
837
+ var DEFAULT_MODEL2 = "claude-opus-5";
838
+ var DEFAULT_MAX_TOKENS = 16e3;
839
+ var ANTHROPIC_BASE_URL = "https://api.anthropic.com";
840
+ var ANTHROPIC_VERSION = "2023-06-01";
841
+ function claudeAdapter(config) {
842
+ if (!config.apiKey && !config.baseURL) {
843
+ throw new Error(
844
+ 'claudeAdapter requires either "apiKey" or "baseURL". Provide an API key for direct access, or a baseURL to route through your backend proxy.'
845
+ );
846
+ }
847
+ const baseURL = config.baseURL ? config.baseURL.replace(/\/+$/, "") : ANTHROPIC_BASE_URL;
848
+ const model = config.model ?? DEFAULT_MODEL2;
849
+ const maxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
850
+ return {
851
+ async sendMessage(request) {
852
+ const headers = {
853
+ "Content-Type": "application/json",
854
+ "anthropic-version": ANTHROPIC_VERSION
855
+ };
856
+ if (config.apiKey) {
857
+ headers["x-api-key"] = config.apiKey;
858
+ }
859
+ Object.assign(headers, config.headers);
860
+ const body = {
861
+ model,
862
+ max_tokens: maxTokens,
863
+ messages: toAnthropicMessages(request.messages)
864
+ };
865
+ if (request.systemPrompt) {
866
+ body.system = request.systemPrompt;
867
+ }
868
+ if (request.tools.length > 0) {
869
+ body.tools = request.tools.map((t) => ({
870
+ name: t.name,
871
+ description: t.description,
872
+ input_schema: t.parameters
873
+ }));
874
+ }
875
+ const url = baseURL.includes("/v1/messages") ? baseURL : `${baseURL}/v1/messages`;
876
+ let res;
877
+ try {
878
+ res = await fetch(url, {
879
+ method: "POST",
880
+ headers,
881
+ body: JSON.stringify(body),
882
+ signal: request.signal
883
+ });
884
+ } catch (error) {
885
+ 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"}`
888
+ );
889
+ }
890
+ if (!res.ok) {
891
+ const text = await res.text().catch(() => "");
892
+ throw new Error(
893
+ `Anthropic API error (${res.status}): ${text || res.statusText}`
894
+ );
895
+ }
896
+ let data;
897
+ try {
898
+ data = await res.json();
899
+ } catch {
900
+ throw new Error("Failed to parse Anthropic API response as JSON");
901
+ }
902
+ return parseResponse2(data);
903
+ }
904
+ };
905
+ }
906
+ function toAnthropicMessages(messages) {
907
+ const result = [];
908
+ let pendingToolResults = [];
909
+ const flushToolResults = () => {
910
+ if (pendingToolResults.length > 0) {
911
+ result.push({ role: "user", content: pendingToolResults });
912
+ pendingToolResults = [];
913
+ }
914
+ };
915
+ for (const message of messages) {
916
+ if (message.role === "tool") {
917
+ pendingToolResults.push({
918
+ type: "tool_result",
919
+ tool_use_id: message.toolCallId ?? "",
920
+ content: message.content
921
+ });
922
+ continue;
923
+ }
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 });
939
+ continue;
940
+ }
941
+ result.push({ role: message.role, content: message.content });
942
+ }
943
+ flushToolResults();
944
+ return result;
945
+ }
946
+ function parseResponse2(data) {
947
+ const obj = data;
948
+ const blocks = obj.content;
949
+ if (!Array.isArray(blocks)) {
950
+ throw new Error("Malformed Anthropic response: no content blocks returned");
951
+ }
952
+ const textParts = [];
953
+ const toolCalls = [];
954
+ for (const block of blocks) {
955
+ if (block?.type === "text") {
956
+ textParts.push(block.text);
957
+ } else if (block?.type === "tool_use") {
958
+ toolCalls.push({
959
+ id: block.id,
960
+ name: block.name,
961
+ arguments: block.input
962
+ });
963
+ }
964
+ }
965
+ const usage = obj.usage;
966
+ return {
967
+ content: textParts.length > 0 ? textParts.join("") : null,
968
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
969
+ usage: usage ? {
970
+ promptTokens: usage.input_tokens,
971
+ completionTokens: usage.output_tokens
972
+ } : void 0
973
+ };
974
+ }
975
+ function isPlainObject2(value) {
976
+ return typeof value === "object" && value !== null && !Array.isArray(value);
977
+ }
978
+ // Annotate the CommonJS export names for ESM import in node:
979
+ 0 && (module.exports = {
980
+ AIAgentProvider,
981
+ claudeAdapter,
982
+ filterState,
983
+ filterTools,
984
+ openAIAdapter,
985
+ registerTool,
986
+ useAgent,
987
+ validateToolCall,
988
+ validateToolNames
989
+ });
990
+ //# sourceMappingURL=index.cjs.map