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