- Fold sf-usage-bar, sf-notify, sf-inturn-guard, sf-permissions, slash-commands into sf extension (ui/, notifications/, guards/, permissions/, commands/legacy/) - Delete vectordrive extension - Migrate uok/kernel.js to TypeScript (kernel.ts) with full interfaces - Add allowJs/checkJs:false to tsconfig.resources.json for incremental TS migration - Add symlink dedup to extension-discovery.ts (seenRealPaths Set) - Add before_provider_request delegate back to native-search.js so session budget tests exercise the middleware end-to-end - Fix parseSfNativeTools() to return all SF manifest tools (drop sf_ filter) - Fix test assertions: plan_milestone/complete_task/validate_milestone - Remove subagent from app-smoke.test.ts (folded into sf/subagent/) - Remove sf-permissions/sf-inturn-guard/subagent from features-inventory test - Fix resolveSearchProvider autonomous mode test to pass 'auto' explicitly - Remove legacy /clear slash command (conflicts with built-in clear_terminal) - Update web-command-parity-contract.test.ts for clear removal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
126 lines
5.2 KiB
JavaScript
126 lines
5.2 KiB
JavaScript
/**
|
|
* Native Anthropic web search extension hooks.
|
|
*
|
|
* The injection logic (before_provider_request) lives in the native provider middleware:
|
|
* packages/coding-agent/src/core/providers/web-search-middleware.ts
|
|
*
|
|
* This file owns only the extension-layer concerns: model_select diagnostics,
|
|
* active-tool management, session reset, and PREFERENCES.md-aware provider resolution.
|
|
*/
|
|
import {
|
|
CUSTOM_SEARCH_TOOL_NAMES,
|
|
setPreferBraveResolver,
|
|
webSearchMiddleware,
|
|
} from "@singularity-forge/coding-agent";
|
|
import { resolveSearchProviderFromPreferences } from "../sf/preferences.js";
|
|
/** Tool names for the Brave-backed custom search tools */
|
|
export const BRAVE_TOOL_NAMES = ["search-the-web", "search_and_read"];
|
|
/** When true, skip native web search injection and keep Brave/custom tools active on Anthropic. */
|
|
export function preferBraveSearch() {
|
|
// PREFERENCES.md takes priority over env var
|
|
const prefsPref = resolveSearchProviderFromPreferences();
|
|
if (
|
|
prefsPref === "brave" ||
|
|
prefsPref === "tavily" ||
|
|
prefsPref === "minimax" ||
|
|
prefsPref === "serper" ||
|
|
prefsPref === "exa" ||
|
|
prefsPref === "ollama" ||
|
|
prefsPref === "combosearch"
|
|
)
|
|
return true;
|
|
if (prefsPref === "native") return false;
|
|
// Fall back to env var
|
|
return (
|
|
process.env.PREFER_BRAVE_SEARCH === "1" ||
|
|
process.env.PREFER_BRAVE_SEARCH === "true"
|
|
);
|
|
}
|
|
/**
|
|
* Register model_select, before_provider_request, and session_start hooks for native Anthropic web search.
|
|
*
|
|
* before_provider_request delegates to the webSearchMiddleware singleton so that tests
|
|
* exercise the same code path as production (sdk.ts calls it natively first; the extension
|
|
* delegate is a no-op in production due to the double-injection guard in the middleware).
|
|
*/
|
|
export function registerNativeSearchHooks(pi) {
|
|
// null = unknown (model_select not yet fired); true/false = provider is/isn't Anthropic.
|
|
let isAnthropicProvider = null;
|
|
// Register the PREFERENCES.md-aware resolver so the native middleware (shared
|
|
// singleton in web-search-middleware.ts) respects search_provider overrides.
|
|
// Called here so each test invocation resets the resolver to the current context.
|
|
setPreferBraveResolver(preferBraveSearch);
|
|
// Reset the shared middleware session counter for this registration.
|
|
// In tests, each registerNativeSearchHooks() call starts a fresh counter.
|
|
// In production, the session_start handler below resets it on each new session.
|
|
webSearchMiddleware.resetSession();
|
|
// Track provider changes via model selection — also handles diagnostics
|
|
// since model_select fires AFTER session_start and knows the provider.
|
|
pi.on("model_select", async (event, ctx) => {
|
|
const wasAnthropic = isAnthropicProvider;
|
|
isAnthropicProvider = event.model.provider === "anthropic";
|
|
const hasSearchKey = !!(
|
|
process.env.BRAVE_API_KEY ||
|
|
process.env.TAVILY_API_KEY ||
|
|
process.env.MINIMAX_CODE_PLAN_KEY ||
|
|
process.env.MINIMAX_CODING_API_KEY ||
|
|
process.env.MINIMAX_API_KEY ||
|
|
process.env.SERPER_API_KEY ||
|
|
process.env.EXA_API_KEY ||
|
|
process.env.OLLAMA_API_KEY
|
|
);
|
|
// When Anthropic (and not preferring Brave): disable custom search tools —
|
|
// native web_search is server-side and more reliable.
|
|
if (isAnthropicProvider && !preferBraveSearch()) {
|
|
const active = pi.getActiveTools();
|
|
pi.setActiveTools(
|
|
active.filter((t) => !CUSTOM_SEARCH_TOOL_NAMES.includes(t)),
|
|
);
|
|
} else if (!isAnthropicProvider && wasAnthropic) {
|
|
// Switching away from Anthropic — re-enable custom search tools (they
|
|
// were disabled while native search was active). If keys are missing,
|
|
// user sees the error rather than tools silently vanishing.
|
|
const active = pi.getActiveTools();
|
|
const toAdd = CUSTOM_SEARCH_TOOL_NAMES.filter((t) => !active.includes(t));
|
|
if (toAdd.length > 0) {
|
|
pi.setActiveTools([...active, ...toAdd]);
|
|
}
|
|
}
|
|
// Show provider-aware diagnostics on first selection or provider change
|
|
if (
|
|
isAnthropicProvider &&
|
|
!preferBraveSearch() &&
|
|
!wasAnthropic &&
|
|
event.source !== "restore"
|
|
) {
|
|
ctx.ui.notify("Native Anthropic web search active", "info");
|
|
} else if (
|
|
isAnthropicProvider &&
|
|
preferBraveSearch() &&
|
|
!wasAnthropic &&
|
|
event.source !== "restore"
|
|
) {
|
|
ctx.ui.notify("Brave search active (PREFER_BRAVE_SEARCH)", "info");
|
|
} else if (!isAnthropicProvider && !hasSearchKey) {
|
|
ctx.ui.notify(
|
|
"Web search: Set BRAVE_API_KEY, TAVILY_API_KEY, MINIMAX_CODE_PLAN_KEY, SERPER_API_KEY, EXA_API_KEY, or OLLAMA_API_KEY, or use an Anthropic model for built-in search",
|
|
"warning",
|
|
);
|
|
}
|
|
});
|
|
// Delegate before_provider_request to the native middleware singleton.
|
|
// In production, sdk.ts already ran applyToPayload before extension hooks fire,
|
|
// so the double-injection guard makes this a no-op. In tests (mock PI without
|
|
// sdk.ts), this is the only path that exercises the injection logic.
|
|
pi.on("before_provider_request", async (event, _ctx) => {
|
|
let modelHint = event.model;
|
|
if (!modelHint && isAnthropicProvider !== null) {
|
|
modelHint = { provider: isAnthropicProvider ? "anthropic" : "not-anthropic" };
|
|
}
|
|
return webSearchMiddleware.applyToPayload(event.payload, modelHint);
|
|
});
|
|
pi.on("session_start", async (_event, _ctx) => {
|
|
// Reset the shared middleware session budget (#1309).
|
|
webSearchMiddleware.resetSession();
|
|
});
|
|
}
|