Skip to content

Commit 13c97c5

Browse files
dutifulbobclawsweeper[bot]osolmaz
authored
feat(agents): support per-agent local model lean mode (#84073)
Summary: - The branch adds per-agent `agents.list[].experimental.localModelLean` config and applies lean tool filtering through agent, session, and default-agent resolution. - Reproducibility: not applicable. this is a feature/config PR rather than a current-main bug report. The chan ... or is supported by source review, focused tests in the branch, and the PR body's redacted live runtime log. Automerge notes: - PR branch already contained follow-up commit before automerge: feat(agents): support per-agent local model lean mode - PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8407… Validation: - ClawSweeper review passed for head 1f9a955. - Required merge gates passed before the squash merge. Prepared head SHA: 1f9a955 Review: #84073 (comment) Co-authored-by: Bob <dutifulbob@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: osolmaz Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
1 parent b7ba7c3 commit 13c97c5

17 files changed

Lines changed: 508 additions & 19 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
66

77
### Changes
88

9+
- Agents/config: allow `agents.list[].experimental.localModelLean` so lean local-model mode can be enabled for one configured agent instead of globally.
910
- Providers/xAI: add device-code OAuth login so remote and headless setups can authorize xAI without a localhost browser callback. (#84005) Thanks @fuller-stack-dev.
1011

1112
### Fixes
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
9e7c2b40f4ee8eaeda4bfce4ba9b403345aff83a0768a09de575e41303af5212 config-baseline.json
2-
40abd62ca070a191d196fc822b90ba9b29438bbce80c62ed23eb91e6078d0855 config-baseline.core.json
1+
bd5e759b254477ffdb357e4eea244ae792525b6c4612d3c0f7544a772f270a9e config-baseline.json
2+
3bf4b07923e314d14e3b2af5689ea155d9030695f58df7ef5adc0d1a98cd934c config-baseline.core.json
33
e068db276fdff1727939d4f3a8001376e550c444bdff3e3443ab26812e2f8c5d config-baseline.channel.json
44
a87fc4c9bc6499c5fb9d9343b8c1c4f0c3381a6afbdb0a676dc8ba9e03ff5755 config-baseline.plugin.json

docs/concepts/experimental-features.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ Treat them differently from normal config:
2121

2222
## Currently documented flags
2323

24-
| Surface | Key | Use it when | More |
25-
| ------------------------ | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
26-
| Local model runtime | `agents.defaults.experimental.localModelLean` | A smaller or stricter local backend chokes on OpenClaw's full default tool surface | [Local Models](/gateway/local-models) |
27-
| Memory search | `agents.defaults.memorySearch.experimental.sessionMemory` | You want `memory_search` to index prior session transcripts and accept the extra storage/indexing cost | [Memory configuration reference](/reference/memory-config#session-memory-search-experimental) |
28-
| Structured planning tool | `tools.experimental.planTool` | You want the structured `update_plan` tool exposed for multi-step work tracking in compatible runtimes and UIs | [Gateway configuration reference](/gateway/config-tools#toolsexperimental) |
24+
| Surface | Key | Use it when | More |
25+
| ------------------------ | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
26+
| Local model runtime | `agents.defaults.experimental.localModelLean`, `agents.list[].experimental.localModelLean` | A smaller or stricter local backend chokes on OpenClaw's full default tool surface | [Local Models](/gateway/local-models) |
27+
| Memory search | `agents.defaults.memorySearch.experimental.sessionMemory` | You want `memory_search` to index prior session transcripts and accept the extra storage/indexing cost | [Memory configuration reference](/reference/memory-config#session-memory-search-experimental) |
28+
| Structured planning tool | `tools.experimental.planTool` | You want the structured `update_plan` tool exposed for multi-step work tracking in compatible runtimes and UIs | [Gateway configuration reference](/gateway/config-tools#toolsexperimental) |
2929

3030
## Local model lean mode
3131

32-
`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.
32+
`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.
3333

3434
### Why these three tools
3535

@@ -69,6 +69,24 @@ Lean mode also does not replace `tools.profile`, `tools.allow`/`tools.deny`, or
6969
}
7070
```
7171

72+
For one agent only:
73+
74+
```json5
75+
{
76+
agents: {
77+
list: [
78+
{
79+
id: "local",
80+
model: "lmstudio/gemma-4-e4b-it",
81+
experimental: {
82+
localModelLean: true,
83+
},
84+
},
85+
],
86+
},
87+
}
88+
```
89+
7290
Restart the Gateway after changing the flag, then confirm the trimmed tool list with:
7391

7492
```bash

docs/providers/ollama.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -627,12 +627,15 @@ Use these as starting points and replace model IDs with the exact names from `ol
627627
```json5
628628
{
629629
agents: {
630-
defaults: {
631-
experimental: {
632-
localModelLean: true,
630+
list: [
631+
{
632+
id: "local",
633+
experimental: {
634+
localModelLean: true,
635+
},
636+
model: { primary: "ollama/gemma4" },
633637
},
634-
model: { primary: "ollama/gemma4" },
635-
},
638+
],
636639
},
637640
models: {
638641
providers: {

src/agents/agent-scope-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type ResolvedAgentConfig = {
2525
contextInjection?: AgentEntry["contextInjection"];
2626
bootstrapMaxChars?: AgentEntry["bootstrapMaxChars"];
2727
bootstrapTotalMaxChars?: AgentEntry["bootstrapTotalMaxChars"];
28+
experimental?: AgentDefaultsConfig["experimental"];
2829
skills?: AgentEntry["skills"];
2930
memorySearch?: AgentEntry["memorySearch"];
3031
humanDelay?: AgentEntry["humanDelay"];
@@ -128,6 +129,10 @@ export function resolveAgentConfig(
128129
contextInjection: entry.contextInjection,
129130
bootstrapMaxChars: entry.bootstrapMaxChars,
130131
bootstrapTotalMaxChars: entry.bootstrapTotalMaxChars,
132+
experimental:
133+
typeof entry.experimental === "object" && entry.experimental
134+
? { ...agentDefaults?.experimental, ...entry.experimental }
135+
: agentDefaults?.experimental,
131136
skills: Array.isArray(entry.skills) ? entry.skills : undefined,
132137
memorySearch: entry.memorySearch,
133138
humanDelay: entry.humanDelay,

src/agents/agent-scope.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,30 @@ describe("resolveAgentConfig", () => {
126126
});
127127
});
128128

129+
it("merges experimental flags from defaults with per-agent overrides", () => {
130+
const cfg: OpenClawConfig = {
131+
agents: {
132+
defaults: {
133+
experimental: {
134+
localModelLean: true,
135+
},
136+
},
137+
list: [
138+
{
139+
id: "main",
140+
experimental: {
141+
localModelLean: false,
142+
},
143+
},
144+
],
145+
},
146+
};
147+
148+
expect(resolveAgentConfig(cfg, "main")?.experimental).toEqual({
149+
localModelLean: false,
150+
});
151+
});
152+
129153
it("merges runRetries from defaults with per-agent overrides", () => {
130154
const cfg: OpenClawConfig = {
131155
agents: {
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { OpenClawConfig } from "../config/config.js";
3+
import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "./local-model-lean.js";
4+
import type { AnyAgentTool } from "./pi-tools.types.js";
5+
6+
function tools(names: string[]): AnyAgentTool[] {
7+
return names.map((name) => ({ name })) as AnyAgentTool[];
8+
}
9+
10+
describe("local model lean tool filtering", () => {
11+
it("filters heavyweight tools for one configured agent", () => {
12+
const cfg: OpenClawConfig = {
13+
agents: {
14+
list: [
15+
{
16+
id: "gemma",
17+
experimental: {
18+
localModelLean: true,
19+
},
20+
},
21+
],
22+
},
23+
};
24+
25+
expect(isLocalModelLeanEnabled({ config: cfg, agentId: "gemma" })).toBe(true);
26+
expect(
27+
filterLocalModelLeanTools({
28+
tools: tools(["read", "browser", "cron", "message", "exec"]),
29+
config: cfg,
30+
agentId: "gemma",
31+
}).map((tool) => tool.name),
32+
).toEqual(["read", "exec"]);
33+
});
34+
35+
it("lets an agent opt out of an inherited global lean setting", () => {
36+
const cfg: OpenClawConfig = {
37+
agents: {
38+
defaults: {
39+
experimental: {
40+
localModelLean: true,
41+
},
42+
},
43+
list: [
44+
{
45+
id: "main",
46+
experimental: {
47+
localModelLean: false,
48+
},
49+
},
50+
],
51+
},
52+
};
53+
54+
expect(isLocalModelLeanEnabled({ config: cfg, agentId: "main" })).toBe(false);
55+
expect(
56+
filterLocalModelLeanTools({
57+
tools: tools(["read", "browser", "cron", "message", "exec"]),
58+
config: cfg,
59+
agentId: "main",
60+
}).map((tool) => tool.name),
61+
).toEqual(["read", "browser", "cron", "message", "exec"]);
62+
});
63+
64+
it("inherits global lean mode when an agent experimental block omits the flag", () => {
65+
const cfg: OpenClawConfig = {
66+
agents: {
67+
defaults: {
68+
experimental: {
69+
localModelLean: true,
70+
},
71+
},
72+
list: [
73+
{
74+
id: "main",
75+
experimental: {},
76+
},
77+
],
78+
},
79+
};
80+
81+
expect(isLocalModelLeanEnabled({ config: cfg, agentId: "main" })).toBe(true);
82+
expect(
83+
filterLocalModelLeanTools({
84+
tools: tools(["read", "browser", "cron", "message", "exec"]),
85+
config: cfg,
86+
agentId: "main",
87+
}).map((tool) => tool.name),
88+
).toEqual(["read", "exec"]);
89+
});
90+
91+
it("keeps global lean mode for an agent id without an agent entry", () => {
92+
const cfg: OpenClawConfig = {
93+
agents: {
94+
defaults: {
95+
experimental: {
96+
localModelLean: true,
97+
},
98+
},
99+
},
100+
};
101+
102+
expect(isLocalModelLeanEnabled({ config: cfg, agentId: "ad-hoc" })).toBe(true);
103+
expect(
104+
filterLocalModelLeanTools({
105+
tools: tools(["read", "browser", "cron", "message", "exec"]),
106+
config: cfg,
107+
agentId: "ad-hoc",
108+
}).map((tool) => tool.name),
109+
).toEqual(["read", "exec"]);
110+
});
111+
112+
it("uses the configured default agent when no agent id is explicit", () => {
113+
const cfg: OpenClawConfig = {
114+
agents: {
115+
list: [
116+
{
117+
id: "gemma",
118+
default: true,
119+
experimental: {
120+
localModelLean: true,
121+
},
122+
},
123+
],
124+
},
125+
};
126+
127+
expect(isLocalModelLeanEnabled({ config: cfg })).toBe(true);
128+
expect(
129+
filterLocalModelLeanTools({
130+
tools: tools(["read", "browser", "cron", "message", "exec"]),
131+
config: cfg,
132+
}).map((tool) => tool.name),
133+
).toEqual(["read", "exec"]);
134+
});
135+
136+
it("uses the agent from an agent session key", () => {
137+
const cfg: OpenClawConfig = {
138+
agents: {
139+
list: [
140+
{
141+
id: "main",
142+
experimental: {
143+
localModelLean: false,
144+
},
145+
},
146+
{
147+
id: "gemma",
148+
experimental: {
149+
localModelLean: true,
150+
},
151+
},
152+
],
153+
},
154+
};
155+
156+
expect(isLocalModelLeanEnabled({ config: cfg, sessionKey: "agent:gemma:main" })).toBe(true);
157+
expect(
158+
filterLocalModelLeanTools({
159+
tools: tools(["read", "browser", "cron", "message", "exec"]),
160+
config: cfg,
161+
sessionKey: "agent:gemma:main",
162+
}).map((tool) => tool.name),
163+
).toEqual(["read", "exec"]);
164+
});
165+
});

src/agents/local-model-lean.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { OpenClawConfig } from "../config/types.openclaw.js";
2+
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
3+
import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope-config.js";
4+
import type { AnyAgentTool } from "./pi-tools.types.js";
5+
6+
const LOCAL_MODEL_LEAN_DENY_TOOL_NAMES = new Set(["browser", "cron", "message"]);
7+
8+
function resolveLocalModelLeanAgentId(params: {
9+
config?: OpenClawConfig;
10+
agentId?: string;
11+
sessionKey?: string;
12+
}): string | undefined {
13+
const explicitAgentId =
14+
typeof params.agentId === "string" && params.agentId.trim()
15+
? normalizeAgentId(params.agentId)
16+
: undefined;
17+
if (explicitAgentId) {
18+
return explicitAgentId;
19+
}
20+
const parsedSessionAgentId = parseAgentSessionKey(params.sessionKey)?.agentId;
21+
if (parsedSessionAgentId) {
22+
return normalizeAgentId(parsedSessionAgentId);
23+
}
24+
return params.config ? resolveDefaultAgentId(params.config) : undefined;
25+
}
26+
27+
export function isLocalModelLeanEnabled(params: {
28+
config?: OpenClawConfig;
29+
agentId?: string;
30+
sessionKey?: string;
31+
}): boolean {
32+
const normalizedAgentId = resolveLocalModelLeanAgentId(params);
33+
const resolvedExperimental =
34+
params.config && normalizedAgentId
35+
? (resolveAgentConfig(params.config, normalizedAgentId)?.experimental ??
36+
params.config.agents?.defaults?.experimental)
37+
: params.config?.agents?.defaults?.experimental;
38+
return resolvedExperimental?.localModelLean ?? false;
39+
}
40+
41+
export function filterLocalModelLeanTools(params: {
42+
tools: AnyAgentTool[];
43+
config?: OpenClawConfig;
44+
agentId?: string;
45+
sessionKey?: string;
46+
}): AnyAgentTool[] {
47+
if (!isLocalModelLeanEnabled(params)) {
48+
return params.tools;
49+
}
50+
return params.tools.filter((tool) => !LOCAL_MODEL_LEAN_DENY_TOOL_NAMES.has(tool.name));
51+
}

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import { isTimeoutError } from "../../failover-error.js";
9696
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
9797
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
9898
import { stripHistoricalRuntimeContextCustomMessages } from "../../internal-runtime-context.js";
99+
import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "../../local-model-lean.js";
99100
import { resolveModelAuthMode } from "../../model-auth.js";
100101
import { resolveDefaultModelForAgent } from "../../model-selection.js";
101102
import { supportsModelTools } from "../../model-tool-support.js";
@@ -1691,7 +1692,11 @@ export async function runEmbeddedAttempt(
16911692
model: params.model,
16921693
})
16931694
: filteredBundledTools;
1694-
const uncompactedEffectiveTools = [...tools, ...normalizedBundledTools];
1695+
const uncompactedEffectiveTools = filterLocalModelLeanTools({
1696+
tools: [...tools, ...normalizedBundledTools],
1697+
config: params.config,
1698+
agentId: sessionAgentId,
1699+
});
16951700
let effectiveTools = uncompactedEffectiveTools;
16961701
const catalogToolHookContext = {
16971702
agentId: sessionAgentId,
@@ -1747,7 +1752,11 @@ export async function runEmbeddedAttempt(
17471752
catalogRef: toolSearchCatalogRef,
17481753
toolHookContext: catalogToolHookContext,
17491754
});
1750-
effectiveTools = toolSearch.tools;
1755+
effectiveTools = filterLocalModelLeanTools({
1756+
tools: toolSearch.tools,
1757+
config: params.config,
1758+
agentId: sessionAgentId,
1759+
});
17511760
if (toolSearch.compacted) {
17521761
prepStages.mark(codeModeControlsEnabledForRun ? "code-mode" : "tool-search");
17531762
log.info(
@@ -2545,6 +2554,10 @@ export async function runEmbeddedAttempt(
25452554
agentId: sessionAgentId,
25462555
messageProvider: params.messageProvider,
25472556
messageChannel: params.messageChannel,
2557+
localModelLean: isLocalModelLeanEnabled({
2558+
config: params.config,
2559+
agentId: sessionAgentId,
2560+
}),
25482561
toolCount: effectiveTools.length,
25492562
clientToolCount: clientToolDefs.length,
25502563
});

0 commit comments

Comments
 (0)