Skip to content

Commit e43eff0

Browse files
committed
fix(agents): keep exec visible for lean local models
1 parent 9f70064 commit e43eff0

8 files changed

Lines changed: 63 additions & 11 deletions

File tree

docs/concepts/experimental-features.md

Lines changed: 2 additions & 2 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. When Tool Search is enabled, OpenClaw also keeps `exec` directly visible instead of hiding it behind the catalog, so small coding models still have an obvious shell path for local workspace checks and simple live-data probes. 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 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 Tool Search on, most optional tools can move into the searchable catalog, but `exec` remains a direct tool in lean mode.
4444

4545
### When to turn it on
4646

docs/gateway/local-models.md

Lines changed: 5 additions & 2 deletions
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`) so the prompt shape is smaller and less brittle. When Tool Search is also on, lean mode keeps `exec` directly visible instead of cataloging it, which helps coding-tuned local models find the shell path without first inventing a domain-specific tool such as `weather`. 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

@@ -340,7 +340,10 @@ If the model loads cleanly but full agent turns misbehave, work top-down — con
340340
fails on Gemma or another local model? Check the provider URL, model ref, auth
341341
marker, and server logs first; local `model run` does not include agent tools.
342342
If local `model run` succeeds but larger agent turns fail, reduce the agent
343-
tool surface with `localModelLean` or `compat.supportsTools: false`.
343+
tool surface with `localModelLean` first. With Tool Search enabled, lean mode
344+
still leaves `exec` visible so the model can choose shell execution directly.
345+
Use `compat.supportsTools: false` only when the model or server cannot handle
346+
tool schemas at all.
344347
- Tool calls show up as raw JSON/XML/ReAct text, or the provider returns an
345348
empty `tool_calls` array? Do not add a proxy that blindly converts assistant
346349
text into tool execution. Fix the server chat template/parser first. If the

docs/providers/ollama.md

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

681681
Use `compat.supportsTools: false` only when the model or server reliably fails on tool schemas. It trades agent capability for stability.
682-
`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.
682+
`localModelLean` removes the browser, cron, and message tools from the agent surface, but it keeps `exec` directly visible when Tool Search is enabled so coding-tuned local models can still discover shell execution. 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.
683683

684684
</Accordion>
685685
</AccordionGroup>

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,11 @@ import { subscribeEmbeddedAgentSession } from "../../embedded-agent-subscribe.js
137137
import { isTimeoutError } from "../../failover-error.js";
138138
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
139139
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
140-
import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "../../local-model-lean.js";
140+
import {
141+
filterLocalModelLeanTools,
142+
isLocalModelLeanEnabled,
143+
shouldCatalogToolForLocalModelLean,
144+
} from "../../local-model-lean.js";
141145
import { resolveModelAuthMode } from "../../model-auth.js";
142146
import { resolveDefaultModelForAgent } from "../../model-selection.js";
143147
import { supportsModelTools } from "../../model-tool-support.js";
@@ -1428,6 +1432,10 @@ export async function runEmbeddedAttempt(
14281432
model: params.model,
14291433
})
14301434
: filteredBundledTools;
1435+
const localModelLeanEnabled = isLocalModelLeanEnabled({
1436+
config: params.config,
1437+
agentId: sessionAgentId,
1438+
});
14311439
const projectedUncompactedEffectiveTools = filterLocalModelLeanTools({
14321440
tools: [...tools, ...normalizedBundledTools],
14331441
config: params.config,
@@ -1498,6 +1506,9 @@ export async function runEmbeddedAttempt(
14981506
runId: params.runId,
14991507
catalogRef: toolSearchCatalogRef,
15001508
toolHookContext: catalogToolHookContext,
1509+
shouldCatalogTool: localModelLeanEnabled
1510+
? shouldCatalogToolForLocalModelLean
1511+
: undefined,
15011512
});
15021513
const projectedToolSearchTools = filterLocalModelLeanTools({
15031514
tools: toolSearch.tools,
@@ -2331,10 +2342,7 @@ export async function runEmbeddedAttempt(
23312342
agentId: sessionAgentId,
23322343
messageProvider: params.messageProvider,
23332344
messageChannel: params.messageChannel,
2334-
localModelLean: isLocalModelLeanEnabled({
2335-
config: params.config,
2336-
agentId: sessionAgentId,
2337-
}),
2345+
localModelLean: localModelLeanEnabled,
23382346
toolCount: effectiveTools.length,
23392347
clientToolCount: clientToolDefs.length,
23402348
});

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { describe, expect, it } from "vitest";
22
import type { OpenClawConfig } from "../config/config.js";
33
import type { AnyAgentTool } from "./agent-tools.types.js";
4-
import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "./local-model-lean.js";
4+
import {
5+
filterLocalModelLeanTools,
6+
isLocalModelLeanEnabled,
7+
shouldCatalogToolForLocalModelLean,
8+
} from "./local-model-lean.js";
59

610
function tools(names: string[]): AnyAgentTool[] {
711
return names.map((name) => ({ name })) as AnyAgentTool[];
@@ -162,4 +166,12 @@ describe("local model lean tool filtering", () => {
162166
}).map((tool) => tool.name),
163167
).toEqual(["read", "exec"]);
164168
});
169+
170+
it("keeps exec direct when Tool Search compacts lean local-model tools", () => {
171+
expect(shouldCatalogToolForLocalModelLean({ name: "exec" } as AnyAgentTool)).toBe(false);
172+
expect(shouldCatalogToolForLocalModelLean({ name: "read" } as AnyAgentTool)).toBe(true);
173+
expect(shouldCatalogToolForLocalModelLean({ name: "fake_weather" } as AnyAgentTool)).toBe(
174+
true,
175+
);
176+
});
165177
});

src/agents/local-model-lean.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope-config.
44
import type { AnyAgentTool } from "./agent-tools.types.js";
55

66
const LOCAL_MODEL_LEAN_DENY_TOOL_NAMES = new Set(["browser", "cron", "message"]);
7+
const LOCAL_MODEL_LEAN_DIRECT_TOOL_NAMES = new Set(["exec"]);
78

89
function resolveLocalModelLeanAgentId(params: {
910
config?: OpenClawConfig;
@@ -49,3 +50,7 @@ export function filterLocalModelLeanTools(params: {
4950
}
5051
return params.tools.filter((tool) => !LOCAL_MODEL_LEAN_DENY_TOOL_NAMES.has(tool.name));
5152
}
53+
54+
export function shouldCatalogToolForLocalModelLean(tool: AnyAgentTool): boolean {
55+
return !LOCAL_MODEL_LEAN_DIRECT_TOOL_NAMES.has(tool.name);
56+
}

src/agents/tool-search.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,29 @@ describe("Tool Search", () => {
153153
expect(telemetry.callCount).toBe(1);
154154
});
155155

156+
it("keeps predicate-excluded tools visible while cataloging the rest", () => {
157+
const codeTool = fakeTool(TOOL_SEARCH_CODE_MODE_TOOL_NAME, "code mode");
158+
const exec = fakeTool("exec", "Run shell command");
159+
const weather = pluginTool("fake_weather", "Read fake weather");
160+
161+
const compacted = applyToolSearchCatalog({
162+
tools: [codeTool, exec, weather],
163+
config: {
164+
tools: {
165+
toolSearch: true,
166+
},
167+
} as never,
168+
sessionId: "session-direct-exec",
169+
shouldCatalogTool: (tool) => tool.name !== "exec",
170+
});
171+
172+
expect(compacted.tools.map((tool) => tool.name)).toEqual([
173+
TOOL_SEARCH_CODE_MODE_TOOL_NAME,
174+
"exec",
175+
]);
176+
expect(compacted.catalogToolCount).toBe(1);
177+
});
178+
156179
it("scopes catalogs by run id when attempts share a session", async () => {
157180
const runATool = pluginTool("fake_run_a", "Tool visible only to run A");
158181
const runBTool = pluginTool("fake_run_b", "Tool visible only to run B");

src/agents/tool-search.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,7 @@ export function applyToolSearchCatalog(params: {
820820
runId?: string;
821821
catalogRef?: ToolSearchCatalogRef;
822822
toolHookContext?: HookContext;
823+
shouldCatalogTool?: (tool: AnyAgentTool) => boolean;
823824
}): {
824825
tools: AnyAgentTool[];
825826
compacted: boolean;

0 commit comments

Comments
 (0)