@browserbridge/bbx 1.7.4 → 1.7.6
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/package.json +6 -6
- package/packages/agent-client/src/atomic-write.js +99 -0
- package/packages/agent-client/src/cli-args.js +152 -0
- package/packages/agent-client/src/cli-batch.js +140 -0
- package/packages/agent-client/src/cli-helpers.js +20 -3
- package/packages/agent-client/src/cli-output.js +57 -0
- package/packages/agent-client/src/cli-proxy-remote.js +350 -0
- package/packages/agent-client/src/cli-setup-commands.js +404 -0
- package/packages/agent-client/src/cli.js +63 -906
- package/packages/agent-client/src/command-registry.js +12 -6
- package/packages/agent-client/src/install.js +39 -4
- package/packages/agent-client/src/mcp-config.js +28 -14
- package/packages/agent-client/src/remotes.js +93 -15
- package/packages/agent-client/src/runtime.js +6 -4
- package/packages/mcp-server/src/bin.js +10 -1
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +8 -5
- package/packages/mcp-server/src/handlers-dom.js +2 -2
- package/packages/mcp-server/src/handlers-navigation.js +20 -9
- package/packages/mcp-server/src/handlers-page.js +111 -111
- package/packages/mcp-server/src/handlers-utils.js +268 -8
- package/packages/mcp-server/src/handlers.js +31 -10
- package/packages/mcp-server/src/lifecycle.js +413 -0
- package/packages/mcp-server/src/server.js +27 -19
- package/packages/native-host/src/auth-token.js +1 -1
- package/packages/native-host/src/config.js +9 -4
- package/packages/native-host/src/daemon-process.js +8 -1
- package/packages/protocol/src/capabilities.js +33 -110
- package/packages/protocol/src/capability-values.js +45 -0
- package/packages/protocol/src/defaults.js +19 -2
- package/packages/protocol/src/protocol.js +106 -5
- package/packages/protocol/src/registry.js +299 -86
- package/packages/protocol/src/types.ts +19 -1
- package/skills/browser-bridge/SKILL.md +1 -1
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_CONSOLE_LIMIT,
|
|
5
5
|
DEFAULT_NETWORK_LIMIT,
|
|
6
|
+
ERROR_CODES,
|
|
7
|
+
getErrorRecovery,
|
|
6
8
|
METHOD_SET,
|
|
7
9
|
} from '../../protocol/src/index.js';
|
|
8
10
|
import { createBridgeClientForDestination } from '../../agent-client/src/remotes.js';
|
|
9
11
|
import {
|
|
10
12
|
annotateBridgeSummary,
|
|
11
13
|
applyLimitBudgetPreset,
|
|
14
|
+
applyMethodBudgetPreset,
|
|
12
15
|
applyPageTextBudgetPreset,
|
|
16
|
+
boundToolValue,
|
|
13
17
|
bridgeMethodNeedsTab,
|
|
14
18
|
callBridgeTool,
|
|
15
19
|
createToolResult,
|
|
@@ -19,9 +23,11 @@ import {
|
|
|
19
23
|
summarizeBatchResponseItem,
|
|
20
24
|
summarizeBridgeResponse,
|
|
21
25
|
summarizeToolError,
|
|
26
|
+
summarizeToolResponse,
|
|
22
27
|
withToolClient,
|
|
23
28
|
REQUEST_SOURCE,
|
|
24
29
|
} from './handlers-utils.js';
|
|
30
|
+
import { createScreenshotResult } from './handlers-capture.js';
|
|
25
31
|
|
|
26
32
|
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
27
33
|
/** @typedef {import('../../agent-client/src/client.js').BridgeClient} BridgeClient */
|
|
@@ -178,92 +184,105 @@ export async function handleBatchTool(args) {
|
|
|
178
184
|
}
|
|
179
185
|
}
|
|
180
186
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
const response = await requestBridgeWithRetry(callClient, method, call.params || {}, {
|
|
213
|
-
tabId,
|
|
214
|
-
source: REQUEST_SOURCE,
|
|
215
|
-
tokenBudget,
|
|
216
|
-
});
|
|
217
|
-
return {
|
|
218
|
-
destinationId,
|
|
219
|
-
...summarizeBatchResponseItem(
|
|
220
|
-
{
|
|
221
|
-
method,
|
|
222
|
-
tabId,
|
|
223
|
-
response,
|
|
224
|
-
durationMs: Date.now() - startTime,
|
|
225
|
-
},
|
|
226
|
-
{ compact: true }
|
|
227
|
-
),
|
|
228
|
-
};
|
|
229
|
-
} catch (error) {
|
|
230
|
-
return summarizeBatchErrorItem(
|
|
231
|
-
{
|
|
232
|
-
method,
|
|
233
|
-
tabId,
|
|
234
|
-
error,
|
|
235
|
-
durationMs: Date.now() - startTime,
|
|
236
|
-
},
|
|
237
|
-
{ compact: true }
|
|
238
|
-
);
|
|
239
|
-
} finally {
|
|
240
|
-
if (destinationId) {
|
|
187
|
+
const results = await mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, async (call) => {
|
|
188
|
+
const method = /** @type {BridgeMethod} */ (call.method);
|
|
189
|
+
const tabId = bridgeMethodNeedsTab(method)
|
|
190
|
+
? typeof call.tabId === 'number'
|
|
191
|
+
? call.tabId
|
|
192
|
+
: null
|
|
193
|
+
: null;
|
|
194
|
+
const tokenBudget = getToolTokenBudget(call);
|
|
195
|
+
const destinationId = typeof call.destinationId === 'string' ? call.destinationId : null;
|
|
196
|
+
const startTime = Date.now();
|
|
197
|
+
/** @type {BridgeClient | null} */
|
|
198
|
+
let callClient = null;
|
|
199
|
+
/** @type {import('../../protocol/src/types.js').BridgeResponse | null} */
|
|
200
|
+
let response = null;
|
|
201
|
+
/** @type {unknown} */
|
|
202
|
+
let callError = null;
|
|
203
|
+
try {
|
|
204
|
+
callClient = await createBridgeClientForDestination(destinationId);
|
|
205
|
+
await callClient.connect();
|
|
206
|
+
const params = applyMethodBudgetPreset(method, call.params || {}, call.budgetPreset);
|
|
207
|
+
response = await requestBridgeWithRetry(callClient, method, params, {
|
|
208
|
+
tabId,
|
|
209
|
+
source: REQUEST_SOURCE,
|
|
210
|
+
tokenBudget,
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
callError = error;
|
|
214
|
+
} finally {
|
|
215
|
+
if (callClient) {
|
|
216
|
+
try {
|
|
241
217
|
await callClient.close();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
callError ??= error;
|
|
242
220
|
}
|
|
243
221
|
}
|
|
244
|
-
}
|
|
222
|
+
}
|
|
223
|
+
if (callError || !response) {
|
|
224
|
+
return {
|
|
225
|
+
destinationId,
|
|
226
|
+
...summarizeThrownBatchError(method, tabId, callError, Date.now() - startTime),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
destinationId,
|
|
231
|
+
...summarizeBatchResponseItem(
|
|
232
|
+
{ method, tabId, response, durationMs: Date.now() - startTime },
|
|
233
|
+
{ compact: true }
|
|
234
|
+
),
|
|
235
|
+
};
|
|
236
|
+
});
|
|
245
237
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
238
|
+
const failureCount = results.filter((result) => !result.ok).length;
|
|
239
|
+
const summary =
|
|
240
|
+
failureCount === 0
|
|
241
|
+
? `Batch executed ${results.length} call(s).`
|
|
242
|
+
: `Batch executed ${results.length} call(s) with ${failureCount} error(s).`;
|
|
243
|
+
return createToolResult(
|
|
244
|
+
summary,
|
|
245
|
+
{
|
|
246
|
+
ok: failureCount === 0,
|
|
247
|
+
successCount: results.length - failureCount,
|
|
248
|
+
failureCount,
|
|
249
|
+
compactOutput: true,
|
|
250
|
+
results,
|
|
251
|
+
},
|
|
252
|
+
failureCount > 0
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @type {ReadonlySet<string>} */
|
|
257
|
+
const RECOGNIZED_ERROR_CODES = new Set(Object.values(ERROR_CODES));
|
|
263
258
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
/**
|
|
260
|
+
* @param {BridgeMethod} method
|
|
261
|
+
* @param {number | null} tabId
|
|
262
|
+
* @param {unknown} error
|
|
263
|
+
* @param {number} durationMs
|
|
264
|
+
*/
|
|
265
|
+
function summarizeThrownBatchError(method, tabId, error, durationMs) {
|
|
266
|
+
const item = summarizeBatchErrorItem({ method, tabId, error, durationMs }, { compact: true });
|
|
267
|
+
const record =
|
|
268
|
+
error && typeof error === 'object' ? /** @type {Record<string, unknown>} */ (error) : {};
|
|
269
|
+
const code = /** @type {import('../../protocol/src/types.js').ErrorCode} */ (
|
|
270
|
+
typeof record.code === 'string' && RECOGNIZED_ERROR_CODES.has(record.code)
|
|
271
|
+
? record.code
|
|
272
|
+
: 'INTERNAL_ERROR'
|
|
273
|
+
);
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error ?? 'Bridge call failed.');
|
|
275
|
+
const details = Object.hasOwn(record, 'details') ? record.details : null;
|
|
276
|
+
const boundedDetails = boundToolValue(details);
|
|
277
|
+
const recovery = getErrorRecovery(code);
|
|
278
|
+
return {
|
|
279
|
+
...item,
|
|
280
|
+
summary: `${code}: ${message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
|
|
281
|
+
evidence: boundedDetails.value,
|
|
282
|
+
recovery,
|
|
283
|
+
error: { code, message, details: boundedDetails.value },
|
|
284
|
+
...(boundedDetails.truncated ? { outputTruncated: true } : {}),
|
|
285
|
+
};
|
|
267
286
|
}
|
|
268
287
|
|
|
269
288
|
/**
|
|
@@ -291,45 +310,26 @@ async function mapWithConcurrency(values, concurrency, callback) {
|
|
|
291
310
|
}
|
|
292
311
|
|
|
293
312
|
/**
|
|
294
|
-
* @param {{ method: string, params?: Record<string, unknown>, tabId?: number, destinationId?: string }} args
|
|
313
|
+
* @param {{ method: string, params?: Record<string, unknown>, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
295
314
|
* @returns {Promise<ToolResult>}
|
|
296
315
|
*/
|
|
297
316
|
export async function handleRawCallTool(args) {
|
|
298
317
|
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
|
|
299
318
|
return summarizeToolError(`Unknown bridge method "${args.method}".`);
|
|
300
319
|
}
|
|
301
|
-
if (args.method === 'setup.install') {
|
|
302
|
-
return summarizeToolError('Use browser_setup for setup changes.');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
320
|
return withToolClient(
|
|
306
321
|
async (client) => {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
args.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
source: REQUEST_SOURCE,
|
|
314
|
-
}
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
if (!response.ok) {
|
|
318
|
-
return createToolResult(
|
|
319
|
-
response.error.message,
|
|
320
|
-
{
|
|
321
|
-
ok: false,
|
|
322
|
-
error: response.error,
|
|
323
|
-
response,
|
|
324
|
-
},
|
|
325
|
-
true
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return createToolResult(`Called ${args.method}.`, {
|
|
330
|
-
ok: true,
|
|
331
|
-
response: response.result,
|
|
322
|
+
const method = /** @type {BridgeMethod} */ (args.method);
|
|
323
|
+
const params = applyMethodBudgetPreset(method, args.params || {}, args.budgetPreset);
|
|
324
|
+
const response = await requestBridgeWithRetry(client, method, params, {
|
|
325
|
+
tabId: typeof args.tabId === 'number' ? args.tabId : null,
|
|
326
|
+
source: REQUEST_SOURCE,
|
|
327
|
+
tokenBudget: method.startsWith('screenshot.') ? null : getToolTokenBudget(args),
|
|
332
328
|
});
|
|
329
|
+
if (response.ok && method.startsWith('screenshot.')) {
|
|
330
|
+
return createScreenshotResult(response, method);
|
|
331
|
+
}
|
|
332
|
+
return summarizeToolResponse(response, method, params);
|
|
333
333
|
},
|
|
334
334
|
{ destinationId: args.destinationId ?? null }
|
|
335
335
|
);
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
BridgeError,
|
|
4
5
|
bridgeMethodNeedsTab,
|
|
6
|
+
DEFAULT_CONSOLE_LIMIT,
|
|
7
|
+
DEFAULT_LOG_TAIL_LIMIT,
|
|
5
8
|
DEFAULT_MAX_HTML_LENGTH,
|
|
9
|
+
DEFAULT_MAX_NODES,
|
|
10
|
+
DEFAULT_NETWORK_LIMIT,
|
|
6
11
|
DEFAULT_PAGE_TEXT_BUDGET,
|
|
12
|
+
DEFAULT_TEXT_BUDGET,
|
|
7
13
|
estimateJsonPayloadCost,
|
|
8
14
|
getBudgetPreset,
|
|
9
15
|
getErrorRecovery,
|
|
@@ -121,10 +127,28 @@ export function createToolResult(
|
|
|
121
127
|
/**
|
|
122
128
|
* @param {BridgeResponse} response
|
|
123
129
|
* @param {string} [method]
|
|
130
|
+
* @param {Record<string, unknown>} [params]
|
|
124
131
|
* @returns {ToolResult}
|
|
125
132
|
*/
|
|
126
|
-
export function summarizeToolResponse(response, method) {
|
|
133
|
+
export function summarizeToolResponse(response, method, params = {}) {
|
|
127
134
|
const summary = annotateBridgeSummary(summarizeBridgeResponse(response, method), response);
|
|
135
|
+
const summaryRecord = /** @type {Record<string, unknown>} */ (summary);
|
|
136
|
+
if (response.ok) {
|
|
137
|
+
const evidence = getRequestAwareEvidence(response.result, method, params, summary.evidence);
|
|
138
|
+
summary.evidence = evidence.value;
|
|
139
|
+
if (evidence.truncated) {
|
|
140
|
+
summaryRecord.outputTruncated = true;
|
|
141
|
+
summaryRecord.outputLimit = evidence.limit;
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
const bounded = boundToolValue(summary.evidence);
|
|
145
|
+
summary.evidence = bounded.value;
|
|
146
|
+
summaryRecord.error = {
|
|
147
|
+
...response.error,
|
|
148
|
+
details: boundToolValue(response.error.details).value,
|
|
149
|
+
};
|
|
150
|
+
if (bounded.truncated) summaryRecord.outputTruncated = true;
|
|
151
|
+
}
|
|
128
152
|
return createToolResult(summary.summary, summary, !summary.ok);
|
|
129
153
|
}
|
|
130
154
|
|
|
@@ -136,15 +160,23 @@ export function summarizeToolError(error) {
|
|
|
136
160
|
const message = error instanceof Error ? error.message : String(error);
|
|
137
161
|
const record =
|
|
138
162
|
error && typeof error === 'object' ? /** @type {Record<string, unknown>} */ (error) : {};
|
|
139
|
-
const code =
|
|
163
|
+
const code =
|
|
164
|
+
typeof record.code === 'string'
|
|
165
|
+
? record.code
|
|
166
|
+
: typeof error === 'string'
|
|
167
|
+
? 'INVALID_REQUEST'
|
|
168
|
+
: 'INTERNAL_ERROR';
|
|
169
|
+
const details = Object.hasOwn(record, 'details') ? record.details : null;
|
|
170
|
+
const boundedDetails = boundToolValue(details);
|
|
140
171
|
const recovery = getErrorRecovery(code);
|
|
141
172
|
return createToolResult(
|
|
142
173
|
`${code}: ${message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
|
|
143
174
|
{
|
|
144
175
|
ok: false,
|
|
145
|
-
evidence:
|
|
146
|
-
error: { code, message },
|
|
176
|
+
evidence: boundedDetails.value,
|
|
177
|
+
error: { code, message, details: boundedDetails.value },
|
|
147
178
|
recovery,
|
|
179
|
+
...(boundedDetails.truncated ? { outputTruncated: true } : {}),
|
|
148
180
|
},
|
|
149
181
|
true
|
|
150
182
|
);
|
|
@@ -185,7 +217,7 @@ export async function resolveToolRef(client, input, tabId = null) {
|
|
|
185
217
|
if (typeof input.selector === 'string' && input.selector) {
|
|
186
218
|
return resolveRef(client, input.selector, tabId, REQUEST_SOURCE);
|
|
187
219
|
}
|
|
188
|
-
throw new
|
|
220
|
+
throw new BridgeError('INVALID_REQUEST', 'Provide either elementRef or selector.');
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
/**
|
|
@@ -314,6 +346,233 @@ export function applyHtmlBudgetPreset(args) {
|
|
|
314
346
|
});
|
|
315
347
|
}
|
|
316
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Apply a preset to method parameters before dispatch. Explicit method params
|
|
351
|
+
* always win over preset defaults.
|
|
352
|
+
*
|
|
353
|
+
* @param {BridgeMethod} method
|
|
354
|
+
* @param {Record<string, unknown>} params
|
|
355
|
+
* @param {unknown} budgetPreset
|
|
356
|
+
* @returns {Record<string, unknown>}
|
|
357
|
+
*/
|
|
358
|
+
export function applyMethodBudgetPreset(method, params, budgetPreset) {
|
|
359
|
+
const args = { ...params, budgetPreset };
|
|
360
|
+
let normalized = args;
|
|
361
|
+
if (method === 'dom.query' || method === 'dom.get_accessibility_tree') {
|
|
362
|
+
normalized = applyTreeBudgetPreset(args);
|
|
363
|
+
} else if (method === 'dom.get_text') {
|
|
364
|
+
normalized = applyTextBudgetPreset(args);
|
|
365
|
+
} else if (method === 'dom.get_html') {
|
|
366
|
+
normalized = applyHtmlBudgetPreset(args);
|
|
367
|
+
} else if (method === 'page.get_text') {
|
|
368
|
+
normalized = applyPageTextBudgetPreset(args);
|
|
369
|
+
} else if (method === 'page.get_console') {
|
|
370
|
+
normalized = applyLimitBudgetPreset(args, {
|
|
371
|
+
quick: 10,
|
|
372
|
+
normal: DEFAULT_CONSOLE_LIMIT,
|
|
373
|
+
deep: 100,
|
|
374
|
+
});
|
|
375
|
+
} else if (method === 'page.get_network') {
|
|
376
|
+
normalized = applyLimitBudgetPreset(args, {
|
|
377
|
+
quick: 10,
|
|
378
|
+
normal: DEFAULT_NETWORK_LIMIT,
|
|
379
|
+
deep: 100,
|
|
380
|
+
});
|
|
381
|
+
} else if (method === 'log.tail') {
|
|
382
|
+
normalized = applyLimitBudgetPreset(args, {
|
|
383
|
+
quick: 10,
|
|
384
|
+
normal: DEFAULT_LOG_TAIL_LIMIT,
|
|
385
|
+
deep: 100,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const { budgetPreset: _budgetPreset, ...methodParams } = normalized;
|
|
389
|
+
return methodParams;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* @param {unknown} input
|
|
394
|
+
* @param {{ maxEntries?: number, maxCharacters?: number, maxStringLength?: number }} [options]
|
|
395
|
+
* @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
|
|
396
|
+
*/
|
|
397
|
+
export function boundToolValue(input, options = {}) {
|
|
398
|
+
const maxEntries = options.maxEntries ?? 500;
|
|
399
|
+
const maxCharacters = options.maxCharacters ?? 20_000;
|
|
400
|
+
const maxStringLength = options.maxStringLength ?? 4_000;
|
|
401
|
+
const state = { entries: 0, characters: 0, truncated: false };
|
|
402
|
+
/** @type {(value: unknown, depth: number) => unknown} */
|
|
403
|
+
const visit = (value, depth) => {
|
|
404
|
+
if (state.entries >= maxEntries || state.characters >= maxCharacters || depth > 8) {
|
|
405
|
+
state.truncated = true;
|
|
406
|
+
return '[truncated]';
|
|
407
|
+
}
|
|
408
|
+
state.entries += 1;
|
|
409
|
+
if (typeof value === 'string') {
|
|
410
|
+
const limit = Math.min(value.length, maxStringLength, maxCharacters - state.characters);
|
|
411
|
+
state.characters += limit;
|
|
412
|
+
if (limit === value.length) return value;
|
|
413
|
+
state.truncated = true;
|
|
414
|
+
return value.slice(0, Math.max(0, limit));
|
|
415
|
+
}
|
|
416
|
+
if (Array.isArray(value)) {
|
|
417
|
+
const limit = Math.min(value.length, 100, maxEntries - state.entries);
|
|
418
|
+
if (limit < value.length) state.truncated = true;
|
|
419
|
+
return value.slice(0, limit).map((entry) => visit(entry, depth + 1));
|
|
420
|
+
}
|
|
421
|
+
if (value && typeof value === 'object') {
|
|
422
|
+
/** @type {Record<string, unknown>} */
|
|
423
|
+
const output = {};
|
|
424
|
+
const entries = Object.entries(/** @type {Record<string, unknown>} */ (value));
|
|
425
|
+
const limit = Math.min(entries.length, 100);
|
|
426
|
+
if (limit < entries.length) state.truncated = true;
|
|
427
|
+
for (const [key, entry] of entries.slice(0, limit)) {
|
|
428
|
+
if (state.entries >= maxEntries || state.characters >= maxCharacters) {
|
|
429
|
+
state.truncated = true;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
state.characters += key.length;
|
|
433
|
+
output[key] = visit(entry, depth + 1);
|
|
434
|
+
}
|
|
435
|
+
return output;
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
value: visit(input, 0),
|
|
441
|
+
truncated: state.truncated,
|
|
442
|
+
limit: { maxEntries, maxCharacters, maxStringLength },
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Preserve useful evidence up to the caller's requested limits instead of
|
|
448
|
+
* fetching normal/deep results and silently returning only a fixed preview.
|
|
449
|
+
*
|
|
450
|
+
* @param {unknown} rawResult
|
|
451
|
+
* @param {string | undefined} method
|
|
452
|
+
* @param {Record<string, unknown>} params
|
|
453
|
+
* @param {unknown} fallback
|
|
454
|
+
* @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
|
|
455
|
+
*/
|
|
456
|
+
function getRequestAwareEvidence(rawResult, method, params, fallback) {
|
|
457
|
+
const result =
|
|
458
|
+
rawResult && typeof rawResult === 'object' && !Array.isArray(rawResult)
|
|
459
|
+
? /** @type {Record<string, unknown>} */ (rawResult)
|
|
460
|
+
: {};
|
|
461
|
+
if (method === 'page.get_text' || method === 'dom.get_text') {
|
|
462
|
+
const text = typeof result.text === 'string' ? result.text : result.value;
|
|
463
|
+
if (typeof text === 'string') {
|
|
464
|
+
const defaultLimit =
|
|
465
|
+
method === 'page.get_text' ? DEFAULT_PAGE_TEXT_BUDGET : DEFAULT_TEXT_BUDGET;
|
|
466
|
+
const requested = positiveInteger(params.textBudget) ?? defaultLimit;
|
|
467
|
+
const maxCharacters = Math.min(requested, 16_000);
|
|
468
|
+
const outputTruncated = text.length > maxCharacters;
|
|
469
|
+
return {
|
|
470
|
+
value: {
|
|
471
|
+
text: text.slice(0, maxCharacters),
|
|
472
|
+
length: typeof result.length === 'number' ? result.length : text.length,
|
|
473
|
+
truncated: result.truncated === true,
|
|
474
|
+
returnedChars: Math.min(text.length, maxCharacters),
|
|
475
|
+
},
|
|
476
|
+
truncated: outputTruncated,
|
|
477
|
+
limit: { maxCharacters },
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (method === 'dom.get_html' && typeof result.html === 'string') {
|
|
482
|
+
const requested = positiveInteger(params.maxLength) ?? DEFAULT_MAX_HTML_LENGTH;
|
|
483
|
+
const maxCharacters = Math.min(requested, 10_000);
|
|
484
|
+
return {
|
|
485
|
+
value: {
|
|
486
|
+
html: result.html.slice(0, maxCharacters),
|
|
487
|
+
truncated: result.truncated === true,
|
|
488
|
+
returnedChars: Math.min(result.html.length, maxCharacters),
|
|
489
|
+
},
|
|
490
|
+
truncated: result.html.length > maxCharacters,
|
|
491
|
+
limit: { maxCharacters },
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (method === 'dom.query' && Array.isArray(result.nodes)) {
|
|
495
|
+
const maxEntries = Math.min(positiveInteger(params.maxNodes) ?? DEFAULT_MAX_NODES, 100);
|
|
496
|
+
const nodes = result.nodes.slice(0, maxEntries).map(compactDomNode);
|
|
497
|
+
return {
|
|
498
|
+
value: nodes,
|
|
499
|
+
truncated: result.truncated === true || result.nodes.length > maxEntries,
|
|
500
|
+
limit: { maxEntries },
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (method === 'dom.get_accessibility_tree' && Array.isArray(result.nodes)) {
|
|
504
|
+
const maxEntries = Math.min(positiveInteger(params.maxNodes) ?? DEFAULT_MAX_NODES, 100);
|
|
505
|
+
const nodes = result.nodes.slice(0, maxEntries).map((value) => {
|
|
506
|
+
const node =
|
|
507
|
+
value && typeof value === 'object' ? /** @type {Record<string, unknown>} */ (value) : {};
|
|
508
|
+
return {
|
|
509
|
+
role: node.role,
|
|
510
|
+
name: node.name,
|
|
511
|
+
...(node.interactive === true ? { interactive: true } : {}),
|
|
512
|
+
...(node.value !== undefined ? { value: node.value } : {}),
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
return {
|
|
516
|
+
value: nodes,
|
|
517
|
+
truncated: result.truncated === true || result.nodes.length > maxEntries,
|
|
518
|
+
limit: { maxEntries },
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (
|
|
522
|
+
(method === 'page.get_console' || method === 'page.get_network' || method === 'log.tail') &&
|
|
523
|
+
Array.isArray(result.entries)
|
|
524
|
+
) {
|
|
525
|
+
const defaultLimit =
|
|
526
|
+
method === 'page.get_console'
|
|
527
|
+
? DEFAULT_CONSOLE_LIMIT
|
|
528
|
+
: method === 'page.get_network'
|
|
529
|
+
? DEFAULT_NETWORK_LIMIT
|
|
530
|
+
: DEFAULT_LOG_TAIL_LIMIT;
|
|
531
|
+
const maxEntries = Math.min(positiveInteger(params.limit) ?? defaultLimit, 100);
|
|
532
|
+
const selected =
|
|
533
|
+
method === 'log.tail'
|
|
534
|
+
? result.entries.slice(-maxEntries)
|
|
535
|
+
: result.entries.slice(0, maxEntries);
|
|
536
|
+
const bounded = boundToolValue(selected, { maxEntries: 500, maxCharacters: 20_000 });
|
|
537
|
+
return {
|
|
538
|
+
value: bounded.value,
|
|
539
|
+
truncated: result.entries.length > maxEntries || bounded.truncated,
|
|
540
|
+
limit: { maxEntries, maxCharacters: 20_000 },
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return boundToolValue(fallback);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** @param {unknown} value */
|
|
547
|
+
function positiveInteger(value) {
|
|
548
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @param {unknown} value
|
|
553
|
+
* @returns {Record<string, unknown>}
|
|
554
|
+
*/
|
|
555
|
+
function compactDomNode(value) {
|
|
556
|
+
const node =
|
|
557
|
+
value && typeof value === 'object' ? /** @type {Record<string, unknown>} */ (value) : {};
|
|
558
|
+
const attrs =
|
|
559
|
+
node.attrs && typeof node.attrs === 'object'
|
|
560
|
+
? /** @type {Record<string, unknown>} */ (node.attrs)
|
|
561
|
+
: {};
|
|
562
|
+
/** @type {Record<string, unknown>} */
|
|
563
|
+
const compact = { ref: node.elementRef, tag: node.tag };
|
|
564
|
+
if (node.id ?? attrs.id) compact.id = node.id ?? attrs.id;
|
|
565
|
+
if (typeof attrs.class === 'string') compact.cls = attrs.class.split(' ').slice(0, 3).join(' ');
|
|
566
|
+
if (node.role ?? attrs.role) compact.role = node.role ?? attrs.role;
|
|
567
|
+
if (node.name ?? attrs['aria-label']) compact.label = node.name ?? attrs['aria-label'];
|
|
568
|
+
if (attrs['data-testid']) compact.testId = attrs['data-testid'];
|
|
569
|
+
const text = typeof node.textExcerpt === 'string' ? node.textExcerpt : node.text;
|
|
570
|
+
if (typeof text === 'string' && text) compact.text = text.slice(0, 120);
|
|
571
|
+
if (Array.isArray(node.children) && node.children.length)
|
|
572
|
+
compact.childCount = node.children.length;
|
|
573
|
+
return compact;
|
|
574
|
+
}
|
|
575
|
+
|
|
317
576
|
/**
|
|
318
577
|
* @param {import('../../agent-client/src/client.js').BridgeClient} client
|
|
319
578
|
* @param {BridgeMethod} method
|
|
@@ -352,7 +611,7 @@ export async function callBridgeTool(method, params = {}, options = {}) {
|
|
|
352
611
|
source: REQUEST_SOURCE,
|
|
353
612
|
tokenBudget: options.tokenBudget ?? null,
|
|
354
613
|
});
|
|
355
|
-
return summarizeToolResponse(response, options.summaryMethod || method);
|
|
614
|
+
return summarizeToolResponse(response, options.summaryMethod || method, params);
|
|
356
615
|
},
|
|
357
616
|
{ destinationId: options.destinationId ?? null }
|
|
358
617
|
);
|
|
@@ -388,12 +647,13 @@ export async function dispatchToolAction(actions, args, toolName) {
|
|
|
388
647
|
requestedTabId
|
|
389
648
|
)
|
|
390
649
|
: undefined;
|
|
391
|
-
const
|
|
650
|
+
const params = entry.params(args, ref);
|
|
651
|
+
const response = await requestBridgeWithRetry(client, entry.method, params, {
|
|
392
652
|
tabId: requestedTabId,
|
|
393
653
|
source: REQUEST_SOURCE,
|
|
394
654
|
tokenBudget: getToolTokenBudget(/** @type {{ budgetPreset?: unknown }} */ (args)),
|
|
395
655
|
});
|
|
396
|
-
return summarizeToolResponse(response, entry.method);
|
|
656
|
+
return summarizeToolResponse(response, entry.method, params);
|
|
397
657
|
},
|
|
398
658
|
{ destinationId: typeof args.destinationId === 'string' ? args.destinationId : null }
|
|
399
659
|
);
|