@nexface/agent 0.1.13-alpha.3 → 0.1.13-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +867 -294
- package/dist/browser-policy/gpt.js +36 -11
- package/dist/browser-policy/jev.js +21 -8
- package/dist/browser-policy/response-error.js +4 -1
- package/dist/browser-policy/shared.js +49 -20
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +3 -3
- package/dist/history-compaction.js +47 -25
- package/dist/history-version.js +11 -3
- package/dist/model/internal-adapter.js +1 -1
- package/dist/page-decision.js +197 -58
- package/dist/page-decisions/shared.js +26 -8
- package/dist/page-diagnostics.js +8 -2
- package/dist/page-observation.js +11 -5
- package/dist/request.js +169 -40
- package/dist/shared/errors.d.ts +1 -1
- package/dist/shared/schema.js +45 -18
- package/dist/tool-catalog.js +23 -24
- package/dist/tool-result.js +7 -2
- package/package.json +2 -2
package/dist/request.js
CHANGED
|
@@ -2,17 +2,25 @@ import { assertCurrentHistory } from "./history-version.js";
|
|
|
2
2
|
import { MessageSchema } from "@ag-ui/core";
|
|
3
3
|
import { getToolResultError } from "./tool-result.js";
|
|
4
4
|
export const approvalResponseSchema = {
|
|
5
|
-
type: "object",
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: { approved: { type: "boolean" } },
|
|
7
|
+
required: ["approved"],
|
|
8
|
+
additionalProperties: false,
|
|
6
9
|
};
|
|
7
10
|
export const toolResultResponseSchema = {
|
|
8
|
-
type: "object",
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: { toolResultMessageId: { type: "string" }, confirmed: { type: "boolean" } },
|
|
13
|
+
required: ["toolResultMessageId"],
|
|
14
|
+
additionalProperties: false,
|
|
9
15
|
};
|
|
10
16
|
export function parseResume(value, allowMetadata = false) {
|
|
11
17
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
18
|
throw new Error("Invalid resume entry.");
|
|
13
19
|
const entry = value;
|
|
14
20
|
const allowed = ["interruptId", "status", "payload", ...(allowMetadata ? ["metadata"] : [])];
|
|
15
|
-
if (Object.keys(entry).some((key) => !allowed.includes(key)) ||
|
|
21
|
+
if (Object.keys(entry).some((key) => !allowed.includes(key)) ||
|
|
22
|
+
typeof entry.interruptId !== "string" ||
|
|
23
|
+
!entry.interruptId)
|
|
16
24
|
throw new Error("Invalid resume entry.");
|
|
17
25
|
if (entry.status === "cancelled") {
|
|
18
26
|
if (entry.payload !== undefined)
|
|
@@ -20,13 +28,31 @@ export function parseResume(value, allowMetadata = false) {
|
|
|
20
28
|
return { interruptId: entry.interruptId, status: "cancelled" };
|
|
21
29
|
}
|
|
22
30
|
const payload = entry.payload;
|
|
23
|
-
if (entry.status !== "resolved" ||
|
|
31
|
+
if (entry.status !== "resolved" ||
|
|
32
|
+
!payload ||
|
|
33
|
+
typeof payload !== "object" ||
|
|
34
|
+
Array.isArray(payload))
|
|
24
35
|
throw new Error("A resolved response requires a payload.");
|
|
25
|
-
if (typeof payload.approved === "boolean" &&
|
|
26
|
-
|
|
36
|
+
if (typeof payload.approved === "boolean" &&
|
|
37
|
+
Object.keys(payload).every((key) => key === "approved")) {
|
|
38
|
+
return {
|
|
39
|
+
interruptId: entry.interruptId,
|
|
40
|
+
status: "resolved",
|
|
41
|
+
payload: { approved: payload.approved },
|
|
42
|
+
};
|
|
27
43
|
}
|
|
28
|
-
if (typeof payload.toolResultMessageId === "string" &&
|
|
29
|
-
|
|
44
|
+
if (typeof payload.toolResultMessageId === "string" &&
|
|
45
|
+
payload.toolResultMessageId &&
|
|
46
|
+
(payload.confirmed === undefined || typeof payload.confirmed === "boolean") &&
|
|
47
|
+
Object.keys(payload).every((key) => key === "toolResultMessageId" || key === "confirmed")) {
|
|
48
|
+
return {
|
|
49
|
+
interruptId: entry.interruptId,
|
|
50
|
+
status: "resolved",
|
|
51
|
+
payload: {
|
|
52
|
+
toolResultMessageId: payload.toolResultMessageId,
|
|
53
|
+
...(payload.confirmed === undefined ? {} : { confirmed: payload.confirmed }),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
30
56
|
}
|
|
31
57
|
throw new Error("Invalid resume payload.");
|
|
32
58
|
}
|
|
@@ -43,23 +69,41 @@ export function convertMessages(values) {
|
|
|
43
69
|
if (message.role === "assistant") {
|
|
44
70
|
for (const call of message.toolCalls ?? [])
|
|
45
71
|
calls.set(call.id, call.function.name);
|
|
46
|
-
return [
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
id: message.id,
|
|
75
|
+
role: "assistant",
|
|
76
|
+
content: message.content ?? null,
|
|
77
|
+
toolCalls: (message.toolCalls ?? []).map((call) => ({ id: call.id, ...call.function })),
|
|
78
|
+
},
|
|
79
|
+
];
|
|
47
80
|
}
|
|
48
81
|
if (message.role === "tool") {
|
|
49
82
|
const name = calls.get(message.toolCallId);
|
|
50
83
|
if (!name)
|
|
51
84
|
throw new Error("A tool result has no preceding tool call.");
|
|
52
85
|
const content = message.error !== undefined && getToolResultError(message.content) === undefined
|
|
53
|
-
? JSON.stringify({
|
|
86
|
+
? JSON.stringify({
|
|
87
|
+
error: { code: "TOOL_ERROR", message: message.error },
|
|
88
|
+
content: message.content,
|
|
89
|
+
})
|
|
90
|
+
: message.content;
|
|
54
91
|
return [{ id: message.id, role: "tool", name, toolCallId: message.toolCallId, content }];
|
|
55
92
|
}
|
|
56
93
|
if (message.role === "user") {
|
|
57
|
-
if (Array.isArray(message.content) &&
|
|
94
|
+
if (Array.isArray(message.content) &&
|
|
95
|
+
message.content.some((part) => part.type !== "text" && part.type !== "image"))
|
|
58
96
|
throw new Error("This Agent supports text and image input only.");
|
|
59
97
|
const content = typeof message.content === "string" ? message.content.trim() : message.content;
|
|
60
98
|
if (!content.length)
|
|
61
99
|
throw new Error("Agent message content is required.");
|
|
62
|
-
return [
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
id: message.id,
|
|
103
|
+
role: "user",
|
|
104
|
+
content: content,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
63
107
|
}
|
|
64
108
|
throw new Error(`Unsupported model history role: ${message.role}.`);
|
|
65
109
|
});
|
|
@@ -68,22 +112,29 @@ export function convertMessages(values) {
|
|
|
68
112
|
}
|
|
69
113
|
export function decodeRequest(value) {
|
|
70
114
|
const extension = value.forwardedProps?.nexface;
|
|
71
|
-
if (extension &&
|
|
115
|
+
if (extension &&
|
|
116
|
+
(extension.version !== 3 || "checkpoint" in extension || "confirmedToolCallId" in extension))
|
|
72
117
|
throw new Error("Unsupported Nexface request version or recovery channel. Upgrade client and server together and start a new conversation.");
|
|
73
118
|
const state = value.state;
|
|
74
119
|
if (state != null && (typeof state !== "object" || Array.isArray(state)))
|
|
75
120
|
throw new Error("Invalid Agent state.");
|
|
76
|
-
if (state &&
|
|
121
|
+
if (state &&
|
|
122
|
+
"nexface" in state &&
|
|
123
|
+
(!state.nexface || typeof state.nexface !== "object" || Array.isArray(state.nexface)))
|
|
77
124
|
throw new Error("Invalid Nexface state.");
|
|
78
125
|
const restored = state?.nexface;
|
|
79
|
-
if (restored &&
|
|
126
|
+
if (restored &&
|
|
127
|
+
(restored.version !== 3 ||
|
|
128
|
+
restored.threadId !== value.threadId ||
|
|
129
|
+
!Array.isArray(restored.timeline)))
|
|
80
130
|
throw new Error("Agent state does not match this thread or version. Start a new conversation.");
|
|
81
131
|
const converted = new Map(convertMessages(value.messages).map((message) => [message.id, message]));
|
|
82
132
|
const source = new Map(value.messages.map((message) => [message.id, message]));
|
|
83
133
|
const used = new Set();
|
|
84
134
|
const messages = (restored?.timeline ?? []).flatMap((entry) => {
|
|
85
135
|
if ("internal" in entry) {
|
|
86
|
-
if (!entry.internal ||
|
|
136
|
+
if (!entry.internal ||
|
|
137
|
+
(entry.internal.role !== "system" && entry.internal.role !== "application_context"))
|
|
87
138
|
throw new Error("Invalid internal history.");
|
|
88
139
|
return [structuredClone(entry.internal)];
|
|
89
140
|
}
|
|
@@ -100,7 +151,10 @@ export function decodeRequest(value) {
|
|
|
100
151
|
const receipts = [];
|
|
101
152
|
let toolResult;
|
|
102
153
|
if (pending) {
|
|
103
|
-
if (!resume ||
|
|
154
|
+
if (!resume ||
|
|
155
|
+
resume.interruptId !== pending.interruptId ||
|
|
156
|
+
!Array.isArray(pending.calls) ||
|
|
157
|
+
!pending.calls.length)
|
|
104
158
|
throw new Error("Resume does not match the pending interrupt.");
|
|
105
159
|
if (pending.kind !== "client-tool-execution")
|
|
106
160
|
throw new Error("Invalid pending kind.");
|
|
@@ -108,12 +162,19 @@ export function decodeRequest(value) {
|
|
|
108
162
|
if (!("toolResultMessageId" in resume.payload))
|
|
109
163
|
throw new Error("A client tool requires a result reference.");
|
|
110
164
|
const message = source.get(resume.payload.toolResultMessageId);
|
|
111
|
-
if (!message ||
|
|
165
|
+
if (!message ||
|
|
166
|
+
used.has(message.id) ||
|
|
167
|
+
message.role !== "tool" ||
|
|
168
|
+
message.toolCallId !== pending.calls[0].id)
|
|
112
169
|
throw new Error("Resume result does not match the pending call.");
|
|
113
170
|
const owner = [...messages].reverse().find((item) => item.role === "assistant");
|
|
114
171
|
const ownerIndex = value.messages.findIndex((item) => item.id === owner?.id);
|
|
115
172
|
const receiptIndex = value.messages.indexOf(message);
|
|
116
|
-
if (ownerIndex < 0 ||
|
|
173
|
+
if (ownerIndex < 0 ||
|
|
174
|
+
receiptIndex <= ownerIndex ||
|
|
175
|
+
value.messages
|
|
176
|
+
.slice(ownerIndex + 1, receiptIndex)
|
|
177
|
+
.some((item) => item.role === "user" || (item.role === "assistant" && item.toolCalls?.length)))
|
|
117
178
|
throw new Error("The receipt does not belong to the pending assistant batch.");
|
|
118
179
|
let payload;
|
|
119
180
|
try {
|
|
@@ -125,11 +186,18 @@ export function decodeRequest(value) {
|
|
|
125
186
|
}
|
|
126
187
|
if (message.error !== undefined)
|
|
127
188
|
payload = { error: { code: payload?.error?.code ?? "TOOL_ERROR", message: message.error } };
|
|
128
|
-
if (!payload ||
|
|
189
|
+
if (!payload ||
|
|
190
|
+
typeof payload !== "object" ||
|
|
191
|
+
(!("result" in payload) && !("error" in payload)))
|
|
129
192
|
throw new Error("Invalid tool result.");
|
|
130
|
-
if (payload.error &&
|
|
193
|
+
if (payload.error &&
|
|
194
|
+
(typeof payload.error.code !== "string" || typeof payload.error.message !== "string"))
|
|
131
195
|
throw new Error("Invalid tool error.");
|
|
132
|
-
toolResult = {
|
|
196
|
+
toolResult = {
|
|
197
|
+
toolCallId: message.toolCallId,
|
|
198
|
+
...(payload.error ? { error: payload.error } : { result: payload.result }),
|
|
199
|
+
...(resume.payload.confirmed === undefined ? {} : { confirmed: resume.payload.confirmed }),
|
|
200
|
+
};
|
|
133
201
|
receipts.push(message);
|
|
134
202
|
}
|
|
135
203
|
else {
|
|
@@ -138,7 +206,9 @@ export function decodeRequest(value) {
|
|
|
138
206
|
for (const message of value.messages.slice(ownerIndex + 1)) {
|
|
139
207
|
if (message.role === "user")
|
|
140
208
|
break;
|
|
141
|
-
if (used.has(message.id) ||
|
|
209
|
+
if (used.has(message.id) ||
|
|
210
|
+
message.role !== "tool" ||
|
|
211
|
+
!pending.calls.some((call) => call.id === message.toolCallId))
|
|
142
212
|
continue;
|
|
143
213
|
let payload;
|
|
144
214
|
try {
|
|
@@ -148,7 +218,9 @@ export function decodeRequest(value) {
|
|
|
148
218
|
throw new Error("Invalid cancellation receipt.");
|
|
149
219
|
}
|
|
150
220
|
const first = message.toolCallId === pending.calls[0].id;
|
|
151
|
-
if (!(first
|
|
221
|
+
if (!(first
|
|
222
|
+
? ["TOOL_CANCELLED", "TOOL_CONFIRMATION_INVALIDATED"]
|
|
223
|
+
: ["TOOL_SKIPPED", "TOOL_CANCELLED"]).includes(payload?.error?.code))
|
|
152
224
|
throw new Error("Cancellation cannot replace an execution result.");
|
|
153
225
|
if (receipts.some((receipt) => receipt.role === "tool" && receipt.toolCallId === message.toolCallId))
|
|
154
226
|
throw new Error("Repeated cancellation receipt.");
|
|
@@ -162,7 +234,9 @@ export function decodeRequest(value) {
|
|
|
162
234
|
const newMessages = value.messages.flatMap((message) => {
|
|
163
235
|
if (used.has(message.id) || receiptIds.has(message.id))
|
|
164
236
|
return [];
|
|
165
|
-
if (pending &&
|
|
237
|
+
if (pending &&
|
|
238
|
+
message.role === "tool" &&
|
|
239
|
+
pending.calls.some((call) => call.id === message.toolCallId))
|
|
166
240
|
throw new Error("Repeated or unexpected pending result.");
|
|
167
241
|
const convertedMessage = converted.get(message.id);
|
|
168
242
|
if (pending && resume?.status === "resolved" && convertedMessage)
|
|
@@ -170,17 +244,33 @@ export function decodeRequest(value) {
|
|
|
170
244
|
return convertedMessage ? [convertedMessage] : [];
|
|
171
245
|
});
|
|
172
246
|
return {
|
|
173
|
-
messages,
|
|
174
|
-
|
|
247
|
+
messages,
|
|
248
|
+
newMessages,
|
|
249
|
+
receipts,
|
|
250
|
+
...(restored ? { continuation: structuredClone(restored) } : {}),
|
|
251
|
+
...(resume ? { resume } : {}),
|
|
252
|
+
...(toolResult ? { toolResult } : {}),
|
|
253
|
+
tools: value.tools.map((tool) => ({
|
|
254
|
+
name: tool.name,
|
|
255
|
+
description: tool.description,
|
|
256
|
+
inputSchema: tool.parameters,
|
|
257
|
+
effect: extension?.toolMetadata?.[tool.name]?.effect ?? "action",
|
|
258
|
+
})),
|
|
175
259
|
contexts: value.context.map((context, index) => {
|
|
176
260
|
let data = context.value;
|
|
177
261
|
try {
|
|
178
262
|
data = JSON.parse(context.value);
|
|
179
263
|
}
|
|
180
|
-
catch {
|
|
264
|
+
catch {
|
|
265
|
+
/* Context values may be plain text. */
|
|
266
|
+
}
|
|
181
267
|
if (data && typeof data === "object" && "name" in data && "value" in data)
|
|
182
268
|
return data;
|
|
183
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
name: `context${index}`,
|
|
271
|
+
description: context.description,
|
|
272
|
+
value: data,
|
|
273
|
+
};
|
|
184
274
|
}),
|
|
185
275
|
};
|
|
186
276
|
}
|
|
@@ -190,10 +280,29 @@ export function toProtocolMessages(messages) {
|
|
|
190
280
|
if (message.role === "system" || message.role === "application_context")
|
|
191
281
|
return [];
|
|
192
282
|
if (message.role === "assistant")
|
|
193
|
-
return [
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
id: message.id,
|
|
286
|
+
role: "assistant",
|
|
287
|
+
...(message.content === null ? {} : { content: message.content }),
|
|
288
|
+
toolCalls: message.toolCalls.map(({ id, name, arguments: args }) => ({
|
|
289
|
+
id,
|
|
290
|
+
type: "function",
|
|
291
|
+
function: { name, arguments: args },
|
|
292
|
+
})),
|
|
293
|
+
},
|
|
294
|
+
];
|
|
194
295
|
if (message.role === "tool") {
|
|
195
296
|
const error = getToolResultError(message.content);
|
|
196
|
-
return [
|
|
297
|
+
return [
|
|
298
|
+
{
|
|
299
|
+
id: message.id,
|
|
300
|
+
role: "tool",
|
|
301
|
+
toolCallId: message.toolCallId,
|
|
302
|
+
content: message.content,
|
|
303
|
+
...(error === undefined ? {} : { error }),
|
|
304
|
+
},
|
|
305
|
+
];
|
|
197
306
|
}
|
|
198
307
|
return [{ id: message.id, role: "user", content: message.content }];
|
|
199
308
|
});
|
|
@@ -215,15 +324,22 @@ export function runResponse(run) {
|
|
|
215
324
|
controller.error(error);
|
|
216
325
|
}
|
|
217
326
|
},
|
|
218
|
-
async cancel() {
|
|
219
|
-
|
|
327
|
+
async cancel() {
|
|
328
|
+
run.cancel();
|
|
329
|
+
await iterator.return?.();
|
|
330
|
+
},
|
|
331
|
+
}), {
|
|
332
|
+
headers: {
|
|
220
333
|
"Content-Type": "text/event-stream",
|
|
221
334
|
"Cache-Control": "no-cache, no-transform",
|
|
222
335
|
"X-Accel-Buffering": "no",
|
|
223
|
-
}
|
|
336
|
+
},
|
|
337
|
+
});
|
|
224
338
|
}
|
|
225
339
|
export function mergeProtocolMessages(messages, previous) {
|
|
226
|
-
const reasoningOwners = new Set(previous
|
|
340
|
+
const reasoningOwners = new Set(previous
|
|
341
|
+
.filter((message) => message.role === "reasoning")
|
|
342
|
+
.map((message) => message.metadata?.nexface?.reasoningOwnerId));
|
|
227
343
|
const remaining = new Map(messages.map((message) => [message.id, message]));
|
|
228
344
|
const mergedHistory = previous.flatMap((old) => {
|
|
229
345
|
const message = remaining.get(old.id);
|
|
@@ -231,17 +347,30 @@ export function mergeProtocolMessages(messages, previous) {
|
|
|
231
347
|
return old.role === "activity" || old.role === "reasoning" ? [old] : [];
|
|
232
348
|
remaining.delete(old.id);
|
|
233
349
|
const merged = { ...old, ...message };
|
|
234
|
-
if (old.role === "assistant" &&
|
|
350
|
+
if (old.role === "assistant" &&
|
|
351
|
+
message.role === "assistant" &&
|
|
352
|
+
old.content === "" &&
|
|
353
|
+
message.content === undefined)
|
|
235
354
|
delete merged.content;
|
|
236
355
|
// Preserve streamed display offsets when a finish tool replaces model text.
|
|
237
|
-
if (old.role === "assistant" &&
|
|
238
|
-
|
|
239
|
-
|
|
356
|
+
if (old.role === "assistant" &&
|
|
357
|
+
message.role === "assistant" &&
|
|
358
|
+
reasoningOwners.has(old.id) &&
|
|
359
|
+
old.content &&
|
|
360
|
+
old.content !== message.content &&
|
|
361
|
+
old.metadata?.nexface?.reasoningText === undefined) {
|
|
362
|
+
merged.metadata = {
|
|
363
|
+
...old.metadata,
|
|
364
|
+
nexface: { ...old.metadata?.nexface, reasoningText: old.content },
|
|
365
|
+
};
|
|
240
366
|
}
|
|
241
367
|
if (message.role === "tool" && old.role === "tool" && old.error !== undefined)
|
|
242
368
|
merged.content = old.content;
|
|
243
369
|
if (message.role === "assistant" && old.role === "assistant")
|
|
244
|
-
merged.toolCalls = message.toolCalls?.map((call) => ({
|
|
370
|
+
merged.toolCalls = message.toolCalls?.map((call) => ({
|
|
371
|
+
...old.toolCalls?.find((item) => item.id === call.id),
|
|
372
|
+
...call,
|
|
373
|
+
}));
|
|
245
374
|
return [MessageSchema.parse(merged)];
|
|
246
375
|
});
|
|
247
376
|
return [...mergedHistory, ...remaining.values()];
|
package/dist/shared/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const TOOL_ERROR_CODES: readonly ["EXECUTION_OUTCOME_UNKNOWN", "ACTION_IN_FLIGHT", "ACTION_NOT_HANDLED", "ACTION_TARGET_UNAVAILABLE", "BROWSER_EXECUTION_FAILED", "BROWSER_SNAPSHOT_REQUIRED", "BROWSER_TOOLS_UNAVAILABLE", "CONTRACT_MISMATCH", "DUPLICATE_BINDING", "INVALID_INPUT", "INVALID_TOOL_RUNTIME", "OPERATION_BUSY", "TOOL_CANCELLED", "TOOL_CONFIRMATION_INVALIDATED", "TOOL_CONFIRMATION_REJECTED", "TOOL_ERROR", "TOOL_REGISTRATION_TIMEOUT", "TOOL_SKIPPED", "TOOL_UNAVAILABLE", "UNKNOWN_TOOL"];
|
|
2
|
-
type AgentToolErrorCode = typeof TOOL_ERROR_CODES[number];
|
|
2
|
+
type AgentToolErrorCode = (typeof TOOL_ERROR_CODES)[number];
|
|
3
3
|
export declare class AgentToolError extends Error {
|
|
4
4
|
readonly code: AgentToolErrorCode;
|
|
5
5
|
readonly details?: unknown | undefined;
|
package/dist/shared/schema.js
CHANGED
|
@@ -3,8 +3,17 @@ const types = new Set(["object", "array", "string", "number", "integer", "boolea
|
|
|
3
3
|
const counts = ["minLength", "maxLength", "minItems", "maxItems"];
|
|
4
4
|
const bounds = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"];
|
|
5
5
|
const keywords = new Set([
|
|
6
|
-
"type",
|
|
7
|
-
|
|
6
|
+
"type",
|
|
7
|
+
"description",
|
|
8
|
+
"properties",
|
|
9
|
+
"required",
|
|
10
|
+
"items",
|
|
11
|
+
"enum",
|
|
12
|
+
"additionalProperties",
|
|
13
|
+
...counts,
|
|
14
|
+
...bounds,
|
|
15
|
+
"pattern",
|
|
16
|
+
"uniqueItems",
|
|
8
17
|
]);
|
|
9
18
|
const checkedSchemas = new WeakSet();
|
|
10
19
|
const patterns = new WeakMap();
|
|
@@ -24,7 +33,9 @@ function assertJson(value, path, ancestors = new Set()) {
|
|
|
24
33
|
invalid(path, "must be JSON-compatible.");
|
|
25
34
|
if (ancestors.has(value))
|
|
26
35
|
invalid(path, "must not contain circular references.");
|
|
27
|
-
if (!Array.isArray(value) &&
|
|
36
|
+
if (!Array.isArray(value) &&
|
|
37
|
+
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
38
|
+
Object.getPrototypeOf(value) !== null) {
|
|
28
39
|
invalid(path, "must be a plain JSON object.");
|
|
29
40
|
}
|
|
30
41
|
if (Object.getOwnPropertySymbols(value).length)
|
|
@@ -85,7 +96,8 @@ function assertSchema(value, path) {
|
|
|
85
96
|
invalid(`${path}.${key}`, "must be a non-negative integer.");
|
|
86
97
|
}
|
|
87
98
|
for (const key of bounds) {
|
|
88
|
-
if (value[key] !== undefined &&
|
|
99
|
+
if (value[key] !== undefined &&
|
|
100
|
+
(typeof value[key] !== "number" || !Number.isFinite(value[key])))
|
|
89
101
|
invalid(`${path}.${key}`, "must be a finite number.");
|
|
90
102
|
}
|
|
91
103
|
if (value.pattern !== undefined) {
|
|
@@ -95,7 +107,8 @@ function assertSchema(value, path) {
|
|
|
95
107
|
}
|
|
96
108
|
if (value.enum !== undefined && (!Array.isArray(value.enum) || value.enum.length === 0))
|
|
97
109
|
invalid(`${path}.enum`, "must be a non-empty array.");
|
|
98
|
-
if (Array.isArray(value.enum) &&
|
|
110
|
+
if (Array.isArray(value.enum) &&
|
|
111
|
+
value.enum.some((item, index, entries) => entries.slice(0, index).some((other) => jsonEqual(item, other))))
|
|
99
112
|
invalid(`${path}.enum`, "must contain unique values.");
|
|
100
113
|
if (value.properties !== undefined) {
|
|
101
114
|
if (value.type !== "object" || !isRecord(value.properties))
|
|
@@ -104,7 +117,10 @@ function assertSchema(value, path) {
|
|
|
104
117
|
assertSchema(property, `${path}.properties.${name}`);
|
|
105
118
|
}
|
|
106
119
|
if (value.required !== undefined) {
|
|
107
|
-
if (value.type !== "object" ||
|
|
120
|
+
if (value.type !== "object" ||
|
|
121
|
+
!Array.isArray(value.required) ||
|
|
122
|
+
value.required.some((name) => typeof name !== "string") ||
|
|
123
|
+
new Set(value.required).size !== value.required.length)
|
|
108
124
|
invalid(`${path}.required`, "must contain unique property names.");
|
|
109
125
|
const properties = value.properties;
|
|
110
126
|
if (value.required.some((name) => !properties || !Object.hasOwn(properties, name)))
|
|
@@ -118,7 +134,7 @@ function assertSchema(value, path) {
|
|
|
118
134
|
}
|
|
119
135
|
export function assertAgentToolInputSchema(value, path = "inputSchema") {
|
|
120
136
|
if (!isRecord(value) || value.type !== "object")
|
|
121
|
-
invalid(path,
|
|
137
|
+
invalid(path, "root type must be object.");
|
|
122
138
|
if (checkedSchemas.has(value))
|
|
123
139
|
return;
|
|
124
140
|
const frozen = assertJson(value, path);
|
|
@@ -130,21 +146,29 @@ function jsonEqual(left, right) {
|
|
|
130
146
|
if (left === right)
|
|
131
147
|
return true;
|
|
132
148
|
if (Array.isArray(left) && Array.isArray(right))
|
|
133
|
-
return left.length === right.length && left.every((item, index) => jsonEqual(item, right[index]));
|
|
149
|
+
return (left.length === right.length && left.every((item, index) => jsonEqual(item, right[index])));
|
|
134
150
|
if (!isRecord(left) || !isRecord(right))
|
|
135
151
|
return false;
|
|
136
152
|
const keys = Object.keys(left);
|
|
137
|
-
return keys.length === Object.keys(right).length &&
|
|
153
|
+
return (keys.length === Object.keys(right).length &&
|
|
154
|
+
keys.every((key) => Object.hasOwn(right, key) && jsonEqual(left[key], right[key])));
|
|
138
155
|
}
|
|
139
156
|
function typeMatches(schema, value) {
|
|
140
157
|
switch (schema.type) {
|
|
141
|
-
case undefined:
|
|
142
|
-
|
|
143
|
-
case "
|
|
144
|
-
|
|
145
|
-
case "
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
case undefined:
|
|
159
|
+
return true;
|
|
160
|
+
case "null":
|
|
161
|
+
return value === null;
|
|
162
|
+
case "array":
|
|
163
|
+
return Array.isArray(value);
|
|
164
|
+
case "object":
|
|
165
|
+
return isRecord(value);
|
|
166
|
+
case "integer":
|
|
167
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
168
|
+
case "number":
|
|
169
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
170
|
+
default:
|
|
171
|
+
return typeof value === schema.type;
|
|
148
172
|
}
|
|
149
173
|
}
|
|
150
174
|
export function validateAgentToolInput(schema, value, path = "input") {
|
|
@@ -177,7 +201,8 @@ export function validateAgentToolInput(schema, value, path = "input") {
|
|
|
177
201
|
errors.push(`${path} must have at least ${schema.minItems} items.`);
|
|
178
202
|
if (schema.maxItems !== undefined && value.length > schema.maxItems)
|
|
179
203
|
errors.push(`${path} must have at most ${schema.maxItems} items.`);
|
|
180
|
-
if (schema.uniqueItems &&
|
|
204
|
+
if (schema.uniqueItems &&
|
|
205
|
+
value.some((item, index) => value.slice(0, index).some((other) => jsonEqual(item, other))))
|
|
181
206
|
errors.push(`${path} must contain unique items.`);
|
|
182
207
|
if (schema.items)
|
|
183
208
|
value.forEach((item, index) => errors.push(...validateAgentToolInput(schema.items, item, `${path}[${index}]`)));
|
|
@@ -188,7 +213,9 @@ export function validateAgentToolInput(schema, value, path = "input") {
|
|
|
188
213
|
errors.push(`${path}.${key} is required.`);
|
|
189
214
|
}
|
|
190
215
|
for (const [key, item] of Object.entries(value)) {
|
|
191
|
-
const property = schema.properties && Object.hasOwn(schema.properties, key)
|
|
216
|
+
const property = schema.properties && Object.hasOwn(schema.properties, key)
|
|
217
|
+
? schema.properties[key]
|
|
218
|
+
: undefined;
|
|
192
219
|
if (property)
|
|
193
220
|
errors.push(...validateAgentToolInput(property, item, `${path}.${key}`));
|
|
194
221
|
else if (schema.additionalProperties === false)
|
package/dist/tool-catalog.js
CHANGED
|
@@ -15,9 +15,7 @@ function invalidResponse(message, details) {
|
|
|
15
15
|
throw new CatalogValidationError("INVALID_TOOL_CATALOG", message, details);
|
|
16
16
|
}
|
|
17
17
|
function assertJsonCompatible(value, path, seen = new WeakSet()) {
|
|
18
|
-
if (value === null
|
|
19
|
-
|| typeof value === "string"
|
|
20
|
-
|| typeof value === "boolean")
|
|
18
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
21
19
|
return;
|
|
22
20
|
if (typeof value === "number") {
|
|
23
21
|
if (!Number.isFinite(value))
|
|
@@ -54,10 +52,7 @@ function canonicalize(value) {
|
|
|
54
52
|
return value;
|
|
55
53
|
return Object.fromEntries(Object.keys(value)
|
|
56
54
|
.sort()
|
|
57
|
-
.map((key) => [
|
|
58
|
-
key,
|
|
59
|
-
canonicalize(value[key]),
|
|
60
|
-
]));
|
|
55
|
+
.map((key) => [key, canonicalize(value[key])]));
|
|
61
56
|
}
|
|
62
57
|
function deepFreeze(value, seen = new WeakSet()) {
|
|
63
58
|
if (!value || typeof value !== "object" || seen.has(value))
|
|
@@ -72,8 +67,10 @@ function sameDefinition(left, right) {
|
|
|
72
67
|
}
|
|
73
68
|
const BUSINESS_TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
74
69
|
const RESERVED_BUSINESS_TOOL_NAMES = new Set([
|
|
75
|
-
"nxf_beginOperation",
|
|
76
|
-
"
|
|
70
|
+
"nxf_beginOperation",
|
|
71
|
+
"nxf_endOperation",
|
|
72
|
+
"nxf_applyAction",
|
|
73
|
+
"nxf_delegatePageTask",
|
|
77
74
|
"nxf_callAppTool",
|
|
78
75
|
"nxf_finish",
|
|
79
76
|
"nxf_getSnapshot",
|
|
@@ -81,8 +78,8 @@ const RESERVED_BUSINESS_TOOL_NAMES = new Set([
|
|
|
81
78
|
"nxf_loadSkill",
|
|
82
79
|
]);
|
|
83
80
|
function assertBusinessToolDefinition(definition) {
|
|
84
|
-
if (!BUSINESS_TOOL_NAME_PATTERN.test(definition.name)
|
|
85
|
-
|
|
81
|
+
if (!BUSINESS_TOOL_NAME_PATTERN.test(definition.name) ||
|
|
82
|
+
RESERVED_BUSINESS_TOOL_NAMES.has(definition.name)) {
|
|
86
83
|
invalidResponse(`Tool catalog returned an invalid tool name "${definition.name}".`);
|
|
87
84
|
}
|
|
88
85
|
if (typeof definition.description !== "string" || !definition.description.trim()) {
|
|
@@ -101,8 +98,7 @@ export function validateToolCatalog(value) {
|
|
|
101
98
|
invalidResponse(`Tool definition at index ${index} must be an object.`);
|
|
102
99
|
}
|
|
103
100
|
const candidate = definition;
|
|
104
|
-
const unexpectedKeys = Object.keys(candidate)
|
|
105
|
-
.filter((key) => !["name", "description", "inputSchema", "effect"].includes(key));
|
|
101
|
+
const unexpectedKeys = Object.keys(candidate).filter((key) => !["name", "description", "inputSchema", "effect"].includes(key));
|
|
106
102
|
if (unexpectedKeys.length > 0) {
|
|
107
103
|
invalidResponse(`Tool definition at index ${index} has unexpected fields: ${unexpectedKeys.join(", ")}.`);
|
|
108
104
|
}
|
|
@@ -143,8 +139,7 @@ export function validateToolCatalog(value) {
|
|
|
143
139
|
}
|
|
144
140
|
for (const definition of value)
|
|
145
141
|
deepFreeze(definition);
|
|
146
|
-
return [...value]
|
|
147
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
142
|
+
return [...value].sort((left, right) => left.name.localeCompare(right.name));
|
|
148
143
|
}
|
|
149
144
|
export function validateContextCatalog(value) {
|
|
150
145
|
if (!Array.isArray(value)) {
|
|
@@ -156,26 +151,30 @@ export function validateContextCatalog(value) {
|
|
|
156
151
|
invalidResponse(`Context at index ${index} must be an object.`);
|
|
157
152
|
}
|
|
158
153
|
const candidate = entry;
|
|
159
|
-
const unexpectedKeys = Object.keys(candidate)
|
|
160
|
-
.filter((key) => !["name", "description", "value"].includes(key));
|
|
154
|
+
const unexpectedKeys = Object.keys(candidate).filter((key) => !["name", "description", "value"].includes(key));
|
|
161
155
|
if (unexpectedKeys.length > 0) {
|
|
162
156
|
invalidResponse(`Context at index ${index} has unexpected fields: ${unexpectedKeys.join(", ")}.`);
|
|
163
157
|
}
|
|
164
|
-
if (!/^[A-Za-z0-9_-]{1,64}$/.test(candidate.name ?? "")
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(candidate.name ?? "") ||
|
|
159
|
+
[
|
|
160
|
+
"nxf_callAppTool",
|
|
161
|
+
"nxf_finish",
|
|
162
|
+
"nxf_getSnapshot",
|
|
163
|
+
"nxf_executeAction",
|
|
164
|
+
"nxf_applyAction",
|
|
165
|
+
"nxf_delegatePageTask",
|
|
166
|
+
].includes(candidate.name)) {
|
|
167
167
|
invalidResponse(`Context at index ${index} has an invalid name.`);
|
|
168
168
|
}
|
|
169
169
|
if (names.has(candidate.name)) {
|
|
170
170
|
invalidResponse(`Tool catalog returned duplicate context name "${candidate.name}".`);
|
|
171
171
|
}
|
|
172
172
|
names.add(candidate.name);
|
|
173
|
-
if (candidate.description !== undefined
|
|
174
|
-
|
|
173
|
+
if (candidate.description !== undefined &&
|
|
174
|
+
(typeof candidate.description !== "string" || !candidate.description.trim())) {
|
|
175
175
|
invalidResponse(`Context "${candidate.name}" must have a non-empty description when provided.`);
|
|
176
176
|
}
|
|
177
177
|
assertJsonCompatible(candidate.value, `contexts[${index}].value`);
|
|
178
178
|
}
|
|
179
|
-
return [...value]
|
|
180
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
179
|
+
return [...value].sort((left, right) => left.name.localeCompare(right.name));
|
|
181
180
|
}
|
package/dist/tool-result.js
CHANGED
|
@@ -3,9 +3,14 @@ export function getToolResultError(content) {
|
|
|
3
3
|
const error = JSON.parse(content)?.error;
|
|
4
4
|
if (typeof error === "string")
|
|
5
5
|
return error;
|
|
6
|
-
if (error &&
|
|
6
|
+
if (error &&
|
|
7
|
+
typeof error === "object" &&
|
|
8
|
+
"message" in error &&
|
|
9
|
+
typeof error.message === "string")
|
|
7
10
|
return error.message;
|
|
8
11
|
}
|
|
9
|
-
catch {
|
|
12
|
+
catch {
|
|
13
|
+
/* Tool results may contain plain text. */
|
|
14
|
+
}
|
|
10
15
|
return undefined;
|
|
11
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexface/agent",
|
|
3
|
-
"version": "0.1.13-alpha.
|
|
3
|
+
"version": "0.1.13-alpha.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.19.0"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@ag-ui/core": "0.0.59"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@nexface/tools": "0.1.13-alpha.
|
|
46
|
+
"@nexface/tools": "0.1.13-alpha.5"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
|
49
49
|
"type": "git",
|