Skip to content

Commit 1e2991d

Browse files
committed
fix(agents): compact lean local tool catalogs
1 parent d4240cd commit 1e2991d

7 files changed

Lines changed: 186 additions & 13 deletions

File tree

docs/concepts/experimental-features.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Treat them differently from normal config:
3030

3131
## Local model lean mode
3232

33-
`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.
33+
`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. It also defaults that run to structured Tool Search controls when `tools.toolSearch` is not explicitly configured, so larger plugin, MCP, or client tool catalogs stay behind `tool_search`, `tool_describe`, and `tool_call` instead of being dumped into the prompt. Runs that require direct `message` delivery keep that tool direct instead of enabling the lean-mode Tool Search default. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
3434

3535
### Why these three tools
3636

@@ -40,7 +40,7 @@ These three tools have the largest descriptions and the most parameter shapes in
4040
- The model picking the right tool vs. emitting malformed tool calls because there are too many similar-looking schemas.
4141
- The Chat Completions adapter staying inside the server's structured-output limits vs. tripping a 400 on tool-call payload size.
4242

43-
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.
43+
Removing them does not silently rewire OpenClaw — it just makes the direct tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available. Extra catalogs remain callable through Tool Search unless you explicitly set `tools.toolSearch: false`.
4444

4545
### When to turn it on
4646

@@ -56,6 +56,8 @@ If your backend handles the full default runtime cleanly, leave this off. Lean m
5656

5757
Lean mode also does not replace `tools.profile`, `tools.allow`/`tools.deny`, or the model `compat.supportsTools: false` escape hatch. If you need a permanent narrower tool surface for a specific agent, prefer those stable knobs over the experimental flag.
5858

59+
If you already tune Tool Search globally, OpenClaw leaves that operator config alone. Set `tools.toolSearch: false` to opt out of the lean-mode Tool Search default.
60+
5961
### Enable
6062

6163
```json5
@@ -94,7 +96,7 @@ Restart the Gateway after changing the flag, then confirm the trimmed tool list
9496
openclaw status --deep
9597
```
9698

97-
The deep status output lists the active agent tools; `browser`, `cron`, and `message` should be absent when lean mode is on.
99+
The deep status output lists the active agent tools; `browser`, `cron`, and `message` should be absent when lean mode is on unless the current delivery mode forces direct `message` replies.
98100

99101
## Experimental does not mean hidden
100102

docs/gateway/local-models.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ If the model loads cleanly but full agent turns misbehave, work top-down — con
315315
openclaw infer model run --gateway --model <provider/model> --prompt "Reply with exactly: pong" --json
316316
```
317317

318-
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.
318+
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`) and defaults larger tool catalogs behind structured Tool Search controls, except for runs that must keep direct `message` delivery semantics. 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.
319319

320320
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.
321321

docs/providers/ollama.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ Use these as starting points and replace model IDs with the exact names from `ol
692692
```
693693

694694
Use `compat.supportsTools: false` only when the model or server reliably fails on tool schemas. It trades agent capability for stability.
695-
`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.
695+
`localModelLean` removes the browser, cron, and message tools from the direct agent surface and defaults larger catalogs behind structured Tool Search controls except when a run must keep direct message delivery semantics, 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.
696696

697697
</Accordion>
698698
</AccordionGroup>

src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,102 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
284284
expect(toolSearchControlsCase.toolSearchCatalogRef).toEqual({});
285285
});
286286

287+
it("defaults local-model lean embedded runs to Tool Search controls", async () => {
288+
await createContextEngineAttemptRunner({
289+
contextEngine: {
290+
assemble: async ({ messages }) => ({ messages, estimatedTokens: 1 }),
291+
},
292+
sessionKey,
293+
tempPaths,
294+
attemptOverrides: {
295+
disableTools: false,
296+
config: {
297+
agents: {
298+
defaults: {
299+
experimental: {
300+
localModelLean: true,
301+
},
302+
},
303+
},
304+
} as OpenClawConfig,
305+
},
306+
});
307+
308+
expect(hoisted.createOpenClawCodingToolsMock).toHaveBeenCalledTimes(1);
309+
const options = mockParams(
310+
hoisted.createOpenClawCodingToolsMock,
311+
0,
312+
"createOpenClawCodingTools options",
313+
);
314+
expect(options.includeToolSearchControls).toBe(true);
315+
const optionsConfig = requireRecord(options.config, "createOpenClawCodingTools config");
316+
const toolsConfig = requireRecord(
317+
optionsConfig.tools,
318+
"createOpenClawCodingTools tools config",
319+
);
320+
expect(toolsConfig.toolSearch).toEqual({
321+
enabled: true,
322+
mode: "tools",
323+
searchDefaultLimit: 5,
324+
maxSearchLimit: 10,
325+
});
326+
});
327+
328+
it("keeps Tool Search controls off for lean message-tool-only delivery", async () => {
329+
hoisted.createOpenClawCodingToolsMock.mockReturnValueOnce([
330+
{
331+
name: "message",
332+
label: "Message",
333+
description: "Send a visible reply.",
334+
parameters: { type: "object", properties: {} },
335+
execute: async () => ({ text: "sent" }),
336+
},
337+
{
338+
name: "browser",
339+
label: "Browser",
340+
description: "Open a browser session.",
341+
parameters: { type: "object", properties: {} },
342+
execute: async () => ({ text: "opened" }),
343+
},
344+
]);
345+
346+
await createContextEngineAttemptRunner({
347+
contextEngine: {
348+
assemble: async ({ messages }) => ({ messages, estimatedTokens: 1 }),
349+
},
350+
sessionKey,
351+
tempPaths,
352+
attemptOverrides: {
353+
disableTools: false,
354+
sourceReplyDeliveryMode: "message_tool_only",
355+
config: {
356+
agents: {
357+
defaults: {
358+
experimental: {
359+
localModelLean: true,
360+
},
361+
},
362+
},
363+
} as OpenClawConfig,
364+
},
365+
});
366+
367+
expect(hoisted.createOpenClawCodingToolsMock).toHaveBeenCalledTimes(1);
368+
const options = mockParams(
369+
hoisted.createOpenClawCodingToolsMock,
370+
0,
371+
"createOpenClawCodingTools options",
372+
);
373+
expect(options.includeToolSearchControls).toBe(false);
374+
const sessionOptions = mockParams(
375+
hoisted.createAgentSessionMock,
376+
0,
377+
"createAgentSession options",
378+
);
379+
const customTools = requireRecords(sessionOptions.customTools, "customTools");
380+
expect(customTools.map((tool) => tool.name)).toEqual(["message"]);
381+
});
382+
287383
it("quarantines unsupported tool schemas before creating the model session", async () => {
288384
hoisted.createOpenClawCodingToolsMock.mockReturnValue([
289385
{

src/agents/embedded-agent-runner/run/attempt.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ import { runAgentEndSideEffects } from "../../harness/agent-end-side-effects.js"
151151
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
152152
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
153153
import {
154+
applyLocalModelLeanToolSearchDefaults,
154155
filterLocalModelLeanTools,
155156
isLocalModelLeanEnabled,
156157
resolveLocalModelLeanPreserveToolNames,
@@ -1096,12 +1097,12 @@ export async function runEmbeddedAttempt(
10961097
});
10971098
};
10981099
const corePluginToolStages = createEmbeddedRunStageTracker();
1100+
const forceDirectMessageTool =
1101+
params.forceMessageTool === true || params.sourceReplyDeliveryMode === "message_tool_only";
10991102
const toolsAllowWithForcedRuntimeTools = mergeForcedEmbeddedAttemptToolsAllow(
11001103
params.toolsAllow,
11011104
{
1102-
forceMessageTool:
1103-
params.forceMessageTool === true ||
1104-
params.sourceReplyDeliveryMode === "message_tool_only",
1105+
forceMessageTool: forceDirectMessageTool,
11051106
},
11061107
);
11071108
const toolConstructionPlan = resolveEmbeddedAttemptToolConstructionPlan({
@@ -1111,6 +1112,13 @@ export async function runEmbeddedAttempt(
11111112
});
11121113
const toolsEnabled = supportsModelTools(params.model);
11131114
const codeModeConfig = resolveCodeModeConfig(params.config, sessionAgentId);
1115+
const toolSearchRuntimeConfig = forceDirectMessageTool
1116+
? params.config
1117+
: applyLocalModelLeanToolSearchDefaults({
1118+
config: params.config,
1119+
agentId: sessionAgentId,
1120+
sessionKey: sandboxSessionKey,
1121+
});
11141122
const codeModeControlsEnabledForRun =
11151123
toolsEnabled &&
11161124
params.disableTools !== true &&
@@ -1123,7 +1131,7 @@ export async function runEmbeddedAttempt(
11231131
!isRawModelRun &&
11241132
params.toolsAllow?.length !== 0 &&
11251133
!codeModeControlsEnabledForRun &&
1126-
resolveToolSearchConfig(params.config).enabled;
1134+
resolveToolSearchConfig(toolSearchRuntimeConfig).enabled;
11271135
const effectiveToolsAllow =
11281136
toolSearchControlsEnabledForRun && toolsAllowWithForcedRuntimeTools
11291137
? [
@@ -1198,7 +1206,7 @@ export async function runEmbeddedAttempt(
11981206
sandbox,
11991207
resolvedWorkspace,
12001208
}),
1201-
config: params.config,
1209+
config: toolSearchRuntimeConfig,
12021210
abortSignal: runAbortController.signal,
12031211
modelProvider: params.provider,
12041212
modelId: params.modelId,
@@ -1583,7 +1591,7 @@ export async function runEmbeddedAttempt(
15831591
})
15841592
: applyToolSearchCatalog({
15851593
tools: effectiveTools,
1586-
config: params.config,
1594+
config: toolSearchRuntimeConfig,
15871595
sessionId: params.sessionId,
15881596
sessionKey: sandboxSessionKey,
15891597
agentId: sessionAgentId,
@@ -2170,7 +2178,7 @@ export async function runEmbeddedAttempt(
21702178
{
21712179
agentId: sessionAgentId,
21722180
sessionKey: sandboxSessionKey,
2173-
config: params.config,
2181+
config: toolSearchRuntimeConfig,
21742182
sessionId: params.sessionId,
21752183
runId: params.runId,
21762184
loopDetection: clientToolLoopDetection,
@@ -2190,7 +2198,7 @@ export async function runEmbeddedAttempt(
21902198
})
21912199
: addClientToolsToToolSearchCatalog({
21922200
tools: clientToolDefs,
2193-
config: params.config,
2201+
config: toolSearchRuntimeConfig,
21942202
sessionId: params.sessionId,
21952203
sessionKey: sandboxSessionKey,
21962204
agentId: sessionAgentId,

src/agents/local-model-lean.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import type { OpenClawConfig } from "../config/config.js";
33
import type { AnyAgentTool } from "./agent-tools.types.js";
44
import {
5+
applyLocalModelLeanToolSearchDefaults,
56
filterLocalModelLeanTools,
67
isLocalModelLeanEnabled,
78
resolveLocalModelLeanPreserveToolNames,
@@ -225,4 +226,44 @@ describe("local model lean tool filtering", () => {
225226
}).map((tool) => tool.name),
226227
).toEqual(["read", "exec"]);
227228
});
229+
230+
it("defaults lean runs to structured Tool Search controls", () => {
231+
const cfg: OpenClawConfig = {
232+
agents: {
233+
defaults: {
234+
experimental: {
235+
localModelLean: true,
236+
},
237+
},
238+
},
239+
};
240+
241+
const resolved = applyLocalModelLeanToolSearchDefaults({ config: cfg, agentId: "main" });
242+
243+
expect(resolved).not.toBe(cfg);
244+
expect(resolved?.tools?.toolSearch).toEqual({
245+
enabled: true,
246+
mode: "tools",
247+
searchDefaultLimit: 5,
248+
maxSearchLimit: 10,
249+
});
250+
expect(cfg.tools?.toolSearch).toBeUndefined();
251+
});
252+
253+
it("preserves explicit Tool Search operator config", () => {
254+
const cfg: OpenClawConfig = {
255+
agents: {
256+
defaults: {
257+
experimental: {
258+
localModelLean: true,
259+
},
260+
},
261+
},
262+
tools: {
263+
toolSearch: false,
264+
},
265+
};
266+
267+
expect(applyLocalModelLeanToolSearchDefaults({ config: cfg, agentId: "main" })).toBe(cfg);
268+
});
228269
});

src/agents/local-model-lean.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import type { AnyAgentTool } from "./agent-tools.types.js";
55
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
66

77
const LOCAL_MODEL_LEAN_DENY_TOOL_NAMES = new Set(["browser", "cron", "message"]);
8+
const LOCAL_MODEL_LEAN_TOOL_SEARCH_DEFAULTS = {
9+
enabled: true,
10+
mode: "tools",
11+
searchDefaultLimit: 5,
12+
maxSearchLimit: 10,
13+
} as const;
814

915
function resolvePreservedLocalModelLeanToolNames(names?: Iterable<string>): Set<string> {
1016
if (!names) {
@@ -81,3 +87,23 @@ export function filterLocalModelLeanTools(params: {
8187
);
8288
});
8389
}
90+
91+
export function applyLocalModelLeanToolSearchDefaults(params: {
92+
config?: OpenClawConfig;
93+
agentId?: string;
94+
sessionKey?: string;
95+
}): OpenClawConfig | undefined {
96+
if (!params.config || !isLocalModelLeanEnabled(params)) {
97+
return params.config;
98+
}
99+
if (params.config.tools?.toolSearch !== undefined) {
100+
return params.config;
101+
}
102+
return {
103+
...params.config,
104+
tools: {
105+
...params.config.tools,
106+
toolSearch: LOCAL_MODEL_LEAN_TOOL_SEARCH_DEFAULTS,
107+
},
108+
};
109+
}

0 commit comments

Comments
 (0)