feat(FN-3313): add plugin slot registry, planning mode rewind, and docker n
This merge lands five features spanning the Fusion stack: research settings key renaming (FN-3313, Steps 2–7) across types, defaults, CLI, and engine packages; static plugin slot-host rendering contract documentation (FN-3260); Docker node provisioning routes with a planning modal (FN-3116); plannin Fusion-Task-Id: FN-3313
This commit is contained in:
5
.changeset/fn-3313-research-settings-rename.md
Normal file
5
.changeset/fn-3313-research-settings-rename.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Rename global research flat settings keys from `research*` to `researchGlobal*` to enforce settings-scope parity and avoid global/project key collisions.
|
||||||
@@ -53,14 +53,14 @@ This also reveals the **Research Defaults** and **Research** settings sections i
|
|||||||
|
|
||||||
### 2. Configure a web search provider
|
### 2. Configure a web search provider
|
||||||
|
|
||||||
Set `researchWebSearchProvider` in global settings to one of the supported backends:
|
Set `researchGlobalWebSearchProvider` in global settings to one of the supported backends:
|
||||||
|
|
||||||
| Provider | Required settings |
|
| Provider | Required settings |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `"searxng"` | `researchSearxngUrl` — URL of your SearXNG instance |
|
| `"searxng"` | `researchGlobalSearxngUrl` — URL of your SearXNG instance |
|
||||||
| `"brave"` | `researchBraveApiKey` — Brave Search API key |
|
| `"brave"` | `researchGlobalBraveApiKey` — Brave Search API key |
|
||||||
| `"google"` | `researchGoogleSearchApiKey` + `researchGoogleSearchCx` — Google Custom Search credentials |
|
| `"google"` | `researchGlobalGoogleSearchApiKey` + `researchGlobalGoogleSearchCx` — Google Custom Search credentials |
|
||||||
| `"tavily"` | `researchTavilyApiKey` — Tavily API key |
|
| `"tavily"` | `researchGlobalTavilyApiKey` — Tavily API key |
|
||||||
| `"none"` | Disables web search (other sources still work) |
|
| `"none"` | Disables web search (other sources still work) |
|
||||||
|
|
||||||
API keys are stored through Fusion's auth credential pipeline (`/api/auth/api-key`), not in settings JSON directly.
|
API keys are stored through Fusion's auth credential pipeline (`/api/auth/api-key`), not in settings JSON directly.
|
||||||
|
|||||||
@@ -85,17 +85,17 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
|
|||||||
| `researchGlobalDefaultTimeout` | `number` | `300000` | Default timeout for end-to-end research runs in milliseconds (5 minutes). |
|
| `researchGlobalDefaultTimeout` | `number` | `300000` | Default timeout for end-to-end research runs in milliseconds (5 minutes). |
|
||||||
| `researchGlobalMaxSourcesPerRun` | `number` | `20` | Maximum number of sources per research run. |
|
| `researchGlobalMaxSourcesPerRun` | `number` | `20` | Maximum number of sources per research run. |
|
||||||
| `researchGlobalMaxSynthesisRounds` | `number` | `2` | Maximum synthesis rounds per research run. |
|
| `researchGlobalMaxSynthesisRounds` | `number` | `2` | Maximum synthesis rounds per research run. |
|
||||||
| `researchWebSearchProvider` | `"searxng" \| "brave" \| "google" \| "tavily" \| "none"` | `"none"` | Web search backend for research. Default: `"none"` (disabled). |
|
| `researchGlobalWebSearchProvider` | `"searxng" \| "brave" \| "google" \| "tavily" \| "none"` | `"none"` | Web search backend for research. Default: `"none"` (disabled). |
|
||||||
| `researchSearxngUrl` | `string` | `undefined` | SearXNG instance URL (required when provider is `"searxng"`). |
|
| `researchGlobalSearxngUrl` | `string` | `undefined` | SearXNG instance URL (required when provider is `"searxng"`). |
|
||||||
| `researchBraveApiKey` | `string` | `undefined` | Brave Search API key (required when provider is `"brave"`). |
|
| `researchGlobalBraveApiKey` | `string` | `undefined` | Brave Search API key (required when provider is `"brave"`). |
|
||||||
| `researchGoogleSearchApiKey` | `string` | `undefined` | Google Custom Search API key (required when provider is `"google"`). |
|
| `researchGlobalGoogleSearchApiKey` | `string` | `undefined` | Google Custom Search API key (required when provider is `"google"`). |
|
||||||
| `researchGoogleSearchCx` | `string` | `undefined` | Google Custom Search engine ID (required when provider is `"google"`). |
|
| `researchGlobalGoogleSearchCx` | `string` | `undefined` | Google Custom Search engine ID (required when provider is `"google"`). |
|
||||||
| `researchTavilyApiKey` | `string` | `undefined` | Tavily API key (required when provider is `"tavily"`). |
|
| `researchGlobalTavilyApiKey` | `string` | `undefined` | Tavily API key (required when provider is `"tavily"`). |
|
||||||
| `researchGitHubEnabled` | `boolean` | `undefined` | Enable GitHub as a research source. |
|
| `researchGlobalGitHubEnabled` | `boolean` | `undefined` | Enable GitHub as a research source. |
|
||||||
| `researchLocalDocsEnabled` | `boolean` | `undefined` | Enable local docs as a research source. |
|
| `researchGlobalLocalDocsEnabled` | `boolean` | `undefined` | Enable local docs as a research source. |
|
||||||
| `researchMaxSearchResults` | `number` | `undefined` | Maximum search results per provider query. |
|
| `researchGlobalMaxSearchResults` | `number` | `undefined` | Maximum search results per provider query. |
|
||||||
| `researchFetchTimeoutMs` | `number` | `30000` | Timeout for individual HTTP fetches in milliseconds. |
|
| `researchGlobalFetchTimeoutMs` | `number` | `30000` | Timeout for individual HTTP fetches in milliseconds. |
|
||||||
| `researchUserAgent` | `string` | `"FusionResearchBot/1.0"` | User-Agent header for HTTP requests made by research providers. |
|
| `researchGlobalUserAgent` | `string` | `"FusionResearchBot/1.0"` | User-Agent header for HTTP requests made by research providers. |
|
||||||
| `experimentalFeatures` | `Record<string, boolean>` | `{}` | Global-scoped experimental feature flags. Includes `experimentalFeatures.researchView` for standalone Research route visibility. |
|
| `experimentalFeatures` | `Record<string, boolean>` | `{}` | Global-scoped experimental feature flags. Includes `experimentalFeatures.researchView` for standalone Research route visibility. |
|
||||||
| `remoteAccess` | `RemoteAccessSettings` | `{ activeProvider: null, providers: {...}, tokenStrategy: {...}, lifecycle: {...} }` | Global-scoped remote access provider + token strategy configuration used by Remote Access routes and tunnel lifecycle controls. |
|
| `remoteAccess` | `RemoteAccessSettings` | `{ activeProvider: null, providers: {...}, tokenStrategy: {...}, lifecycle: {...} }` | Global-scoped remote access provider + token strategy configuration used by Remote Access routes and tunnel lifecycle controls. |
|
||||||
|
|
||||||
|
|||||||
@@ -122,13 +122,13 @@ async function enableResearch(cwd: string): Promise<TaskStore> {
|
|||||||
await store.updateGlobalSettings({
|
await store.updateGlobalSettings({
|
||||||
researchGlobalEnabled: true,
|
researchGlobalEnabled: true,
|
||||||
researchGlobalDefaults: { searchProvider: "searxng" },
|
researchGlobalDefaults: { searchProvider: "searxng" },
|
||||||
researchSearxngUrl: "http://localhost:8888",
|
researchGlobalSearxngUrl: "http://localhost:8888",
|
||||||
});
|
});
|
||||||
await store.updateSettings({
|
await store.updateSettings({
|
||||||
researchEnabled: true,
|
researchEnabled: true,
|
||||||
researchSettings: { enabled: true, searchProvider: "searxng" },
|
researchSettings: { enabled: true, searchProvider: "searxng" },
|
||||||
researchWebSearchProvider: "searxng",
|
researchGlobalWebSearchProvider: "searxng",
|
||||||
researchSearxngUrl: "http://localhost:8888",
|
researchGlobalSearxngUrl: "http://localhost:8888",
|
||||||
});
|
});
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe("research extension tools", () => {
|
|||||||
const store = new TaskStore(tmpDir);
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
await store.updateSettings({
|
await store.updateSettings({
|
||||||
researchWebSearchProvider: "tavily",
|
researchGlobalWebSearchProvider: "tavily",
|
||||||
researchSettings: { enabled: true },
|
researchSettings: { enabled: true },
|
||||||
researchGlobalDefaults: { searchProvider: "tavily" },
|
researchGlobalDefaults: { searchProvider: "tavily" },
|
||||||
});
|
});
|
||||||
@@ -88,8 +88,8 @@ describe("research extension tools", () => {
|
|||||||
const store = new TaskStore(tmpDir);
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
await store.updateSettings({
|
await store.updateSettings({
|
||||||
researchWebSearchProvider: "tavily",
|
researchGlobalWebSearchProvider: "tavily",
|
||||||
researchTavilyApiKey: "test-key",
|
researchGlobalTavilyApiKey: "test-key",
|
||||||
researchSettings: { enabled: true },
|
researchSettings: { enabled: true },
|
||||||
researchGlobalDefaults: { searchProvider: "tavily" },
|
researchGlobalDefaults: { searchProvider: "tavily" },
|
||||||
});
|
});
|
||||||
@@ -117,8 +117,8 @@ describe("research extension tools", () => {
|
|||||||
const store = new TaskStore(tmpDir);
|
const store = new TaskStore(tmpDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
await store.updateSettings({
|
await store.updateSettings({
|
||||||
researchWebSearchProvider: "tavily",
|
researchGlobalWebSearchProvider: "tavily",
|
||||||
researchTavilyApiKey: "test-key",
|
researchGlobalTavilyApiKey: "test-key",
|
||||||
researchSettings: { enabled: true },
|
researchSettings: { enabled: true },
|
||||||
researchGlobalDefaults: { searchProvider: "tavily" },
|
researchGlobalDefaults: { searchProvider: "tavily" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const researchStoreMock = {
|
|||||||
|
|
||||||
const storeMock = {
|
const storeMock = {
|
||||||
init: vi.fn(),
|
init: vi.fn(),
|
||||||
getSettings: vi.fn(async () => ({ researchSettings: { enabled: true }, researchWebSearchProvider: "tavily", researchTavilyApiKey: "x" })),
|
getSettings: vi.fn(async () => ({ researchSettings: { enabled: true }, researchGlobalWebSearchProvider: "tavily", researchGlobalTavilyApiKey: "x" })),
|
||||||
getResearchStore: vi.fn(() => researchStoreMock),
|
getResearchStore: vi.fn(() => researchStoreMock),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ describe("research commands", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("errors when provider credentials are missing", async () => {
|
it("errors when provider credentials are missing", async () => {
|
||||||
storeMock.getSettings.mockResolvedValueOnce({ researchSettings: { enabled: true }, researchWebSearchProvider: "tavily" });
|
storeMock.getSettings.mockResolvedValueOnce({ researchSettings: { enabled: true }, researchGlobalWebSearchProvider: "tavily" });
|
||||||
await expect(runResearchCreate({ query: "hello" })).rejects.toThrow("process.exit:1");
|
await expect(runResearchCreate({ query: "hello" })).rejects.toThrow("process.exit:1");
|
||||||
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("missing-credentials"));
|
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("missing-credentials"));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ async function getStore(projectName?: string): Promise<TaskStore> {
|
|||||||
|
|
||||||
function hasProviderCredentials(settings: Awaited<ReturnType<TaskStore["getSettings"]>>, providerId: string | undefined): boolean {
|
function hasProviderCredentials(settings: Awaited<ReturnType<TaskStore["getSettings"]>>, providerId: string | undefined): boolean {
|
||||||
if (!providerId) return false;
|
if (!providerId) return false;
|
||||||
if (providerId === "searxng") return Boolean(settings.researchSearxngUrl);
|
if (providerId === "searxng") return Boolean(settings.researchGlobalSearxngUrl);
|
||||||
if (providerId === "brave") return Boolean(settings.researchBraveApiKey);
|
if (providerId === "brave") return Boolean(settings.researchGlobalBraveApiKey);
|
||||||
if (providerId === "google") return Boolean(settings.researchGoogleSearchApiKey && settings.researchGoogleSearchCx);
|
if (providerId === "google") return Boolean(settings.researchGlobalGoogleSearchApiKey && settings.researchGlobalGoogleSearchCx);
|
||||||
if (providerId === "tavily") return Boolean(settings.researchTavilyApiKey);
|
if (providerId === "tavily") return Boolean(settings.researchGlobalTavilyApiKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ async function getResearchRuntime(store: TaskStore) {
|
|||||||
throw new Error("feature-disabled: Research is disabled in settings.");
|
throw new Error("feature-disabled: Research is disabled in settings.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const configuredProvider = (resolved.searchProvider as string | undefined) ?? settings.researchWebSearchProvider;
|
const configuredProvider = (resolved.searchProvider as string | undefined) ?? settings.researchGlobalWebSearchProvider;
|
||||||
if (!configuredProvider) {
|
if (!configuredProvider) {
|
||||||
throw new Error("provider-unavailable: Research providers are not configured. Add provider credentials in settings.");
|
throw new Error("provider-unavailable: Research providers are not configured. Add provider credentials in settings.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,15 +139,15 @@ async function getResearchAvailability(store: TaskStore): Promise<{ ok: boolean;
|
|||||||
return { ok: false, code: "feature-disabled", message: "Research is disabled in settings." };
|
return { ok: false, code: "feature-disabled", message: "Research is disabled in settings." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const backend = (resolved.searchProvider as string | undefined) ?? settings.researchWebSearchProvider;
|
const backend = (resolved.searchProvider as string | undefined) ?? settings.researchGlobalWebSearchProvider;
|
||||||
const configured = backend === "searxng"
|
const configured = backend === "searxng"
|
||||||
? Boolean(settings.researchSearxngUrl)
|
? Boolean(settings.researchGlobalSearxngUrl)
|
||||||
: backend === "brave"
|
: backend === "brave"
|
||||||
? Boolean(settings.researchBraveApiKey)
|
? Boolean(settings.researchGlobalBraveApiKey)
|
||||||
: backend === "google"
|
: backend === "google"
|
||||||
? Boolean(settings.researchGoogleSearchApiKey && settings.researchGoogleSearchCx)
|
? Boolean(settings.researchGlobalGoogleSearchApiKey && settings.researchGlobalGoogleSearchCx)
|
||||||
: backend === "tavily"
|
: backend === "tavily"
|
||||||
? Boolean(settings.researchTavilyApiKey)
|
? Boolean(settings.researchGlobalTavilyApiKey)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
if (!backend) {
|
if (!backend) {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ describe("resolveResearchSettings", () => {
|
|||||||
it("falls back through legacy and hardcoded limit chains", () => {
|
it("falls back through legacy and hardcoded limit chains", () => {
|
||||||
const fromLegacyProjectTimeout = resolveResearchSettings({
|
const fromLegacyProjectTimeout = resolveResearchSettings({
|
||||||
researchDefaultTimeout: 111_000,
|
researchDefaultTimeout: 111_000,
|
||||||
researchFetchTimeoutMs: 8_000,
|
researchGlobalFetchTimeoutMs: 8_000,
|
||||||
});
|
});
|
||||||
expect(fromLegacyProjectTimeout.limits.maxDurationMs).toBe(111_000);
|
expect(fromLegacyProjectTimeout.limits.maxDurationMs).toBe(111_000);
|
||||||
expect(fromLegacyProjectTimeout.limits.requestTimeoutMs).toBe(8_000);
|
expect(fromLegacyProjectTimeout.limits.requestTimeoutMs).toBe(8_000);
|
||||||
|
|||||||
@@ -98,6 +98,29 @@ describe("settings key parity", () => {
|
|||||||
|
|
||||||
// ── Model Lane Key Parity Regression Tests (FN-1729) ────────────────────────
|
// ── Model Lane Key Parity Regression Tests (FN-1729) ────────────────────────
|
||||||
|
|
||||||
|
describe("research global key parity regression (FN-3313)", () => {
|
||||||
|
const globalResearchFlatKeys = [
|
||||||
|
"researchGlobalWebSearchProvider",
|
||||||
|
"researchGlobalSearxngUrl",
|
||||||
|
"researchGlobalBraveApiKey",
|
||||||
|
"researchGlobalGoogleSearchApiKey",
|
||||||
|
"researchGlobalGoogleSearchCx",
|
||||||
|
"researchGlobalTavilyApiKey",
|
||||||
|
"researchGlobalGitHubEnabled",
|
||||||
|
"researchGlobalLocalDocsEnabled",
|
||||||
|
"researchGlobalMaxSearchResults",
|
||||||
|
"researchGlobalFetchTimeoutMs",
|
||||||
|
"researchGlobalUserAgent",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
it.each(globalResearchFlatKeys)("%s is global-scoped only", (key) => {
|
||||||
|
expect(isGlobalSettingsKey(key)).toBe(true);
|
||||||
|
expect(isProjectSettingsKey(key)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Model Lane Key Parity Regression Tests (FN-1729) ────────────────────────
|
||||||
|
|
||||||
describe("model lane key parity regression (FN-1729)", () => {
|
describe("model lane key parity regression (FN-1729)", () => {
|
||||||
// All model lane provider/modelId pairs that should exist
|
// All model lane provider/modelId pairs that should exist
|
||||||
const allModelLanePairs = [
|
const allModelLanePairs = [
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function resolveResearchSettings(settings: Partial<Settings> | undefined)
|
|||||||
maxConcurrentRuns: projectSettings?.limits?.maxConcurrentRuns ?? settings?.researchMaxConcurrentRuns ?? settings?.researchGlobalMaxConcurrentRuns ?? 3,
|
maxConcurrentRuns: projectSettings?.limits?.maxConcurrentRuns ?? settings?.researchMaxConcurrentRuns ?? settings?.researchGlobalMaxConcurrentRuns ?? 3,
|
||||||
maxSourcesPerRun: projectSettings?.limits?.maxSourcesPerRun ?? globalDefaults?.maxSourcesPerRun ?? settings?.researchMaxSourcesPerRun ?? settings?.researchGlobalMaxSourcesPerRun ?? 20,
|
maxSourcesPerRun: projectSettings?.limits?.maxSourcesPerRun ?? globalDefaults?.maxSourcesPerRun ?? settings?.researchMaxSourcesPerRun ?? settings?.researchGlobalMaxSourcesPerRun ?? 20,
|
||||||
maxDurationMs: projectSettings?.limits?.maxDurationMs ?? settings?.researchDefaultTimeout ?? settings?.researchGlobalDefaultTimeout ?? 300000,
|
maxDurationMs: projectSettings?.limits?.maxDurationMs ?? settings?.researchDefaultTimeout ?? settings?.researchGlobalDefaultTimeout ?? 300000,
|
||||||
requestTimeoutMs: projectSettings?.limits?.requestTimeoutMs ?? settings?.researchFetchTimeoutMs ?? 30000,
|
requestTimeoutMs: projectSettings?.limits?.requestTimeoutMs ?? settings?.researchGlobalFetchTimeoutMs ?? 30000,
|
||||||
},
|
},
|
||||||
defaultExportFormat: globalDefaults?.defaultExportFormat ?? "markdown",
|
defaultExportFormat: globalDefaults?.defaultExportFormat ?? "markdown",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -100,17 +100,17 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
|||||||
researchGlobalDefaultTimeout: 300000,
|
researchGlobalDefaultTimeout: 300000,
|
||||||
researchGlobalMaxSourcesPerRun: 20,
|
researchGlobalMaxSourcesPerRun: 20,
|
||||||
researchGlobalMaxSynthesisRounds: 2,
|
researchGlobalMaxSynthesisRounds: 2,
|
||||||
researchWebSearchProvider: "none",
|
researchGlobalWebSearchProvider: "none",
|
||||||
researchSearxngUrl: undefined,
|
researchGlobalSearxngUrl: undefined,
|
||||||
researchBraveApiKey: undefined,
|
researchGlobalBraveApiKey: undefined,
|
||||||
researchGoogleSearchApiKey: undefined,
|
researchGlobalGoogleSearchApiKey: undefined,
|
||||||
researchGoogleSearchCx: undefined,
|
researchGlobalGoogleSearchCx: undefined,
|
||||||
researchTavilyApiKey: undefined,
|
researchGlobalTavilyApiKey: undefined,
|
||||||
researchGitHubEnabled: false,
|
researchGlobalGitHubEnabled: false,
|
||||||
researchLocalDocsEnabled: true,
|
researchGlobalLocalDocsEnabled: true,
|
||||||
researchMaxSearchResults: 10,
|
researchGlobalMaxSearchResults: 10,
|
||||||
researchFetchTimeoutMs: 30_000,
|
researchGlobalFetchTimeoutMs: 30_000,
|
||||||
researchUserAgent: "FusionResearchBot/1.0",
|
researchGlobalUserAgent: "FusionResearchBot/1.0",
|
||||||
remoteAccess: {
|
remoteAccess: {
|
||||||
activeProvider: null,
|
activeProvider: null,
|
||||||
providers: {
|
providers: {
|
||||||
|
|||||||
@@ -1519,27 +1519,27 @@ export interface GlobalSettings {
|
|||||||
* Default: 2. */
|
* Default: 2. */
|
||||||
researchGlobalMaxSynthesisRounds?: number;
|
researchGlobalMaxSynthesisRounds?: number;
|
||||||
/** Web search backend for auto-research. Default: "none" (disabled). */
|
/** Web search backend for auto-research. Default: "none" (disabled). */
|
||||||
researchWebSearchProvider?: WebSearchBackend;
|
researchGlobalWebSearchProvider?: WebSearchBackend;
|
||||||
/** SearXNG instance URL (required when researchWebSearchProvider is "searxng"). */
|
/** SearXNG instance URL (required when researchGlobalWebSearchProvider is "searxng"). */
|
||||||
researchSearxngUrl?: string;
|
researchGlobalSearxngUrl?: string;
|
||||||
/** Brave Search API key (required when researchWebSearchProvider is "brave"). */
|
/** Brave Search API key (required when researchGlobalWebSearchProvider is "brave"). */
|
||||||
researchBraveApiKey?: string;
|
researchGlobalBraveApiKey?: string;
|
||||||
/** Google Custom Search API key (required when researchWebSearchProvider is "google"). */
|
/** Google Custom Search API key (required when researchGlobalWebSearchProvider is "google"). */
|
||||||
researchGoogleSearchApiKey?: string;
|
researchGlobalGoogleSearchApiKey?: string;
|
||||||
/** Google Custom Search engine ID (required when researchWebSearchProvider is "google"). */
|
/** Google Custom Search engine ID (required when researchGlobalWebSearchProvider is "google"). */
|
||||||
researchGoogleSearchCx?: string;
|
researchGlobalGoogleSearchCx?: string;
|
||||||
/** Tavily API key (required when researchWebSearchProvider is "tavily"). */
|
/** Tavily API key (required when researchGlobalWebSearchProvider is "tavily"). */
|
||||||
researchTavilyApiKey?: string;
|
researchGlobalTavilyApiKey?: string;
|
||||||
/** Enable GitHub repository/issue search provider. Default: false. */
|
/** Enable GitHub repository/issue search provider. Default: false. */
|
||||||
researchGitHubEnabled?: boolean;
|
researchGlobalGitHubEnabled?: boolean;
|
||||||
/** Enable local project documentation search provider. Default: true. */
|
/** Enable local project documentation search provider. Default: true. */
|
||||||
researchLocalDocsEnabled?: boolean;
|
researchGlobalLocalDocsEnabled?: boolean;
|
||||||
/** Maximum search results per provider query. Default: 10. */
|
/** Maximum search results per provider query. Default: 10. */
|
||||||
researchMaxSearchResults?: number;
|
researchGlobalMaxSearchResults?: number;
|
||||||
/** HTTP fetch timeout in milliseconds for page/content fetching. Default: 30000. */
|
/** HTTP fetch timeout in milliseconds for page/content fetching. Default: 30000. */
|
||||||
researchFetchTimeoutMs?: number;
|
researchGlobalFetchTimeoutMs?: number;
|
||||||
/** User-Agent header for HTTP requests made by research providers. Default: "FusionResearchBot/1.0". */
|
/** User-Agent header for HTTP requests made by research providers. Default: "FusionResearchBot/1.0". */
|
||||||
researchUserAgent?: string;
|
researchGlobalUserAgent?: string;
|
||||||
/** Global-scoped remote access configuration persisted in `~/.fusion/settings.json`.
|
/** Global-scoped remote access configuration persisted in `~/.fusion/settings.json`.
|
||||||
* Stores both provider configs, active provider selection, token strategy,
|
* Stores both provider configs, active provider selection, token strategy,
|
||||||
* and lifecycle restart metadata for remote tunnel orchestration. */
|
* and lifecycle restart metadata for remote tunnel orchestration. */
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ describe("createResearchTools", () => {
|
|||||||
researchGlobalMaxConcurrentRuns: 2,
|
researchGlobalMaxConcurrentRuns: 2,
|
||||||
researchGlobalDefaultTimeout: 30_000,
|
researchGlobalDefaultTimeout: 30_000,
|
||||||
researchGlobalMaxSynthesisRounds: 2,
|
researchGlobalMaxSynthesisRounds: 2,
|
||||||
researchWebSearchProvider: "none",
|
researchGlobalWebSearchProvider: "none",
|
||||||
researchSettings: { enabled: true },
|
researchSettings: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -897,7 +897,7 @@ describe("createResearchTools", () => {
|
|||||||
const tools = createResearchTools({
|
const tools = createResearchTools({
|
||||||
store: store as any,
|
store: store as any,
|
||||||
rootDir: process.cwd(),
|
rootDir: process.cwd(),
|
||||||
getSettings: async () => ({ ...baseSettings, researchTavilyApiKey: "key", researchWebSearchProvider: "tavily" } as any),
|
getSettings: async () => ({ ...baseSettings, researchGlobalTavilyApiKey: "key", researchGlobalWebSearchProvider: "tavily" } as any),
|
||||||
});
|
});
|
||||||
|
|
||||||
const runTool = tools.find((tool) => tool.name === "fn_research_run")!;
|
const runTool = tools.find((tool) => tool.name === "fn_research_run")!;
|
||||||
|
|||||||
@@ -11,19 +11,19 @@ describe("ResearchProviderRegistry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("detects search backend from credentials", () => {
|
it("detects search backend from credentials", () => {
|
||||||
const tavily = new ResearchProviderRegistry({ researchTavilyApiKey: "key" }, process.cwd());
|
const tavily = new ResearchProviderRegistry({ researchGlobalTavilyApiKey: "key" }, process.cwd());
|
||||||
expect(tavily.isProviderAvailable("web-search")).toBe(true);
|
expect(tavily.isProviderAvailable("web-search")).toBe(true);
|
||||||
|
|
||||||
const searx = new ResearchProviderRegistry({ researchSearxngUrl: "https://sx.local" }, process.cwd());
|
const searx = new ResearchProviderRegistry({ researchGlobalSearxngUrl: "https://sx.local" }, process.cwd());
|
||||||
expect(searx.isProviderAvailable("web-search")).toBe(true);
|
expect(searx.isProviderAvailable("web-search")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns only configured providers in available list", () => {
|
it("returns only configured providers in available list", () => {
|
||||||
const registry = new ResearchProviderRegistry(
|
const registry = new ResearchProviderRegistry(
|
||||||
{
|
{
|
||||||
researchWebSearchProvider: "brave",
|
researchGlobalWebSearchProvider: "brave",
|
||||||
researchBraveApiKey: "token",
|
researchGlobalBraveApiKey: "token",
|
||||||
researchGitHubEnabled: true,
|
researchGlobalGitHubEnabled: true,
|
||||||
},
|
},
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
);
|
);
|
||||||
@@ -34,15 +34,15 @@ describe("ResearchProviderRegistry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes providers after settings changes", () => {
|
it("refreshes providers after settings changes", () => {
|
||||||
const registry = new ResearchProviderRegistry({ researchWebSearchProvider: "none" }, process.cwd());
|
const registry = new ResearchProviderRegistry({ researchGlobalWebSearchProvider: "none" }, process.cwd());
|
||||||
expect(registry.isProviderAvailable("web-search")).toBe(false);
|
expect(registry.isProviderAvailable("web-search")).toBe(false);
|
||||||
|
|
||||||
registry.refreshSettings({ researchWebSearchProvider: "tavily", researchTavilyApiKey: "key" });
|
registry.refreshSettings({ researchGlobalWebSearchProvider: "tavily", researchGlobalTavilyApiKey: "key" });
|
||||||
expect(registry.isProviderAvailable("web-search")).toBe(true);
|
expect(registry.isProviderAvailable("web-search")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gracefully degrades disabled providers", () => {
|
it("gracefully degrades disabled providers", () => {
|
||||||
const registry = new ResearchProviderRegistry({ researchGitHubEnabled: false, researchLocalDocsEnabled: false }, process.cwd());
|
const registry = new ResearchProviderRegistry({ researchGlobalGitHubEnabled: false, researchGlobalLocalDocsEnabled: false }, process.cwd());
|
||||||
expect(registry.isProviderAvailable("github")).toBe(false);
|
expect(registry.isProviderAvailable("github")).toBe(false);
|
||||||
expect(registry.isProviderAvailable("local-docs")).toBe(false);
|
expect(registry.isProviderAvailable("local-docs")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,30 +44,30 @@ export class ResearchProviderRegistry {
|
|||||||
|
|
||||||
private instantiateProviders(): void {
|
private instantiateProviders(): void {
|
||||||
const backend = this.resolveSearchBackend();
|
const backend = this.resolveSearchBackend();
|
||||||
const maxResults = Number(this.settings.researchMaxSearchResults ?? 10);
|
const maxResults = Number(this.settings.researchGlobalMaxSearchResults ?? 10);
|
||||||
const fetchTimeoutMs = Number(this.settings.researchFetchTimeoutMs ?? 30_000);
|
const fetchTimeoutMs = Number(this.settings.researchGlobalFetchTimeoutMs ?? 30_000);
|
||||||
const userAgent = this.settings.researchUserAgent ?? "FusionResearchBot/1.0";
|
const userAgent = this.settings.researchGlobalUserAgent ?? "FusionResearchBot/1.0";
|
||||||
|
|
||||||
this.providers = new Map<ResearchProviderType, ResearchProvider>([
|
this.providers = new Map<ResearchProviderType, ResearchProvider>([
|
||||||
[
|
[
|
||||||
"web-search",
|
"web-search",
|
||||||
new WebSearchProvider({
|
new WebSearchProvider({
|
||||||
backend,
|
backend,
|
||||||
searxngUrl: this.settings.researchSearxngUrl,
|
searxngUrl: this.settings.researchGlobalSearxngUrl,
|
||||||
braveApiKey: this.settings.researchBraveApiKey,
|
braveApiKey: this.settings.researchGlobalBraveApiKey,
|
||||||
googleApiKey: this.settings.researchGoogleSearchApiKey,
|
googleApiKey: this.settings.researchGlobalGoogleSearchApiKey,
|
||||||
googleCx: this.settings.researchGoogleSearchCx,
|
googleCx: this.settings.researchGlobalGoogleSearchCx,
|
||||||
tavilyApiKey: this.settings.researchTavilyApiKey,
|
tavilyApiKey: this.settings.researchGlobalTavilyApiKey,
|
||||||
maxResults,
|
maxResults,
|
||||||
timeoutMs: fetchTimeoutMs,
|
timeoutMs: fetchTimeoutMs,
|
||||||
userAgent,
|
userAgent,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
["page-fetch", new PageFetchProvider({ timeoutMs: fetchTimeoutMs, userAgent })],
|
["page-fetch", new PageFetchProvider({ timeoutMs: fetchTimeoutMs, userAgent })],
|
||||||
["github", this.settings.researchGitHubEnabled ? new GitHubProvider() : new DisabledProvider("github")],
|
["github", this.settings.researchGlobalGitHubEnabled ? new GitHubProvider() : new DisabledProvider("github")],
|
||||||
[
|
[
|
||||||
"local-docs",
|
"local-docs",
|
||||||
this.settings.researchLocalDocsEnabled === false
|
this.settings.researchGlobalLocalDocsEnabled === false
|
||||||
? new DisabledProvider("local-docs")
|
? new DisabledProvider("local-docs")
|
||||||
: new LocalDocsProvider({ projectRoot: this.projectRoot, timeoutMs: fetchTimeoutMs, maxResults }),
|
: new LocalDocsProvider({ projectRoot: this.projectRoot, timeoutMs: fetchTimeoutMs, maxResults }),
|
||||||
],
|
],
|
||||||
@@ -78,13 +78,13 @@ export class ResearchProviderRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private resolveSearchBackend(): WebSearchBackend {
|
private resolveSearchBackend(): WebSearchBackend {
|
||||||
const explicit = this.settings.researchWebSearchProvider;
|
const explicit = this.settings.researchGlobalWebSearchProvider;
|
||||||
if (explicit && explicit !== "none") return explicit;
|
if (explicit && explicit !== "none") return explicit;
|
||||||
|
|
||||||
if (this.settings.researchSearxngUrl) return "searxng";
|
if (this.settings.researchGlobalSearxngUrl) return "searxng";
|
||||||
if (this.settings.researchTavilyApiKey) return "tavily";
|
if (this.settings.researchGlobalTavilyApiKey) return "tavily";
|
||||||
if (this.settings.researchBraveApiKey) return "brave";
|
if (this.settings.researchGlobalBraveApiKey) return "brave";
|
||||||
if (this.settings.researchGoogleSearchApiKey && this.settings.researchGoogleSearchCx) return "google";
|
if (this.settings.researchGlobalGoogleSearchApiKey && this.settings.researchGlobalGoogleSearchCx) return "google";
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user