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:
5
.changeset/fn-3713-web-tools.md
Normal file
5
.changeset/fn-3713-web-tools.md
Normal 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.
|
||||
@@ -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. |
|
||||
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user