Skip to content

Commit 8b03fd1

Browse files
authored
fix(agents): compact lean local tool catalogs
Default localModelLean runs to compact Tool Search controls when the operator has not configured tools.toolSearch, while preserving explicit Tool Search settings and direct message-tool delivery semantics. Verification: local focused Vitest/docs/format/lint/diff/autoreview proof; GitHub CI, CodeQL/Security High, CodeQL Critical Quality, OpenGrep PR Diff, Real behavior proof, Dependency Guard, and Workflow Sanity passed on 6153fb5. Refs #86599
1 parent 3ffb360 commit 8b03fd1

7 files changed

Lines changed: 193 additions & 20 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
@@ -291,6 +291,102 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
291291
expect(toolSearchControlsCase.toolSearchCatalogRef).toEqual({});
292292
});
293293

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

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ import { runAgentHarnessBeforeAgentFinalizeHook } from "../../harness/lifecycle-
157157
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
158158
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
159159
import {
160+
applyLocalModelLeanToolSearchDefaults,
160161
filterLocalModelLeanTools,
161162
isLocalModelLeanEnabled,
162163
resolveLocalModelLeanPreserveToolNames,
@@ -176,10 +177,6 @@ import {
176177
import type { AgentMessage } from "../../runtime/index.js";
177178
import { resolveSandboxContext } from "../../sandbox.js";
178179
import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js";
179-
import {
180-
mapSandboxSkillEntriesForPrompt,
181-
resolveSandboxSkillRuntimeInputs,
182-
} from "../sandbox-skills.js";
183180
import { repairSessionFileIfNeeded } from "../../session-file-repair.js";
184181
import { guardSessionManager } from "../../session-tool-result-guard-wrapper.js";
185182
import { sanitizeToolUseResultPairing } from "../../session-transcript-repair.js";
@@ -266,6 +263,10 @@ import {
266263
updateActiveEmbeddedRunSnapshot,
267264
} from "../runs.js";
268265
import { buildEmbeddedSandboxInfo, resolveEmbeddedSandboxInfoExecPolicy } from "../sandbox-info.js";
266+
import {
267+
mapSandboxSkillEntriesForPrompt,
268+
resolveSandboxSkillRuntimeInputs,
269+
} from "../sandbox-skills.js";
269270
import { prewarmSessionFile, trackSessionManagerAccess } from "../session-manager-cache.js";
270271
import { prepareSessionManagerForRun } from "../session-manager-init.js";
271272
import {
@@ -1063,9 +1064,9 @@ export async function runEmbeddedAttempt(
10631064
config: params.config,
10641065
})
10651066
: applySkillEnvOverrides({
1066-
skills: skillEntries ?? [],
1067-
config: params.config,
1068-
});
1067+
skills: skillEntries ?? [],
1068+
config: params.config,
1069+
});
10691070
const promptSkillEntries = mapSandboxSkillEntriesForPrompt({
10701071
entries: shouldLoadSkillEntries ? skillEntries : undefined,
10711072
skillsWorkspaceDir: effectiveSkillsWorkspace,
@@ -1140,12 +1141,12 @@ export async function runEmbeddedAttempt(
11401141
});
11411142
};
11421143
const corePluginToolStages = createEmbeddedRunStageTracker();
1144+
const forceDirectMessageTool =
1145+
params.forceMessageTool === true || params.sourceReplyDeliveryMode === "message_tool_only";
11431146
const toolsAllowWithForcedRuntimeTools = mergeForcedEmbeddedAttemptToolsAllow(
11441147
params.toolsAllow,
11451148
{
1146-
forceMessageTool:
1147-
params.forceMessageTool === true ||
1148-
params.sourceReplyDeliveryMode === "message_tool_only",
1149+
forceMessageTool: forceDirectMessageTool,
11491150
},
11501151
);
11511152
const toolConstructionPlan = resolveEmbeddedAttemptToolConstructionPlan({
@@ -1155,6 +1156,13 @@ export async function runEmbeddedAttempt(
11551156
});
11561157
const toolsEnabled = supportsModelTools(params.model);
11571158
const codeModeConfig = resolveCodeModeConfig(params.config, sessionAgentId);
1159+
const toolSearchRuntimeConfig = forceDirectMessageTool
1160+
? params.config
1161+
: applyLocalModelLeanToolSearchDefaults({
1162+
config: params.config,
1163+
agentId: sessionAgentId,
1164+
sessionKey: sandboxSessionKey,
1165+
});
11581166
const codeModeControlsEnabledForRun =
11591167
toolsEnabled &&
11601168
params.disableTools !== true &&
@@ -1167,7 +1175,7 @@ export async function runEmbeddedAttempt(
11671175
!isRawModelRun &&
11681176
params.toolsAllow?.length !== 0 &&
11691177
!codeModeControlsEnabledForRun &&
1170-
resolveToolSearchConfig(params.config).enabled;
1178+
resolveToolSearchConfig(toolSearchRuntimeConfig).enabled;
11711179
const effectiveToolsAllow =
11721180
toolSearchControlsEnabledForRun && toolsAllowWithForcedRuntimeTools
11731181
? [
@@ -1242,7 +1250,7 @@ export async function runEmbeddedAttempt(
12421250
sandbox,
12431251
resolvedWorkspace,
12441252
}),
1245-
config: params.config,
1253+
config: toolSearchRuntimeConfig,
12461254
abortSignal: runAbortController.signal,
12471255
modelProvider: params.provider,
12481256
modelId: params.modelId,
@@ -1628,7 +1636,7 @@ export async function runEmbeddedAttempt(
16281636
})
16291637
: applyToolSearchCatalog({
16301638
tools: effectiveTools,
1631-
config: params.config,
1639+
config: toolSearchRuntimeConfig,
16321640
sessionId: params.sessionId,
16331641
sessionKey: sandboxSessionKey,
16341642
agentId: sessionAgentId,
@@ -2221,7 +2229,7 @@ export async function runEmbeddedAttempt(
22212229
{
22222230
agentId: sessionAgentId,
22232231
sessionKey: sandboxSessionKey,
2224-
config: params.config,
2232+
config: toolSearchRuntimeConfig,
22252233
sessionId: params.sessionId,
22262234
runId: params.runId,
22272235
loopDetection: clientToolLoopDetection,
@@ -2241,7 +2249,7 @@ export async function runEmbeddedAttempt(
22412249
})
22422250
: addClientToolsToToolSearchCatalog({
22432251
tools: clientToolDefs,
2244-
config: params.config,
2252+
config: toolSearchRuntimeConfig,
22452253
sessionId: params.sessionId,
22462254
sessionKey: sandboxSessionKey,
22472255
agentId: sessionAgentId,

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

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

src/agents/local-model-lean.ts

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

1212
const LOCAL_MODEL_LEAN_DENY_TOOL_NAMES = new Set(["browser", "cron", "message"]);
13+
const LOCAL_MODEL_LEAN_TOOL_SEARCH_DEFAULTS = {
14+
enabled: true,
15+
mode: "tools",
16+
searchDefaultLimit: 5,
17+
maxSearchLimit: 10,
18+
} as const;
1319

1420
function resolvePreservedLocalModelLeanToolNames(names?: Iterable<string>): Set<string> {
1521
if (!names) {
@@ -91,3 +97,23 @@ export function filterLocalModelLeanTools(params: {
9197
);
9298
});
9399
}
100+
101+
export function applyLocalModelLeanToolSearchDefaults(params: {
102+
config?: OpenClawConfig;
103+
agentId?: string;
104+
sessionKey?: string;
105+
}): OpenClawConfig | undefined {
106+
if (!params.config || !isLocalModelLeanEnabled(params)) {
107+
return params.config;
108+
}
109+
if (params.config.tools?.toolSearch !== undefined) {
110+
return params.config;
111+
}
112+
return {
113+
...params.config,
114+
tools: {
115+
...params.config.tools,
116+
toolSearch: LOCAL_MODEL_LEAN_TOOL_SEARCH_DEFAULTS,
117+
},
118+
};
119+
}

0 commit comments

Comments
 (0)