feat(FN-3705): gate research tools behind experimental flag
This merge introduces two major themes. First, research tools in both the engine and CLI are now gated behind an experimental flag, using a shared helper from core — the research tools are documented as experimental and the dashboard settings reference is updated. Second, the testing suite receives Fusion-Task-Id: FN-3705
This commit is contained in:
@@ -123,6 +123,7 @@ async function enableResearch(cwd: string): Promise<TaskStore> {
|
||||
researchGlobalEnabled: true,
|
||||
researchGlobalDefaults: { searchProvider: "searxng" },
|
||||
researchGlobalSearxngUrl: "http://localhost:8888",
|
||||
experimentalFeatures: { researchView: true } as Record<string, boolean>,
|
||||
});
|
||||
await store.updateSettings({
|
||||
researchEnabled: true,
|
||||
|
||||
@@ -56,10 +56,10 @@ describe("research extension tools", () => {
|
||||
expect(api.tools.has("fn_research_retry")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns actionable disabled response when research is off", async () => {
|
||||
it("returns feature-disabled response when experimental research flag is off", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
await store.updateSettings({ researchSettings: { enabled: false } });
|
||||
await store.updateSettings({ researchSettings: { enabled: true }, experimentalFeatures: { researchView: false } as Record<string, boolean> });
|
||||
|
||||
const runTool = api.tools.get("fn_research_run")!;
|
||||
const result = await runTool.execute("call-1", { query: "fusion" }, undefined, undefined, makeCtx(tmpDir));
|
||||
@@ -68,14 +68,38 @@ describe("research extension tools", () => {
|
||||
expect(result.content[0].text).toContain("disabled");
|
||||
});
|
||||
|
||||
it("returns feature-disabled contract for list/get/cancel/retry when flag is off", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
await store.updateSettings({ researchSettings: { enabled: true }, experimentalFeatures: { researchView: false } as Record<string, boolean> });
|
||||
|
||||
const listResult = await api.tools.get("fn_research_list")!.execute("call-list", {}, undefined, undefined, makeCtx(tmpDir));
|
||||
expect(listResult.details.setup.code).toBe("feature-disabled");
|
||||
|
||||
const getResult = await api.tools.get("fn_research_get")!.execute("call-get", { id: "RR-1" }, undefined, undefined, makeCtx(tmpDir));
|
||||
expect(getResult.details.setup.code).toBe("feature-disabled");
|
||||
|
||||
const cancelResult = await api.tools.get("fn_research_cancel")!.execute("call-cancel", { id: "RR-1" }, undefined, undefined, makeCtx(tmpDir));
|
||||
expect(cancelResult.isError).toBe(true);
|
||||
expect(cancelResult.details.setup.code).toBe("feature-disabled");
|
||||
|
||||
const retryResult = await api.tools.get("fn_research_retry")!.execute("call-retry", { id: "RR-1" }, undefined, undefined, makeCtx(tmpDir));
|
||||
expect(retryResult.isError).toBe(true);
|
||||
expect(retryResult.details.setup.code).toBe("feature-disabled");
|
||||
});
|
||||
|
||||
it("returns actionable missing-credentials response", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
await store.updateSettings({
|
||||
await store.updateGlobalSettings({
|
||||
experimentalFeatures: { researchView: true } as Record<string, boolean>,
|
||||
researchGlobalEnabled: true,
|
||||
researchGlobalWebSearchProvider: "tavily",
|
||||
researchSettings: { enabled: true },
|
||||
researchGlobalDefaults: { searchProvider: "tavily" },
|
||||
});
|
||||
await store.updateSettings({
|
||||
researchSettings: { enabled: true },
|
||||
});
|
||||
|
||||
const runTool = api.tools.get("fn_research_run")!;
|
||||
const result = await runTool.execute("call-0", { query: "fusion" }, undefined, undefined, makeCtx(tmpDir));
|
||||
@@ -87,11 +111,15 @@ describe("research extension tools", () => {
|
||||
it("creates, reads, lists, and cancels runs", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
await store.updateGlobalSettings({
|
||||
experimentalFeatures: { researchView: true } as Record<string, boolean>,
|
||||
researchGlobalEnabled: true,
|
||||
researchGlobalWebSearchProvider: "searxng",
|
||||
researchGlobalSearxngUrl: "http://localhost:8888",
|
||||
researchGlobalDefaults: { searchProvider: "searxng" },
|
||||
});
|
||||
await store.updateSettings({
|
||||
researchGlobalWebSearchProvider: "tavily",
|
||||
researchGlobalTavilyApiKey: "test-key",
|
||||
researchSettings: { enabled: true },
|
||||
researchGlobalDefaults: { searchProvider: "tavily" },
|
||||
researchSettings: { enabled: true, searchProvider: "searxng" },
|
||||
});
|
||||
|
||||
const created = store.getResearchStore().createRun({ query: "fusion architecture", topic: "fusion architecture" });
|
||||
@@ -116,6 +144,16 @@ describe("research extension tools", () => {
|
||||
it("returns structured missing-run details for get and cancel", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
await store.updateGlobalSettings({
|
||||
experimentalFeatures: { researchView: true } as Record<string, boolean>,
|
||||
researchGlobalEnabled: true,
|
||||
researchGlobalWebSearchProvider: "searxng",
|
||||
researchGlobalSearxngUrl: "http://localhost:8888",
|
||||
researchGlobalDefaults: { searchProvider: "searxng" },
|
||||
});
|
||||
await store.updateSettings({
|
||||
researchSettings: { enabled: true, searchProvider: "searxng" },
|
||||
});
|
||||
|
||||
const getTool = api.tools.get("fn_research_get")!;
|
||||
const getResult = await getTool.execute("call-missing-get", { id: "RR-404" }, undefined, undefined, makeCtx(tmpDir));
|
||||
@@ -133,6 +171,16 @@ describe("research extension tools", () => {
|
||||
it("returns completed-run structured findings and citations", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
await store.updateGlobalSettings({
|
||||
experimentalFeatures: { researchView: true } as Record<string, boolean>,
|
||||
researchGlobalEnabled: true,
|
||||
researchGlobalWebSearchProvider: "searxng",
|
||||
researchGlobalSearxngUrl: "http://localhost:8888",
|
||||
researchGlobalDefaults: { searchProvider: "searxng" },
|
||||
});
|
||||
await store.updateSettings({
|
||||
researchSettings: { enabled: true, searchProvider: "searxng" },
|
||||
});
|
||||
|
||||
const run = store.getResearchStore().createRun({ query: "fusion", topic: "fusion" });
|
||||
store.getResearchStore().setResults(run.id, {
|
||||
@@ -157,6 +205,16 @@ describe("research extension tools", () => {
|
||||
it("retries failed run and returns retry linkage metadata", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
await store.updateGlobalSettings({
|
||||
experimentalFeatures: { researchView: true } as Record<string, boolean>,
|
||||
researchGlobalEnabled: true,
|
||||
researchGlobalWebSearchProvider: "searxng",
|
||||
researchGlobalSearxngUrl: "http://localhost:8888",
|
||||
researchGlobalDefaults: { searchProvider: "searxng" },
|
||||
});
|
||||
await store.updateSettings({
|
||||
researchSettings: { enabled: true, searchProvider: "searxng" },
|
||||
});
|
||||
|
||||
const run = store.getResearchStore().createRun({
|
||||
query: "fusion",
|
||||
@@ -187,11 +245,15 @@ describe("research extension tools", () => {
|
||||
it("returns INVALID_TRANSITION for cancel on terminal run", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
await store.updateGlobalSettings({
|
||||
experimentalFeatures: { researchView: true } as Record<string, boolean>,
|
||||
researchGlobalEnabled: true,
|
||||
researchGlobalWebSearchProvider: "searxng",
|
||||
researchGlobalSearxngUrl: "http://localhost:8888",
|
||||
researchGlobalDefaults: { searchProvider: "searxng" },
|
||||
});
|
||||
await store.updateSettings({
|
||||
researchGlobalWebSearchProvider: "tavily",
|
||||
researchGlobalTavilyApiKey: "test-key",
|
||||
researchSettings: { enabled: true },
|
||||
researchGlobalDefaults: { searchProvider: "tavily" },
|
||||
researchSettings: { enabled: true, searchProvider: "searxng" },
|
||||
});
|
||||
|
||||
const run = store.getResearchStore().createRun({ query: "fusion", topic: "fusion" });
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type ResearchRun,
|
||||
type ResearchRunStatus,
|
||||
RESEARCH_RUN_STATUSES,
|
||||
isResearchExperimentalEnabled,
|
||||
resolveResearchSettings,
|
||||
} from "@fusion/core";
|
||||
import {
|
||||
@@ -200,6 +201,10 @@ function formatTaskLine(t: Task): string {
|
||||
|
||||
async function getResearchAvailability(store: TaskStore): Promise<{ ok: boolean; code?: string; message?: string }> {
|
||||
const settings = await store.getSettings();
|
||||
if (!isResearchExperimentalEnabled(settings)) {
|
||||
return { ok: false, code: "feature-disabled", message: "Research tools are disabled. Enable experimentalFeatures.researchView first." };
|
||||
}
|
||||
|
||||
const resolved = resolveResearchSettings(settings);
|
||||
if (!resolved.enabled) {
|
||||
return { ok: false, code: "feature-disabled", message: "Research is disabled in settings." };
|
||||
@@ -1426,6 +1431,14 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const availability = await getResearchAvailability(store);
|
||||
if (!availability.ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: availability.message! }],
|
||||
details: { runs: [], setup: { code: availability.code, message: availability.message } },
|
||||
};
|
||||
}
|
||||
|
||||
const runs = store.getResearchStore().listRuns({ status: params.status as ResearchRunStatus | undefined, limit: params.limit ?? 10 });
|
||||
const text = runs.length ? runs.map((run) => `- ${run.id} [${run.status}] ${run.query}`).join("\n") : "No research runs found.";
|
||||
return { content: [{ type: "text", text }], details: { runs: runs.map(toResearchRunDetails) } };
|
||||
@@ -1439,6 +1452,22 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
parameters: Type.Object({ id: Type.String({ description: "Research run ID" }) }),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const availability = await getResearchAvailability(store);
|
||||
if (!availability.ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: availability.message! }],
|
||||
details: {
|
||||
runId: params.id,
|
||||
status: "unavailable",
|
||||
summary: null,
|
||||
findings: [],
|
||||
citations: [],
|
||||
error: availability.message,
|
||||
setup: { code: availability.code, message: availability.message },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const run = store.getResearchStore().getRun(params.id);
|
||||
if (!run) {
|
||||
return {
|
||||
@@ -1465,6 +1494,23 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
parameters: Type.Object({ id: Type.String({ description: "Research run ID" }) }),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const availability = await getResearchAvailability(store);
|
||||
if (!availability.ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: availability.message! }],
|
||||
isError: true,
|
||||
details: {
|
||||
runId: params.id,
|
||||
status: "unavailable",
|
||||
summary: null,
|
||||
findings: [],
|
||||
citations: [],
|
||||
error: availability.message,
|
||||
setup: { code: availability.code, message: availability.message },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const researchStore = store.getResearchStore();
|
||||
const run = researchStore.getRun(params.id);
|
||||
if (!run) {
|
||||
@@ -1510,6 +1556,23 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
parameters: Type.Object({ id: Type.String({ description: "Research run ID" }) }),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const availability = await getResearchAvailability(store);
|
||||
if (!availability.ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: availability.message! }],
|
||||
isError: true,
|
||||
details: {
|
||||
runId: params.id,
|
||||
status: "unavailable",
|
||||
summary: null,
|
||||
findings: [],
|
||||
citations: [],
|
||||
error: availability.message,
|
||||
setup: { code: availability.code, message: availability.message },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const researchStore = store.getResearchStore();
|
||||
const run = researchStore.getRun(params.id);
|
||||
if (!run) {
|
||||
|
||||
Reference in New Issue
Block a user