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:
5
.changeset/fn-3705-research-tools-gate.md
Normal file
5
.changeset/fn-3705-research-tools-gate.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Gate `fn_research_*` tool availability behind `experimentalFeatures.researchView` so CLI and agent sessions consistently return feature-disabled responses when Research is not experimentally enabled.
|
||||
@@ -35,7 +35,7 @@ Research is **not** a replacement for reading source code or local docs — use
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Research requires provider configuration before runs can execute. If setup is incomplete, the dashboard shows a setup prompt and CLI/agent tools return actionable error codes.
|
||||
Research requires both the experimental flag and provider configuration before runs can execute. If setup is incomplete, the dashboard shows a setup prompt and CLI/agent tools return actionable error codes.
|
||||
|
||||
### 1. Enable the feature flag
|
||||
|
||||
@@ -49,7 +49,7 @@ The Research view is gated behind an experimental feature flag. Set in global se
|
||||
}
|
||||
```
|
||||
|
||||
This also reveals the **Research Defaults** and **Research** settings sections in the dashboard Settings modal.
|
||||
This also reveals the **Research Defaults** and **Research** settings sections in the dashboard Settings modal, and enables agent/CLI research tools (`fn_research_*`).
|
||||
|
||||
### 2. Configure a web search provider
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
|
||||
| `researchGlobalMaxSearchResults` | `number` | `undefined` | Maximum search results per provider query. |
|
||||
| `researchGlobalFetchTimeoutMs` | `number` | `30000` | Timeout for individual HTTP fetches in milliseconds. |
|
||||
| `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`, which gates all Research surfaces and tools (dashboard view, engine task-session tools, and CLI `fn_research_*` tools). |
|
||||
| `remoteAccess` | `RemoteAccessSettings` | `{ activeProvider: null, providers: {...}, tokenStrategy: {...}, lifecycle: {...} }` | Global-scoped remote access provider + token strategy configuration used by Remote Access routes and tunnel lifecycle controls. |
|
||||
|
||||
### Notification providers (pluggable)
|
||||
@@ -293,7 +293,7 @@ This applies to:
|
||||
- run limits (`maxConcurrentRuns`, `maxSourcesPerRun`, `maxDurationMs`, `requestTimeoutMs`)
|
||||
- export default (`defaultExportFormat`)
|
||||
|
||||
The standalone Research route is feature-gated separately via `experimentalFeatures.researchView`.
|
||||
Research is globally feature-gated via `experimentalFeatures.researchView`.
|
||||
When that flag is disabled, the Settings modal also hides both Research sections (`Research Defaults` and project `Research`) and falls back to the first visible section if a hidden research section is requested directly.
|
||||
|
||||
Research failures are normalized to a shared error-code contract (`FEATURE_DISABLED`, `MISSING_CREDENTIALS`, `PROVIDER_UNAVAILABLE`, `RATE_LIMITED`, `PROVIDER_TIMEOUT`, `RUN_CANCELLED`, `RETRY_EXHAUSTED`, `INVALID_TRANSITION`, `NON_RETRYABLE_PROVIDER_ERROR`, `INTERNAL_ERROR`) with retryability metadata so dashboard, API, CLI, and agent tooling show consistent recovery guidance.
|
||||
@@ -302,7 +302,7 @@ Recovery entrypoints in the dashboard:
|
||||
- **Settings → Research Defaults**: fix missing default provider configuration and provider-level readiness.
|
||||
- **Settings → Authentication**: repair missing provider credentials (`MISSING_CREDENTIALS`).
|
||||
- **Settings → Research (project)**: re-enable project research or source toggles when runs are blocked by project settings.
|
||||
- **Settings → Experimental Features**: enable `researchView` when the standalone Research route/surfaces are hidden.
|
||||
- **Settings → Experimental Features**: enable `researchView` when Research surfaces or `fn_research_*` tools report feature-disabled.
|
||||
|
||||
**Credential storage rule:** API keys for Research providers are not stored in settings JSON. They are managed through the existing auth storage pipeline (`/api/auth/status`, `POST /api/auth/api-key`, `DELETE /api/auth/api-key`) and persisted in auth credential storage with masked hints in API responses.
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveResearchSettings } from "../research-settings.js";
|
||||
import { isResearchExperimentalEnabled, resolveResearchSettings } from "../research-settings.js";
|
||||
import type { Settings } from "../types.js";
|
||||
|
||||
describe("isResearchExperimentalEnabled", () => {
|
||||
it("returns false when settings are missing", () => {
|
||||
expect(isResearchExperimentalEnabled(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when researchView is false", () => {
|
||||
expect(isResearchExperimentalEnabled({ experimentalFeatures: { researchView: false } as Record<string, boolean> } as Settings)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when researchView is true", () => {
|
||||
expect(isResearchExperimentalEnabled({ experimentalFeatures: { researchView: true } as Record<string, boolean> } as Settings)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveResearchSettings", () => {
|
||||
it("resolves global-only defaults", () => {
|
||||
const resolved = resolveResearchSettings({
|
||||
|
||||
@@ -721,7 +721,7 @@ export type {
|
||||
ResearchCancellationState,
|
||||
} from "./research-types.js";
|
||||
|
||||
export { resolveResearchSettings } from "./research-settings.js";
|
||||
export { isResearchExperimentalEnabled, resolveResearchSettings } from "./research-settings.js";
|
||||
export type { ResolvedResearchSettings } from "./research-settings.js";
|
||||
export { resolveEvalSettings } from "./eval-settings.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { ResearchEnabledSources, Settings } from "./types.js";
|
||||
|
||||
export function isResearchExperimentalEnabled(settings: Partial<Settings> | undefined): boolean {
|
||||
return settings?.experimentalFeatures?.researchView === true;
|
||||
}
|
||||
|
||||
export interface ResolvedResearchSettings {
|
||||
enabled: boolean;
|
||||
searchProvider?: string;
|
||||
|
||||
@@ -4684,8 +4684,11 @@ const mockedReviewStep = vi.mocked(mockedReviewStepFn);
|
||||
* Helper: executes a task and captures the custom tools passed to createFnAgent.
|
||||
* Returns a map of tool name → tool execute function for direct testing.
|
||||
*/
|
||||
async function captureTools(): Promise<Record<string, (id: string, params: any) => Promise<any>>> {
|
||||
async function captureTools(settingsOverride?: Record<string, unknown>): Promise<Record<string, (id: string, params: any) => Promise<any>>> {
|
||||
const store = createMockStore();
|
||||
if (settingsOverride) {
|
||||
store.getSettings.mockResolvedValue({ ...(await store.getSettings()), ...settingsOverride });
|
||||
}
|
||||
// Simulate the real TaskStore: forward transitions persist, but in-progress
|
||||
// regressions on done/skipped steps are rejected so executor.ts can surface
|
||||
// the "already <status>" diagnostic.
|
||||
@@ -4908,14 +4911,22 @@ describe("Code review verdict enforcement - fn_task_update blocking", () => {
|
||||
expect(allowed.content[0].text).toContain("→ done");
|
||||
});
|
||||
|
||||
it("registers research runtime tools in customTools", async () => {
|
||||
const tools = await captureTools();
|
||||
it("registers research runtime tools in customTools when researchView experimental flag is enabled", async () => {
|
||||
const tools = await captureTools({ experimentalFeatures: { researchView: true } });
|
||||
expect(tools.fn_research_run).toBeTypeOf("function");
|
||||
expect(tools.fn_research_list).toBeTypeOf("function");
|
||||
expect(tools.fn_research_get).toBeTypeOf("function");
|
||||
expect(tools.fn_research_cancel).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("does not register research runtime tools when researchView experimental flag is disabled", async () => {
|
||||
const tools = await captureTools({ experimentalFeatures: { researchView: false } });
|
||||
expect(tools.fn_research_run).toBeUndefined();
|
||||
expect(tools.fn_research_list).toBeUndefined();
|
||||
expect(tools.fn_research_get).toBeUndefined();
|
||||
expect(tools.fn_research_cancel).toBeUndefined();
|
||||
});
|
||||
|
||||
it("REVISE tool response text includes re-review instructions", async () => {
|
||||
mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Bug found", summary: "Issues" });
|
||||
|
||||
|
||||
@@ -838,6 +838,14 @@ describe("fast-mode triage", () => {
|
||||
await mkdir(join(rootDir, ".fusion", "tasks", task.id), { recursive: true });
|
||||
|
||||
const store = createMockStore({
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
experimentalFeatures: { researchView: true },
|
||||
} as Settings),
|
||||
getTask: vi.fn().mockResolvedValue({ ...mockTaskDetail, id: task.id, attachments: [], comments: [] }),
|
||||
parseDependenciesFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
parseStepsFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
@@ -880,6 +888,43 @@ describe("fast-mode triage", () => {
|
||||
await cleanupTriageFixtureRoot(rootDir);
|
||||
}
|
||||
});
|
||||
|
||||
it("omits research tools when researchView experimental flag is disabled", async () => {
|
||||
const task = createTriageTask({ id: "FN-FAST-005", executionMode: "fast" });
|
||||
const store = createMockStore({
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
experimentalFeatures: { researchView: false },
|
||||
} as Settings),
|
||||
getTask: vi.fn().mockResolvedValue({ ...mockTaskDetail, id: task.id, attachments: [], comments: [] }),
|
||||
});
|
||||
|
||||
let capturedTools: any[] = [];
|
||||
mockCreateFnAgent.mockImplementationOnce(async (opts: any) => {
|
||||
capturedTools = opts.customTools;
|
||||
return {
|
||||
session: {
|
||||
state: {},
|
||||
sessionManager: { getLeafId: vi.fn().mockReturnValue(null) },
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
navigateTree: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const processor = new TriageProcessor(store, "/tmp/root");
|
||||
await processor.specifyTask(task);
|
||||
|
||||
expect(capturedTools.some((tool: any) => tool.name === "fn_research_run")).toBe(false);
|
||||
expect(capturedTools.some((tool: any) => tool.name === "fn_research_list")).toBe(false);
|
||||
expect(capturedTools.some((tool: any) => tool.name === "fn_research_get")).toBe(false);
|
||||
expect(capturedTools.some((tool: any) => tool.name === "fn_research_cancel")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readAttachmentContents", () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
buildExecutionMemoryInstructions,
|
||||
getTaskMergeBlocker,
|
||||
isEphemeralAgent,
|
||||
isResearchExperimentalEnabled,
|
||||
resolveAgentPrompt,
|
||||
resolveProjectDefaultModel,
|
||||
type RunCommandResult,
|
||||
@@ -2826,11 +2827,13 @@ export class TaskExecutor {
|
||||
this.createSpawnAgentTool(task.id, worktreePath, settings),
|
||||
this.createTaskDocumentWriteTool(task.id),
|
||||
this.createTaskDocumentReadTool(task.id),
|
||||
...createResearchTools({
|
||||
store: this.store,
|
||||
rootDir: this.rootDir,
|
||||
getSettings: async () => this.store.getSettings(),
|
||||
}),
|
||||
...(isResearchExperimentalEnabled(settings)
|
||||
? createResearchTools({
|
||||
store: this.store,
|
||||
rootDir: this.rootDir,
|
||||
getSettings: async () => this.store.getSettings(),
|
||||
})
|
||||
: []),
|
||||
...createMemoryTools(this.rootDir, settings, assignedAgent ? {
|
||||
agentMemory: {
|
||||
agentId: assignedAgent.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from "@fusion/core";
|
||||
import {
|
||||
buildTriageMemoryInstructions,
|
||||
isResearchExperimentalEnabled,
|
||||
resolveAgentPrompt,
|
||||
sortTasksByPriorityThenAgeAndId,
|
||||
} from "@fusion/core";
|
||||
@@ -941,11 +942,13 @@ export class TriageProcessor {
|
||||
}),
|
||||
createTaskDocumentWriteTool(this.store, task.id),
|
||||
createTaskDocumentReadTool(this.store, task.id),
|
||||
...createResearchTools({
|
||||
store: this.store,
|
||||
rootDir: this.rootDir,
|
||||
getSettings: async () => this.store.getSettings(),
|
||||
}),
|
||||
...(isResearchExperimentalEnabled(settings)
|
||||
? createResearchTools({
|
||||
store: this.store,
|
||||
rootDir: this.rootDir,
|
||||
getSettings: async () => this.store.getSettings(),
|
||||
})
|
||||
: []),
|
||||
...createMemoryTools(this.rootDir, settings),
|
||||
// Agent delegation tools — discover and delegate work to other agents.
|
||||
...(this.options.agentStore ? [
|
||||
|
||||
Reference in New Issue
Block a user