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