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.js ADDED
@@ -0,0 +1,955 @@
1
+ // src/tools/registerTool.ts
2
+ function registerTool(name, handler, options) {
3
+ return {
4
+ name,
5
+ handler,
6
+ description: options?.description,
7
+ parameters: options?.parameters,
8
+ confirm: options?.confirm ?? false
9
+ };
10
+ }
11
+
12
+ // src/tools/validateToolNames.ts
13
+ var RESERVED_PREFIX = "__";
14
+ function validateToolNames(tools) {
15
+ const seen = /* @__PURE__ */ new Set();
16
+ for (const tool of tools) {
17
+ if (tool.name.startsWith(RESERVED_PREFIX)) {
18
+ throw new Error(
19
+ `Tool name "${tool.name}" uses the reserved "${RESERVED_PREFIX}" prefix. Names beginning with "${RESERVED_PREFIX}" are reserved for internal tools.`
20
+ );
21
+ }
22
+ if (seen.has(tool.name)) {
23
+ throw new Error(
24
+ `Duplicate tool name "${tool.name}". All tools passed to a single provider must have unique names.`
25
+ );
26
+ }
27
+ seen.add(tool.name);
28
+ }
29
+ }
30
+
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
+ // src/tools/validateArgs.ts
95
+ function validateArgs(args, schema) {
96
+ const errors = [];
97
+ validateValue(args, schema, "", errors);
98
+ return { valid: errors.length === 0, errors };
99
+ }
100
+ function describeType(value) {
101
+ if (value === null) return "null";
102
+ if (value === void 0) return "undefined";
103
+ if (Array.isArray(value)) return "array";
104
+ if (typeof value === "number") {
105
+ return Number.isInteger(value) ? "integer" : "number";
106
+ }
107
+ return typeof value;
108
+ }
109
+ function matchesType(value, expected) {
110
+ const actual = describeType(value);
111
+ if (expected === "number") return actual === "number" || actual === "integer";
112
+ return actual === expected;
113
+ }
114
+ function sameValue(a, b) {
115
+ if (a === b) return true;
116
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
117
+ return false;
118
+ }
119
+ return JSON.stringify(a) === JSON.stringify(b);
120
+ }
121
+ function label(path) {
122
+ return path === "" ? "value" : path;
123
+ }
124
+ function validateValue(value, schema, path, errors) {
125
+ if (!schema || typeof schema !== "object") return;
126
+ const expectedType = schema.type;
127
+ if (typeof expectedType === "string" && !matchesType(value, expectedType)) {
128
+ errors.push(
129
+ `${label(path)} should be ${expectedType}, got ${describeType(value)}`
130
+ );
131
+ return;
132
+ }
133
+ if (Array.isArray(schema.enum) && !schema.enum.some((option) => sameValue(option, value))) {
134
+ errors.push(
135
+ `${label(path)} should be one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
136
+ );
137
+ }
138
+ if (isPlainObject(value)) {
139
+ validateObject(value, schema, path, errors);
140
+ }
141
+ if (Array.isArray(value) && isPlainObject(schema.items)) {
142
+ const itemSchema = schema.items;
143
+ value.forEach((item, index) => {
144
+ validateValue(item, itemSchema, `${label(path)}[${index}]`, errors);
145
+ });
146
+ }
147
+ }
148
+ function validateObject(value, schema, path, errors) {
149
+ if (Array.isArray(schema.required)) {
150
+ for (const key of schema.required) {
151
+ if (typeof key === "string" && value[key] === void 0) {
152
+ errors.push(`${path === "" ? "" : `${path}.`}${key} is required`);
153
+ }
154
+ }
155
+ }
156
+ if (isPlainObject(schema.properties)) {
157
+ const properties = schema.properties;
158
+ for (const [key, propertySchema] of Object.entries(properties)) {
159
+ if (value[key] !== void 0 && isPlainObject(propertySchema)) {
160
+ validateValue(
161
+ value[key],
162
+ propertySchema,
163
+ path === "" ? key : `${path}.${key}`,
164
+ errors
165
+ );
166
+ }
167
+ }
168
+ }
169
+ }
170
+ function isPlainObject(value) {
171
+ return typeof value === "object" && value !== null && !Array.isArray(value);
172
+ }
173
+
174
+ // src/provider/executeAgentLoop.ts
175
+ var DEFAULT_MAX_TURNS = 5;
176
+ var READ_STATE_TOOL_NAME = "__readState";
177
+ var EMPTY_OBJECT_SCHEMA = { type: "object", properties: {} };
178
+ var UsageTotal = class {
179
+ constructor() {
180
+ this.promptTokens = 0;
181
+ this.completionTokens = 0;
182
+ this.reported = false;
183
+ }
184
+ add(usage) {
185
+ if (!usage) return;
186
+ this.reported = true;
187
+ this.promptTokens += usage.promptTokens ?? 0;
188
+ this.completionTokens += usage.completionTokens ?? 0;
189
+ }
190
+ /** Undefined when no adapter response carried usage, rather than a false zero. */
191
+ total() {
192
+ if (!this.reported) return void 0;
193
+ return {
194
+ promptTokens: this.promptTokens,
195
+ completionTokens: this.completionTokens
196
+ };
197
+ }
198
+ };
199
+ function isAbortError(error) {
200
+ return error instanceof Error && error.name === "AbortError";
201
+ }
202
+ function buildStateManifest(canAccess, descriptions) {
203
+ return canAccess.map((key) => ({
204
+ key,
205
+ description: descriptions?.[key] ?? key
206
+ }));
207
+ }
208
+ function buildStateManifestPrompt(manifest) {
209
+ if (manifest.length === 0) return "";
210
+ const lines = manifest.map((m) => `- ${m.key}: ${m.description}`);
211
+ return [
212
+ "Available application state (use the __readState tool to access specific keys when needed):",
213
+ ...lines,
214
+ "",
215
+ "Only request state keys relevant to the user's question. Do not read all keys at once unless necessary."
216
+ ].join("\n");
217
+ }
218
+ function buildReadStateToolDef() {
219
+ return {
220
+ name: READ_STATE_TOOL_NAME,
221
+ 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
+ }
233
+ };
234
+ }
235
+ async function executeAgentLoop(message, ctx) {
236
+ const { model, state, tools, permissions, options, signal } = ctx;
237
+ const debug = options?.debug ?? false;
238
+ const maxTurns = options?.maxTurns ?? DEFAULT_MAX_TURNS;
239
+ const stateManifest = buildStateManifest(
240
+ permissions.canAccess,
241
+ permissions.stateDescriptions
242
+ );
243
+ if (debug) {
244
+ console.log("[react-observer-agent] State manifest:", stateManifest.map((m) => m.key));
245
+ }
246
+ const allowedTools = filterTools(tools, permissions.canExecute);
247
+ const llmTools = [];
248
+ for (const tool of allowedTools) {
249
+ if (!tool.description) {
250
+ if (debug) {
251
+ console.warn(
252
+ `[react-observer-agent] Tool "${tool.name}" has no description and is hidden from the LLM.`
253
+ );
254
+ }
255
+ continue;
256
+ }
257
+ llmTools.push({
258
+ name: tool.name,
259
+ description: tool.description,
260
+ parameters: tool.parameters ?? EMPTY_OBJECT_SCHEMA
261
+ });
262
+ }
263
+ if (stateManifest.length > 0) {
264
+ llmTools.push(buildReadStateToolDef());
265
+ }
266
+ if (debug) {
267
+ console.log("[react-observer-agent] Available tools:", llmTools.map((t) => t.name));
268
+ }
269
+ const toolMap = new Map(allowedTools.map((t) => [t.name, t]));
270
+ const messages = [
271
+ ...ctx.conversationHistory,
272
+ { role: "user", content: message }
273
+ ];
274
+ const manifestPrompt = buildStateManifestPrompt(stateManifest);
275
+ const systemPrompt = [options?.systemPrompt, manifestPrompt].filter(Boolean).join("\n\n") || void 0;
276
+ const allToolCalls = [];
277
+ const usage = new UsageTotal();
278
+ let turns = 0;
279
+ let finalMessage = "";
280
+ let completed = false;
281
+ const abortedResult = () => ({
282
+ response: {
283
+ message: "",
284
+ toolCalls: allToolCalls,
285
+ error: { message: "Interaction aborted", code: "ABORTED" },
286
+ usage: usage.total()
287
+ },
288
+ messages
289
+ });
290
+ while (turns < maxTurns) {
291
+ if (signal?.aborted) return abortedResult();
292
+ turns++;
293
+ if (debug) {
294
+ console.log(`[react-observer-agent] Turn ${turns}/${maxTurns}`);
295
+ }
296
+ const modelRequest = {
297
+ // A snapshot, since the loop keeps appending to `messages` after this
298
+ // call and an adapter that reads it asynchronously would see the churn.
299
+ messages: [...messages],
300
+ tools: llmTools,
301
+ state: {},
302
+ systemPrompt,
303
+ stateManifest,
304
+ signal
305
+ };
306
+ if (debug) {
307
+ console.log("[react-observer-agent] LLM request:", {
308
+ messageCount: modelRequest.messages.length,
309
+ toolCount: modelRequest.tools.length,
310
+ hasSystemPrompt: !!modelRequest.systemPrompt
311
+ });
312
+ }
313
+ let modelResponse;
314
+ try {
315
+ modelResponse = await model.sendMessage(modelRequest);
316
+ } catch (error) {
317
+ if (isAbortError(error) || signal?.aborted) {
318
+ return abortedResult();
319
+ }
320
+ throw error;
321
+ }
322
+ usage.add(modelResponse.usage);
323
+ if (signal?.aborted) return abortedResult();
324
+ if (debug) {
325
+ console.log("[react-observer-agent] LLM response:", {
326
+ content: modelResponse.content?.slice(0, 200),
327
+ toolCalls: modelResponse.toolCalls?.map((tc) => tc.name)
328
+ });
329
+ }
330
+ if (!modelResponse.toolCalls || modelResponse.toolCalls.length === 0) {
331
+ finalMessage = modelResponse.content ?? "";
332
+ completed = true;
333
+ break;
334
+ }
335
+ messages.push({
336
+ role: "assistant",
337
+ content: modelResponse.content ?? "",
338
+ toolCalls: modelResponse.toolCalls
339
+ });
340
+ for (const llmCall of modelResponse.toolCalls) {
341
+ if (signal?.aborted) return abortedResult();
342
+ if (llmCall.name === READ_STATE_TOOL_NAME) {
343
+ 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);
347
+ if (debug) {
348
+ console.log("[react-observer-agent] readState requested:", requestedKeys);
349
+ console.log("[react-observer-agent] readState allowed:", allowedKeys);
350
+ console.log("[react-observer-agent] readState result:", snapshot);
351
+ }
352
+ messages.push({
353
+ role: "tool",
354
+ content: JSON.stringify(snapshot),
355
+ toolCallId: llmCall.id
356
+ });
357
+ continue;
358
+ }
359
+ if (!validateToolCall(llmCall.name, permissions.canExecute)) {
360
+ const deniedResult = {
361
+ toolName: llmCall.name,
362
+ args: llmCall.arguments,
363
+ result: `Tool "${llmCall.name}" is not permitted`,
364
+ status: "denied"
365
+ };
366
+ allToolCalls.push(deniedResult);
367
+ options?.onToolCall?.({
368
+ toolName: llmCall.name,
369
+ args: llmCall.arguments,
370
+ result: deniedResult.result,
371
+ status: "denied"
372
+ });
373
+ messages.push({
374
+ role: "tool",
375
+ content: JSON.stringify({ error: deniedResult.result }),
376
+ toolCallId: llmCall.id
377
+ });
378
+ continue;
379
+ }
380
+ const toolDef = toolMap.get(llmCall.name);
381
+ if (!toolDef) {
382
+ const deniedResult = {
383
+ toolName: llmCall.name,
384
+ args: llmCall.arguments,
385
+ result: `Tool "${llmCall.name}" not found`,
386
+ status: "denied"
387
+ };
388
+ allToolCalls.push(deniedResult);
389
+ messages.push({
390
+ role: "tool",
391
+ content: JSON.stringify({ error: deniedResult.result }),
392
+ toolCallId: llmCall.id
393
+ });
394
+ continue;
395
+ }
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;
422
+ }
423
+ }
424
+ if (toolDef.confirm) {
425
+ if (!options?.onConfirm) {
426
+ if (debug) {
427
+ console.warn(
428
+ `[react-observer-agent] Tool "${llmCall.name}" requires confirmation but no onConfirm handler provided. Skipping.`
429
+ );
430
+ }
431
+ const cancelledResult = {
432
+ toolName: llmCall.name,
433
+ args: llmCall.arguments,
434
+ result: "Tool execution cancelled: no confirmation handler provided",
435
+ 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
+ });
444
+ messages.push({
445
+ role: "tool",
446
+ content: JSON.stringify({ status: "cancelled", reason: "No confirmation handler" }),
447
+ toolCallId: llmCall.id
448
+ });
449
+ continue;
450
+ }
451
+ const confirmed = await options.onConfirm({
452
+ toolName: llmCall.name,
453
+ args: llmCall.arguments,
454
+ description: toolDef.description
455
+ });
456
+ if (!confirmed) {
457
+ const cancelledResult = {
458
+ toolName: llmCall.name,
459
+ args: llmCall.arguments,
460
+ result: "Tool execution cancelled by user",
461
+ 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
+ });
470
+ messages.push({
471
+ role: "tool",
472
+ content: JSON.stringify({ status: "cancelled", reason: "User denied" }),
473
+ toolCallId: llmCall.id
474
+ });
475
+ continue;
476
+ }
477
+ }
478
+ 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 });
483
+ }
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
+ });
502
+ } catch (error) {
503
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
504
+ const errorResult = {
505
+ 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,
515
+ status: "error"
516
+ });
517
+ messages.push({
518
+ role: "tool",
519
+ content: JSON.stringify({ error: errorMessage }),
520
+ toolCallId: llmCall.id
521
+ });
522
+ }
523
+ }
524
+ }
525
+ if (!completed) {
526
+ if (debug) {
527
+ console.warn(`[react-observer-agent] Max turns (${maxTurns}) reached`);
528
+ }
529
+ return {
530
+ response: {
531
+ message: "",
532
+ toolCalls: allToolCalls,
533
+ error: {
534
+ message: `Agent did not produce a final response within ${maxTurns} turns`,
535
+ code: "MAX_TURNS"
536
+ },
537
+ usage: usage.total()
538
+ },
539
+ messages
540
+ };
541
+ }
542
+ messages.push({ role: "assistant", content: finalMessage });
543
+ return {
544
+ response: {
545
+ message: finalMessage,
546
+ toolCalls: allToolCalls,
547
+ usage: usage.total()
548
+ },
549
+ messages
550
+ };
551
+ }
552
+
553
+ // src/provider/AIAgentProvider.tsx
554
+ import { jsx } from "react/jsx-runtime";
555
+ var AgentContextValue = createContext(null);
556
+ function AIAgentProvider({
557
+ model,
558
+ state,
559
+ tools,
560
+ permissions,
561
+ options,
562
+ children
563
+ }) {
564
+ const [isProcessing, setIsProcessing] = useState(false);
565
+ const [history, setHistory] = useState([]);
566
+ const [lastResponse, setLastResponse] = useState(null);
567
+ const modelRef = useRef(model);
568
+ const stateRef = useRef(state);
569
+ const toolsRef = useRef(tools);
570
+ const permissionsRef = useRef(permissions);
571
+ const optionsRef = useRef(options);
572
+ modelRef.current = model;
573
+ stateRef.current = state;
574
+ toolsRef.current = tools;
575
+ permissionsRef.current = permissions;
576
+ optionsRef.current = options;
577
+ useEffect(() => {
578
+ validateToolNames(tools);
579
+ }, [tools]);
580
+ const transcriptRef = useRef([]);
581
+ const clearHistory = useCallback(() => {
582
+ setHistory([]);
583
+ setLastResponse(null);
584
+ transcriptRef.current = [];
585
+ }, []);
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,
611
+ timestamp: Date.now()
612
+ };
613
+ setHistory((prev) => [...prev, assistantEntry]);
614
+ setLastResponse(response);
615
+ if (response.error && response.error.code !== "ABORTED") {
616
+ optionsRef.current?.onError?.(response.error);
617
+ }
618
+ 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
+ }, []);
636
+ const contextValue = useMemo(
637
+ () => ({
638
+ send,
639
+ isProcessing,
640
+ history,
641
+ clearHistory,
642
+ lastResponse
643
+ }),
644
+ [send, isProcessing, history, clearHistory, lastResponse]
645
+ );
646
+ return /* @__PURE__ */ jsx(AgentContextValue.Provider, { value: contextValue, children });
647
+ }
648
+
649
+ // src/provider/useAgent.ts
650
+ import { useContext } from "react";
651
+ function useAgent() {
652
+ const context = useContext(AgentContextValue);
653
+ if (!context) {
654
+ throw new Error(
655
+ "useAgent() must be used within an <AIAgentProvider>. Wrap your component tree with <AIAgentProvider> to use this hook."
656
+ );
657
+ }
658
+ return context;
659
+ }
660
+
661
+ // src/permissions/filterState.ts
662
+ function filterState(state, canAccess) {
663
+ const filtered = {};
664
+ for (const key of canAccess) {
665
+ if (key in state) {
666
+ filtered[key] = state[key];
667
+ }
668
+ }
669
+ return filtered;
670
+ }
671
+
672
+ // src/adapters/openai.ts
673
+ var DEFAULT_MODEL = "gpt-4o";
674
+ var DEFAULT_TEMPERATURE = 0.2;
675
+ var OPENAI_BASE_URL = "https://api.openai.com/v1";
676
+ function openAIAdapter(config) {
677
+ if (!config.apiKey && !config.baseURL) {
678
+ throw new Error(
679
+ 'openAIAdapter requires either "apiKey" or "baseURL". Provide an API key for direct access, or a baseURL to route through your backend proxy.'
680
+ );
681
+ }
682
+ const baseURL = config.baseURL ? config.baseURL.replace(/\/+$/, "") : OPENAI_BASE_URL;
683
+ const model = config.model ?? DEFAULT_MODEL;
684
+ const temperature = config.temperature ?? DEFAULT_TEMPERATURE;
685
+ return {
686
+ async sendMessage(request) {
687
+ const headers = {
688
+ "Content-Type": "application/json",
689
+ ...config.headers
690
+ };
691
+ if (config.apiKey) {
692
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
693
+ }
694
+ const messages = request.systemPrompt ? [
695
+ { role: "system", content: request.systemPrompt },
696
+ ...request.messages.map(formatMessage)
697
+ ] : request.messages.map(formatMessage);
698
+ const tools = request.tools.length > 0 ? request.tools.map((t) => ({
699
+ type: "function",
700
+ function: {
701
+ name: t.name,
702
+ description: t.description,
703
+ parameters: t.parameters
704
+ }
705
+ })) : void 0;
706
+ const body = {
707
+ model,
708
+ messages,
709
+ temperature
710
+ };
711
+ if (tools) {
712
+ body.tools = tools;
713
+ }
714
+ const url = baseURL.includes("/chat/completions") ? baseURL : `${baseURL}/chat/completions`;
715
+ let res;
716
+ try {
717
+ res = await fetch(url, {
718
+ method: "POST",
719
+ headers,
720
+ body: JSON.stringify(body),
721
+ signal: request.signal
722
+ });
723
+ } catch (error) {
724
+ 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"}`
727
+ );
728
+ }
729
+ if (!res.ok) {
730
+ const text = await res.text().catch(() => "");
731
+ throw new Error(
732
+ `OpenAI API error (${res.status}): ${text || res.statusText}`
733
+ );
734
+ }
735
+ let data;
736
+ try {
737
+ data = await res.json();
738
+ } catch {
739
+ throw new Error("Failed to parse OpenAI API response as JSON");
740
+ }
741
+ return parseResponse(data);
742
+ }
743
+ };
744
+ }
745
+ function formatMessage(msg) {
746
+ const formatted = {
747
+ role: msg.role,
748
+ content: msg.content
749
+ };
750
+ if (msg.toolCallId) {
751
+ formatted.tool_call_id = msg.toolCallId;
752
+ }
753
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
754
+ formatted.tool_calls = msg.toolCalls.map((tc) => ({
755
+ id: tc.id,
756
+ type: "function",
757
+ function: {
758
+ name: tc.name,
759
+ arguments: typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments ?? {})
760
+ }
761
+ }));
762
+ if (msg.content === "") {
763
+ formatted.content = null;
764
+ }
765
+ }
766
+ return formatted;
767
+ }
768
+ function parseResponse(data) {
769
+ const obj = data;
770
+ const choices = obj.choices;
771
+ if (!choices || choices.length === 0) {
772
+ throw new Error("Malformed OpenAI response: no choices returned");
773
+ }
774
+ const message = choices[0].message;
775
+ if (!message) {
776
+ throw new Error("Malformed OpenAI response: no message in first choice");
777
+ }
778
+ const content = message.content ?? null;
779
+ const toolCalls = message.tool_calls;
780
+ const usage = obj.usage;
781
+ return {
782
+ content,
783
+ toolCalls: toolCalls?.map((tc) => ({
784
+ id: tc.id,
785
+ name: tc.function.name,
786
+ arguments: safeParseJSON(tc.function.arguments)
787
+ })),
788
+ usage: usage ? {
789
+ promptTokens: usage.prompt_tokens,
790
+ completionTokens: usage.completion_tokens
791
+ } : void 0
792
+ };
793
+ }
794
+ function safeParseJSON(str) {
795
+ try {
796
+ return JSON.parse(str);
797
+ } catch {
798
+ return str;
799
+ }
800
+ }
801
+
802
+ // src/adapters/claude.ts
803
+ var DEFAULT_MODEL2 = "claude-opus-5";
804
+ var DEFAULT_MAX_TOKENS = 16e3;
805
+ var ANTHROPIC_BASE_URL = "https://api.anthropic.com";
806
+ var ANTHROPIC_VERSION = "2023-06-01";
807
+ function claudeAdapter(config) {
808
+ if (!config.apiKey && !config.baseURL) {
809
+ throw new Error(
810
+ 'claudeAdapter requires either "apiKey" or "baseURL". Provide an API key for direct access, or a baseURL to route through your backend proxy.'
811
+ );
812
+ }
813
+ const baseURL = config.baseURL ? config.baseURL.replace(/\/+$/, "") : ANTHROPIC_BASE_URL;
814
+ const model = config.model ?? DEFAULT_MODEL2;
815
+ const maxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
816
+ return {
817
+ async sendMessage(request) {
818
+ const headers = {
819
+ "Content-Type": "application/json",
820
+ "anthropic-version": ANTHROPIC_VERSION
821
+ };
822
+ if (config.apiKey) {
823
+ headers["x-api-key"] = config.apiKey;
824
+ }
825
+ Object.assign(headers, config.headers);
826
+ const body = {
827
+ model,
828
+ max_tokens: maxTokens,
829
+ messages: toAnthropicMessages(request.messages)
830
+ };
831
+ if (request.systemPrompt) {
832
+ body.system = request.systemPrompt;
833
+ }
834
+ if (request.tools.length > 0) {
835
+ body.tools = request.tools.map((t) => ({
836
+ name: t.name,
837
+ description: t.description,
838
+ input_schema: t.parameters
839
+ }));
840
+ }
841
+ const url = baseURL.includes("/v1/messages") ? baseURL : `${baseURL}/v1/messages`;
842
+ let res;
843
+ try {
844
+ res = await fetch(url, {
845
+ method: "POST",
846
+ headers,
847
+ body: JSON.stringify(body),
848
+ signal: request.signal
849
+ });
850
+ } catch (error) {
851
+ 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"}`
854
+ );
855
+ }
856
+ if (!res.ok) {
857
+ const text = await res.text().catch(() => "");
858
+ throw new Error(
859
+ `Anthropic API error (${res.status}): ${text || res.statusText}`
860
+ );
861
+ }
862
+ let data;
863
+ try {
864
+ data = await res.json();
865
+ } catch {
866
+ throw new Error("Failed to parse Anthropic API response as JSON");
867
+ }
868
+ return parseResponse2(data);
869
+ }
870
+ };
871
+ }
872
+ function toAnthropicMessages(messages) {
873
+ const result = [];
874
+ let pendingToolResults = [];
875
+ const flushToolResults = () => {
876
+ if (pendingToolResults.length > 0) {
877
+ result.push({ role: "user", content: pendingToolResults });
878
+ pendingToolResults = [];
879
+ }
880
+ };
881
+ for (const message of messages) {
882
+ if (message.role === "tool") {
883
+ pendingToolResults.push({
884
+ type: "tool_result",
885
+ tool_use_id: message.toolCallId ?? "",
886
+ content: message.content
887
+ });
888
+ continue;
889
+ }
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 });
905
+ continue;
906
+ }
907
+ result.push({ role: message.role, content: message.content });
908
+ }
909
+ flushToolResults();
910
+ return result;
911
+ }
912
+ function parseResponse2(data) {
913
+ const obj = data;
914
+ const blocks = obj.content;
915
+ if (!Array.isArray(blocks)) {
916
+ throw new Error("Malformed Anthropic response: no content blocks returned");
917
+ }
918
+ const textParts = [];
919
+ const toolCalls = [];
920
+ for (const block of blocks) {
921
+ if (block?.type === "text") {
922
+ textParts.push(block.text);
923
+ } else if (block?.type === "tool_use") {
924
+ toolCalls.push({
925
+ id: block.id,
926
+ name: block.name,
927
+ arguments: block.input
928
+ });
929
+ }
930
+ }
931
+ const usage = obj.usage;
932
+ return {
933
+ content: textParts.length > 0 ? textParts.join("") : null,
934
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
935
+ usage: usage ? {
936
+ promptTokens: usage.input_tokens,
937
+ completionTokens: usage.output_tokens
938
+ } : void 0
939
+ };
940
+ }
941
+ function isPlainObject2(value) {
942
+ return typeof value === "object" && value !== null && !Array.isArray(value);
943
+ }
944
+ export {
945
+ AIAgentProvider,
946
+ claudeAdapter,
947
+ filterState,
948
+ filterTools,
949
+ openAIAdapter,
950
+ registerTool,
951
+ useAgent,
952
+ validateToolCall,
953
+ validateToolNames
954
+ };
955
+ //# sourceMappingURL=index.js.map