@cydm/pie 1.0.7 → 1.0.9
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 +10 -5
- package/dist/builtin/extensions/ask-user/index.js +1 -1
- package/dist/builtin/extensions/init/index.js +70 -68
- package/dist/builtin/extensions/plan-mode/index.js +1 -1
- package/dist/builtin/extensions/subagent/index.js +10 -65
- package/dist/builtin/extensions/todo/index.js +1 -1
- package/dist/builtin/skills/browser-tools/SKILL.md +14 -14
- package/dist/chunks/{chunk-BHNULR7U.js → chunk-EJGQAAKS.js} +841 -433
- package/dist/chunks/chunk-NTYHFBUA.js +36 -0
- package/dist/chunks/{chunk-GDTN4UPJ.js → chunk-XHZP5EK4.js} +297 -9
- package/dist/cli.js +570 -488
- package/package.json +2 -2
- package/dist/builtin/skills/pie-unity-rpc/SKILL.md +0 -121
- package/dist/builtin/skills/pie-unity-rpc/pie-unity-rpc.js +0 -417
|
@@ -259,7 +259,7 @@ function appendFileOperationsToSummary(summary, messages) {
|
|
|
259
259
|
}
|
|
260
260
|
function isSyntheticUserEnvelope(text) {
|
|
261
261
|
const trimmed = text.trimStart();
|
|
262
|
-
return trimmed.startsWith("<environment_context>") || trimmed.startsWith("<turn_aborted>") || trimmed.startsWith("# AGENTS.md instructions");
|
|
262
|
+
return trimmed.startsWith("<environment_context>") || trimmed.startsWith("<turn_aborted>") || trimmed.startsWith("# AGENTS.md instructions") || trimmed.startsWith("## Project Context\nProject-specific instructions from AGENTS.md");
|
|
263
263
|
}
|
|
264
264
|
function isCompactionCompatibleMessage(message) {
|
|
265
265
|
if (message.role === "user") return true;
|
|
@@ -1791,6 +1791,11 @@ var AgentSessionController = class {
|
|
|
1791
1791
|
}
|
|
1792
1792
|
return;
|
|
1793
1793
|
}
|
|
1794
|
+
const lastMessage = messages[messages.length - 1];
|
|
1795
|
+
if (lastMessage?.role === "assistant" && (lastMessage.stopReason === "error" || lastMessage.stopReason === "aborted")) {
|
|
1796
|
+
this.emit({ type: "auto_continue_skipped", reason: "last_turn_failed" });
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1794
1799
|
const result = await this.getAutoContinueMessage({
|
|
1795
1800
|
agent: this.agent,
|
|
1796
1801
|
sessionManager: this.sessionManager,
|
|
@@ -1935,133 +1940,41 @@ async function requestInteraction(request) {
|
|
|
1935
1940
|
return await currentInteractionHandler(request);
|
|
1936
1941
|
}
|
|
1937
1942
|
|
|
1938
|
-
// ../../packages/agent-framework/src/
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
for (const line of lines) {
|
|
1943
|
-
const trimmed = line.trim();
|
|
1944
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1945
|
-
const colonIndex = trimmed.indexOf(":");
|
|
1946
|
-
if (colonIndex === -1) continue;
|
|
1947
|
-
const key = trimmed.slice(0, colonIndex).trim();
|
|
1948
|
-
let value = trimmed.slice(colonIndex + 1).trim();
|
|
1949
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1950
|
-
value = value.slice(1, -1);
|
|
1951
|
-
}
|
|
1952
|
-
switch (key) {
|
|
1953
|
-
case "title":
|
|
1954
|
-
result.title = value;
|
|
1955
|
-
break;
|
|
1956
|
-
case "desc":
|
|
1957
|
-
case "description":
|
|
1958
|
-
result.description = value;
|
|
1959
|
-
break;
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
return result;
|
|
1943
|
+
// ../../packages/agent-framework/src/context-files.ts
|
|
1944
|
+
var AGENTS_CONTEXT_FILE_NAME = "AGENTS.md";
|
|
1945
|
+
function normalizeContextPath(path3) {
|
|
1946
|
+
return String(path3 || "").replace(/\\/g, "/").replace(/^\/+/, "").trim() || AGENTS_CONTEXT_FILE_NAME;
|
|
1963
1947
|
}
|
|
1964
|
-
function
|
|
1965
|
-
|
|
1966
|
-
if (!match) {
|
|
1967
|
-
return { metadata: {}, body: content };
|
|
1968
|
-
}
|
|
1969
|
-
return {
|
|
1970
|
-
metadata: parseFrontmatterBlock(match[1]),
|
|
1971
|
-
body: content.slice(match[0].length)
|
|
1972
|
-
};
|
|
1948
|
+
function normalizeContextContent(content) {
|
|
1949
|
+
return String(content || "").replace(/\r\n/g, "\n").trim();
|
|
1973
1950
|
}
|
|
1974
|
-
function
|
|
1975
|
-
const
|
|
1976
|
-
const
|
|
1977
|
-
for (const
|
|
1978
|
-
const
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
if (level > maxLevel) continue;
|
|
1983
|
-
headings.push({ level, text: match[2].trim() });
|
|
1984
|
-
}
|
|
1985
|
-
return headings;
|
|
1986
|
-
}
|
|
1987
|
-
function extractTitle(metadata, headings) {
|
|
1988
|
-
if (metadata.title && metadata.title.trim()) return metadata.title.trim();
|
|
1989
|
-
const firstHeading = headings.find((heading) => heading.level === 1) || headings[0];
|
|
1990
|
-
return firstHeading?.text?.trim() || "";
|
|
1991
|
-
}
|
|
1992
|
-
function extractDescription(metadata, body) {
|
|
1993
|
-
const explicit = metadata.description;
|
|
1994
|
-
if (explicit && explicit.trim()) {
|
|
1995
|
-
return explicit.trim();
|
|
1996
|
-
}
|
|
1997
|
-
const lines = body.split("\n");
|
|
1998
|
-
for (const rawLine of lines.slice(0, 24)) {
|
|
1999
|
-
const line = rawLine.trim();
|
|
2000
|
-
if (!line) continue;
|
|
2001
|
-
const descriptionMatch = line.match(/^description:\s*(.+)$/i);
|
|
2002
|
-
if (descriptionMatch) {
|
|
2003
|
-
return descriptionMatch[1].trim();
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
let seenHeading = false;
|
|
2007
|
-
let paragraphLines = [];
|
|
2008
|
-
for (const rawLine of body.split("\n")) {
|
|
2009
|
-
const line = rawLine.trim();
|
|
2010
|
-
if (!line) {
|
|
2011
|
-
if (seenHeading && paragraphLines.length > 0) {
|
|
2012
|
-
break;
|
|
2013
|
-
}
|
|
2014
|
-
continue;
|
|
2015
|
-
}
|
|
2016
|
-
if (/^#{1,6}\s+/.test(line)) {
|
|
2017
|
-
if (seenHeading && paragraphLines.length > 0) {
|
|
2018
|
-
break;
|
|
2019
|
-
}
|
|
2020
|
-
seenHeading = true;
|
|
2021
|
-
continue;
|
|
2022
|
-
}
|
|
2023
|
-
if (!seenHeading) {
|
|
2024
|
-
continue;
|
|
2025
|
-
}
|
|
2026
|
-
if (/^description:\s*/i.test(line)) {
|
|
1951
|
+
function dedupeProjectContextFiles(files) {
|
|
1952
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1953
|
+
const result = [];
|
|
1954
|
+
for (const file of files) {
|
|
1955
|
+
const path3 = normalizeContextPath(file.path);
|
|
1956
|
+
const key = path3.toLowerCase();
|
|
1957
|
+
const content = normalizeContextContent(file.content);
|
|
1958
|
+
if (!content || seen.has(key)) {
|
|
2027
1959
|
continue;
|
|
2028
1960
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
if (paragraphLines.length > 0) {
|
|
2032
|
-
return paragraphLines.join(" ").trim();
|
|
1961
|
+
seen.add(key);
|
|
1962
|
+
result.push({ path: path3, content });
|
|
2033
1963
|
}
|
|
2034
|
-
return
|
|
2035
|
-
}
|
|
2036
|
-
function buildKnowledgeIndexEntry(path3, content) {
|
|
2037
|
-
const trimmed = String(content || "").trim();
|
|
2038
|
-
if (!trimmed) return null;
|
|
2039
|
-
const { metadata, body } = stripFrontmatter(trimmed);
|
|
2040
|
-
const headings = extractHeadings(body, 3);
|
|
2041
|
-
const title = extractTitle(metadata, headings);
|
|
2042
|
-
const description = extractDescription(metadata, body);
|
|
2043
|
-
return {
|
|
2044
|
-
path: path3,
|
|
2045
|
-
title,
|
|
2046
|
-
description,
|
|
2047
|
-
headings
|
|
2048
|
-
};
|
|
2049
|
-
}
|
|
2050
|
-
function formatHeadings(headings) {
|
|
2051
|
-
if (headings.length === 0) return "(none)";
|
|
2052
|
-
return headings.map((heading) => `${"#".repeat(heading.level)} ${heading.text}`).join(" | ");
|
|
1964
|
+
return result;
|
|
2053
1965
|
}
|
|
2054
|
-
function
|
|
2055
|
-
const
|
|
2056
|
-
if (
|
|
2057
|
-
|
|
2058
|
-
for (const entry of filtered) {
|
|
2059
|
-
lines.push(`- path: ${entry.path}`);
|
|
2060
|
-
if (entry.title) lines.push(` title: ${entry.title}`);
|
|
2061
|
-
if (entry.description) lines.push(` description: ${entry.description}`);
|
|
2062
|
-
lines.push(` headings: ${formatHeadings(entry.headings)}`);
|
|
1966
|
+
function buildProjectContextSection(files, options = {}) {
|
|
1967
|
+
const contextFiles = dedupeProjectContextFiles(files);
|
|
1968
|
+
if (contextFiles.length === 0) {
|
|
1969
|
+
return "";
|
|
2063
1970
|
}
|
|
2064
|
-
|
|
1971
|
+
const heading = options.heading?.trim() || "Project Context";
|
|
1972
|
+
const intro = options.intro?.trim() || "Project-specific instructions from AGENTS.md files. Apply them in order; later files may add narrower instructions.";
|
|
1973
|
+
const lines = [`## ${heading}`, intro, ""];
|
|
1974
|
+
for (const file of contextFiles) {
|
|
1975
|
+
lines.push(`### ${file.path}`, file.content, "");
|
|
1976
|
+
}
|
|
1977
|
+
return lines.join("\n").trimEnd();
|
|
2065
1978
|
}
|
|
2066
1979
|
|
|
2067
1980
|
// ../../packages/shared-headless-capabilities/src/loop.ts
|
|
@@ -2124,7 +2037,7 @@ function restoreLoopState(raw) {
|
|
|
2124
2037
|
if (!raw || typeof raw !== "object") return null;
|
|
2125
2038
|
const value = raw;
|
|
2126
2039
|
if (value.mode !== "todo_closure") return null;
|
|
2127
|
-
const status = value.status === "active" || value.status === "completed" || value.status === "aborted" || value.status === "failed" || value.status === "stalled" ? value.status : "active";
|
|
2040
|
+
const status = value.status === "active" || value.status === "paused" || value.status === "completed" || value.status === "aborted" || value.status === "failed" || value.status === "stalled" ? value.status : "active";
|
|
2128
2041
|
return {
|
|
2129
2042
|
goal: typeof value.goal === "string" && value.goal.trim().length > 0 ? value.goal : "Drive the active todo list to a terminal cleared state.",
|
|
2130
2043
|
mode: "todo_closure",
|
|
@@ -2196,8 +2109,8 @@ function evaluateTodoClosureAfterFailedTurn(loop) {
|
|
|
2196
2109
|
return { loop, action: "none", reason: "already_terminal" };
|
|
2197
2110
|
}
|
|
2198
2111
|
return {
|
|
2199
|
-
loop: { ...loop, status: "
|
|
2200
|
-
action: "
|
|
2112
|
+
loop: { ...loop, status: "paused", updatedAt: nowIso() },
|
|
2113
|
+
action: "pause_failed",
|
|
2201
2114
|
reason: "failed_turn"
|
|
2202
2115
|
};
|
|
2203
2116
|
}
|
|
@@ -2256,6 +2169,16 @@ function inferLastCompletedStepId(steps) {
|
|
|
2256
2169
|
const completed = [...steps].reverse().find((step) => step.status === "completed");
|
|
2257
2170
|
return completed?.id ?? null;
|
|
2258
2171
|
}
|
|
2172
|
+
function createRestoredTodoLoop(state, lifecycle, restoredLoop) {
|
|
2173
|
+
if (state.mode !== "todo" || lifecycle !== "active" && lifecycle !== "paused" || state.steps.length === 0) {
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
if (restoredLoop) {
|
|
2177
|
+
return restoredLoop;
|
|
2178
|
+
}
|
|
2179
|
+
const loop = createTodoClosureLoop({ ...state, lifecycle: "active" });
|
|
2180
|
+
return lifecycle === "paused" && loop ? { ...loop, status: "paused" } : loop;
|
|
2181
|
+
}
|
|
2259
2182
|
function createEmptyExecutionState() {
|
|
2260
2183
|
return {
|
|
2261
2184
|
mode: "idle",
|
|
@@ -2282,7 +2205,7 @@ function restoreExecutionState(raw) {
|
|
|
2282
2205
|
}
|
|
2283
2206
|
const state = raw;
|
|
2284
2207
|
const steps = normalizeSteps(state.steps || []);
|
|
2285
|
-
const lifecycle = steps.length === 0 ? "stale" : state.lifecycle === "active" || state.lifecycle === "aborted" || state.lifecycle === "completed" || state.lifecycle === "stale" || state.lifecycle === "stalled" ? state.lifecycle : "active";
|
|
2208
|
+
const lifecycle = steps.length === 0 ? "stale" : state.lifecycle === "active" || state.lifecycle === "paused" || state.lifecycle === "aborted" || state.lifecycle === "completed" || state.lifecycle === "stale" || state.lifecycle === "stalled" ? state.lifecycle : "active";
|
|
2286
2209
|
const mode = state.mode === "todo" || state.mode === "plan" || state.mode === "idle" ? state.mode : steps.length > 0 ? "todo" : "idle";
|
|
2287
2210
|
const loop = restoreLoopState(state.loop);
|
|
2288
2211
|
return {
|
|
@@ -2292,7 +2215,7 @@ function restoreExecutionState(raw) {
|
|
|
2292
2215
|
currentStepId: typeof state.currentStepId === "number" ? state.currentStepId : inferCurrentStepId(steps),
|
|
2293
2216
|
lastCompletedStepId: typeof state.lastCompletedStepId === "number" ? state.lastCompletedStepId : inferLastCompletedStepId(steps),
|
|
2294
2217
|
source: state.source === "user_todo" || state.source === "plan_mode" || state.source === "system_auto" ? state.source : "system_auto",
|
|
2295
|
-
loop:
|
|
2218
|
+
loop: createRestoredTodoLoop({
|
|
2296
2219
|
mode,
|
|
2297
2220
|
lifecycle,
|
|
2298
2221
|
steps,
|
|
@@ -2303,7 +2226,7 @@ function restoreExecutionState(raw) {
|
|
|
2303
2226
|
awaitingContinuation: Boolean(state.awaitingContinuation),
|
|
2304
2227
|
failureSummary: typeof state.failureSummary === "string" ? state.failureSummary : null,
|
|
2305
2228
|
updatedAt: typeof state.updatedAt === "string" && state.updatedAt.length > 0 ? state.updatedAt : nowIso2()
|
|
2306
|
-
}
|
|
2229
|
+
}, lifecycle, loop),
|
|
2307
2230
|
awaitingContinuation: Boolean(state.awaitingContinuation),
|
|
2308
2231
|
failureSummary: typeof state.failureSummary === "string" ? state.failureSummary : null,
|
|
2309
2232
|
updatedAt: typeof state.updatedAt === "string" && state.updatedAt.length > 0 ? state.updatedAt : nowIso2()
|
|
@@ -2379,7 +2302,7 @@ function executionStateToTodos(state) {
|
|
|
2379
2302
|
}));
|
|
2380
2303
|
}
|
|
2381
2304
|
function createTodoWidgetView(state) {
|
|
2382
|
-
if (state.lifecycle !== "active" && state.lifecycle !== "aborted" && state.lifecycle !== "stalled") {
|
|
2305
|
+
if (state.lifecycle !== "active" && state.lifecycle !== "paused" && state.lifecycle !== "aborted" && state.lifecycle !== "stalled") {
|
|
2383
2306
|
return [];
|
|
2384
2307
|
}
|
|
2385
2308
|
return state.steps.map((step) => ({ ...step }));
|
|
@@ -2420,16 +2343,36 @@ function continueExecutionState(state) {
|
|
|
2420
2343
|
if (state.steps.length === 0) {
|
|
2421
2344
|
return createEmptyExecutionState();
|
|
2422
2345
|
}
|
|
2423
|
-
|
|
2346
|
+
const activeState = restoreExecutionState({
|
|
2424
2347
|
...state,
|
|
2425
2348
|
lifecycle: "active",
|
|
2426
2349
|
currentStepId: inferCurrentStepId(state.steps),
|
|
2427
|
-
loop:
|
|
2350
|
+
loop: null,
|
|
2351
|
+
awaitingContinuation: false,
|
|
2352
|
+
failureSummary: null,
|
|
2353
|
+
updatedAt: nowIso2()
|
|
2354
|
+
});
|
|
2355
|
+
return restoreExecutionState({
|
|
2356
|
+
...activeState,
|
|
2357
|
+
loop: activeState.mode === "todo" ? createTodoClosureLoop(activeState) : null,
|
|
2428
2358
|
awaitingContinuation: false,
|
|
2429
2359
|
failureSummary: null,
|
|
2430
2360
|
updatedAt: nowIso2()
|
|
2431
2361
|
});
|
|
2432
2362
|
}
|
|
2363
|
+
function pauseExecutionState(state, summary = null) {
|
|
2364
|
+
if (state.steps.length === 0) {
|
|
2365
|
+
return createEmptyExecutionState();
|
|
2366
|
+
}
|
|
2367
|
+
return restoreExecutionState({
|
|
2368
|
+
...state,
|
|
2369
|
+
lifecycle: "paused",
|
|
2370
|
+
loop: state.loop ? { ...state.loop, status: "paused" } : null,
|
|
2371
|
+
awaitingContinuation: false,
|
|
2372
|
+
failureSummary: summary,
|
|
2373
|
+
updatedAt: nowIso2()
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2433
2376
|
function supersedeExecutionState() {
|
|
2434
2377
|
return createEmptyExecutionState();
|
|
2435
2378
|
}
|
|
@@ -2453,13 +2396,11 @@ var BUILTIN_TOOL_CAPABILITY_METADATA = {
|
|
|
2453
2396
|
write_file: { riskClass: "write", concurrencySafe: false, requiresPermission: true, permissionScope: "filesystem", writesFile: true, highRisk: true },
|
|
2454
2397
|
edit_file: { riskClass: "write", concurrencySafe: false, requiresPermission: true, permissionScope: "filesystem", writesFile: true, highRisk: true },
|
|
2455
2398
|
bash: { riskClass: "shell", concurrencySafe: false, requiresPermission: true, permissionScope: "shell", executesShell: true, availableInPlanMode: true, allowedForSubagentByDefault: true, highRisk: true, maxOutputChars: 1e5 },
|
|
2399
|
+
web_research: { riskClass: "network", concurrencySafe: true, permissionScope: "network", readsNetwork: true, availableInPlanMode: true, allowedForSubagentByDefault: true, maxOutputChars: 1e5 },
|
|
2456
2400
|
web_search: { riskClass: "network", concurrencySafe: true, permissionScope: "network", readsNetwork: true, availableInPlanMode: true, allowedForSubagentByDefault: true, maxOutputChars: 1e5 },
|
|
2457
2401
|
web_fetch: { riskClass: "network", concurrencySafe: true, permissionScope: "network", readsNetwork: true, availableInPlanMode: true, allowedForSubagentByDefault: true, maxOutputChars: 1e5 },
|
|
2458
2402
|
code_intel: { riskClass: "read_only", concurrencySafe: true, permissionScope: "filesystem", readsFile: true, availableInPlanMode: true, allowedForSubagentByDefault: true, maxOutputChars: 1e5 },
|
|
2459
2403
|
spawn_subagents_parallel: { riskClass: "read_only", concurrencySafe: false, permissionScope: "host", readsHostResource: true, availableInPlanMode: true, maxOutputChars: 1e5 },
|
|
2460
|
-
read_skill: { riskClass: "read_only", concurrencySafe: true, permissionScope: "host", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
2461
|
-
read_resource: { riskClass: "read_only", concurrencySafe: true, permissionScope: "host", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
2462
|
-
resolve_resource: { riskClass: "read_only", concurrencySafe: true, permissionScope: "host", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
2463
2404
|
ask_user_multi: { riskClass: "user_interaction", concurrencySafe: false, permissionScope: "user", asksUser: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
2464
2405
|
manage_todo_list: { riskClass: "session_mutation", concurrencySafe: false, permissionScope: "session", mutatesSession: true, availableInPlanMode: true },
|
|
2465
2406
|
unity_project_inspect: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
@@ -2468,9 +2409,7 @@ var BUILTIN_TOOL_CAPABILITY_METADATA = {
|
|
|
2468
2409
|
unity_log_read: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true, maxOutputChars: 1e5 },
|
|
2469
2410
|
unity_scene_object_edit: { riskClass: "external_side_effect", concurrencySafe: false, requiresPermission: true, permissionScope: "unity", externalSideEffect: true, highRisk: true },
|
|
2470
2411
|
unity_refresh: { riskClass: "external_side_effect", concurrencySafe: false, permissionScope: "unity", externalSideEffect: true },
|
|
2471
|
-
unity_script_run: { riskClass: "external_side_effect", concurrencySafe: false, requiresPermission: true, permissionScope: "unity", externalSideEffect: true, highRisk: true }
|
|
2472
|
-
read_project_memory: { riskClass: "read_only", concurrencySafe: true, permissionScope: "host", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
2473
|
-
write_project_memory: { riskClass: "write", concurrencySafe: false, requiresPermission: true, permissionScope: "host", writesHostResource: true, externalSideEffect: true, highRisk: true }
|
|
2412
|
+
unity_script_run: { riskClass: "external_side_effect", concurrencySafe: false, requiresPermission: true, permissionScope: "unity", externalSideEffect: true, highRisk: true }
|
|
2474
2413
|
};
|
|
2475
2414
|
function getToolCapabilityMetadata(tool) {
|
|
2476
2415
|
return tool.capabilityMetadata ?? tool.risk ?? BUILTIN_TOOL_CAPABILITY_METADATA[tool.name];
|
|
@@ -2622,268 +2561,6 @@ function createPlanCapability() {
|
|
|
2622
2561
|
});
|
|
2623
2562
|
}
|
|
2624
2563
|
|
|
2625
|
-
// ../../packages/shared-headless-capabilities/src/read-skill.ts
|
|
2626
|
-
var ReadSkillParamsSchema = Type.Object({
|
|
2627
|
-
name: Type.String({ description: "Exact skill name from the available_skills list." })
|
|
2628
|
-
});
|
|
2629
|
-
function validateSkillNameArgument(name) {
|
|
2630
|
-
if (name.includes("<|tool")) {
|
|
2631
|
-
return "The skill name must be only an exact name from the available_skills list, not tool-call markup.";
|
|
2632
|
-
}
|
|
2633
|
-
if (name.includes("\n")) {
|
|
2634
|
-
return "The skill name must be a single exact name from the available_skills list, not multi-line text.";
|
|
2635
|
-
}
|
|
2636
|
-
return null;
|
|
2637
|
-
}
|
|
2638
|
-
function formatSkillResources(resources) {
|
|
2639
|
-
if (!Array.isArray(resources) || resources.length === 0) return "";
|
|
2640
|
-
const lines = resources.map((resource) => {
|
|
2641
|
-
const path3 = String(resource.path || "").trim();
|
|
2642
|
-
const kind = String(resource.kind || "").trim();
|
|
2643
|
-
return kind ? `- ${path3} (${kind})` : `- ${path3}`;
|
|
2644
|
-
});
|
|
2645
|
-
return `
|
|
2646
|
-
|
|
2647
|
-
## Skill Resources
|
|
2648
|
-
${lines.join("\n")}`;
|
|
2649
|
-
}
|
|
2650
|
-
function createReadSkillCapability(deps) {
|
|
2651
|
-
async function resolveSkill(name) {
|
|
2652
|
-
if (deps.resolveSkill) {
|
|
2653
|
-
return deps.resolveSkill(name);
|
|
2654
|
-
}
|
|
2655
|
-
const skill = deps.skillLoader?.getSkill?.(name) ?? deps.skillLoader?.loadSkill?.(name);
|
|
2656
|
-
if (!skill) {
|
|
2657
|
-
return null;
|
|
2658
|
-
}
|
|
2659
|
-
return {
|
|
2660
|
-
name: skill.name,
|
|
2661
|
-
content: skill.prompt,
|
|
2662
|
-
resources: skill.resourceRefs
|
|
2663
|
-
};
|
|
2664
|
-
}
|
|
2665
|
-
const tool = {
|
|
2666
|
-
name: "read_skill",
|
|
2667
|
-
label: "read_skill",
|
|
2668
|
-
description: "Load the full contents of a skill by exact name from the available_skills list only. Do not pass tool names.",
|
|
2669
|
-
parameters: ReadSkillParamsSchema,
|
|
2670
|
-
async execute(toolCallIdOrArgs, maybeArgs) {
|
|
2671
|
-
const args = typeof toolCallIdOrArgs === "string" ? maybeArgs : toolCallIdOrArgs;
|
|
2672
|
-
const skillName = String(args?.name || "").trim();
|
|
2673
|
-
const validationError = validateSkillNameArgument(skillName);
|
|
2674
|
-
if (validationError) {
|
|
2675
|
-
return {
|
|
2676
|
-
content: [{ type: "text", text: `Invalid skill name: ${validationError}` }],
|
|
2677
|
-
details: { found: false, name: skillName || void 0 },
|
|
2678
|
-
isError: true
|
|
2679
|
-
};
|
|
2680
|
-
}
|
|
2681
|
-
const skill = await resolveSkill(skillName);
|
|
2682
|
-
if (!skill?.content) {
|
|
2683
|
-
return {
|
|
2684
|
-
content: [{ type: "text", text: `Skill not found: ${skillName || "(empty)"}` }],
|
|
2685
|
-
details: { found: false, name: skillName || void 0 },
|
|
2686
|
-
isError: true
|
|
2687
|
-
};
|
|
2688
|
-
}
|
|
2689
|
-
return {
|
|
2690
|
-
content: [{
|
|
2691
|
-
type: "text",
|
|
2692
|
-
text: `# ${skill.name}
|
|
2693
|
-
|
|
2694
|
-
${skill.content}${formatSkillResources(skill.resources)}`
|
|
2695
|
-
}],
|
|
2696
|
-
details: {
|
|
2697
|
-
found: true,
|
|
2698
|
-
name: skill.name,
|
|
2699
|
-
...Array.isArray(skill.resources) ? { resources: skill.resources } : {}
|
|
2700
|
-
}
|
|
2701
|
-
};
|
|
2702
|
-
}
|
|
2703
|
-
};
|
|
2704
|
-
return {
|
|
2705
|
-
...defineSharedCapability({
|
|
2706
|
-
id: "read-skill",
|
|
2707
|
-
description: "Shared capability for loading full skill contents through a host-provided skill resolver.",
|
|
2708
|
-
tools: [tool]
|
|
2709
|
-
}),
|
|
2710
|
-
tool
|
|
2711
|
-
};
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
// ../../packages/shared-headless-capabilities/src/read-resource.ts
|
|
2715
|
-
var ReadResourceParamsSchema = Type.Object({
|
|
2716
|
-
owner: Type.String({ description: "Exact skill name that owns the resource." }),
|
|
2717
|
-
path: Type.String({ description: "Relative path inside the skill package." })
|
|
2718
|
-
});
|
|
2719
|
-
function inferKind(path3) {
|
|
2720
|
-
const lower = path3.toLowerCase();
|
|
2721
|
-
if (lower.endsWith(".md")) return "document";
|
|
2722
|
-
if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".sh") || lower.endsWith(".py")) return "script";
|
|
2723
|
-
return void 0;
|
|
2724
|
-
}
|
|
2725
|
-
function inferMimeType(path3) {
|
|
2726
|
-
const lower = path3.toLowerCase();
|
|
2727
|
-
if (lower.endsWith(".md")) return "text/markdown";
|
|
2728
|
-
if (lower.endsWith(".js") || lower.endsWith(".mjs")) return "application/javascript";
|
|
2729
|
-
if (lower.endsWith(".sh")) return "text/x-shellscript";
|
|
2730
|
-
if (lower.endsWith(".py")) return "text/x-python";
|
|
2731
|
-
if (lower.endsWith(".json")) return "application/json";
|
|
2732
|
-
if (lower.endsWith(".txt")) return "text/plain";
|
|
2733
|
-
return void 0;
|
|
2734
|
-
}
|
|
2735
|
-
function validateRelativeResourcePath(path3) {
|
|
2736
|
-
const trimmed = String(path3 || "").trim().replace(/\\/g, "/");
|
|
2737
|
-
if (!trimmed) {
|
|
2738
|
-
throw new Error("Resource path cannot be empty");
|
|
2739
|
-
}
|
|
2740
|
-
if (trimmed.startsWith("/") || /^[A-Za-z]:\//.test(trimmed)) {
|
|
2741
|
-
throw new Error("Resource path must be relative");
|
|
2742
|
-
}
|
|
2743
|
-
const segments = trimmed.split("/");
|
|
2744
|
-
if (segments.some((segment) => segment === "..")) {
|
|
2745
|
-
throw new Error("Resource path cannot escape the skill package");
|
|
2746
|
-
}
|
|
2747
|
-
return trimmed.replace(/^\.\/+/, "");
|
|
2748
|
-
}
|
|
2749
|
-
function createReadResourceCapability(deps) {
|
|
2750
|
-
const tool = {
|
|
2751
|
-
name: "read_resource",
|
|
2752
|
-
label: "read_resource",
|
|
2753
|
-
description: "Load a relative resource file from inside a skill package by owner and path.",
|
|
2754
|
-
parameters: ReadResourceParamsSchema,
|
|
2755
|
-
async execute(toolCallIdOrArgs, maybeArgs) {
|
|
2756
|
-
const args = typeof toolCallIdOrArgs === "string" ? maybeArgs : toolCallIdOrArgs;
|
|
2757
|
-
const owner = String(args?.owner || "").trim();
|
|
2758
|
-
let resourcePath = "";
|
|
2759
|
-
try {
|
|
2760
|
-
resourcePath = validateRelativeResourcePath(args?.path || "");
|
|
2761
|
-
} catch (error) {
|
|
2762
|
-
return {
|
|
2763
|
-
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
2764
|
-
details: { found: false, owner: owner || void 0, path: args?.path || void 0 },
|
|
2765
|
-
isError: true
|
|
2766
|
-
};
|
|
2767
|
-
}
|
|
2768
|
-
const resource = await deps.resolveResource?.(owner, resourcePath);
|
|
2769
|
-
if (!resource?.content) {
|
|
2770
|
-
return {
|
|
2771
|
-
content: [{ type: "text", text: `Resource not found: ${owner || "(empty)"}/${resourcePath || "(empty)"}` }],
|
|
2772
|
-
details: { found: false, owner: owner || void 0, path: resourcePath || void 0 },
|
|
2773
|
-
isError: true
|
|
2774
|
-
};
|
|
2775
|
-
}
|
|
2776
|
-
const kind = resource.kind || inferKind(resource.path);
|
|
2777
|
-
const mimeType = resource.mimeType || inferMimeType(resource.path);
|
|
2778
|
-
return {
|
|
2779
|
-
content: [{
|
|
2780
|
-
type: "text",
|
|
2781
|
-
text: `# ${resource.owner}/${resource.path}
|
|
2782
|
-
|
|
2783
|
-
${resource.content}`
|
|
2784
|
-
}],
|
|
2785
|
-
details: {
|
|
2786
|
-
found: true,
|
|
2787
|
-
owner: resource.owner,
|
|
2788
|
-
path: resource.path,
|
|
2789
|
-
kind,
|
|
2790
|
-
mimeType
|
|
2791
|
-
}
|
|
2792
|
-
};
|
|
2793
|
-
}
|
|
2794
|
-
};
|
|
2795
|
-
return {
|
|
2796
|
-
...defineSharedCapability({
|
|
2797
|
-
id: "read-resource",
|
|
2798
|
-
description: "Shared capability for loading resources scoped to a skill package.",
|
|
2799
|
-
tools: [tool]
|
|
2800
|
-
}),
|
|
2801
|
-
tool
|
|
2802
|
-
};
|
|
2803
|
-
}
|
|
2804
|
-
|
|
2805
|
-
// ../../packages/shared-headless-capabilities/src/resolve-resource.ts
|
|
2806
|
-
var ResolveResourceParamsSchema = Type.Object({
|
|
2807
|
-
owner: Type.String({ description: "Exact skill name that owns the resource." }),
|
|
2808
|
-
path: Type.String({ description: "Relative path inside the skill package." })
|
|
2809
|
-
});
|
|
2810
|
-
function inferKind2(path3) {
|
|
2811
|
-
const lower = path3.toLowerCase();
|
|
2812
|
-
if (lower.endsWith(".md")) return "document";
|
|
2813
|
-
if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".sh") || lower.endsWith(".py")) return "script";
|
|
2814
|
-
return void 0;
|
|
2815
|
-
}
|
|
2816
|
-
function validateRelativeResourcePath2(path3) {
|
|
2817
|
-
const trimmed = String(path3 || "").trim().replace(/\\/g, "/");
|
|
2818
|
-
if (!trimmed) {
|
|
2819
|
-
throw new Error("Resource path cannot be empty");
|
|
2820
|
-
}
|
|
2821
|
-
if (trimmed.startsWith("/") || /^[A-Za-z]:\//.test(trimmed)) {
|
|
2822
|
-
throw new Error("Resource path must be relative");
|
|
2823
|
-
}
|
|
2824
|
-
const segments = trimmed.split("/");
|
|
2825
|
-
if (segments.some((segment) => segment === "..")) {
|
|
2826
|
-
throw new Error("Resource path cannot escape the skill package");
|
|
2827
|
-
}
|
|
2828
|
-
return trimmed.replace(/^\.\/+/, "");
|
|
2829
|
-
}
|
|
2830
|
-
function createResolveResourceCapability(deps) {
|
|
2831
|
-
const tool = {
|
|
2832
|
-
name: "resolve_resource",
|
|
2833
|
-
label: "resolve_resource",
|
|
2834
|
-
description: "Resolve a relative skill resource to a host executable/readable path without loading its content.",
|
|
2835
|
-
parameters: ResolveResourceParamsSchema,
|
|
2836
|
-
async execute(toolCallIdOrArgs, maybeArgs) {
|
|
2837
|
-
const args = typeof toolCallIdOrArgs === "string" ? maybeArgs : toolCallIdOrArgs;
|
|
2838
|
-
const owner = String(args?.owner || "").trim();
|
|
2839
|
-
let resourcePath = "";
|
|
2840
|
-
try {
|
|
2841
|
-
resourcePath = validateRelativeResourcePath2(args?.path || "");
|
|
2842
|
-
} catch (error) {
|
|
2843
|
-
return {
|
|
2844
|
-
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
2845
|
-
details: { found: false, owner: owner || void 0, path: args?.path || void 0 },
|
|
2846
|
-
isError: true
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
const resource = await deps.resolveResource?.(owner, resourcePath);
|
|
2850
|
-
if (!resource?.resolvedPath) {
|
|
2851
|
-
return {
|
|
2852
|
-
content: [{ type: "text", text: `Resource not found: ${owner || "(empty)"}/${resourcePath || "(empty)"}` }],
|
|
2853
|
-
details: { found: false, owner: owner || void 0, path: resourcePath || void 0 },
|
|
2854
|
-
isError: true
|
|
2855
|
-
};
|
|
2856
|
-
}
|
|
2857
|
-
return {
|
|
2858
|
-
content: [{
|
|
2859
|
-
type: "text",
|
|
2860
|
-
text: [
|
|
2861
|
-
"Resolved skill resource:",
|
|
2862
|
-
`owner: ${resource.owner}`,
|
|
2863
|
-
`path: ${resource.path}`,
|
|
2864
|
-
`resolvedPath: ${resource.resolvedPath}`
|
|
2865
|
-
].join("\n")
|
|
2866
|
-
}],
|
|
2867
|
-
details: {
|
|
2868
|
-
found: true,
|
|
2869
|
-
owner: resource.owner,
|
|
2870
|
-
path: resource.path,
|
|
2871
|
-
kind: resource.kind || inferKind2(resource.path),
|
|
2872
|
-
resolvedPath: resource.resolvedPath
|
|
2873
|
-
}
|
|
2874
|
-
};
|
|
2875
|
-
}
|
|
2876
|
-
};
|
|
2877
|
-
return {
|
|
2878
|
-
...defineSharedCapability({
|
|
2879
|
-
id: "resolve-resource",
|
|
2880
|
-
description: "Shared capability for resolving a skill resource to a host path without loading its contents.",
|
|
2881
|
-
tools: [tool]
|
|
2882
|
-
}),
|
|
2883
|
-
tool
|
|
2884
|
-
};
|
|
2885
|
-
}
|
|
2886
|
-
|
|
2887
2564
|
// ../../packages/shared-headless-capabilities/src/session-trace.ts
|
|
2888
2565
|
function isRecord(value) {
|
|
2889
2566
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -2914,9 +2591,20 @@ function summarizeToolResultDetails(result) {
|
|
|
2914
2591
|
"failureCategory",
|
|
2915
2592
|
"fallbackReason",
|
|
2916
2593
|
"qualityWarnings",
|
|
2594
|
+
"qualityScore",
|
|
2917
2595
|
"sourceCount",
|
|
2596
|
+
"sources",
|
|
2597
|
+
"fetchedSources",
|
|
2598
|
+
"browserRequiredSources",
|
|
2918
2599
|
"actualQueries",
|
|
2919
2600
|
"attempts",
|
|
2601
|
+
"providerAttempts",
|
|
2602
|
+
"providerHealth",
|
|
2603
|
+
"routeRankReason",
|
|
2604
|
+
"latencyMetrics",
|
|
2605
|
+
"costEstimate",
|
|
2606
|
+
"browserReadSummary",
|
|
2607
|
+
"guardFindings",
|
|
2920
2608
|
"timedOut"
|
|
2921
2609
|
];
|
|
2922
2610
|
const summary = {};
|
|
@@ -2939,6 +2627,54 @@ function summarizeToolResultDetails(result) {
|
|
|
2939
2627
|
});
|
|
2940
2628
|
continue;
|
|
2941
2629
|
}
|
|
2630
|
+
if ((key === "sources" || key === "fetchedSources" || key === "browserRequiredSources") && Array.isArray(value)) {
|
|
2631
|
+
summary[key] = {
|
|
2632
|
+
count: value.length,
|
|
2633
|
+
items: value.slice(0, 5).map((entry) => {
|
|
2634
|
+
if (!isRecord(entry)) return entry;
|
|
2635
|
+
return {
|
|
2636
|
+
url: entry.finalUrl || entry.url,
|
|
2637
|
+
title: entry.title,
|
|
2638
|
+
score: entry.score,
|
|
2639
|
+
requiresBrowser: entry.requiresBrowser,
|
|
2640
|
+
failureCategory: entry.failureCategory,
|
|
2641
|
+
extractionMethod: entry.extractionMethod,
|
|
2642
|
+
browserReadSummary: entry.browserReadSummary
|
|
2643
|
+
};
|
|
2644
|
+
})
|
|
2645
|
+
};
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
if (key === "providerHealth" && Array.isArray(value)) {
|
|
2649
|
+
summary[key] = value.slice(0, 5).map((entry) => {
|
|
2650
|
+
if (!isRecord(entry)) return entry;
|
|
2651
|
+
return {
|
|
2652
|
+
routeKey: entry.routeKey,
|
|
2653
|
+
attempts: entry.attempts,
|
|
2654
|
+
successes: entry.successes,
|
|
2655
|
+
failures: entry.failures,
|
|
2656
|
+
lowQuality: entry.lowQuality,
|
|
2657
|
+
rateLimited: entry.rateLimited,
|
|
2658
|
+
timeouts: entry.timeouts
|
|
2659
|
+
};
|
|
2660
|
+
});
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
if (key === "providerAttempts" && Array.isArray(value)) {
|
|
2664
|
+
summary[key] = value.slice(-5).map((attempt) => {
|
|
2665
|
+
if (!isRecord(attempt)) return attempt;
|
|
2666
|
+
return {
|
|
2667
|
+
provider: attempt.provider,
|
|
2668
|
+
modelId: attempt.modelId,
|
|
2669
|
+
providerMode: attempt.providerMode,
|
|
2670
|
+
routeSource: attempt.routeSource,
|
|
2671
|
+
failureCategory: attempt.failureCategory,
|
|
2672
|
+
sourceCount: attempt.sourceCount,
|
|
2673
|
+
qualityWarnings: attempt.qualityWarnings
|
|
2674
|
+
};
|
|
2675
|
+
});
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2942
2678
|
summary[key] = typeof value === "string" && value.length > 300 ? `${value.slice(0, 300)}...` : value;
|
|
2943
2679
|
}
|
|
2944
2680
|
return Object.keys(summary).length > 0 ? summary : void 0;
|
|
@@ -3636,12 +3372,9 @@ function formatSkillSummariesForPrompt(skills) {
|
|
|
3636
3372
|
if (skills.length === 0) return "";
|
|
3637
3373
|
const lines = [
|
|
3638
3374
|
"\n\nThe following skills provide specialized instructions for specific tasks.",
|
|
3639
|
-
"Use
|
|
3640
|
-
"
|
|
3641
|
-
"Skills
|
|
3642
|
-
"When read_skill returns skill-local resources in details.resources, use read_resource(owner, path) for documents or config files that you need to inspect.",
|
|
3643
|
-
"When read_skill returns a script or executable resource in details.resources, use resolve_resource(owner, path) before execution.",
|
|
3644
|
-
"Do not guess host filesystem paths for skill-local resources.",
|
|
3375
|
+
"Use read_file to load a skill's entry file when the task matches its description.",
|
|
3376
|
+
"When a skill file references a relative path, resolve it against that skill's baseDir and use the resulting ordinary path with file or bash tools.",
|
|
3377
|
+
"Skills are ordinary files in the current runtime filesystem; do not use special skill/resource tool protocols.",
|
|
3645
3378
|
"",
|
|
3646
3379
|
"<available_skills>"
|
|
3647
3380
|
];
|
|
@@ -3649,6 +3382,12 @@ function formatSkillSummariesForPrompt(skills) {
|
|
|
3649
3382
|
lines.push(" <skill>");
|
|
3650
3383
|
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
3651
3384
|
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
3385
|
+
if (skill.filePath) {
|
|
3386
|
+
lines.push(` <entry>${escapeXml(skill.filePath)}</entry>`);
|
|
3387
|
+
}
|
|
3388
|
+
if (skill.baseDir) {
|
|
3389
|
+
lines.push(` <baseDir>${escapeXml(skill.baseDir)}</baseDir>`);
|
|
3390
|
+
}
|
|
3652
3391
|
lines.push(" </skill>");
|
|
3653
3392
|
}
|
|
3654
3393
|
lines.push("</available_skills>");
|
|
@@ -4875,8 +4614,14 @@ function createPolicyEnforcedTools(tools, options = {}) {
|
|
|
4875
4614
|
return tools.map((tool) => createPolicyEnforcedTool(tool, options));
|
|
4876
4615
|
}
|
|
4877
4616
|
|
|
4617
|
+
// ../../packages/shared-headless-capabilities/src/builtin/fs/read.ts
|
|
4618
|
+
import * as nodePath from "node:path";
|
|
4619
|
+
|
|
4878
4620
|
// ../../packages/shared-headless-capabilities/src/builtin/fs/path-utils.ts
|
|
4879
4621
|
function isInAllowlist(path3, allowlistedDirs) {
|
|
4622
|
+
if (allowlistedDirs.some((dir) => isGlobalFilesystemSentinel(dir))) {
|
|
4623
|
+
return true;
|
|
4624
|
+
}
|
|
4880
4625
|
const normalized = normalizeForComparison(path3);
|
|
4881
4626
|
for (const dir of allowlistedDirs) {
|
|
4882
4627
|
const normalizedDir = normalizeForComparison(dir);
|
|
@@ -4888,9 +4633,19 @@ function isInAllowlist(path3, allowlistedDirs) {
|
|
|
4888
4633
|
return false;
|
|
4889
4634
|
}
|
|
4890
4635
|
function normalizeForComparison(path3) {
|
|
4891
|
-
const
|
|
4636
|
+
const slashNormalized = path3.replace(/\\/g, "/");
|
|
4637
|
+
const isWindowsPath = /^[a-zA-Z]:($|\/)/.test(slashNormalized) || /^\/{2}[^/]+\/[^/]+/.test(slashNormalized);
|
|
4638
|
+
const normalized = slashNormalized.replace(/\/+/g, "/");
|
|
4892
4639
|
if (normalized === "/") return normalized;
|
|
4893
|
-
|
|
4640
|
+
const trimmed = normalized.replace(/\/+$/, "");
|
|
4641
|
+
return isWindowsPath ? trimmed.toLowerCase() : trimmed;
|
|
4642
|
+
}
|
|
4643
|
+
function isGlobalFilesystemSentinel(path3) {
|
|
4644
|
+
return normalizeForComparison(String(path3 || "").trim()) === "/";
|
|
4645
|
+
}
|
|
4646
|
+
function isAbsoluteUserPath(path3) {
|
|
4647
|
+
const normalized = path3.replace(/\\/g, "/");
|
|
4648
|
+
return /^[a-zA-Z]:\//.test(normalized) || /^\/{2}[^/]+\/[^/]+/.test(normalized);
|
|
4894
4649
|
}
|
|
4895
4650
|
function validateToolPathArgument(filePath) {
|
|
4896
4651
|
const trimmed = filePath.trim();
|
|
@@ -4908,10 +4663,31 @@ function withTrailingSep(path3) {
|
|
|
4908
4663
|
return path3.endsWith("/") ? path3 : `${path3}/`;
|
|
4909
4664
|
}
|
|
4910
4665
|
function isWithinRoot(path3, root) {
|
|
4666
|
+
if (isGlobalFilesystemSentinel(root)) {
|
|
4667
|
+
return true;
|
|
4668
|
+
}
|
|
4911
4669
|
const normalizedPath = normalizeForComparison(path3);
|
|
4912
4670
|
const normalizedRoot = normalizeForComparison(root);
|
|
4913
4671
|
return normalizedPath === normalizedRoot || normalizedPath.startsWith(withTrailingSep(normalizedRoot));
|
|
4914
4672
|
}
|
|
4673
|
+
function getAccessRoots(sandboxRoot, options) {
|
|
4674
|
+
const fs2 = getFileSystem();
|
|
4675
|
+
const roots = /* @__PURE__ */ new Set();
|
|
4676
|
+
roots.add(fs2.normalize(sandboxRoot));
|
|
4677
|
+
if (options?.accessRoot) {
|
|
4678
|
+
roots.add(fs2.normalize(options.accessRoot));
|
|
4679
|
+
}
|
|
4680
|
+
for (const root of options?.accessRoots ?? []) {
|
|
4681
|
+
const trimmed = String(root || "").trim();
|
|
4682
|
+
if (trimmed) {
|
|
4683
|
+
roots.add(fs2.normalize(trimmed));
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return Array.from(roots);
|
|
4687
|
+
}
|
|
4688
|
+
function isWithinAnyRoot(path3, roots) {
|
|
4689
|
+
return roots.some((root) => isWithinRoot(path3, root));
|
|
4690
|
+
}
|
|
4915
4691
|
function normalizePrefix(prefix) {
|
|
4916
4692
|
const normalized = normalizeForComparison(prefix);
|
|
4917
4693
|
return normalized.startsWith("/") ? normalized.slice(1) : normalized;
|
|
@@ -4992,12 +4768,13 @@ function getResolutionRoot(filePath, sandboxRoot, options, rootName) {
|
|
|
4992
4768
|
function resolveInSandbox(filePath, sandboxRoot, options, rootName) {
|
|
4993
4769
|
const fs2 = getFileSystem();
|
|
4994
4770
|
const allowlistedDirs = options?.allowlistedDirs ?? [];
|
|
4771
|
+
const accessRoots = getAccessRoots(sandboxRoot, options);
|
|
4995
4772
|
if (!filePath) {
|
|
4996
4773
|
throw new Error("Path cannot be empty");
|
|
4997
4774
|
}
|
|
4998
4775
|
validateToolPathArgument(filePath);
|
|
4999
4776
|
const cleaned = filePath.trim().startsWith("@") ? filePath.trim().slice(1) : filePath.trim();
|
|
5000
|
-
if (fs2.isAbsolute(cleaned)) {
|
|
4777
|
+
if (fs2.isAbsolute(cleaned) || isAbsoluteUserPath(cleaned)) {
|
|
5001
4778
|
const normalized = fs2.normalize(cleaned);
|
|
5002
4779
|
const selectedRoot2 = rootName ? getRootByName(rootName, sandboxRoot, options) : void 0;
|
|
5003
4780
|
if (rootName && !selectedRoot2) {
|
|
@@ -5020,7 +4797,7 @@ function resolveInSandbox(filePath, sandboxRoot, options, rootName) {
|
|
|
5020
4797
|
if (isInAllowlist(normalized, allowlistedDirs)) {
|
|
5021
4798
|
return normalized;
|
|
5022
4799
|
}
|
|
5023
|
-
if (!
|
|
4800
|
+
if (!isWithinAnyRoot(normalized, accessRoots)) {
|
|
5024
4801
|
throw new Error(`Absolute path outside sandbox: ${filePath}`);
|
|
5025
4802
|
}
|
|
5026
4803
|
return normalized;
|
|
@@ -5040,7 +4817,7 @@ function resolveInSandbox(filePath, sandboxRoot, options, rootName) {
|
|
|
5040
4817
|
}
|
|
5041
4818
|
return resolved;
|
|
5042
4819
|
}
|
|
5043
|
-
if (!
|
|
4820
|
+
if (!isWithinAnyRoot(resolved, accessRoots) && !isInAllowlist(resolved, allowlistedDirs)) {
|
|
5044
4821
|
throw new Error(`Path escapes sandbox: ${filePath}`);
|
|
5045
4822
|
}
|
|
5046
4823
|
return resolved;
|
|
@@ -5309,6 +5086,10 @@ function createReadTool(cwd, options) {
|
|
|
5309
5086
|
const truncation = truncateHead(selectedContent);
|
|
5310
5087
|
let outputText;
|
|
5311
5088
|
let details;
|
|
5089
|
+
const sourceHeader = `File: ${absolutePath}
|
|
5090
|
+
Base directory: ${nodePath.dirname(absolutePath)}
|
|
5091
|
+
|
|
5092
|
+
`;
|
|
5312
5093
|
if (truncation.firstLineExceedsLimit) {
|
|
5313
5094
|
const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8"));
|
|
5314
5095
|
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit.]`;
|
|
@@ -5337,6 +5118,7 @@ function createReadTool(cwd, options) {
|
|
|
5337
5118
|
} else {
|
|
5338
5119
|
outputText = truncation.content;
|
|
5339
5120
|
}
|
|
5121
|
+
outputText = sourceHeader + outputText;
|
|
5340
5122
|
const textContent = [{ type: "text", text: outputText }];
|
|
5341
5123
|
if (aborted) return;
|
|
5342
5124
|
if (signal) {
|
|
@@ -7473,7 +7255,7 @@ function decodeBytes(bytes, contentType = "") {
|
|
|
7473
7255
|
};
|
|
7474
7256
|
}
|
|
7475
7257
|
}
|
|
7476
|
-
function
|
|
7258
|
+
function extractTitle(html) {
|
|
7477
7259
|
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
7478
7260
|
return match?.[1]?.replace(/\s+/g, " ").trim();
|
|
7479
7261
|
}
|
|
@@ -7533,6 +7315,15 @@ function createTimeoutSignal(parentSignal, timeoutMs) {
|
|
|
7533
7315
|
function assistantText(message) {
|
|
7534
7316
|
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
|
|
7535
7317
|
}
|
|
7318
|
+
async function withAbort(promise, signal) {
|
|
7319
|
+
if (!signal) return promise;
|
|
7320
|
+
if (signal.aborted) throw signal.reason ?? new Error("operation aborted");
|
|
7321
|
+
return await new Promise((resolve2, reject) => {
|
|
7322
|
+
const onAbort = () => reject(signal.reason ?? new Error("operation aborted"));
|
|
7323
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
7324
|
+
promise.then(resolve2, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
7325
|
+
});
|
|
7326
|
+
}
|
|
7536
7327
|
async function applyPromptWithModel(deps, prompt, url, content, signal) {
|
|
7537
7328
|
const model = deps.getModel?.();
|
|
7538
7329
|
const apiKey = deps.getApiKey?.();
|
|
@@ -7548,24 +7339,27 @@ async function applyPromptWithModel(deps, prompt, url, content, signal) {
|
|
|
7548
7339
|
};
|
|
7549
7340
|
}
|
|
7550
7341
|
try {
|
|
7551
|
-
const response = await
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7342
|
+
const response = await withAbort(
|
|
7343
|
+
completeSimple(
|
|
7344
|
+
model,
|
|
7345
|
+
{
|
|
7346
|
+
systemPrompt: "You are a web_fetch extraction tool. Answer only from the fetched page content. Include uncertainty if the page does not contain the requested information.",
|
|
7347
|
+
messages: [{
|
|
7348
|
+
role: "user",
|
|
7349
|
+
timestamp: Date.now(),
|
|
7350
|
+
content: [{
|
|
7351
|
+
type: "text",
|
|
7352
|
+
text: [`URL: ${url}`, `Task: ${prompt}`, "", "Fetched content:", content].join("\n")
|
|
7353
|
+
}]
|
|
7561
7354
|
}]
|
|
7562
|
-
}
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7355
|
+
},
|
|
7356
|
+
{
|
|
7357
|
+
apiKey,
|
|
7358
|
+
maxTokens: Math.min(model.maxTokens || 4096, 4096),
|
|
7359
|
+
signal
|
|
7360
|
+
}
|
|
7361
|
+
),
|
|
7362
|
+
signal
|
|
7569
7363
|
);
|
|
7570
7364
|
const text = assistantText(response) || response.errorMessage || "(web_fetch extraction model returned no text)";
|
|
7571
7365
|
return { text, model: modelLabel2(model), isError: response.stopReason === "error" || response.stopReason === "aborted" };
|
|
@@ -7651,7 +7445,7 @@ function createSharedWebFetchTool(deps = {}) {
|
|
|
7651
7445
|
const raw = decoded.text;
|
|
7652
7446
|
const isHtml = /html/i.test(contentType) || /<\/?[a-z][\s\S]*>/i.test(raw.slice(0, 2e3));
|
|
7653
7447
|
const isPdf = /pdf/i.test(contentType) || raw.startsWith("%PDF");
|
|
7654
|
-
const title = isHtml ?
|
|
7448
|
+
const title = isHtml ? extractTitle(raw) : void 0;
|
|
7655
7449
|
const htmlExtraction = isHtml ? htmlToReadableMarkdown(raw) : void 0;
|
|
7656
7450
|
let text = htmlExtraction?.text ?? (isPdf ? compactPdfText(raw) : raw.trim());
|
|
7657
7451
|
const extractionMethod = isHtml ? "html_readability" : isPdf ? "pdf_text" : "plain_text";
|
|
@@ -7728,6 +7522,621 @@ function createSharedWebFetchTool(deps = {}) {
|
|
|
7728
7522
|
};
|
|
7729
7523
|
}
|
|
7730
7524
|
|
|
7525
|
+
// ../../packages/shared-headless-capabilities/src/web-research.ts
|
|
7526
|
+
var webResearchSchema = Type.Object({
|
|
7527
|
+
query: Type.String({
|
|
7528
|
+
minLength: 2,
|
|
7529
|
+
description: "The research question or topic. Use for end-to-end public web research."
|
|
7530
|
+
}),
|
|
7531
|
+
allowed_domains: Type.Optional(Type.Array(Type.String({
|
|
7532
|
+
description: "Only include sources from these domains. Use only when the user explicitly asks to limit sources."
|
|
7533
|
+
}))),
|
|
7534
|
+
blocked_domains: Type.Optional(Type.Array(Type.String({
|
|
7535
|
+
description: "Exclude sources from these domains. Use only when the user explicitly asks to avoid sources."
|
|
7536
|
+
}))),
|
|
7537
|
+
max_sources: Type.Optional(Type.Number({
|
|
7538
|
+
minimum: 1,
|
|
7539
|
+
maximum: 8,
|
|
7540
|
+
description: "Maximum credible sources to fetch and cite. Defaults to 3."
|
|
7541
|
+
}))
|
|
7542
|
+
});
|
|
7543
|
+
var DEFAULT_WEB_RESEARCH_SEARCH_ATTEMPT_TIMEOUT_MS = 45e3;
|
|
7544
|
+
var DEFAULT_WEB_RESEARCH_FETCH_ATTEMPT_TIMEOUT_MS = 3e4;
|
|
7545
|
+
var DEFAULT_WEB_RESEARCH_BROWSER_ATTEMPT_TIMEOUT_MS = 45e3;
|
|
7546
|
+
function isRecord3(value) {
|
|
7547
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
7548
|
+
}
|
|
7549
|
+
function textFromResult(result) {
|
|
7550
|
+
return result.content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim();
|
|
7551
|
+
}
|
|
7552
|
+
function clamp(value, min, max) {
|
|
7553
|
+
return Math.min(max, Math.max(min, value));
|
|
7554
|
+
}
|
|
7555
|
+
function normalizeHost(hostname) {
|
|
7556
|
+
return hostname.toLowerCase().replace(/^www\./, "");
|
|
7557
|
+
}
|
|
7558
|
+
function parseResearchUrl(value) {
|
|
7559
|
+
const trimmed = value.trim();
|
|
7560
|
+
const match = /^(https?):\/\/([^/?#]+)([^?#]*)?(\?[^#]*)?(#.*)?$/i.exec(trimmed);
|
|
7561
|
+
if (!match) return null;
|
|
7562
|
+
const protocol = match[1].toLowerCase();
|
|
7563
|
+
const rawHost = match[2].replace(/^[^@]+@/, "");
|
|
7564
|
+
const hostname = rawHost.startsWith("[") ? rawHost : rawHost.split(":")[0];
|
|
7565
|
+
const pathname = match[3] && match[3].startsWith("/") ? match[3] : "/";
|
|
7566
|
+
const search = match[4] || "";
|
|
7567
|
+
const href = `${protocol}://${rawHost}${pathname}${search}`;
|
|
7568
|
+
return {
|
|
7569
|
+
href,
|
|
7570
|
+
hostname,
|
|
7571
|
+
pathname,
|
|
7572
|
+
search,
|
|
7573
|
+
hasSearchParam(name) {
|
|
7574
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7575
|
+
return new RegExp(`(?:^\\?|&)${escaped}(?:=|&|$)`, "i").test(search);
|
|
7576
|
+
}
|
|
7577
|
+
};
|
|
7578
|
+
}
|
|
7579
|
+
function sourceKey(url) {
|
|
7580
|
+
const parsed = parseResearchUrl(url);
|
|
7581
|
+
if (!parsed) return url.trim();
|
|
7582
|
+
return parsed.href.replace(/\/$/, "");
|
|
7583
|
+
}
|
|
7584
|
+
function isSearchPage(url) {
|
|
7585
|
+
const host = normalizeHost(url.hostname);
|
|
7586
|
+
if (/(google|bing|duckduckgo|baidu|yahoo|yandex)\./.test(host)) return true;
|
|
7587
|
+
return /(^|\/)(search|results)(\/|$)/i.test(url.pathname) || url.hasSearchParam("q");
|
|
7588
|
+
}
|
|
7589
|
+
function isHomepage(url) {
|
|
7590
|
+
return (url.pathname === "" || url.pathname === "/") && !url.search;
|
|
7591
|
+
}
|
|
7592
|
+
function looksOfficial(url) {
|
|
7593
|
+
const host = normalizeHost(url.hostname);
|
|
7594
|
+
return host.startsWith("docs.") || host.startsWith("developer.") || host.startsWith("developers.") || host.startsWith("learn.") || host.startsWith("api.") || /(^|\.)gov$/.test(host) || /(^|\.)edu$/.test(host) || /\/(docs|documentation|reference|guide|guides|api|manual)\b/i.test(url.pathname);
|
|
7595
|
+
}
|
|
7596
|
+
function looksGithubAuthoritative(url) {
|
|
7597
|
+
const host = normalizeHost(url.hostname);
|
|
7598
|
+
if (host !== "github.com") return false;
|
|
7599
|
+
return /\/(releases|issues|pull|wiki|blob|tree|commit)\b/i.test(url.pathname);
|
|
7600
|
+
}
|
|
7601
|
+
function looksNewsAuthority(url) {
|
|
7602
|
+
const host = normalizeHost(url.hostname);
|
|
7603
|
+
return [
|
|
7604
|
+
"apnews.com",
|
|
7605
|
+
"reuters.com",
|
|
7606
|
+
"bbc.com",
|
|
7607
|
+
"pbs.org",
|
|
7608
|
+
"npr.org",
|
|
7609
|
+
"theguardian.com",
|
|
7610
|
+
"nytimes.com",
|
|
7611
|
+
"wsj.com",
|
|
7612
|
+
"chinadaily.com.cn",
|
|
7613
|
+
"xinhuanet.com"
|
|
7614
|
+
].some((domain) => host === domain || host.endsWith(`.${domain}`));
|
|
7615
|
+
}
|
|
7616
|
+
function scoreWebResearchSource(source) {
|
|
7617
|
+
const reasons = [];
|
|
7618
|
+
let score = 45;
|
|
7619
|
+
const url = parseResearchUrl(source.url);
|
|
7620
|
+
if (!url) {
|
|
7621
|
+
score = 0;
|
|
7622
|
+
reasons.push("invalid_url");
|
|
7623
|
+
return { source, score, reasons };
|
|
7624
|
+
}
|
|
7625
|
+
if (looksOfficial(url)) {
|
|
7626
|
+
score += 35;
|
|
7627
|
+
reasons.push("official_or_docs");
|
|
7628
|
+
}
|
|
7629
|
+
if (looksGithubAuthoritative(url)) {
|
|
7630
|
+
score += 25;
|
|
7631
|
+
reasons.push("github_primary_source");
|
|
7632
|
+
}
|
|
7633
|
+
if (looksNewsAuthority(url)) {
|
|
7634
|
+
score += 20;
|
|
7635
|
+
reasons.push("news_authority");
|
|
7636
|
+
}
|
|
7637
|
+
if (isHomepage(url)) {
|
|
7638
|
+
score -= 35;
|
|
7639
|
+
reasons.push("homepage");
|
|
7640
|
+
}
|
|
7641
|
+
if (isSearchPage(url)) {
|
|
7642
|
+
score -= 60;
|
|
7643
|
+
reasons.push("search_page");
|
|
7644
|
+
}
|
|
7645
|
+
if (!source.title?.trim()) {
|
|
7646
|
+
score -= 15;
|
|
7647
|
+
reasons.push("missing_title");
|
|
7648
|
+
}
|
|
7649
|
+
if (/\.pdf($|\?)/i.test(url.pathname)) {
|
|
7650
|
+
score += 10;
|
|
7651
|
+
reasons.push("pdf_document");
|
|
7652
|
+
}
|
|
7653
|
+
return { source, score: clamp(score, 0, 100), reasons };
|
|
7654
|
+
}
|
|
7655
|
+
function dedupeSources(sources) {
|
|
7656
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7657
|
+
const deduped = [];
|
|
7658
|
+
for (const source of sources) {
|
|
7659
|
+
if (!source.url) continue;
|
|
7660
|
+
const key = sourceKey(source.url);
|
|
7661
|
+
if (seen.has(key)) continue;
|
|
7662
|
+
seen.add(key);
|
|
7663
|
+
deduped.push(source);
|
|
7664
|
+
}
|
|
7665
|
+
return deduped;
|
|
7666
|
+
}
|
|
7667
|
+
function hasChinese(text) {
|
|
7668
|
+
return /[\u3400-\u9fff]/.test(text);
|
|
7669
|
+
}
|
|
7670
|
+
function isRecentQuery(text) {
|
|
7671
|
+
return /\b(today|latest|current|recent|news|202[0-9]|release|changelog)\b/i.test(text) || /(今天|最新|新闻|最近|当前|今日|发布|更新)/.test(text);
|
|
7672
|
+
}
|
|
7673
|
+
function isDocsQuery(text) {
|
|
7674
|
+
return /\b(docs?|documentation|api|reference|guide|manual|release notes?|changelog|github|issue)\b/i.test(text) || /(文档|官方|接口|指南|发布说明|更新日志|issue|问题)/i.test(text);
|
|
7675
|
+
}
|
|
7676
|
+
function planWebResearchQueries(query, dateContext) {
|
|
7677
|
+
const trimmed = query.trim();
|
|
7678
|
+
const variants = /* @__PURE__ */ new Set([trimmed]);
|
|
7679
|
+
const year = dateContext?.currentDate?.slice(0, 4) || String((/* @__PURE__ */ new Date()).getFullYear());
|
|
7680
|
+
if (isRecentQuery(trimmed)) {
|
|
7681
|
+
variants.add(`${trimmed} ${year}`);
|
|
7682
|
+
}
|
|
7683
|
+
if (isDocsQuery(trimmed)) {
|
|
7684
|
+
variants.add(`${trimmed} official documentation`);
|
|
7685
|
+
variants.add(`${trimmed} GitHub release changelog issue`);
|
|
7686
|
+
} else {
|
|
7687
|
+
variants.add(`${trimmed} official sources documentation GitHub release changelog`);
|
|
7688
|
+
}
|
|
7689
|
+
if (hasChinese(trimmed)) {
|
|
7690
|
+
variants.add(`${trimmed} \u5B98\u65B9 \u6587\u6863 \u6765\u6E90`);
|
|
7691
|
+
if (isRecentQuery(trimmed)) variants.add(`${trimmed} ${year} today news`);
|
|
7692
|
+
}
|
|
7693
|
+
return [...variants].slice(0, 5);
|
|
7694
|
+
}
|
|
7695
|
+
function routeKeyFromAttempt(attempt) {
|
|
7696
|
+
if (!attempt.provider && !attempt.modelId && !attempt.providerMode) return void 0;
|
|
7697
|
+
return `${attempt.provider || "unknown"}/${attempt.modelId || "unknown"}:${attempt.providerMode}`;
|
|
7698
|
+
}
|
|
7699
|
+
function routeKeyFromDecision(route) {
|
|
7700
|
+
const model = route.model;
|
|
7701
|
+
const providerMode = model?.webSearch?.type || "unavailable";
|
|
7702
|
+
return `${model?.provider || "unknown"}/${model?.id || "unknown"}:${providerMode}`;
|
|
7703
|
+
}
|
|
7704
|
+
function healthPenalty(health) {
|
|
7705
|
+
if (!health || health.attempts === 0) return 0;
|
|
7706
|
+
const failureRate = health.failures / health.attempts;
|
|
7707
|
+
const lowQualityRate = health.lowQuality / health.attempts;
|
|
7708
|
+
const rateLimitRate = health.rateLimited / health.attempts;
|
|
7709
|
+
return failureRate * 40 + lowQualityRate * 25 + rateLimitRate * 25;
|
|
7710
|
+
}
|
|
7711
|
+
function sortCandidatesByHealth(candidates, healthEntries) {
|
|
7712
|
+
if (!healthEntries?.length) return candidates;
|
|
7713
|
+
const healthByKey = new Map(healthEntries.map((entry) => [entry.routeKey, entry]));
|
|
7714
|
+
return [...candidates].sort((a, b) => healthPenalty(healthByKey.get(routeKeyFromDecision(a))) - healthPenalty(healthByKey.get(routeKeyFromDecision(b))));
|
|
7715
|
+
}
|
|
7716
|
+
function describeRouteRanking(candidates, healthEntries) {
|
|
7717
|
+
if (!candidates.length) return ["no configured web_search route candidates"];
|
|
7718
|
+
const healthByKey = new Map((healthEntries || []).map((entry) => [entry.routeKey, entry]));
|
|
7719
|
+
return candidates.slice(0, 5).map((candidate, index) => {
|
|
7720
|
+
const key = routeKeyFromDecision(candidate);
|
|
7721
|
+
const health = healthByKey.get(key);
|
|
7722
|
+
if (!health || health.attempts === 0) return `${index + 1}. ${key}: no prior health data`;
|
|
7723
|
+
const successRate = Math.round(health.successes / Math.max(1, health.attempts) * 100);
|
|
7724
|
+
const lowQualityRate = Math.round(health.lowQuality / Math.max(1, health.attempts) * 100);
|
|
7725
|
+
const medianLatencyMs = Math.round(health.totalDurationMs / Math.max(1, health.attempts));
|
|
7726
|
+
return `${index + 1}. ${key}: success=${successRate}%, lowQuality=${lowQualityRate}%, avgLatencyMs=${medianLatencyMs}, penalty=${Math.round(healthPenalty(health))}`;
|
|
7727
|
+
});
|
|
7728
|
+
}
|
|
7729
|
+
function updateProviderHealth(current, attempts) {
|
|
7730
|
+
const byKey = new Map((current || []).map((entry) => [entry.routeKey, { ...entry }]));
|
|
7731
|
+
for (const attempt of attempts) {
|
|
7732
|
+
const key = routeKeyFromAttempt(attempt);
|
|
7733
|
+
if (!key) continue;
|
|
7734
|
+
const entry = byKey.get(key) ?? {
|
|
7735
|
+
routeKey: key,
|
|
7736
|
+
attempts: 0,
|
|
7737
|
+
successes: 0,
|
|
7738
|
+
failures: 0,
|
|
7739
|
+
lowQuality: 0,
|
|
7740
|
+
rateLimited: 0,
|
|
7741
|
+
timeouts: 0,
|
|
7742
|
+
totalDurationMs: 0,
|
|
7743
|
+
totalSources: 0,
|
|
7744
|
+
updatedAt: Date.now()
|
|
7745
|
+
};
|
|
7746
|
+
entry.attempts += 1;
|
|
7747
|
+
entry.totalDurationMs += Math.round(attempt.durationSeconds * 1e3);
|
|
7748
|
+
entry.totalSources += attempt.sourceCount || 0;
|
|
7749
|
+
if (attempt.failureCategory) entry.failures += 1;
|
|
7750
|
+
else entry.successes += 1;
|
|
7751
|
+
if (attempt.failureCategory === "low_quality_result" || attempt.qualityWarnings?.length) entry.lowQuality += 1;
|
|
7752
|
+
if (attempt.failureCategory === "rate_limited") entry.rateLimited += 1;
|
|
7753
|
+
if (attempt.failureCategory === "timeout") entry.timeouts += 1;
|
|
7754
|
+
entry.updatedAt = Date.now();
|
|
7755
|
+
byKey.set(key, entry);
|
|
7756
|
+
}
|
|
7757
|
+
return [...byKey.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 20);
|
|
7758
|
+
}
|
|
7759
|
+
function contentDetails(result) {
|
|
7760
|
+
return isRecord3(result.details) ? result.details : {};
|
|
7761
|
+
}
|
|
7762
|
+
function excerpt(text, max = 1400) {
|
|
7763
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
7764
|
+
return clean.length > max ? `${clean.slice(0, max)}...` : clean;
|
|
7765
|
+
}
|
|
7766
|
+
function qualityScore(fetched, scored) {
|
|
7767
|
+
if (!fetched.length) return 0;
|
|
7768
|
+
const fetchScore = Math.min(70, fetched.length * 25);
|
|
7769
|
+
const averageSourceScore = fetched.length ? fetched.reduce((sum, item) => sum + item.score, 0) / fetched.length : scored.length ? scored.slice(0, Math.max(1, fetched.length)).reduce((sum, item) => sum + item.score, 0) / Math.max(1, Math.min(scored.length, fetched.length)) : 0;
|
|
7770
|
+
const citationBonus = fetched.some((item) => item.citationAnchors?.length) ? 10 : 0;
|
|
7771
|
+
return clamp(Math.round(fetchScore + averageSourceScore * 0.25 + citationBonus), 0, 100);
|
|
7772
|
+
}
|
|
7773
|
+
function percentile(values, p) {
|
|
7774
|
+
const sorted = values.filter((value) => Number.isFinite(value)).sort((a, b) => a - b);
|
|
7775
|
+
if (!sorted.length) return void 0;
|
|
7776
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1));
|
|
7777
|
+
return sorted[index];
|
|
7778
|
+
}
|
|
7779
|
+
function formatResearchPack(query, fetchedSources, warnings, browserRequiredSources) {
|
|
7780
|
+
const lines = [
|
|
7781
|
+
`Web research pack for: ${query}`,
|
|
7782
|
+
"",
|
|
7783
|
+
"Use the fetched sources below for the final answer. Do not rely on snippets alone.",
|
|
7784
|
+
"If the fetched sources answer the user's request, answer directly; do not run another web_search or web_fetch loop for the same request.",
|
|
7785
|
+
"",
|
|
7786
|
+
"Fetched sources:"
|
|
7787
|
+
];
|
|
7788
|
+
fetchedSources.forEach((source, index) => {
|
|
7789
|
+
lines.push(`${index + 1}. ${source.title || source.finalUrl || source.url}`);
|
|
7790
|
+
lines.push(` URL: ${source.finalUrl || source.url}`);
|
|
7791
|
+
lines.push(` Excerpt: ${source.textExcerpt || "(no excerpt)"}`);
|
|
7792
|
+
});
|
|
7793
|
+
if (browserRequiredSources.length) {
|
|
7794
|
+
lines.push("");
|
|
7795
|
+
lines.push("Browser-required sources:");
|
|
7796
|
+
for (const source of browserRequiredSources) {
|
|
7797
|
+
lines.push(`- ${source.finalUrl || source.url}`);
|
|
7798
|
+
}
|
|
7799
|
+
}
|
|
7800
|
+
if (warnings.length) {
|
|
7801
|
+
lines.push("");
|
|
7802
|
+
lines.push("Warnings:");
|
|
7803
|
+
for (const warning of warnings) lines.push(`- ${warning}`);
|
|
7804
|
+
}
|
|
7805
|
+
lines.push("");
|
|
7806
|
+
lines.push("Sources:");
|
|
7807
|
+
for (const source of fetchedSources) lines.push(`- ${source.finalUrl || source.url}`);
|
|
7808
|
+
return lines.join("\n");
|
|
7809
|
+
}
|
|
7810
|
+
function createChildSignal(parentSignal) {
|
|
7811
|
+
const controller = new AbortController();
|
|
7812
|
+
const onAbort = () => controller.abort(parentSignal?.reason);
|
|
7813
|
+
if (parentSignal?.aborted) onAbort();
|
|
7814
|
+
else parentSignal?.addEventListener("abort", onAbort, { once: true });
|
|
7815
|
+
return {
|
|
7816
|
+
signal: controller.signal,
|
|
7817
|
+
abort: (reason) => controller.abort(reason),
|
|
7818
|
+
dispose: () => parentSignal?.removeEventListener("abort", onAbort)
|
|
7819
|
+
};
|
|
7820
|
+
}
|
|
7821
|
+
async function runWithDeadline(operation, timeoutMs, message, parentSignal) {
|
|
7822
|
+
const child = createChildSignal(parentSignal);
|
|
7823
|
+
let timer;
|
|
7824
|
+
try {
|
|
7825
|
+
return await new Promise((resolve2, reject) => {
|
|
7826
|
+
timer = setTimeout(() => {
|
|
7827
|
+
const error = new Error(message);
|
|
7828
|
+
child.abort(error);
|
|
7829
|
+
reject(error);
|
|
7830
|
+
}, timeoutMs);
|
|
7831
|
+
operation(child.signal).then(resolve2, reject);
|
|
7832
|
+
});
|
|
7833
|
+
} finally {
|
|
7834
|
+
if (timer) clearTimeout(timer);
|
|
7835
|
+
child.dispose();
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
function createSharedWebResearchTool(deps) {
|
|
7839
|
+
const initialHealth = deps.loadProviderHealth?.();
|
|
7840
|
+
const initialCandidates = deps.resolveToolModelCandidates?.("web_search") ?? [];
|
|
7841
|
+
const webSearchTool = deps.webSearchTool ?? createSharedWebSearchTool({
|
|
7842
|
+
...deps,
|
|
7843
|
+
resolveToolModelCandidates: (purpose) => sortCandidatesByHealth(deps.resolveToolModelCandidates?.(purpose) ?? [], deps.loadProviderHealth?.() ?? initialHealth)
|
|
7844
|
+
});
|
|
7845
|
+
const webFetchTool = deps.webFetchTool ?? createSharedWebFetchTool(deps);
|
|
7846
|
+
return {
|
|
7847
|
+
name: "web_research",
|
|
7848
|
+
label: "web_research",
|
|
7849
|
+
description: "Run end-to-end public web research: plan search queries, search provider-native sources, fetch credible pages, and return a cited research pack. Use for general online research, current information, official docs, news, releases, issues, and webpage summaries. Use web_search/web_fetch directly only when you need precise low-level control. Do not use bash/curl/Python as a web research fallback.",
|
|
7850
|
+
parameters: webResearchSchema,
|
|
7851
|
+
capabilityMetadata: {
|
|
7852
|
+
riskClass: "network",
|
|
7853
|
+
concurrencySafe: true,
|
|
7854
|
+
permissionScope: "network",
|
|
7855
|
+
readsNetwork: true,
|
|
7856
|
+
availableInPlanMode: true,
|
|
7857
|
+
allowedForSubagentByDefault: true,
|
|
7858
|
+
maxOutputChars: 1e5
|
|
7859
|
+
},
|
|
7860
|
+
execute: async (_toolCallId, rawParams, signal, onUpdate) => {
|
|
7861
|
+
const input = rawParams;
|
|
7862
|
+
const totalStartedAt = Date.now();
|
|
7863
|
+
const currentHealth = deps.loadProviderHealth?.() ?? initialHealth;
|
|
7864
|
+
const activeCandidates = deps.resolveToolModelCandidates?.("web_search") ?? initialCandidates;
|
|
7865
|
+
const rankedCandidates = sortCandidatesByHealth(activeCandidates, currentHealth);
|
|
7866
|
+
const routeRankReason = describeRouteRanking(rankedCandidates.length ? rankedCandidates : activeCandidates, currentHealth);
|
|
7867
|
+
const query = String(input.query || "").trim();
|
|
7868
|
+
const maxSources = clamp(Math.floor(input.max_sources ?? 3), 1, 8);
|
|
7869
|
+
const searchAttemptTimeoutMs = Math.max(1e3, deps.searchAttemptTimeoutMs ?? deps.timeoutMs ?? DEFAULT_WEB_RESEARCH_SEARCH_ATTEMPT_TIMEOUT_MS);
|
|
7870
|
+
const fetchAttemptTimeoutMs = Math.max(1e3, deps.fetchAttemptTimeoutMs ?? DEFAULT_WEB_RESEARCH_FETCH_ATTEMPT_TIMEOUT_MS);
|
|
7871
|
+
const browserAttemptTimeoutMs = Math.max(1e3, deps.browserAttemptTimeoutMs ?? DEFAULT_WEB_RESEARCH_BROWSER_ATTEMPT_TIMEOUT_MS);
|
|
7872
|
+
const attempts = [];
|
|
7873
|
+
const providerAttempts = [];
|
|
7874
|
+
const warnings = [];
|
|
7875
|
+
const sourceReadDurations = [];
|
|
7876
|
+
const actualQueries = [];
|
|
7877
|
+
const allSources = [];
|
|
7878
|
+
const dateContext = deps.getCurrentDateContext?.();
|
|
7879
|
+
const queryVariants = planWebResearchQueries(query, dateContext);
|
|
7880
|
+
const minimumSearchQueries = 1;
|
|
7881
|
+
let searchedQueries = 0;
|
|
7882
|
+
let searchDurationMs = 0;
|
|
7883
|
+
let fetchDurationMs = 0;
|
|
7884
|
+
let browserDurationMs = 0;
|
|
7885
|
+
if (!query) {
|
|
7886
|
+
return {
|
|
7887
|
+
content: [{ type: "text", text: "web_research requires a non-empty query." }],
|
|
7888
|
+
details: {
|
|
7889
|
+
query,
|
|
7890
|
+
actualQueries: [],
|
|
7891
|
+
sources: [],
|
|
7892
|
+
fetchedSources: [],
|
|
7893
|
+
providerAttempts: [],
|
|
7894
|
+
attempts: [],
|
|
7895
|
+
qualityScore: 0,
|
|
7896
|
+
warnings: ["missing_query"],
|
|
7897
|
+
browserRequiredSources: [],
|
|
7898
|
+
routeRankReason,
|
|
7899
|
+
latencyMetrics: { totalDurationMs: Date.now() - totalStartedAt, searchDurationMs: 0, fetchDurationMs: 0, browserDurationMs: 0 },
|
|
7900
|
+
costEstimate: { status: "unknown", reason: "Provider usage is not yet normalized across web research search/fetch/browser routes." }
|
|
7901
|
+
},
|
|
7902
|
+
isError: true
|
|
7903
|
+
};
|
|
7904
|
+
}
|
|
7905
|
+
for (const variant of queryVariants) {
|
|
7906
|
+
const startedAt = Date.now();
|
|
7907
|
+
const allowedDomains = input.allowed_domains?.length ? input.allowed_domains : void 0;
|
|
7908
|
+
const blockedDomains = allowedDomains ? void 0 : input.blocked_domains;
|
|
7909
|
+
if (allowedDomains && input.blocked_domains?.length && warnings.length === 0) {
|
|
7910
|
+
warnings.push("Both allowed_domains and blocked_domains were provided; web_research used allowed_domains and ignored blocked_domains for provider compatibility.");
|
|
7911
|
+
}
|
|
7912
|
+
const searchInput = {
|
|
7913
|
+
query: variant,
|
|
7914
|
+
allowed_domains: allowedDomains,
|
|
7915
|
+
blocked_domains: blockedDomains
|
|
7916
|
+
};
|
|
7917
|
+
onUpdate?.({
|
|
7918
|
+
content: [{ type: "text", text: `Searching web for: ${variant} (timeout ${Math.ceil(searchAttemptTimeoutMs / 1e3)}s)` }],
|
|
7919
|
+
details: { query, actualQueries: [...actualQueries, variant], attempts, providerAttempts, fetchedSources: [], sources: dedupeSources(allSources), qualityScore: 0, warnings, browserRequiredSources: [] }
|
|
7920
|
+
});
|
|
7921
|
+
let result;
|
|
7922
|
+
try {
|
|
7923
|
+
result = await runWithDeadline(
|
|
7924
|
+
(childSignal) => webSearchTool.execute("web_research_search", searchInput, childSignal),
|
|
7925
|
+
searchAttemptTimeoutMs,
|
|
7926
|
+
`web_research search attempt timed out after ${Math.ceil(searchAttemptTimeoutMs / 1e3)} seconds`,
|
|
7927
|
+
signal
|
|
7928
|
+
);
|
|
7929
|
+
} catch (error) {
|
|
7930
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7931
|
+
attempts.push({
|
|
7932
|
+
phase: "search",
|
|
7933
|
+
query: variant,
|
|
7934
|
+
status: "failed",
|
|
7935
|
+
failureCategory: "timeout",
|
|
7936
|
+
warnings: [message],
|
|
7937
|
+
durationMs: Date.now() - startedAt
|
|
7938
|
+
});
|
|
7939
|
+
searchDurationMs += Date.now() - startedAt;
|
|
7940
|
+
warnings.push(`${variant}: ${message}`);
|
|
7941
|
+
actualQueries.push(variant);
|
|
7942
|
+
break;
|
|
7943
|
+
}
|
|
7944
|
+
const details2 = contentDetails(result);
|
|
7945
|
+
searchDurationMs += Date.now() - startedAt;
|
|
7946
|
+
attempts.push({
|
|
7947
|
+
phase: "search",
|
|
7948
|
+
query: variant,
|
|
7949
|
+
status: result.isError ? "failed" : "completed",
|
|
7950
|
+
failureCategory: details2.failureCategory,
|
|
7951
|
+
warnings: details2.qualityWarnings,
|
|
7952
|
+
durationMs: Date.now() - startedAt
|
|
7953
|
+
});
|
|
7954
|
+
if (details2.actualQueries?.length) actualQueries.push(...details2.actualQueries);
|
|
7955
|
+
else actualQueries.push(variant);
|
|
7956
|
+
if (details2.attempts?.length) providerAttempts.push(...details2.attempts);
|
|
7957
|
+
if (details2.qualityWarnings?.length) warnings.push(...details2.qualityWarnings.map((warning) => `${variant}: ${warning}`));
|
|
7958
|
+
if (Array.isArray(details2.sources)) allSources.push(...details2.sources);
|
|
7959
|
+
searchedQueries += 1;
|
|
7960
|
+
const dedupedSoFar = dedupeSources(allSources);
|
|
7961
|
+
const scoredSoFar = dedupedSoFar.map(scoreWebResearchSource).sort((a, b) => b.score - a.score);
|
|
7962
|
+
const highQualitySources = scoredSoFar.filter((item) => item.score >= 70).length;
|
|
7963
|
+
const searchSourceTarget = Math.min(maxSources, 3);
|
|
7964
|
+
const enoughHighQuality = highQualitySources >= Math.min(searchSourceTarget, 2) || highQualitySources >= 1 && dedupedSoFar.length >= searchSourceTarget * 2 || highQualitySources >= 1 && maxSources === 1;
|
|
7965
|
+
if (searchedQueries >= minimumSearchQueries && enoughHighQuality && !result.isError) {
|
|
7966
|
+
warnings.push(`Fast path used after ${searchedQueries} search quer${searchedQueries === 1 ? "y" : "ies"}: high-quality source target reached.`);
|
|
7967
|
+
break;
|
|
7968
|
+
}
|
|
7969
|
+
if (searchedQueries >= minimumSearchQueries && dedupeSources(allSources).length >= maxSources * 3 && !result.isError) break;
|
|
7970
|
+
}
|
|
7971
|
+
const nextHealth = updateProviderHealth(deps.loadProviderHealth?.() ?? initialHealth, providerAttempts);
|
|
7972
|
+
await deps.saveProviderHealth?.(nextHealth);
|
|
7973
|
+
const dedupedSources = dedupeSources(allSources);
|
|
7974
|
+
const scoredSources = dedupedSources.map(scoreWebResearchSource).sort((a, b) => b.score - a.score);
|
|
7975
|
+
const fetchedSources = [];
|
|
7976
|
+
const browserRequiredSources = [];
|
|
7977
|
+
const minimumFetchedSources = Math.min(maxSources, 2);
|
|
7978
|
+
const targetFetchedSources = Math.min(maxSources, 3);
|
|
7979
|
+
const candidateSources = scoredSources.filter((scored) => {
|
|
7980
|
+
if (scored.score < 35) {
|
|
7981
|
+
warnings.push(`Filtered low-quality source: ${scored.source.url} (${scored.reasons.join(", ") || "low_score"})`);
|
|
7982
|
+
return false;
|
|
7983
|
+
}
|
|
7984
|
+
return true;
|
|
7985
|
+
});
|
|
7986
|
+
const fetchScoredSource = async (scored) => {
|
|
7987
|
+
const startedAt = Date.now();
|
|
7988
|
+
onUpdate?.({
|
|
7989
|
+
content: [{ type: "text", text: `Fetching source: ${scored.source.url}` }],
|
|
7990
|
+
details: { query, actualQueries: [...new Set(actualQueries)], sources: dedupedSources, fetchedSources, providerAttempts, attempts, qualityScore: qualityScore(fetchedSources, scoredSources), warnings, browserRequiredSources }
|
|
7991
|
+
});
|
|
7992
|
+
const fetchResult = await runWithDeadline(
|
|
7993
|
+
(childSignal) => webFetchTool.execute("web_research_fetch", { url: scored.source.url }, childSignal),
|
|
7994
|
+
fetchAttemptTimeoutMs,
|
|
7995
|
+
`web_research fetch attempt timed out after ${Math.ceil(fetchAttemptTimeoutMs / 1e3)} seconds`,
|
|
7996
|
+
signal
|
|
7997
|
+
).catch((error) => ({
|
|
7998
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
7999
|
+
details: {
|
|
8000
|
+
url: scored.source.url,
|
|
8001
|
+
sourceUrl: scored.source.url,
|
|
8002
|
+
durationMs: Date.now() - startedAt,
|
|
8003
|
+
failureCategory: "timeout"
|
|
8004
|
+
},
|
|
8005
|
+
isError: true
|
|
8006
|
+
}));
|
|
8007
|
+
const details2 = contentDetails(fetchResult);
|
|
8008
|
+
const text = textFromResult(fetchResult);
|
|
8009
|
+
fetchDurationMs += Date.now() - startedAt;
|
|
8010
|
+
sourceReadDurations.push(Date.now() - startedAt);
|
|
8011
|
+
attempts.push({
|
|
8012
|
+
phase: "fetch",
|
|
8013
|
+
url: scored.source.url,
|
|
8014
|
+
status: fetchResult.isError ? "failed" : "completed",
|
|
8015
|
+
failureCategory: details2.failureCategory,
|
|
8016
|
+
durationMs: Date.now() - startedAt
|
|
8017
|
+
});
|
|
8018
|
+
if (details2.requiresBrowser) {
|
|
8019
|
+
const browserEntry = {
|
|
8020
|
+
url: scored.source.url,
|
|
8021
|
+
finalUrl: details2.finalUrl,
|
|
8022
|
+
title: details2.title || scored.source.title,
|
|
8023
|
+
score: scored.score,
|
|
8024
|
+
scoreReasons: scored.reasons,
|
|
8025
|
+
requiresBrowser: true,
|
|
8026
|
+
failureCategory: details2.failureCategory
|
|
8027
|
+
};
|
|
8028
|
+
if (deps.renderedPageReader) {
|
|
8029
|
+
let browserStartedAt = Date.now();
|
|
8030
|
+
try {
|
|
8031
|
+
onUpdate?.({
|
|
8032
|
+
content: [{ type: "text", text: `Reading browser-rendered source: ${details2.finalUrl || scored.source.url}` }],
|
|
8033
|
+
details: { query, actualQueries: [...new Set(actualQueries)], sources: dedupedSources, fetchedSources, providerAttempts, attempts, qualityScore: qualityScore(fetchedSources, scoredSources), warnings, browserRequiredSources }
|
|
8034
|
+
});
|
|
8035
|
+
const rendered = await runWithDeadline(
|
|
8036
|
+
(childSignal) => deps.renderedPageReader({ url: details2.finalUrl || scored.source.url, query }, childSignal),
|
|
8037
|
+
browserAttemptTimeoutMs,
|
|
8038
|
+
`web_research browser reader timed out after ${Math.ceil(browserAttemptTimeoutMs / 1e3)} seconds`,
|
|
8039
|
+
signal
|
|
8040
|
+
);
|
|
8041
|
+
browserDurationMs += Date.now() - browserStartedAt;
|
|
8042
|
+
sourceReadDurations.push(Date.now() - browserStartedAt);
|
|
8043
|
+
attempts.push({ phase: "browser", url: rendered.finalUrl || rendered.url, status: "completed", durationMs: Date.now() - browserStartedAt });
|
|
8044
|
+
fetchedSources.push({
|
|
8045
|
+
url: scored.source.url,
|
|
8046
|
+
finalUrl: rendered.finalUrl || rendered.url,
|
|
8047
|
+
title: rendered.title || details2.title || scored.source.title,
|
|
8048
|
+
textExcerpt: excerpt(rendered.text),
|
|
8049
|
+
score: scored.score,
|
|
8050
|
+
scoreReasons: scored.reasons,
|
|
8051
|
+
extractionMethod: "browser_rendered",
|
|
8052
|
+
citationAnchors: rendered.citationAnchors,
|
|
8053
|
+
requiresBrowser: false,
|
|
8054
|
+
browserReadSummary: {
|
|
8055
|
+
screenshotPath: rendered.screenshotPath,
|
|
8056
|
+
consoleMessageCount: rendered.consoleMessages?.length,
|
|
8057
|
+
networkRequestCount: rendered.networkRequests?.length
|
|
8058
|
+
}
|
|
8059
|
+
});
|
|
8060
|
+
return;
|
|
8061
|
+
} catch (error) {
|
|
8062
|
+
browserDurationMs += Date.now() - browserStartedAt;
|
|
8063
|
+
attempts.push({ phase: "browser", url: details2.finalUrl || scored.source.url, status: "failed", failureCategory: "requires_browser", durationMs: Date.now() - browserStartedAt });
|
|
8064
|
+
warnings.push(`Browser reader failed for ${details2.finalUrl || scored.source.url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8065
|
+
}
|
|
8066
|
+
}
|
|
8067
|
+
browserRequiredSources.push(browserEntry);
|
|
8068
|
+
return;
|
|
8069
|
+
}
|
|
8070
|
+
if (fetchResult.isError || !text) {
|
|
8071
|
+
warnings.push(`Could not fetch ${scored.source.url}: ${details2.failureCategory || "fetch_failed"}`);
|
|
8072
|
+
return;
|
|
8073
|
+
}
|
|
8074
|
+
fetchedSources.push({
|
|
8075
|
+
url: scored.source.url,
|
|
8076
|
+
finalUrl: details2.finalUrl || scored.source.url,
|
|
8077
|
+
title: details2.title || scored.source.title,
|
|
8078
|
+
textExcerpt: excerpt(text),
|
|
8079
|
+
score: scored.score,
|
|
8080
|
+
scoreReasons: scored.reasons,
|
|
8081
|
+
extractionMethod: details2.extractionMethod,
|
|
8082
|
+
citationAnchors: details2.citationAnchors,
|
|
8083
|
+
requiresBrowser: false,
|
|
8084
|
+
failureCategory: details2.failureCategory
|
|
8085
|
+
});
|
|
8086
|
+
};
|
|
8087
|
+
for (let index = 0; index < candidateSources.length && fetchedSources.length < maxSources; index += 3) {
|
|
8088
|
+
if (fetchedSources.length >= targetFetchedSources && qualityScore(fetchedSources, scoredSources) >= 75) {
|
|
8089
|
+
warnings.push(`Stopped after ${fetchedSources.length} credible fetched source(s); quality target reached before max_sources=${maxSources}.`);
|
|
8090
|
+
break;
|
|
8091
|
+
}
|
|
8092
|
+
const remaining = maxSources - fetchedSources.length;
|
|
8093
|
+
const batch = candidateSources.slice(index, index + Math.min(3, remaining)).filter((scored) => fetchedSources.length < minimumFetchedSources || scored.score >= 55);
|
|
8094
|
+
if (!batch.length) continue;
|
|
8095
|
+
await Promise.all(batch.map((scored) => fetchScoredSource(scored)));
|
|
8096
|
+
}
|
|
8097
|
+
const score = qualityScore(fetchedSources, scoredSources);
|
|
8098
|
+
const details = {
|
|
8099
|
+
query,
|
|
8100
|
+
actualQueries: [...new Set(actualQueries)],
|
|
8101
|
+
sources: dedupedSources,
|
|
8102
|
+
fetchedSources,
|
|
8103
|
+
providerAttempts,
|
|
8104
|
+
attempts,
|
|
8105
|
+
qualityScore: score,
|
|
8106
|
+
warnings: [...new Set(warnings)],
|
|
8107
|
+
browserRequiredSources,
|
|
8108
|
+
providerHealth: nextHealth,
|
|
8109
|
+
routeRankReason,
|
|
8110
|
+
latencyMetrics: {
|
|
8111
|
+
totalDurationMs: Date.now() - totalStartedAt,
|
|
8112
|
+
searchDurationMs,
|
|
8113
|
+
fetchDurationMs,
|
|
8114
|
+
browserDurationMs,
|
|
8115
|
+
p95SourceReadMs: percentile(sourceReadDurations, 95)
|
|
8116
|
+
},
|
|
8117
|
+
costEstimate: {
|
|
8118
|
+
status: "unknown",
|
|
8119
|
+
reason: "Provider usage is not yet normalized across web research search/fetch/browser routes."
|
|
8120
|
+
}
|
|
8121
|
+
};
|
|
8122
|
+
if (fetchedSources.length === 0) {
|
|
8123
|
+
const reason = browserRequiredSources.length ? "web_research found candidate sources, but they require browser-rendered reading and no host browser reader completed." : "web_research could not fetch any credible source for this query.";
|
|
8124
|
+
return {
|
|
8125
|
+
content: [{ type: "text", text: `${reason}
|
|
8126
|
+
|
|
8127
|
+
Queries tried: ${details.actualQueries.join("; ") || query}` }],
|
|
8128
|
+
details,
|
|
8129
|
+
isError: true
|
|
8130
|
+
};
|
|
8131
|
+
}
|
|
8132
|
+
return {
|
|
8133
|
+
content: [{ type: "text", text: formatResearchPack(query, fetchedSources, details.warnings, browserRequiredSources) }],
|
|
8134
|
+
details
|
|
8135
|
+
};
|
|
8136
|
+
}
|
|
8137
|
+
};
|
|
8138
|
+
}
|
|
8139
|
+
|
|
7731
8140
|
// ../../packages/shared-headless-capabilities/src/code-intel.ts
|
|
7732
8141
|
import * as fs from "node:fs";
|
|
7733
8142
|
import * as path2 from "node:path";
|
|
@@ -7943,8 +8352,8 @@ export {
|
|
|
7943
8352
|
registerInteractionHandler,
|
|
7944
8353
|
hasInteractionHandler,
|
|
7945
8354
|
requestInteraction,
|
|
7946
|
-
|
|
7947
|
-
|
|
8355
|
+
AGENTS_CONTEXT_FILE_NAME,
|
|
8356
|
+
buildProjectContextSection,
|
|
7948
8357
|
evaluateTodoClosureAfterCompletedTurn,
|
|
7949
8358
|
evaluateTodoClosureAfterFailedTurn,
|
|
7950
8359
|
evaluateTodoClosureAfterAbort,
|
|
@@ -7958,6 +8367,7 @@ export {
|
|
|
7958
8367
|
buildExecutionReminder,
|
|
7959
8368
|
abortExecutionState,
|
|
7960
8369
|
continueExecutionState,
|
|
8370
|
+
pauseExecutionState,
|
|
7961
8371
|
supersedeExecutionState,
|
|
7962
8372
|
shouldPreserveExecutionStateForUserText,
|
|
7963
8373
|
BUILTIN_TOOL_CAPABILITY_METADATA,
|
|
@@ -7966,11 +8376,8 @@ export {
|
|
|
7966
8376
|
isPlanModeSafeCommand,
|
|
7967
8377
|
markCompletedPlanSteps,
|
|
7968
8378
|
createPlanCapability,
|
|
7969
|
-
createReadSkillCapability,
|
|
7970
|
-
createReadResourceCapability,
|
|
7971
8379
|
modalityForReadFileUnderstandingKind,
|
|
7972
8380
|
createReadFileUnderstandingHandler,
|
|
7973
|
-
createResolveResourceCapability,
|
|
7974
8381
|
attachAgentEventsToSessionTrace,
|
|
7975
8382
|
SessionTraceSnapshotWriter,
|
|
7976
8383
|
formatSkillSummariesForPrompt,
|
|
@@ -7986,6 +8393,7 @@ export {
|
|
|
7986
8393
|
buildToolsPromptSection,
|
|
7987
8394
|
createSharedWebSearchTool,
|
|
7988
8395
|
createSharedWebFetchTool,
|
|
8396
|
+
createSharedWebResearchTool,
|
|
7989
8397
|
createLightweightCodeIntelProvider,
|
|
7990
8398
|
createCodeIntelTool
|
|
7991
8399
|
};
|