@nexface/tools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/controller.d.ts +30 -0
- package/dist/browser/controller.js +783 -0
- package/dist/browser/controller.js.map +1 -0
- package/dist/browser/dom.d.ts +43 -0
- package/dist/browser/dom.js +438 -0
- package/dist/browser/dom.js.map +1 -0
- package/dist/browser/feedback.d.ts +4 -0
- package/dist/browser/feedback.js +5 -0
- package/dist/browser/feedback.js.map +1 -0
- package/dist/browser/locator.d.ts +19 -0
- package/dist/browser/locator.js +277 -0
- package/dist/browser/locator.js.map +1 -0
- package/dist/browser/prompt.d.ts +5 -0
- package/dist/browser/prompt.generated.d.ts +1 -0
- package/dist/browser/prompt.generated.js +50 -0
- package/dist/browser/prompt.generated.js.map +1 -0
- package/dist/browser/prompt.js +9 -0
- package/dist/browser/prompt.js.map +1 -0
- package/dist/browser/snapshot.d.ts +7 -0
- package/dist/browser/snapshot.js +424 -0
- package/dist/browser/snapshot.js.map +1 -0
- package/dist/browser/toolset.d.ts +35 -0
- package/dist/browser/toolset.js +383 -0
- package/dist/browser/toolset.js.map +1 -0
- package/dist/browser/types.d.ts +129 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/browser/worker-runner.d.ts +11 -0
- package/dist/browser/worker-runner.js +400 -0
- package/dist/browser/worker-runner.js.map +1 -0
- package/dist/definition.d.ts +16 -0
- package/dist/definition.js +72 -0
- package/dist/definition.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal-react.d.ts +12 -0
- package/dist/internal-react.js +23 -0
- package/dist/internal-react.js.map +1 -0
- package/dist/internal-types.d.ts +11 -0
- package/dist/internal-types.js +2 -0
- package/dist/internal-types.js.map +1 -0
- package/dist/internal.d.ts +9 -0
- package/dist/internal.js +5 -0
- package/dist/internal.js.map +1 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.js +258 -0
- package/dist/react.js.map +1 -0
- package/dist/runtime.d.ts +93 -0
- package/dist/runtime.js +492 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +46 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/webmcp.d.ts +2 -0
- package/dist/webmcp.js +73 -0
- package/dist/webmcp.js.map +1 -0
- package/package.json +26 -0
- package/src/browser/controller.ts +984 -0
- package/src/browser/dom.ts +507 -0
- package/src/browser/feedback.ts +5 -0
- package/src/browser/locator.ts +383 -0
- package/src/browser/prompt.generated.ts +50 -0
- package/src/browser/prompt.ts +10 -0
- package/src/browser/snapshot.ts +542 -0
- package/src/browser/toolset.ts +522 -0
- package/src/browser/types.ts +172 -0
- package/src/browser/worker-runner.ts +468 -0
- package/src/definition.ts +115 -0
- package/src/index.ts +35 -0
- package/src/internal-react.tsx +62 -0
- package/src/internal-types.ts +12 -0
- package/src/internal.ts +36 -0
- package/src/react.tsx +397 -0
- package/src/runtime.ts +722 -0
- package/src/schema.ts +52 -0
- package/src/types.ts +123 -0
- package/src/webmcp.ts +102 -0
- package/tsconfig.json +8 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import { isValidAgentContextName } from "./definition.js";
|
|
2
|
+
import { validateAgentToolInput } from "./schema.js";
|
|
3
|
+
import type {
|
|
4
|
+
AgentContextEntry,
|
|
5
|
+
AgentContextInput,
|
|
6
|
+
AgentContextRegistration,
|
|
7
|
+
AgentToolDefinition,
|
|
8
|
+
AgentToolExecutor,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
import type { AgentToolExecutionEvent } from "./internal-types.js";
|
|
11
|
+
import {
|
|
12
|
+
createBrowserToolset,
|
|
13
|
+
type BrowserToolsConfig,
|
|
14
|
+
type BrowserToolset,
|
|
15
|
+
} from "./browser/toolset.js";
|
|
16
|
+
|
|
17
|
+
export interface ToolRuntimeOptions {
|
|
18
|
+
readonly browserTools?: BrowserToolsConfig;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CONTEXT_ERROR_CODES = [
|
|
22
|
+
"CONTEXT_DISPOSED",
|
|
23
|
+
"CONTEXT_NAME_MISMATCH",
|
|
24
|
+
"DUPLICATE_CONTEXT",
|
|
25
|
+
"INVALID_CONTEXT",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
const TOOL_ERROR_CODES = [
|
|
29
|
+
"ACTION_IN_FLIGHT",
|
|
30
|
+
"ACTION_NOT_HANDLED",
|
|
31
|
+
"ACTION_TARGET_UNAVAILABLE",
|
|
32
|
+
"BROWSER_EXECUTION_FAILED",
|
|
33
|
+
"BROWSER_SNAPSHOT_REQUIRED",
|
|
34
|
+
"BROWSER_TOOLS_UNAVAILABLE",
|
|
35
|
+
"CONTRACT_MISMATCH",
|
|
36
|
+
"DUPLICATE_BINDING",
|
|
37
|
+
"INVALID_INPUT",
|
|
38
|
+
"INVALID_TOOL_RUNTIME",
|
|
39
|
+
"TOOL_CANCELLED",
|
|
40
|
+
"TOOL_REGISTRATION_TIMEOUT",
|
|
41
|
+
"TOOL_SKIPPED",
|
|
42
|
+
"TOOL_UNAVAILABLE",
|
|
43
|
+
"UNKNOWN_TOOL",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
type AgentContextErrorCode = typeof CONTEXT_ERROR_CODES[number];
|
|
47
|
+
type AgentToolErrorCode = typeof TOOL_ERROR_CODES[number];
|
|
48
|
+
|
|
49
|
+
const CONTEXT_ERROR_CODE_SET = new Set<string>(CONTEXT_ERROR_CODES);
|
|
50
|
+
const TOOL_ERROR_CODE_SET = new Set<string>(TOOL_ERROR_CODES);
|
|
51
|
+
|
|
52
|
+
export function isToolRuntimeError(error: unknown): error is {
|
|
53
|
+
readonly type: "tool_runtime_error";
|
|
54
|
+
readonly domain: "tool" | "context";
|
|
55
|
+
readonly code: string;
|
|
56
|
+
readonly message: string;
|
|
57
|
+
readonly details?: unknown;
|
|
58
|
+
} {
|
|
59
|
+
if (!error || typeof error !== "object") return false;
|
|
60
|
+
const candidate = error as Record<string, unknown>;
|
|
61
|
+
if (
|
|
62
|
+
candidate.type !== "tool_runtime_error"
|
|
63
|
+
|| typeof candidate.message !== "string"
|
|
64
|
+
|| typeof candidate.code !== "string"
|
|
65
|
+
) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (candidate.domain === "tool") {
|
|
69
|
+
return TOOL_ERROR_CODE_SET.has(candidate.code);
|
|
70
|
+
}
|
|
71
|
+
if (candidate.domain === "context") {
|
|
72
|
+
return CONTEXT_ERROR_CODE_SET.has(candidate.code);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare const toolRuntimeBrand: unique symbol;
|
|
78
|
+
|
|
79
|
+
export interface ToolRuntime {
|
|
80
|
+
readonly [toolRuntimeBrand]: true;
|
|
81
|
+
|
|
82
|
+
getTools(): readonly AgentToolDefinition[];
|
|
83
|
+
getContexts(options?: {
|
|
84
|
+
signal?: AbortSignal;
|
|
85
|
+
}): Promise<readonly AgentContextEntry[]>;
|
|
86
|
+
execute<TInput, TResult>(
|
|
87
|
+
definition: AgentToolDefinition<TInput, TResult>,
|
|
88
|
+
input: TInput,
|
|
89
|
+
options?: { signal?: AbortSignal },
|
|
90
|
+
): Promise<TResult>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type RuntimeBinding = {
|
|
94
|
+
definition: AgentToolDefinition<any, any>;
|
|
95
|
+
execute: AgentToolExecutor<any, any>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function copyBrowserToolsConfig(
|
|
99
|
+
config: BrowserToolsConfig | undefined,
|
|
100
|
+
): BrowserToolsConfig {
|
|
101
|
+
if (config === undefined) return false;
|
|
102
|
+
if (typeof config === "boolean") return config;
|
|
103
|
+
return {
|
|
104
|
+
...config,
|
|
105
|
+
...(config.limits ? { limits: { ...config.limits } } : {}),
|
|
106
|
+
...(config.snapshotLimits
|
|
107
|
+
? { snapshotLimits: { ...config.snapshotLimits } }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function compareStrings(left: string, right: string): number {
|
|
113
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function assertContextName(name: string): void {
|
|
117
|
+
if (!isValidAgentContextName(name)) {
|
|
118
|
+
throw new AgentContextError(
|
|
119
|
+
"INVALID_CONTEXT",
|
|
120
|
+
`Invalid or reserved context name: ${name}.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeContextValue(
|
|
126
|
+
value: unknown,
|
|
127
|
+
path = "value",
|
|
128
|
+
seen = new WeakSet<object>(),
|
|
129
|
+
): AgentContextEntry["value"] {
|
|
130
|
+
if (
|
|
131
|
+
value === null
|
|
132
|
+
|| typeof value === "string"
|
|
133
|
+
|| typeof value === "boolean"
|
|
134
|
+
) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "number") {
|
|
138
|
+
if (!Number.isFinite(value)) {
|
|
139
|
+
throw new AgentContextError(
|
|
140
|
+
"INVALID_CONTEXT",
|
|
141
|
+
`${path} must contain only finite numbers.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
if (!value || typeof value !== "object") {
|
|
147
|
+
throw new AgentContextError(
|
|
148
|
+
"INVALID_CONTEXT",
|
|
149
|
+
`${path} must be JSON serializable.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (seen.has(value)) {
|
|
153
|
+
throw new AgentContextError(
|
|
154
|
+
"INVALID_CONTEXT",
|
|
155
|
+
`${path} contains a circular reference.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
seen.add(value);
|
|
159
|
+
try {
|
|
160
|
+
if (Array.isArray(value)) {
|
|
161
|
+
return value.map((item, index) =>
|
|
162
|
+
normalizeContextValue(item, `${path}[${index}]`, seen));
|
|
163
|
+
}
|
|
164
|
+
const prototype = Object.getPrototypeOf(value);
|
|
165
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
166
|
+
throw new AgentContextError(
|
|
167
|
+
"INVALID_CONTEXT",
|
|
168
|
+
`${path} must contain only plain objects.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return Object.fromEntries(
|
|
172
|
+
Object.keys(value)
|
|
173
|
+
.sort(compareStrings)
|
|
174
|
+
.map((key) => [
|
|
175
|
+
key,
|
|
176
|
+
normalizeContextValue(
|
|
177
|
+
(value as Record<string, unknown>)[key],
|
|
178
|
+
`${path}.${key}`,
|
|
179
|
+
seen,
|
|
180
|
+
),
|
|
181
|
+
]),
|
|
182
|
+
);
|
|
183
|
+
} finally {
|
|
184
|
+
seen.delete(value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeContext(context: AgentContextInput): AgentContextEntry {
|
|
189
|
+
assertContextName(context.name);
|
|
190
|
+
if (!context.description.trim()) {
|
|
191
|
+
throw new AgentContextError(
|
|
192
|
+
"INVALID_CONTEXT",
|
|
193
|
+
"Context description is required.",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
name: context.name,
|
|
198
|
+
description: context.description,
|
|
199
|
+
value: normalizeContextValue(context.value),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class DetailEvent<T> extends Event {
|
|
204
|
+
constructor(type: string, readonly detail: T) {
|
|
205
|
+
super(type);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export class AgentToolError extends Error {
|
|
210
|
+
readonly type = "tool_runtime_error" as const;
|
|
211
|
+
readonly domain = "tool" as const;
|
|
212
|
+
|
|
213
|
+
constructor(
|
|
214
|
+
readonly code: AgentToolErrorCode,
|
|
215
|
+
message: string,
|
|
216
|
+
readonly details?: unknown,
|
|
217
|
+
) {
|
|
218
|
+
super(message);
|
|
219
|
+
this.name = "ToolRuntimeError";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export class AgentContextError extends Error {
|
|
224
|
+
readonly type = "tool_runtime_error" as const;
|
|
225
|
+
readonly domain = "context" as const;
|
|
226
|
+
|
|
227
|
+
constructor(
|
|
228
|
+
readonly code: AgentContextErrorCode,
|
|
229
|
+
message: string,
|
|
230
|
+
) {
|
|
231
|
+
super(message);
|
|
232
|
+
this.name = "ToolRuntimeError";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface InternalToolRuntime extends ToolRuntime, EventTarget {
|
|
237
|
+
register<TInput, TResult>(
|
|
238
|
+
definition: AgentToolDefinition<TInput, TResult>,
|
|
239
|
+
execute: AgentToolExecutor<TInput, TResult>,
|
|
240
|
+
options?: { signal?: AbortSignal },
|
|
241
|
+
): () => void;
|
|
242
|
+
registerContext(
|
|
243
|
+
input: AgentContextInput,
|
|
244
|
+
options?: { signal?: AbortSignal },
|
|
245
|
+
): AgentContextRegistration;
|
|
246
|
+
getApplicationTools(): readonly AgentToolDefinition[];
|
|
247
|
+
isRegistered(name: string): boolean;
|
|
248
|
+
executeApplicationTool(
|
|
249
|
+
name: string,
|
|
250
|
+
input: unknown,
|
|
251
|
+
options?: {
|
|
252
|
+
signal?: AbortSignal;
|
|
253
|
+
expectedDefinition?: AgentToolDefinition;
|
|
254
|
+
},
|
|
255
|
+
): Promise<unknown>;
|
|
256
|
+
waitForTool(
|
|
257
|
+
definition: AgentToolDefinition<any, any>,
|
|
258
|
+
options?: { signal?: AbortSignal; timeoutMs?: number },
|
|
259
|
+
): Promise<void>;
|
|
260
|
+
getContextRevision(): number;
|
|
261
|
+
getContextsNow(): readonly AgentContextEntry[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const runtimeInternals = new WeakMap<
|
|
265
|
+
ToolRuntime,
|
|
266
|
+
ToolRuntimeImplementation
|
|
267
|
+
>();
|
|
268
|
+
|
|
269
|
+
export function createToolRuntime(
|
|
270
|
+
options: ToolRuntimeOptions = {},
|
|
271
|
+
): ToolRuntime {
|
|
272
|
+
return new ToolRuntimeImplementation(options);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function getInternalToolRuntime(
|
|
276
|
+
runtime: ToolRuntime,
|
|
277
|
+
): InternalToolRuntime {
|
|
278
|
+
const internal = runtimeInternals.get(runtime);
|
|
279
|
+
if (!internal) {
|
|
280
|
+
throw new AgentToolError(
|
|
281
|
+
"INVALID_TOOL_RUNTIME",
|
|
282
|
+
"ToolRuntime must be created by createToolRuntime().",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return internal;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export class ToolRuntimeImplementation
|
|
289
|
+
extends EventTarget
|
|
290
|
+
implements ToolRuntime, InternalToolRuntime
|
|
291
|
+
{
|
|
292
|
+
declare readonly [toolRuntimeBrand]: true;
|
|
293
|
+
|
|
294
|
+
readonly #browserToolsConfig: BrowserToolsConfig;
|
|
295
|
+
readonly #bindings = new Map<string, RuntimeBinding>();
|
|
296
|
+
readonly #contexts = new Map<string, AgentContextEntry>();
|
|
297
|
+
#contextRevision = 0;
|
|
298
|
+
#browserToolset: BrowserToolset | undefined;
|
|
299
|
+
#executionTail: Promise<void> | undefined;
|
|
300
|
+
|
|
301
|
+
constructor(options: ToolRuntimeOptions = {}) {
|
|
302
|
+
super();
|
|
303
|
+
this.#browserToolsConfig = copyBrowserToolsConfig(options.browserTools);
|
|
304
|
+
runtimeInternals.set(this, this);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
register<TInput, TResult>(
|
|
308
|
+
definition: AgentToolDefinition<TInput, TResult>,
|
|
309
|
+
execute: AgentToolExecutor<TInput, TResult>,
|
|
310
|
+
options: { signal?: AbortSignal } = {},
|
|
311
|
+
): () => void {
|
|
312
|
+
if (this.#bindings.has(definition.name)) {
|
|
313
|
+
throw new AgentToolError(
|
|
314
|
+
"DUPLICATE_BINDING",
|
|
315
|
+
`Tool "${definition.name}" already has an active binding.`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const binding: RuntimeBinding = { definition, execute };
|
|
319
|
+
this.#bindings.set(definition.name, binding);
|
|
320
|
+
this.dispatchEvent(
|
|
321
|
+
new DetailEvent("toolchange", {
|
|
322
|
+
name: definition.name,
|
|
323
|
+
registered: true,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
let disposed = false;
|
|
328
|
+
const dispose = () => {
|
|
329
|
+
if (disposed || this.#bindings.get(definition.name) !== binding) return;
|
|
330
|
+
disposed = true;
|
|
331
|
+
options.signal?.removeEventListener("abort", dispose);
|
|
332
|
+
this.#bindings.delete(definition.name);
|
|
333
|
+
this.dispatchEvent(
|
|
334
|
+
new DetailEvent("toolchange", {
|
|
335
|
+
name: definition.name,
|
|
336
|
+
registered: false,
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
if (options.signal?.aborted) dispose();
|
|
341
|
+
else options.signal?.addEventListener("abort", dispose, { once: true });
|
|
342
|
+
return dispose;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
registerContext(
|
|
346
|
+
input: AgentContextInput,
|
|
347
|
+
options: { signal?: AbortSignal } = {},
|
|
348
|
+
): AgentContextRegistration {
|
|
349
|
+
const initial = normalizeContext(input);
|
|
350
|
+
if (this.#contexts.has(initial.name)) {
|
|
351
|
+
throw new AgentContextError(
|
|
352
|
+
"DUPLICATE_CONTEXT",
|
|
353
|
+
`Context "${initial.name}" already has an active registration.`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
this.#contexts.set(initial.name, initial);
|
|
357
|
+
this.#notifyContextChange(initial.name, "registered");
|
|
358
|
+
|
|
359
|
+
let disposed = false;
|
|
360
|
+
const dispose = () => {
|
|
361
|
+
if (disposed) return;
|
|
362
|
+
disposed = true;
|
|
363
|
+
options.signal?.removeEventListener("abort", dispose);
|
|
364
|
+
if (this.#contexts.delete(initial.name)) {
|
|
365
|
+
this.#notifyContextChange(initial.name, "disposed");
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const update = (nextInput: AgentContextInput) => {
|
|
369
|
+
if (disposed) {
|
|
370
|
+
throw new AgentContextError(
|
|
371
|
+
"CONTEXT_DISPOSED",
|
|
372
|
+
`Context "${initial.name}" registration has been disposed.`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const next = normalizeContext(nextInput);
|
|
376
|
+
if (next.name !== initial.name) {
|
|
377
|
+
throw new AgentContextError(
|
|
378
|
+
"CONTEXT_NAME_MISMATCH",
|
|
379
|
+
`Context registration "${initial.name}" cannot be updated to "${next.name}".`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (JSON.stringify(this.#contexts.get(initial.name)) === JSON.stringify(next)) return;
|
|
383
|
+
this.#contexts.set(initial.name, next);
|
|
384
|
+
this.#notifyContextChange(initial.name, "updated");
|
|
385
|
+
};
|
|
386
|
+
if (options.signal?.aborted) dispose();
|
|
387
|
+
else options.signal?.addEventListener("abort", dispose, { once: true });
|
|
388
|
+
return { update, dispose };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
getContextsNow(): readonly AgentContextEntry[] {
|
|
392
|
+
return [...this.#contexts.values()]
|
|
393
|
+
.sort((left, right) => compareStrings(left.name, right.name))
|
|
394
|
+
.map(normalizeContext);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
getContextRevision(): number {
|
|
398
|
+
return this.#contextRevision;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async getContexts(
|
|
402
|
+
options: { signal?: AbortSignal } = {},
|
|
403
|
+
): Promise<readonly AgentContextEntry[]> {
|
|
404
|
+
options.signal?.throwIfAborted();
|
|
405
|
+
await Promise.resolve();
|
|
406
|
+
await new Promise<void>((resolve, reject) => {
|
|
407
|
+
let settled = false;
|
|
408
|
+
let frameId: number | undefined;
|
|
409
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
410
|
+
const cleanup = () => {
|
|
411
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
412
|
+
if (frameId !== undefined && typeof cancelAnimationFrame === "function") {
|
|
413
|
+
cancelAnimationFrame(frameId);
|
|
414
|
+
}
|
|
415
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
416
|
+
};
|
|
417
|
+
const settle = () => {
|
|
418
|
+
if (settled) return;
|
|
419
|
+
settled = true;
|
|
420
|
+
cleanup();
|
|
421
|
+
resolve();
|
|
422
|
+
};
|
|
423
|
+
const onAbort = () => {
|
|
424
|
+
if (settled) return;
|
|
425
|
+
settled = true;
|
|
426
|
+
cleanup();
|
|
427
|
+
reject(options.signal?.reason);
|
|
428
|
+
};
|
|
429
|
+
const afterTask = () => {
|
|
430
|
+
timeoutId = undefined;
|
|
431
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
432
|
+
settle();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
frameId = requestAnimationFrame(settle);
|
|
436
|
+
timeoutId = setTimeout(settle, 50);
|
|
437
|
+
};
|
|
438
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
439
|
+
if (options.signal?.aborted) {
|
|
440
|
+
onAbort();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
timeoutId = setTimeout(afterTask, 0);
|
|
444
|
+
});
|
|
445
|
+
await Promise.resolve();
|
|
446
|
+
options.signal?.throwIfAborted();
|
|
447
|
+
return this.getContextsNow();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#notifyContextChange(
|
|
451
|
+
name: string,
|
|
452
|
+
change: "registered" | "updated" | "disposed",
|
|
453
|
+
): void {
|
|
454
|
+
this.#contextRevision += 1;
|
|
455
|
+
this.dispatchEvent(new DetailEvent("contextchange", {
|
|
456
|
+
name,
|
|
457
|
+
change,
|
|
458
|
+
revision: this.#contextRevision,
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
getApplicationTools(): readonly AgentToolDefinition[] {
|
|
463
|
+
return [...this.#bindings.values()]
|
|
464
|
+
.map(({ definition }) => definition)
|
|
465
|
+
.sort((left, right) => compareStrings(left.name, right.name));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
isRegistered(name: string): boolean {
|
|
469
|
+
return this.#bindings.has(name);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
getTools(): readonly AgentToolDefinition[] {
|
|
473
|
+
const browserTools = this.#getBrowserToolset()?.definitions ?? [];
|
|
474
|
+
return [...this.getApplicationTools(), ...browserTools]
|
|
475
|
+
.sort((left, right) => compareStrings(left.name, right.name));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
waitForTool(
|
|
479
|
+
definition: AgentToolDefinition<any, any>,
|
|
480
|
+
options: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
481
|
+
): Promise<void> {
|
|
482
|
+
const isTargetRegistered = () => this.#bindings.has(definition.name);
|
|
483
|
+
if (isTargetRegistered()) return Promise.resolve();
|
|
484
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? 5_000);
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
let settled = false;
|
|
487
|
+
const cleanup = () => {
|
|
488
|
+
this.removeEventListener("toolchange", onToolChange);
|
|
489
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
490
|
+
clearTimeout(timeoutId);
|
|
491
|
+
};
|
|
492
|
+
const settle = (callback: () => void) => {
|
|
493
|
+
if (settled) return;
|
|
494
|
+
settled = true;
|
|
495
|
+
cleanup();
|
|
496
|
+
callback();
|
|
497
|
+
};
|
|
498
|
+
const onToolChange = (event: Event) => {
|
|
499
|
+
const detail = (event as Event & {
|
|
500
|
+
detail?: { name?: string; registered?: boolean };
|
|
501
|
+
}).detail;
|
|
502
|
+
if (detail?.name !== definition.name || !detail.registered) return;
|
|
503
|
+
if (isTargetRegistered()) settle(resolve);
|
|
504
|
+
};
|
|
505
|
+
const onAbort = () => settle(() => reject(options.signal?.reason));
|
|
506
|
+
const timeoutId = setTimeout(() => {
|
|
507
|
+
settle(() =>
|
|
508
|
+
reject(
|
|
509
|
+
new AgentToolError(
|
|
510
|
+
"TOOL_REGISTRATION_TIMEOUT",
|
|
511
|
+
`Tool "${definition.name}" did not register within ${timeoutMs}ms.`,
|
|
512
|
+
{
|
|
513
|
+
toolName: definition.name,
|
|
514
|
+
timeoutMs,
|
|
515
|
+
retryable: false,
|
|
516
|
+
browserFallbackAllowed: true,
|
|
517
|
+
toolExecuted: false,
|
|
518
|
+
},
|
|
519
|
+
),
|
|
520
|
+
));
|
|
521
|
+
}, timeoutMs);
|
|
522
|
+
this.addEventListener("toolchange", onToolChange);
|
|
523
|
+
if (options.signal?.aborted) onAbort();
|
|
524
|
+
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
525
|
+
if (isTargetRegistered()) settle(resolve);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
execute<TInput, TResult>(
|
|
530
|
+
definition: AgentToolDefinition<TInput, TResult>,
|
|
531
|
+
input: TInput,
|
|
532
|
+
options: { signal?: AbortSignal } = {},
|
|
533
|
+
): Promise<TResult> {
|
|
534
|
+
const name = definition.name;
|
|
535
|
+
const binding = this.#bindings.get(name);
|
|
536
|
+
if (binding) {
|
|
537
|
+
if (binding.definition !== definition) {
|
|
538
|
+
return Promise.reject(
|
|
539
|
+
new AgentToolError(
|
|
540
|
+
"CONTRACT_MISMATCH",
|
|
541
|
+
`Tool "${name}" is registered with a different Definition.`,
|
|
542
|
+
),
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
return this.#enqueue(
|
|
546
|
+
() => this.#executeApplication(name, input, options, binding),
|
|
547
|
+
options.signal,
|
|
548
|
+
) as Promise<TResult>;
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const browserToolset = this.#getBrowserToolset();
|
|
552
|
+
const browserHandler = browserToolset?.handlers.get(name);
|
|
553
|
+
if (!browserHandler) {
|
|
554
|
+
return Promise.reject(
|
|
555
|
+
new AgentToolError("UNKNOWN_TOOL", `Unknown tool: ${name}.`),
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (
|
|
559
|
+
!browserToolset?.definitions.some(
|
|
560
|
+
(candidate) => candidate === definition,
|
|
561
|
+
)
|
|
562
|
+
) {
|
|
563
|
+
return Promise.reject(
|
|
564
|
+
new AgentToolError(
|
|
565
|
+
"CONTRACT_MISMATCH",
|
|
566
|
+
`Tool "${name}" is registered with a different Definition.`,
|
|
567
|
+
),
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return this.#enqueue(async () => {
|
|
571
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
572
|
+
signal.throwIfAborted();
|
|
573
|
+
return browserHandler.execute(input as Record<string, unknown>, {
|
|
574
|
+
signal,
|
|
575
|
+
});
|
|
576
|
+
}, options.signal) as Promise<TResult>;
|
|
577
|
+
} catch (error) {
|
|
578
|
+
return Promise.reject(error);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
executeApplicationTool(
|
|
583
|
+
name: string,
|
|
584
|
+
input: unknown,
|
|
585
|
+
options: {
|
|
586
|
+
signal?: AbortSignal;
|
|
587
|
+
expectedDefinition?: AgentToolDefinition;
|
|
588
|
+
} = {},
|
|
589
|
+
): Promise<unknown> {
|
|
590
|
+
const binding = this.#bindings.get(name);
|
|
591
|
+
if (!binding) {
|
|
592
|
+
return Promise.reject(
|
|
593
|
+
new AgentToolError("UNKNOWN_TOOL", `Unknown tool: ${name}.`),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return this.#enqueue(
|
|
597
|
+
() => {
|
|
598
|
+
const activeBinding = this.#bindings.get(name);
|
|
599
|
+
if (!activeBinding) {
|
|
600
|
+
throw new AgentToolError("UNKNOWN_TOOL", `Unknown tool: ${name}.`);
|
|
601
|
+
}
|
|
602
|
+
if (
|
|
603
|
+
options.expectedDefinition
|
|
604
|
+
&& activeBinding.definition !== options.expectedDefinition
|
|
605
|
+
) {
|
|
606
|
+
throw new AgentToolError(
|
|
607
|
+
"CONTRACT_MISMATCH",
|
|
608
|
+
`Tool "${name}" changed after the model request was sent.`,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return this.#executeApplication(name, input, options, activeBinding);
|
|
612
|
+
},
|
|
613
|
+
options.signal,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#enqueue<TResult>(
|
|
618
|
+
execute: () => Promise<TResult>,
|
|
619
|
+
signal?: AbortSignal,
|
|
620
|
+
): Promise<TResult> {
|
|
621
|
+
const previous = this.#executionTail;
|
|
622
|
+
let removeQueuedAbort = () => {};
|
|
623
|
+
const queuedExecution = previous
|
|
624
|
+
? previous.then(() => {
|
|
625
|
+
removeQueuedAbort();
|
|
626
|
+
signal?.throwIfAborted();
|
|
627
|
+
return execute();
|
|
628
|
+
})
|
|
629
|
+
: execute();
|
|
630
|
+
let execution = queuedExecution;
|
|
631
|
+
if (previous && signal) {
|
|
632
|
+
execution = new Promise((resolve, reject) => {
|
|
633
|
+
const onAbort = () => {
|
|
634
|
+
removeQueuedAbort();
|
|
635
|
+
reject(signal.reason);
|
|
636
|
+
};
|
|
637
|
+
removeQueuedAbort = () => signal.removeEventListener("abort", onAbort);
|
|
638
|
+
queuedExecution.then(resolve, reject);
|
|
639
|
+
if (signal.aborted) onAbort();
|
|
640
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const tail = queuedExecution.then(
|
|
644
|
+
() => undefined,
|
|
645
|
+
() => undefined,
|
|
646
|
+
);
|
|
647
|
+
this.#executionTail = tail;
|
|
648
|
+
void tail.then(() => {
|
|
649
|
+
if (this.#executionTail === tail) this.#executionTail = undefined;
|
|
650
|
+
});
|
|
651
|
+
return execution;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async #executeApplication(
|
|
655
|
+
name: string,
|
|
656
|
+
input: unknown,
|
|
657
|
+
options: { signal?: AbortSignal },
|
|
658
|
+
binding: RuntimeBinding,
|
|
659
|
+
): Promise<unknown> {
|
|
660
|
+
const id = globalThis.crypto?.randomUUID?.()
|
|
661
|
+
?? `${Date.now()}-${Math.random()}`;
|
|
662
|
+
const startedAt = Date.now();
|
|
663
|
+
const running: AgentToolExecutionEvent = {
|
|
664
|
+
id,
|
|
665
|
+
name,
|
|
666
|
+
status: "running",
|
|
667
|
+
startedAt,
|
|
668
|
+
input,
|
|
669
|
+
};
|
|
670
|
+
this.dispatchEvent(new DetailEvent("toolexecution", running));
|
|
671
|
+
try {
|
|
672
|
+
if (this.#bindings.get(name) !== binding) {
|
|
673
|
+
throw new AgentToolError(
|
|
674
|
+
"UNKNOWN_TOOL",
|
|
675
|
+
`Tool "${name}" is no longer registered for this call.`,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
const errors = validateAgentToolInput(binding.definition.inputSchema, input);
|
|
679
|
+
if (errors.length) {
|
|
680
|
+
throw new AgentToolError("INVALID_INPUT", errors.join(" "), { errors });
|
|
681
|
+
}
|
|
682
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
683
|
+
signal.throwIfAborted();
|
|
684
|
+
const result = await binding.execute(input, {
|
|
685
|
+
signal,
|
|
686
|
+
waitForTool: (definition, waitOptions = {}) =>
|
|
687
|
+
this.waitForTool(definition, { signal, ...waitOptions }),
|
|
688
|
+
});
|
|
689
|
+
signal.throwIfAborted();
|
|
690
|
+
this.dispatchEvent(
|
|
691
|
+
new DetailEvent("toolexecution", {
|
|
692
|
+
...running,
|
|
693
|
+
status: "success",
|
|
694
|
+
finishedAt: Date.now(),
|
|
695
|
+
result,
|
|
696
|
+
} satisfies AgentToolExecutionEvent),
|
|
697
|
+
);
|
|
698
|
+
return result;
|
|
699
|
+
} catch (error) {
|
|
700
|
+
this.dispatchEvent(
|
|
701
|
+
new DetailEvent("toolexecution", {
|
|
702
|
+
...running,
|
|
703
|
+
status: "error",
|
|
704
|
+
finishedAt: Date.now(),
|
|
705
|
+
error,
|
|
706
|
+
} satisfies AgentToolExecutionEvent),
|
|
707
|
+
);
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
#getBrowserToolset(): BrowserToolset | undefined {
|
|
713
|
+
if (this.#browserToolsConfig === false) return undefined;
|
|
714
|
+
if (!this.#browserToolset) {
|
|
715
|
+
const options = this.#browserToolsConfig === true
|
|
716
|
+
? {}
|
|
717
|
+
: this.#browserToolsConfig;
|
|
718
|
+
this.#browserToolset = createBrowserToolset(options);
|
|
719
|
+
}
|
|
720
|
+
return this.#browserToolset;
|
|
721
|
+
}
|
|
722
|
+
}
|