refactor(config): promote planTool to canonical tools.updatePlan (#113958)

The update_plan checklist tool has been default-on with no model gating for a
while, so keeping it under tools.experimental misrepresented it and kept a
container key alive for a single boolean. Rename it to a plain boolean
tools.updatePlan (camel-cased tool id, matching tools.agentToAgent and
tools.toolSearch), keeping the !== false default-on semantics.

The strict schema rejects the retired key, so openclaw doctor --fix now lifts
tools.experimental.planTool to tools.updatePlan and deletes the emptied
container. Registration also drops five parameters that were never read
(agentSessionKey, agentId, modelProvider, modelId, pluginToolAllowlist).

Docs: config-tools.md dropped a stale claim that the tool defaults off behind a
strict-agentic GPT-5 rule that no longer exists, experimental-features.md no
longer lists the tool and now cites the real Codex app-server floor (0.143.0,
per MIN_CODEX_APP_SERVER_VERSION).

Config baseline core count drops 2307 -> 2306.
This commit is contained in:
Peter Steinberger
2026-07-25 18:31:23 -07:00
committed by GitHub
parent 9349a0e880
commit d7ef7fc5a6
19 changed files with 77 additions and 125 deletions

View File

@@ -1,5 +1,5 @@
{
"core": 2307,
"core": 2306,
"channel": 3630,
"plugin": 3556
}

View File

@@ -1,4 +1,4 @@
f1956566f629dc6e6d7f0b2ce268e25dede588e352faf3d9c1009d3856eb4d53 config-baseline.json
8824942e998e77a20892b58f27bf739e5566da71fc81c101c73bc37d99c13a47 config-baseline.core.json
b5a60d7a0cfb860835eb4f38acb6421db736c76121a891311f37dede4c26e5a4 config-baseline.json
e51de41a04b71b26f3ed5edd8e663982f2dee84026b7ecfab6ef75ecb2e7eb4d config-baseline.core.json
af6ca0e70007113462270d46fa14ef0551e577e2fa157d2b9f6d0a93d36f96f1 config-baseline.channel.json
d47f7eb4268480e99ab85b6c1b5ee07f6c570ce0555e3db682ec26037014b47e config-baseline.plugin.json

View File

@@ -16,13 +16,12 @@ Experimental features are preview surfaces behind explicit flags. They need more
## Currently documented flags
| Surface | Key | Use it when | More |
| ------------------------ | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Local model runtime | `agents.defaults.experimental.localModelLean`, `agents.entries.*.experimental.localModelLean` | A smaller or stricter local backend chokes on OpenClaw's full default tool surface | [Local Models](/gateway/local-models) |
| Codex harness | `plugins.entries.codex.config.appServer.experimental.sandboxExecServer` | You want native Codex app-server 0.132.0 or newer to target an OpenClaw sandbox-backed exec-server instead of disabling Code Mode | [Codex harness reference](/plugins/codex-harness-reference#sandboxed-native-execution) |
| 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) |
| Code Mode | `tools.codeMode.enabled` | You want compact code-orchestrated access to a hidden OpenClaw tool catalog | [Code Mode](/tools/code-mode) |
| Swarm | `tools.swarm.enabled` | You want Code Mode scripts to orchestrate bounded groups of sub-agents in parallel | [Swarm](/tools/swarm) |
| Surface | Key | Use it when | More |
| ------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Local model runtime | `agents.defaults.experimental.localModelLean`, `agents.entries.*.experimental.localModelLean` | A smaller or stricter local backend chokes on OpenClaw's full default tool surface | [Local Models](/gateway/local-models) |
| Codex harness | `plugins.entries.codex.config.appServer.experimental.sandboxExecServer` | You want native Codex app-server 0.143.0 or newer to target an OpenClaw sandbox-backed exec-server instead of disabling Code Mode | [Codex harness reference](/plugins/codex-harness-reference#sandboxed-native-execution) |
| Code Mode | `tools.codeMode.enabled` | You want compact code-orchestrated access to a hidden OpenClaw tool catalog | [Code Mode](/tools/code-mode) |
| Swarm | `tools.swarm.enabled` | You want Code Mode scripts to orchestrate bounded groups of sub-agents in parallel | [Swarm](/tools/swarm) |
## Control UI Labs

View File

@@ -30,7 +30,7 @@ The bundled OpenAI/Codex GPT-5-family overlay (`resolveGpt5SystemPromptContribut
The prompt is compact, with fixed sections:
- **Tooling**: structured-tool source-of-truth reminder plus runtime tool-use guidance. When the experimental `update_plan` tool is enabled (`tools.experimental.planTool`), its own tool description adds: use it only for non-trivial multi-step work, keep at most one step `in_progress`, and skip it for simple one-step work.
- **Tooling**: structured-tool source-of-truth reminder plus runtime tool-use guidance. When the `update_plan` tool is enabled (`tools.updatePlan`, on by default), its own tool description adds: use it only for non-trivial multi-step work, keep at most one step `in_progress`, and skip it for simple one-step work.
- **Execution Bias**: act in-turn on actionable requests, continue until done or blocked, recover from weak tool results, check mutable state live, and verify before finalizing.
- **Promised Work**: promising future, background, delegated, or continued work creates follow-through ownership: arrange a push-based completion or watch path before ending the turn, proactively return with the result or a concrete blocker, and never treat progress (like `running`) as completion.
- **Safety**: short guardrail reminder against power-seeking behavior or bypassing oversight.

View File

@@ -3404,7 +3404,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
- H3: tools.agentToAgent
- H3: tools.sessions
- H3: `tools.sessions_spawn`
- H3: tools.experimental
- H3: tools.updatePlan
- H3: agents.defaults.subagents
- H2: Custom providers and base URLs
- H3: Provider field details

View File

@@ -403,25 +403,25 @@ Controls inline attachment support for `sessions_spawn`.
</Accordion>
</AccordionGroup>
<a id="toolsexperimental"></a>
<a id="toolsupdateplan"></a>
### `tools.experimental`
### `tools.updatePlan`
Experimental built-in tool flags. Default off unless a strict-agentic GPT-5 auto-enable rule applies.
Kill switch for the structured `update_plan` checklist tool used for non-trivial multi-step work tracking.
```json5
{
tools: {
experimental: {
planTool: true, // enable experimental update_plan
},
updatePlan: false, // hide update_plan from every run
},
}
```
- `planTool`: enables the structured `update_plan` tool for non-trivial multi-step work tracking.
- Default: `false` unless `agents.defaults.embeddedAgent.executionContract` (or a per-agent override) is set to `"strict-agentic"` for an `openai` provider run against a GPT-5-family model id (this covers OpenAI Codex CLI runs too, since Codex auth/model routing lives under the `openai` provider). Set `true` to force the tool on outside that scope, or `false` to keep it off even for strict-agentic GPT-5 runs.
- When enabled, the system prompt also adds usage guidance so the model only uses it for substantial work and keeps at most one step `in_progress`.
- Default: `true` for every provider and model. Set `false` to keep the tool off; there is no model-specific auto-enable rule.
- The tool description adds usage guidance so the model only uses it for substantial work and keeps at most one step `in_progress`.
- `tools.deny: ["update_plan"]` also removes the tool, so use whichever surface already carries your tool policy.
Older configs used `tools.experimental.planTool`. Run `openclaw doctor --fix` to move the value to `tools.updatePlan`.
### `agents.defaults.subagents`

View File

@@ -243,9 +243,7 @@ describe("createOpenClawTools browser plugin integration", () => {
allow: ["browser"],
},
tools: {
experimental: {
planTool: true,
},
updatePlan: true,
},
} as OpenClawConfig;
let capturedRuntimeConfig: OpenClawConfig | undefined;
@@ -292,9 +290,7 @@ describe("createOpenClawTools browser plugin integration", () => {
allow: ["browser"],
},
tools: {
experimental: {
planTool: true,
},
updatePlan: true,
},
} as OpenClawConfig;
let capturedRuntimeConfig: OpenClawConfig | undefined;

View File

@@ -22,35 +22,20 @@ export function collectPresentOpenClawTools(
return candidates.filter((tool): tool is AnyAgentTool => tool !== null && tool !== undefined);
}
/** Resolves the default-on update_plan switch with an explicit kill switch. */
function isUpdatePlanToolEnabledForOpenClawTools(params: {
config?: OpenClawConfig;
agentSessionKey?: string;
agentId?: string | null;
modelProvider?: string;
modelId?: string;
}): boolean {
return params.config?.tools?.experimental?.planTool !== false;
}
/** Decides whether update_plan should be included in the assembled OpenClaw tool set. */
export function shouldIncludeUpdatePlanToolForOpenClawTools(params: {
config?: OpenClawConfig;
agentSessionKey?: string;
agentId?: string | null;
modelProvider?: string;
modelId?: string;
pluginToolAllowlist?: string[];
pluginToolDenylist?: string[];
}): boolean {
// Default-on with an explicit kill switch: only `false` opts out.
if (params.config?.tools?.updatePlan === false) {
return false;
}
const deny = uniqueStrings([
...(params.config?.tools?.deny ?? []),
...(params.pluginToolDenylist ?? []),
]);
return (
isUpdatePlanToolEnabledForOpenClawTools(params) &&
isToolAllowedByPolicyName("update_plan", { deny })
);
return isToolAllowedByPolicyName("update_plan", { deny });
}
/** Includes ask_user only on a primary session and when normal deny policy permits it. */

View File

@@ -452,11 +452,6 @@ export function createOpenClawTools(
const effectiveCallGateway = embedded ? createEmbeddedCallGateway() : callGateway;
const includeUpdatePlanTool = shouldIncludeUpdatePlanToolForOpenClawTools({
config: resolvedConfig,
agentSessionKey: options?.agentSessionKey,
agentId: options?.requesterAgentIdOverride,
modelProvider: options?.modelProvider,
modelId: options?.modelId,
pluginToolAllowlist: options?.pluginToolAllowlist,
pluginToolDenylist: options?.pluginToolDenylist,
});
const includeAskUserTool = shouldIncludeAskUserToolForOpenClawTools({

View File

@@ -96,16 +96,9 @@ describe("openclaw-tools update_plan gating", () => {
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
const emptyAllowlistParams = {
config: {} as OpenClawConfig,
pluginToolAllowlist: [],
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
};
expect(defaultTools).toContain("update_plan");
expect(defaultTools).not.toContain("ask_user");
expect(shouldIncludeUpdatePlanToolForOpenClawTools(emptyAllowlistParams)).toBe(true);
});
it("keeps ask_user on primary sessions and excludes spawned worker sessions", () => {
@@ -280,13 +273,7 @@ describe("openclaw-tools update_plan gating", () => {
});
it("registers update_plan when explicitly enabled", () => {
const config = {
tools: {
experimental: {
planTool: true,
},
},
} as OpenClawConfig;
const config = { tools: { updatePlan: true } } as OpenClawConfig;
expectUpdatePlanEnabled({ config }, true);
expect(createUpdatePlanTool().displaySummary).toBe("Track short work plan.");
@@ -306,19 +293,6 @@ describe("openclaw-tools update_plan gating", () => {
it("includes update_plan when a config allowlist group includes it", () => {
const includeUpdatePlan = shouldIncludeUpdatePlanToolForOpenClawTools({
config: { tools: { allow: ["group:agents"] } } as OpenClawConfig,
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
expect(includeUpdatePlan).toBe(true);
});
it("includes update_plan when a runtime allowlist group includes it", () => {
const includeUpdatePlan = shouldIncludeUpdatePlanToolForOpenClawTools({
config: {} as OpenClawConfig,
pluginToolAllowlist: ["group:agents"],
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
expect(includeUpdatePlan).toBe(true);
@@ -336,23 +310,9 @@ describe("openclaw-tools update_plan gating", () => {
expect(tools).not.toContain("update_plan");
});
it("lets explicit planTool false disable every model and override allowlists", () => {
const cfg = {
tools: {
experimental: {
planTool: false,
},
},
} as OpenClawConfig;
expectUpdatePlanEnabled({ config: cfg, modelProvider: "openai", modelId: "gpt-5.4" }, false);
it("lets an explicit updatePlan false override an allowlist that includes the tool", () => {
expectUpdatePlanEnabled(
{
config: cfg,
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
pluginToolAllowlist: ["update_plan"],
},
{ config: { tools: { updatePlan: false, allow: ["update_plan"] } } as OpenClawConfig },
false,
);
});

View File

@@ -184,9 +184,7 @@ describe("resolveQueuedReplyExecutionConfig channel scope", () => {
},
},
tools: {
experimental: {
planTool: true,
},
updatePlan: true,
},
} as unknown as OpenClawConfig;
setRuntimeConfigSnapshot(staleRuntimeConfig, sourceConfig);

View File

@@ -530,6 +530,28 @@ describe("retired runtime config migrations", () => {
expect(result.changes.length).toBeGreaterThan(8);
});
it("lifts the retired plan-tool switch out of the tools.experimental container", () => {
const result = applyAll({ tools: { experimental: { planTool: false } } });
expect(result.raw).toHaveProperty("tools.updatePlan", false);
expect(result.raw).not.toHaveProperty("tools.experimental");
expect(result.changes).toContain("Moved tools.experimental.planTool → tools.updatePlan.");
});
it("drops the tools.experimental container when the canonical plan-tool switch wins", () => {
const canonicalWins = applyAll({
tools: { updatePlan: true, experimental: { planTool: false } },
});
const emptyContainer = applyAll({ tools: { experimental: {} } });
expect(canonicalWins.raw).toHaveProperty("tools.updatePlan", true);
expect(canonicalWins.raw).not.toHaveProperty("tools.experimental");
expect(emptyContainer.raw).not.toHaveProperty("tools.experimental");
expect(emptyContainer.changes).toContain(
"Removed tools.experimental; tools.updatePlan now owns the switch.",
);
});
it("consolidates the approved tier-eval tranche with canonical values winning", () => {
const result = applyAll({
mcp: { servers: { docs: { cwd: "/canonical", workingDirectory: "/legacy" } } },

View File

@@ -500,6 +500,7 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED: LegacyConfigMigrationSpec
["tools", "message", "allowCrossContextSend"],
"tools.message.allowCrossContextSend moved to tools.message.crossContext.",
),
rule(["tools", "experimental"], "tools.experimental.planTool moved to tools.updatePlan."),
rule(
["talk", "realtime", "voice"],
"talk.realtime.voice moved to talk.realtime.speakerVoice.",
@@ -561,6 +562,19 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED: LegacyConfigMigrationSpec
}
delete messageTool.allowCrossContextSend;
}
// planTool was the only tools.experimental member, so the strict schema now
// rejects the whole container; lift the value, then drop the empty parent.
const tools = getRecord(raw.tools);
const experimentalTools = getRecord(tools?.experimental);
if (tools && experimentalTools) {
if (Object.hasOwn(experimentalTools, "planTool") && tools.updatePlan === undefined) {
tools.updatePlan = experimentalTools.planTool;
changes.push("Moved tools.experimental.planTool → tools.updatePlan.");
} else {
changes.push("Removed tools.experimental; tools.updatePlan now owns the switch.");
}
delete tools.experimental;
}
const talkRealtime = getRecord(getRecord(raw.talk)?.realtime);
if (talkRealtime) {
moveVoice(talkRealtime, "talk.realtime", changes);

View File

@@ -145,9 +145,7 @@ describe("runtime snapshot state", () => {
const scopedResolvedConfig: OpenClawConfig = {
...runtimeConfig,
tools: {
experimental: {
planTool: true,
},
updatePlan: true,
},
};

View File

@@ -95,10 +95,8 @@ export const RUNTIME_FIELD_HELP: Record<string, string> = {
"Enables the agent_to_agent tool surface so one agent can invoke another agent at runtime. Keep off in simple deployments and enable only when orchestration value outweighs complexity.",
"tools.agentToAgent.allow":
"Allowlist of target agent IDs permitted for agent_to_agent calls when orchestration is enabled. Use explicit allowlists to avoid uncontrolled cross-agent call graphs.",
"tools.experimental":
"Experimental built-in tool flags. Use each tool's switch to opt in or out of its documented default.",
"tools.experimental.planTool":
"Structured `update_plan` checklist tool for non-trivial multi-step work. Enabled by default for embedded models; set false to opt out.",
"tools.updatePlan":
"Structured `update_plan` checklist tool for non-trivial multi-step work. Enabled by default; set false to opt out.",
"tools.toolSearch":
"Compact large OpenClaw, MCP, and client tool catalogs. Set to true for the default code bridge or use the object form to choose structured controls or a compact visible tool directory.",
"tools.toolSearch.enabled":

View File

@@ -236,8 +236,7 @@ export const FIELD_LABELS: Record<string, string> = {
"tools.agentToAgent": "Agent-to-Agent Tool Access",
"tools.agentToAgent.enabled": "Enable Agent-to-Agent Tool",
"tools.agentToAgent.allow": "Agent-to-Agent Target Allowlist",
"tools.experimental": "Experimental Tools",
"tools.experimental.planTool": "Enable Structured Plan Tool",
"tools.updatePlan": "Enable Structured Plan Tool",
"tools.toolSearch": "Tool Search",
"tools.toolSearch.enabled": "Enable Tool Search",
"tools.toolSearch.mode": "Tool Search Surface",

View File

@@ -785,17 +785,13 @@ describe("config schema", () => {
).toBe(false);
});
it("accepts experimental tool flags in the runtime zod schema", () => {
const parsed = ToolsSchema.parse({
experimental: {
planTool: true,
},
});
it("accepts the update_plan tool switch in the runtime zod schema", () => {
const parsed = ToolsSchema.parse({ updatePlan: false });
if (!parsed) {
throw new Error("expected parsed tools config");
}
expect(parsed?.experimental?.planTool).toBe(true);
expect(parsed?.updatePlan).toBe(false);
});
it("accepts simplified Tool Search config in the runtime zod schema", () => {

View File

@@ -529,11 +529,8 @@ export type ToolsConfig = {
sandbox?: {
tools?: ToolAllowDenyPolicyConfig;
};
/** Experimental tool flags. */
experimental?: {
/** Structured checklist tool; enabled by default. Set false to opt out. */
planTool?: boolean;
};
/** Structured update_plan checklist tool; enabled by default. Set false to opt out. */
updatePlan?: boolean;
};
export type MessageToolsConfig = {

View File

@@ -1023,12 +1023,7 @@ export const ToolsSchema = z
})
.strict()
.optional(),
experimental: z
.object({
planTool: z.boolean().optional(),
})
.strict()
.optional(),
updatePlan: z.boolean().optional(),
})
.strict()
.superRefine((value, ctx) => {