Compare commits

...

1 Commits

Author SHA1 Message Date
Vincent Koc
5a5d3b7d63 fix(agents): keep lean tools behind catalog controls 2026-05-29 19:50:36 +02:00
8 changed files with 122 additions and 13 deletions

View File

@@ -30,7 +30,7 @@ Treat them differently from normal config:
## Local model lean mode
`agents.defaults.experimental.localModelLean: true` is a pressure-release valve for weaker local-model setups. When it is on, OpenClaw drops three default tools — `browser`, `cron`, and `message` — from the agent's tool surface for every turn. Nothing else changes. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
`agents.defaults.experimental.localModelLean: true` is a pressure-release valve for weaker local-model setups. When it is on, OpenClaw drops three default tools — `browser`, `cron`, and `message` — from the model-visible tool surface for every turn. When Code Mode or Tool Search is enabled, those tools can still stay in the hidden catalog behind the compact controls. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
### Why these three tools
@@ -40,7 +40,7 @@ These three tools have the largest descriptions and the most parameter shapes in
- The model picking the right tool vs. emitting malformed tool calls because there are too many similar-looking schemas.
- The Chat Completions adapter staying inside the server's structured-output limits vs. tripping a 400 on tool-call payload size.
Removing them does not silently rewire OpenClaw — it just makes the tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available.
Removing them does not silently rewire OpenClaw — it just makes the visible tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available. With Code Mode or Tool Search, the compact control can still search for and call hidden catalog tools that policy allowed for the run.
### When to turn it on
@@ -94,7 +94,7 @@ Restart the Gateway after changing the flag, then confirm the trimmed tool list
openclaw status --deep
```
The deep status output lists the active agent tools; `browser`, `cron`, and `message` should be absent when lean mode is on.
The deep status output lists the active model-visible agent tools; `browser`, `cron`, and `message` should be absent when lean mode is on. If Code Mode or Tool Search is enabled, they may still be available through the hidden catalog.
## Experimental does not mean hidden

View File

@@ -315,7 +315,7 @@ If the model loads cleanly but full agent turns misbehave, work top-down — con
openclaw infer model run --gateway --model <provider/model> --prompt "Reply with exactly: pong" --json
```
3. **Try lean mode.** If both probes pass but real agent turns fail with malformed tool calls or oversized prompts, enable `agents.defaults.experimental.localModelLean: true`. It drops the three heaviest default tools (`browser`, `cron`, `message`) so the prompt shape is smaller and less brittle. See [Experimental Features → Local model lean mode](/concepts/experimental-features#local-model-lean-mode) for the full explanation, when to use it, and how to confirm it is on.
3. **Try lean mode.** If both probes pass but real agent turns fail with malformed tool calls or oversized prompts, enable `agents.defaults.experimental.localModelLean: true`. It drops the three heaviest default tools (`browser`, `cron`, `message`) from the visible model surface so the prompt shape is smaller and less brittle. When Code Mode or Tool Search is enabled, those tools can still sit behind the compact catalog controls. See [Experimental Features → Local model lean mode](/concepts/experimental-features#local-model-lean-mode) for the full explanation, when to use it, and how to confirm it is on.
4. **Disable tools entirely as a last resort.** If lean mode is not enough, set `models.providers.<provider>.models[].compat.supportsTools: false` for that model entry. The agent will then operate without tool calls on that model.

View File

@@ -679,7 +679,7 @@ Use these as starting points and replace model IDs with the exact names from `ol
```
Use `compat.supportsTools: false` only when the model or server reliably fails on tool schemas. It trades agent capability for stability.
`localModelLean` removes the browser, cron, and message tools from the agent surface, but it does not change Ollama's runtime context or thinking mode. Pair it with explicit `params.num_ctx` and `params.thinking: false` for small Qwen-style thinking models that loop or spend their response budget on hidden reasoning.
`localModelLean` removes the browser, cron, and message tools from the model-visible agent surface, but it does not change Ollama's runtime context or thinking mode. If Code Mode or Tool Search is enabled, those tools can still be called from the hidden catalog. Pair lean mode with explicit `params.num_ctx` and `params.thinking: false` for small Qwen-style thinking models that loop or spend their response budget on hidden reasoning.
</Accordion>
</AccordionGroup>

View File

@@ -198,6 +198,37 @@ describe("Agent-specific tool filtering", () => {
expect(toolNames).not.toContain("message");
});
it("can defer lean local-model filtering for hidden catalog registration", () => {
const cfg: OpenClawConfig = {
agents: {
list: [
{
id: "local",
default: true,
experimental: {
localModelLean: true,
},
},
],
},
};
const tools = createOpenClawCodingTools({
config: cfg,
sessionKey: "agent:local:main",
workspaceDir: "/tmp/test",
agentDir: "/tmp/agent-local",
modelProvider: "ollama",
modelId: "qwen3.5:9b",
deferLocalModelLeanToolFilter: true,
});
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("browser");
expect(toolNames).toContain("cron");
expect(toolNames).toContain("message");
});
it("should allow apply_patch for OpenAI models when write is allow-listed", () => {
const cfg: OpenClawConfig = {
tools: {

View File

@@ -245,14 +245,17 @@ function applyModelProviderToolPolicy(
agentDir?: string;
modelCompat?: ModelCompatConfig;
suppressManagedWebSearch?: boolean;
deferLocalModelLeanToolFilter?: boolean;
},
): AnyAgentTool[] {
tools = filterLocalModelLeanTools({
tools,
config: params?.config,
agentId: params?.agentId,
sessionKey: params?.sessionKey,
});
if (params?.deferLocalModelLeanToolFilter !== true) {
tools = filterLocalModelLeanTools({
tools,
config: params?.config,
agentId: params?.agentId,
sessionKey: params?.sessionKey,
});
}
if (
params?.suppressManagedWebSearch !== false &&
@@ -490,6 +493,8 @@ export function createOpenClawCodingTools(options?: {
forceHeartbeatTool?: boolean;
/** If false, build plugin tools only while preserving the shared policy pipeline. */
includeCoreTools?: boolean;
/** Keep local-lean denied tools available for hidden catalog registration. */
deferLocalModelLeanToolFilter?: boolean;
/** Include Tool Search control tools when enabled for this run. */
includeToolSearchControls?: boolean;
/** Executes cataloged tools through the active agent run lifecycle. */
@@ -1063,6 +1068,7 @@ export function createOpenClawCodingTools(options?: {
agentDir: options?.agentDir,
modelCompat: options?.modelCompat,
suppressManagedWebSearch: options?.suppressManagedWebSearch,
deferLocalModelLeanToolFilter: options?.deferLocalModelLeanToolFilter,
});
options?.recordToolPrepStage?.("model-provider-policy");
// Sender identity is carried for command/channel-action auth; tool visibility

View File

@@ -143,7 +143,11 @@ import { subscribeEmbeddedAgentSession } from "../../embedded-agent-subscribe.js
import { isTimeoutError } from "../../failover-error.js";
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "../../local-model-lean.js";
import {
filterLocalModelLeanPreCatalogTools,
filterLocalModelLeanTools,
isLocalModelLeanEnabled,
} from "../../local-model-lean.js";
import { resolveModelAuthMode } from "../../model-auth.js";
import { resolveDefaultModelForAgent } from "../../model-selection.js";
import { supportsModelTools } from "../../model-tool-support.js";
@@ -1117,6 +1121,8 @@ export async function runEmbeddedAttempt(
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
includeCoreTools: toolConstructionPlan.includeCoreTools,
deferLocalModelLeanToolFilter:
toolSearchControlsEnabledForRun || codeModeControlsEnabledForRun,
includeToolSearchControls: toolSearchControlsEnabledForRun,
toolSearchCatalogExecutor: (toolParams) => {
if (!toolSearchCatalogExecutor) {
@@ -1397,8 +1403,9 @@ export async function runEmbeddedAttempt(
model: params.model,
})
: filteredBundledTools;
const projectedUncompactedEffectiveTools = filterLocalModelLeanTools({
const projectedUncompactedEffectiveTools = filterLocalModelLeanPreCatalogTools({
tools: [...tools, ...normalizedBundledTools],
controlsEnabled: toolSearchControlsEnabledForRun || codeModeControlsEnabledForRun,
config: params.config,
agentId: sessionAgentId,
});

View File

@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
import {
filterLocalModelLeanPreCatalogTools,
filterLocalModelLeanTools,
} from "./local-model-lean.js";
function tools(names: string[]): AnyAgentTool[] {
return names.map((name) => ({ name })) as AnyAgentTool[];
}
describe("local model lean catalog filtering", () => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
};
it("preserves heavyweight tools before catalog compaction when compact controls are enabled", () => {
expect(
filterLocalModelLeanPreCatalogTools({
tools: tools(["tool_search_code", "read", "browser", "cron", "message", "exec"]),
controlsEnabled: true,
config: cfg,
}).map((tool) => tool.name),
).toEqual(["tool_search_code", "read", "browser", "cron", "message", "exec"]);
});
it("still trims heavyweight tools from the final visible surface", () => {
expect(
filterLocalModelLeanTools({
tools: tools(["tool_search_code", "read", "browser", "cron", "message", "exec"]),
config: cfg,
}).map((tool) => tool.name),
).toEqual(["tool_search_code", "read", "exec"]);
});
it("filters before catalog compaction when compact controls are unavailable", () => {
expect(
filterLocalModelLeanPreCatalogTools({
tools: tools(["read", "browser", "cron", "message", "exec"]),
controlsEnabled: false,
config: cfg,
}).map((tool) => tool.name),
).toEqual(["read", "exec"]);
});
});

View File

@@ -49,3 +49,16 @@ export function filterLocalModelLeanTools(params: {
}
return params.tools.filter((tool) => !LOCAL_MODEL_LEAN_DENY_TOOL_NAMES.has(tool.name));
}
export function filterLocalModelLeanPreCatalogTools(params: {
tools: AnyAgentTool[];
controlsEnabled: boolean;
config?: OpenClawConfig;
agentId?: string;
sessionKey?: string;
}): AnyAgentTool[] {
if (params.controlsEnabled) {
return params.tools;
}
return filterLocalModelLeanTools(params);
}