feat(FN-3713): add web builtin tool opt-in for AI agents

Added agent permission policy model with persistence in core, and wired web-builtin tool opt-in enabling planning and synthesis web tools in the dashboard with corresponding docs and a changeset. Also added an inline fast-mode toggle wired into peer executor state, retry logic for cluster task-ID ov

Fusion-Task-Id: FN-3713
This commit is contained in:
Fusion
2026-05-07 12:03:33 -07:00
committed by gsxdsm
parent 51c6ddb9a5
commit 81da75f661
10 changed files with 83 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Enable planning-mode and research synthesis agent sessions to opt into runtime builtin `WebSearch` and `WebFetch` tools when supported, while keeping readonly defaults unchanged for other sessions.

View File

@@ -75,7 +75,7 @@ Every task shows its plan, its reviews, its diffs, and its file changes in real
| 🏢 **Agent companies** | Import pre-built teams — 440+ agents across 16 companies — and run them autonomously for weeks. |
| 📬 **Inter-agent messaging** | Built-in mailbox between agents. Delegate, clarify, coordinate. |
| 🗺️ **Missions** | Hierarchical planning (Mission → Milestone → Slice → Feature → Task) with autopilot and validation contracts. |
| 🔬 **Research** | Bounded research runs with web search, GitHub, local docs, and LLM synthesis. Turn findings into tasks. ([Docs](./docs/research.md)) |
| 🔬 **Research** | Bounded research runs with web search, GitHub, local docs, and LLM synthesis (plus runtime builtin WebSearch/WebFetch support in planning + synthesis flows when available). Turn findings into tasks. ([Docs](./docs/research.md)) |
| 🧪 **Self-improvement** | Agents reflect on their own output and update their prompts as they learn your codebase. |
| 🔓 **Open source. MIT.** | No vendor lock-in. Run it on your own hardware. Shipping weekly. |

View File

@@ -400,6 +400,8 @@ For AI-guided task specification, see [Planning mode](#planning-mode).
Use planning mode to turn a rough idea into a triage task through an interactive AI-guided Q&A flow.
When supported by your configured runtime/model provider, planning sessions can also use builtin `WebSearch` and `WebFetch` tools for live context gathering.
```bash
fn task plan [description]
```

View File

@@ -274,6 +274,8 @@ When `available` is `false`, the response includes `reason` and `setupInstructio
AI agents (triage, executor, and custom roles) can use research tools during planning and execution sessions. These tools are registered in the pi extension (`packages/cli/src/extension.ts`).
Additionally, planning-mode sessions and the LLM synthesis agent can opt into runtime builtin `WebSearch`/`WebFetch` tools (when the selected runtime supports them), complementing configured research providers.
### Available tools
| Tool | Description |

View File

@@ -361,6 +361,18 @@ describe("planning module", () => {
expect(session?.agent).toBeDefined();
});
it("passes builtin web tool allowlist when creating non-streaming planning agent", async () => {
const createFnAgentSpy = vi.fn(async () => createMockAgent(STANDARD_QUESTION_RESPONSES));
__setCreateFnAgent(createFnAgentSpy as any);
await createSession(getUniqueIp(), initialPlan, undefined, TEST_ROOT_DIR);
expect(createFnAgentSpy).toHaveBeenCalledWith(expect.objectContaining({
tools: "readonly",
builtinToolsAllowlist: ["WebSearch", "WebFetch"],
}));
});
it("cleans up session on agent failure", async () => {
__setCreateFnAgent(async () => {
throw new Error("Agent creation failed");
@@ -447,6 +459,7 @@ describe("planning module", () => {
const callArg = createFnAgentSpy.mock.calls[0]?.[0] as Record<string, unknown>;
expect(callArg?.defaultProvider).toBeUndefined();
expect(callArg?.defaultModelId).toBeUndefined();
expect(callArg?.builtinToolsAllowlist).toEqual(["WebSearch", "WebFetch"]);
});
it("uses custom prompt from promptOverrides when provided", async () => {

View File

@@ -35,6 +35,8 @@ import * as engineModule from "@fusion/engine";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AgentResult = any;
const PLANNING_BUILTIN_WEB_TOOLS = ["WebSearch", "WebFetch"] as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createFnAgent: any = engineCreateFnAgent;
@@ -785,6 +787,7 @@ export async function createSession(
cwd: rootDir,
systemPrompt,
tools: "readonly",
builtinToolsAllowlist: [...PLANNING_BUILTIN_WEB_TOOLS],
onThinking: () => {
// Non-streaming path ignores thinking output
},
@@ -1296,6 +1299,7 @@ async function createPlanningAgent(
cwd: rootDir,
systemPrompt,
tools: "readonly",
builtinToolsAllowlist: [...PLANNING_BUILTIN_WEB_TOOLS],
...(modelProvider && modelId
? {
defaultProvider: modelProvider,

View File

@@ -694,6 +694,40 @@ describe("createFnAgent", () => {
expect(createSessionArgs.customTools.map((tool) => tool.name)).toContain("fn_list_agents");
});
it("does not allow extra builtin tools in readonly sessions by default", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp",
systemPrompt: "test",
tools: "readonly",
});
const createSessionArgs = createAgentSessionMock.mock.calls[0]?.[0] as { tools?: string[] };
expect(createSessionArgs.tools).toBeUndefined();
});
it("passes opt-in builtin web tool allowlist to createAgentSession", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp",
systemPrompt: "test",
tools: "readonly",
builtinToolsAllowlist: ["WebSearch", "WebFetch"],
});
const createSessionArgs = createAgentSessionMock.mock.calls[0]?.[0] as { tools?: string[] };
expect(createSessionArgs.tools).toEqual(expect.arrayContaining([
"read",
"grep",
"find",
"ls",
"WebSearch",
"WebFetch",
]));
});
it("keeps caller customTools in coding sessions", async () => {
createCodingToolsMock.mockReturnValueOnce([{ name: "read" }, { name: "write" }] as any);
const customTool = {

View File

@@ -429,11 +429,15 @@ export interface FallbackModelUsedPayload {
timestamp?: string;
}
export type BuiltinWebToolName = "WebSearch" | "WebFetch";
export interface AgentOptions {
cwd: string;
systemPrompt: string;
tools?: "coding" | "readonly";
customTools?: ToolDefinition[];
/** Optional allowlist of builtin runtime web tools to keep enabled. */
builtinToolsAllowlist?: BuiltinWebToolName[];
onText?: (delta: string) => void;
onThinking?: (delta: string) => void;
onToolStart?: (name: string, args?: Record<string, unknown>) => void;
@@ -1243,7 +1247,7 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
if (options.beforeSpawnSession) {
await options.beforeSpawnSession();
}
return createAgentSession({
const createSessionOptions: Parameters<typeof createAgentSession>[0] = {
cwd: options.cwd,
authStorage,
modelRegistry,
@@ -1253,7 +1257,18 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
sessionManager,
settingsManager,
...(modelOverride ? { model: modelOverride } : {}),
});
};
if (options.builtinToolsAllowlist && options.builtinToolsAllowlist.length > 0) {
createSessionOptions.tools = [
...new Set([
...customToolList.map((tool) => tool.name),
...options.builtinToolsAllowlist,
]),
];
}
return createAgentSession(createSessionOptions);
};
const emitFallbackUsed = async (triggerPoint: "session-creation" | "prompt-time"): Promise<void> => {

View File

@@ -35,6 +35,9 @@ describe("LLMSynthesisProvider", () => {
);
expect(promptWithFallbackMock).toHaveBeenCalled();
expect(createFnAgentMock).toHaveBeenCalledWith(expect.objectContaining({
builtinToolsAllowlist: ["WebSearch", "WebFetch"],
}));
expect(result.citations).toEqual(["https://a"]);
expect(result.confidence).toBe(0.8);
expect(disposeMock).toHaveBeenCalled();

View File

@@ -6,6 +6,7 @@ import { ResearchProviderError } from "../types.js";
const log = createLogger("research:llm-synthesis");
const DEFAULT_TIMEOUT_MS = 120_000;
const SYNTHESIS_BUILTIN_WEB_TOOLS = ["WebSearch", "WebFetch"] as const;
const LARGE_MODEL_CONTEXT_CHARS = 100_000;
const SMALL_MODEL_CONTEXT_CHARS = 30_000;
@@ -54,6 +55,7 @@ export class LLMSynthesisProvider implements ResearchProvider {
const { session } = await createFnAgent({
cwd: this.options.projectRoot,
tools: "readonly",
builtinToolsAllowlist: [...SYNTHESIS_BUILTIN_WEB_TOOLS],
systemPrompt: "You synthesize research findings into concise, cited outputs.",
defaultProvider: modelSelection.provider,
defaultModelId: modelSelection.modelId,