@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.
@@ -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/knowledge-index.ts
1939
- function parseFrontmatterBlock(yaml) {
1940
- const result = {};
1941
- const lines = yaml.split("\n");
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 stripFrontmatter(content) {
1965
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
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 extractHeadings(body, maxLevel = 3) {
1975
- const headings = [];
1976
- const lines = body.split("\n");
1977
- for (const rawLine of lines) {
1978
- const line = rawLine.trim();
1979
- const match = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
1980
- if (!match) continue;
1981
- const level = match[1].length;
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
- paragraphLines.push(line);
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 buildKnowledgeIndexSection(entries, heading = "Project Knowledge Index") {
2055
- const filtered = entries.filter((entry) => entry && (entry.title || entry.description || entry.headings.length > 0));
2056
- if (filtered.length === 0) return "";
2057
- const lines = [`[${heading}]`];
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
- return lines.join("\n");
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: "failed", updatedAt: nowIso() },
2200
- action: "clear_failed",
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: mode === "todo" && lifecycle === "active" && steps.length > 0 ? loop ?? createTodoClosureLoop({
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
- }) : null,
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
- return restoreExecutionState({
2346
+ const activeState = restoreExecutionState({
2424
2347
  ...state,
2425
2348
  lifecycle: "active",
2426
2349
  currentStepId: inferCurrentStepId(state.steps),
2427
- loop: state.mode === "todo" ? createTodoClosureLoop(state) : null,
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 the read_skill tool only with an exact name from <available_skills> when the task matches its description.",
3640
- "Tools such as bash, read_file, grep_text, or manage_todo_list are not skills and must not be passed to read_skill.",
3641
- "Skills may reference package-local resources using relative paths.",
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 normalized = path3.replace(/\\/g, "/").replace(/\/+/g, "/");
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
- return normalized.replace(/\/+$/, "");
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 (!isWithinRoot(normalized, sandboxRoot)) {
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 (!isWithinRoot(resolved, sandboxRoot) && !isInAllowlist(resolved, allowlistedDirs)) {
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 extractTitle2(html) {
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 completeSimple(
7552
- model,
7553
- {
7554
- 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.",
7555
- messages: [{
7556
- role: "user",
7557
- timestamp: Date.now(),
7558
- content: [{
7559
- type: "text",
7560
- text: [`URL: ${url}`, `Task: ${prompt}`, "", "Fetched content:", content].join("\n")
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
- apiKey,
7566
- maxTokens: Math.min(model.maxTokens || 4096, 4096),
7567
- signal
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 ? extractTitle2(raw) : void 0;
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
- buildKnowledgeIndexEntry,
7947
- buildKnowledgeIndexSection,
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
  };