mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 15:31:07 +00:00
Agents: add per-agent defaults and safe model fallback (#51974)
* Agents: add per-agent defaults and safe model fallback * Docs: add per-agent thinking/reasoning/fast defaults to config reference and thinking docs * Format get-reply directives * Auto-reply: guard agent reasoning defaults * Docs: update config baseline
This commit is contained in:
@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Models/OpenAI: switch the default OpenAI setup model to `openai/gpt-5.4`, keep Codex on `openai-codex/gpt-5.4`, and centralize OpenAI chat, image, TTS, transcription, and embedding defaults in one shared module so future default-model updates stay low-churn. Thanks @vincentkoc.
|
||||
- Memory/plugins: let the active memory plugin register its own system-prompt section while preserving cache-clear and snapshot-load prompt isolation. (#40126) Thanks @jarimustonen.
|
||||
- Control UI/usage: improve usage overview styling, localization, and responsive chat/context-notice presentation, including safer theme color handling and unclipped usage-header menus. (#51951) Thanks @BunsDev.
|
||||
- Agents: add per-agent thinking/reasoning/fast defaults and auto-revert disallowed model overrides to the agent's default selection. Thanks @xuanmingguo and @vincentkoc.
|
||||
- Control UI/usage: drop the empty session-detail placeholder card so the usage view stays single-column until a real session detail panel is selected. (#52013) Thanks @BunsDev.
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -4111,6 +4111,20 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "agents.list.*.fastModeDefault",
|
||||
"kind": "core",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Agent Fast Mode Default",
|
||||
"help": "Optional per-agent default for fast mode. Applies when no per-message or session fast-mode override is set.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "agents.list.*.groupChat",
|
||||
"kind": "core",
|
||||
@@ -5226,6 +5240,25 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "agents.list.*.reasoningDefault",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"enumValues": [
|
||||
"on",
|
||||
"off",
|
||||
"stream"
|
||||
],
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Agent Reasoning Default",
|
||||
"help": "Optional per-agent default reasoning visibility (on|off|stream). Applies when no per-message or session reasoning override is set.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "agents.list.*.runtime",
|
||||
"kind": "core",
|
||||
@@ -6287,6 +6320,29 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "agents.list.*.thinkingDefault",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"enumValues": [
|
||||
"off",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"adaptive"
|
||||
],
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Agent Thinking Default",
|
||||
"help": "Optional per-agent default thinking level. Overrides agents.defaults.thinkingDefault for this agent when no per-message or session override is set.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "agents.list.*.tools",
|
||||
"kind": "core",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5563}
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5566}
|
||||
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -354,6 +354,7 @@
|
||||
{"recordType":"path","path":"agents.list.*","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.list.*.agentDir","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.default","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.fastModeDefault","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Agent Fast Mode Default","help":"Optional per-agent default for fast mode. Applies when no per-message or session fast-mode override is set.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.groupChat","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.list.*.groupChat.historyLimit","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.groupChat.mentionPatterns","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@@ -463,6 +464,7 @@
|
||||
{"recordType":"path","path":"agents.list.*.name","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.params","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.list.*.params.*","kind":"core","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.reasoningDefault","kind":"core","type":"string","required":false,"enumValues":["on","off","stream"],"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Agent Reasoning Default","help":"Optional per-agent default reasoning visibility (on|off|stream). Applies when no per-message or session reasoning override is set.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.runtime","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Agent Runtime","help":"Optional runtime descriptor for this agent. Use embedded for default OpenClaw execution or acp for external ACP harness defaults.","hasChildren":true}
|
||||
{"recordType":"path","path":"agents.list.*.runtime.acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Agent ACP Runtime","help":"ACP runtime defaults for this agent when runtime.type=acp. Binding-level ACP overrides still take precedence per conversation.","hasChildren":true}
|
||||
{"recordType":"path","path":"agents.list.*.runtime.acp.agent","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Agent ACP Harness Agent","help":"Optional ACP harness agent id to use for this OpenClaw agent (for example codex, claude).","hasChildren":false}
|
||||
@@ -561,6 +563,7 @@
|
||||
{"recordType":"path","path":"agents.list.*.subagents.model.fallbacks.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.subagents.model.primary","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.subagents.thinking","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.thinkingDefault","kind":"core","type":"string","required":false,"enumValues":["off","minimal","low","medium","high","xhigh","adaptive"],"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Agent Thinking Default","help":"Optional per-agent default thinking level. Overrides agents.defaults.thinkingDefault for this agent when no per-message or session override is set.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.tools","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.list.*.tools.allow","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.list.*.tools.allow.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
|
||||
@@ -303,6 +303,20 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
},
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
default: true,
|
||||
thinkingDefault: "high", // per-agent thinking override
|
||||
reasoningDefault: "on", // per-agent reasoning visibility
|
||||
fastModeDefault: false, // per-agent fast mode
|
||||
},
|
||||
{
|
||||
id: "quick",
|
||||
fastModeDefault: true, // this agent always runs fast
|
||||
thinkingDefault: "off",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
tools: {
|
||||
|
||||
@@ -1372,6 +1372,9 @@ scripts/sandbox-browser-setup.sh # optional browser image
|
||||
workspace: "~/.openclaw/workspace",
|
||||
agentDir: "~/.openclaw/agents/main/agent",
|
||||
model: "anthropic/claude-opus-4-6", // or { primary, fallbacks }
|
||||
thinkingDefault: "high", // per-agent thinking level override
|
||||
reasoningDefault: "on", // per-agent reasoning visibility override
|
||||
fastModeDefault: false, // per-agent fast mode override
|
||||
params: { cacheRetention: "none" }, // overrides matching defaults.models params by key
|
||||
identity: {
|
||||
name: "Samantha",
|
||||
@@ -1407,6 +1410,9 @@ scripts/sandbox-browser-setup.sh # optional browser image
|
||||
- `default`: when multiple are set, first wins (warning logged). If none set, first list entry is default.
|
||||
- `model`: string form overrides `primary` only; object form `{ primary, fallbacks }` overrides both (`[]` disables global fallbacks). Cron jobs that only override `primary` still inherit default fallbacks unless you set `fallbacks: []`.
|
||||
- `params`: per-agent stream params merged over the selected model entry in `agents.defaults.models`. Use this for agent-specific overrides like `cacheRetention`, `temperature`, or `maxTokens` without duplicating the whole model catalog.
|
||||
- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set.
|
||||
- `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Applies when no per-message or session reasoning override is set.
|
||||
- `fastModeDefault`: optional per-agent default for fast mode (`true | false`). Applies when no per-message or session fast-mode override is set.
|
||||
- `runtime`: optional per-agent runtime descriptor. Use `type: "acp"` with `runtime.acp` defaults (`agent`, `backend`, `mode`, `cwd`) when the agent should default to ACP harness sessions.
|
||||
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
|
||||
- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.
|
||||
|
||||
@@ -28,8 +28,9 @@ title: "Thinking Levels"
|
||||
|
||||
1. Inline directive on the message (applies only to that message).
|
||||
2. Session override (set by sending a directive-only message).
|
||||
3. Global default (`agents.defaults.thinkingDefault` in config).
|
||||
4. Fallback: `adaptive` for Anthropic Claude 4.6 models, `low` for other reasoning-capable models, `off` otherwise.
|
||||
3. Per-agent default (`agents.list[].thinkingDefault` in config).
|
||||
4. Global default (`agents.defaults.thinkingDefault` in config).
|
||||
5. Fallback: `adaptive` for Anthropic Claude 4.6 models, `low` for other reasoning-capable models, `off` otherwise.
|
||||
|
||||
## Setting a session default
|
||||
|
||||
@@ -50,8 +51,9 @@ title: "Thinking Levels"
|
||||
- OpenClaw resolves fast mode in this order:
|
||||
1. Inline/directive-only `/fast on|off`
|
||||
2. Session override
|
||||
3. Per-model config: `agents.defaults.models["<provider>/<model>"].params.fastMode`
|
||||
4. Fallback: `off`
|
||||
3. Per-agent default (`agents.list[].fastModeDefault`)
|
||||
4. Per-model config: `agents.defaults.models["<provider>/<model>"].params.fastMode`
|
||||
5. Fallback: `off`
|
||||
- For `openai/*`, fast mode applies the OpenAI fast profile: `service_tier=priority` when supported, plus low reasoning effort and low text verbosity.
|
||||
- For `openai-codex/*`, fast mode applies the same low-latency profile on Codex Responses. OpenClaw keeps one shared `/fast` toggle across both auth paths.
|
||||
- For direct `anthropic/*` API-key requests, fast mode maps to Anthropic service tiers: `/fast on` sets `service_tier=auto`, `/fast off` sets `service_tier=standard_only`.
|
||||
@@ -76,6 +78,7 @@ title: "Thinking Levels"
|
||||
- `stream` (Telegram only): streams reasoning into the Telegram draft bubble while the reply is generating, then sends the final answer without reasoning.
|
||||
- Alias: `/reason`.
|
||||
- Send `/reasoning` (or `/reasoning:`) with no argument to see the current reasoning level.
|
||||
- Resolution order: inline directive, then session override, then per-agent default (`agents.list[].reasoningDefault`), then fallback (`off`).
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ type ResolvedAgentConfig = {
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: AgentEntry["model"];
|
||||
thinkingDefault?: AgentEntry["thinkingDefault"];
|
||||
reasoningDefault?: AgentEntry["reasoningDefault"];
|
||||
fastModeDefault?: AgentEntry["fastModeDefault"];
|
||||
skills?: AgentEntry["skills"];
|
||||
memorySearch?: AgentEntry["memorySearch"];
|
||||
humanDelay?: AgentEntry["humanDelay"];
|
||||
@@ -132,6 +135,9 @@ export function resolveAgentConfig(
|
||||
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
|
||||
? entry.model
|
||||
: undefined,
|
||||
thinkingDefault: entry.thinkingDefault,
|
||||
reasoningDefault: entry.reasoningDefault,
|
||||
fastModeDefault: entry.fastModeDefault,
|
||||
skills: Array.isArray(entry.skills) ? entry.skills : undefined,
|
||||
memorySearch: entry.memorySearch,
|
||||
humanDelay: entry.humanDelay,
|
||||
|
||||
67
src/agents/fast-mode.test.ts
Normal file
67
src/agents/fast-mode.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveFastModeState } from "./fast-mode.js";
|
||||
|
||||
describe("resolveFastModeState", () => {
|
||||
it("prefers session overrides", () => {
|
||||
const state = resolveFastModeState({
|
||||
cfg: {} as OpenClawConfig,
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
sessionEntry: { fastMode: true },
|
||||
});
|
||||
|
||||
expect(state.enabled).toBe(true);
|
||||
expect(state.source).toBe("session");
|
||||
});
|
||||
|
||||
it("uses agent fastModeDefault when present", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
list: [{ id: "alpha", fastModeDefault: true }],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const state = resolveFastModeState({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
agentId: "alpha",
|
||||
});
|
||||
|
||||
expect(state.enabled).toBe(true);
|
||||
expect(state.source).toBe("agent");
|
||||
});
|
||||
|
||||
it("falls back to model config when agent default is absent", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4o": { params: { fastMode: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const state = resolveFastModeState({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
});
|
||||
|
||||
expect(state.enabled).toBe(true);
|
||||
expect(state.source).toBe("config");
|
||||
});
|
||||
|
||||
it("defaults to off when unset", () => {
|
||||
const state = resolveFastModeState({
|
||||
cfg: {} as OpenClawConfig,
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
});
|
||||
|
||||
expect(state.enabled).toBe(false);
|
||||
expect(state.source).toBe("default");
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
import { normalizeFastMode } from "../auto-reply/thinking.shared.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { resolveAgentConfig } from "./agent-scope.js";
|
||||
|
||||
export type FastModeState = {
|
||||
enabled: boolean;
|
||||
source: "session" | "config" | "default";
|
||||
source: "session" | "agent" | "config" | "default";
|
||||
};
|
||||
|
||||
export function resolveFastModeParam(
|
||||
@@ -41,6 +42,7 @@ export function resolveFastModeState(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
provider: string;
|
||||
model: string;
|
||||
agentId?: string;
|
||||
sessionEntry?: Pick<SessionEntry, "fastMode"> | undefined;
|
||||
}): FastModeState {
|
||||
const sessionOverride = normalizeFastMode(params.sessionEntry?.fastMode);
|
||||
@@ -48,6 +50,14 @@ export function resolveFastModeState(params: {
|
||||
return { enabled: sessionOverride, source: "session" };
|
||||
}
|
||||
|
||||
const agentDefault =
|
||||
params.agentId && params.cfg
|
||||
? resolveAgentConfig(params.cfg, params.agentId)?.fastModeDefault
|
||||
: undefined;
|
||||
if (typeof agentDefault === "boolean") {
|
||||
return { enabled: agentDefault, source: "agent" };
|
||||
}
|
||||
|
||||
const configuredRaw = resolveConfiguredFastModeRaw(params);
|
||||
const configured = normalizeFastMode(configuredRaw as string | boolean | null | undefined);
|
||||
if (configured !== undefined) {
|
||||
|
||||
@@ -357,10 +357,17 @@ export const handleFastCommand: CommandHandler = async (params, allowTextCommand
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
agentId: params.agentId,
|
||||
sessionEntry: params.sessionEntry,
|
||||
});
|
||||
const suffix =
|
||||
state.source === "config" ? " (config)" : state.source === "default" ? " (default)" : "";
|
||||
state.source === "agent"
|
||||
? " (agent)"
|
||||
: state.source === "config"
|
||||
? " (config)"
|
||||
: state.source === "default"
|
||||
? " (default)"
|
||||
: "";
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: `⚙️ Current fast mode: ${state.enabled ? "on" : "off"}${suffix}.` },
|
||||
|
||||
@@ -169,6 +169,7 @@ export async function buildStatusReply(params: {
|
||||
cfg,
|
||||
provider,
|
||||
model,
|
||||
agentId: statusAgentId,
|
||||
sessionEntry,
|
||||
}).enabled;
|
||||
const statusText = buildStatusMessage({
|
||||
|
||||
@@ -137,6 +137,7 @@ export async function handleDirectiveOnly(
|
||||
cfg: params.cfg,
|
||||
provider: resolvedProvider,
|
||||
model: resolvedModel,
|
||||
agentId: activeAgentId,
|
||||
sessionEntry,
|
||||
});
|
||||
const effectiveFastMode = directives.fastMode ?? currentFastMode ?? fastModeState.enabled;
|
||||
|
||||
@@ -33,4 +33,78 @@ describe("resolveCurrentDirectiveLevels", () => {
|
||||
expect(result.currentThinkLevel).toBe("minimal");
|
||||
expect(resolveDefaultThinkingLevel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prefers session fastMode over agent default", async () => {
|
||||
const resolveDefaultThinkingLevel = vi.fn().mockResolvedValue("low");
|
||||
|
||||
const result = await resolveCurrentDirectiveLevels({
|
||||
sessionEntry: {
|
||||
fastMode: true,
|
||||
},
|
||||
agentEntry: {
|
||||
fastModeDefault: false,
|
||||
},
|
||||
resolveDefaultThinkingLevel,
|
||||
});
|
||||
|
||||
expect(result.currentFastMode).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to agent fastModeDefault when session override is absent", async () => {
|
||||
const resolveDefaultThinkingLevel = vi.fn().mockResolvedValue("low");
|
||||
|
||||
const result = await resolveCurrentDirectiveLevels({
|
||||
sessionEntry: {},
|
||||
agentEntry: {
|
||||
fastModeDefault: true,
|
||||
},
|
||||
resolveDefaultThinkingLevel,
|
||||
});
|
||||
|
||||
expect(result.currentFastMode).toBe(true);
|
||||
});
|
||||
|
||||
it("prefers session reasoningLevel over agent default", async () => {
|
||||
const resolveDefaultThinkingLevel = vi.fn().mockResolvedValue("low");
|
||||
|
||||
const result = await resolveCurrentDirectiveLevels({
|
||||
sessionEntry: {
|
||||
reasoningLevel: "on",
|
||||
},
|
||||
agentEntry: {
|
||||
reasoningDefault: "off",
|
||||
},
|
||||
resolveDefaultThinkingLevel,
|
||||
});
|
||||
|
||||
expect(result.currentReasoningLevel).toBe("on");
|
||||
});
|
||||
|
||||
it("falls back to agent reasoningDefault when session override is absent", async () => {
|
||||
const resolveDefaultThinkingLevel = vi.fn().mockResolvedValue("off");
|
||||
|
||||
const result = await resolveCurrentDirectiveLevels({
|
||||
sessionEntry: {},
|
||||
agentEntry: {
|
||||
reasoningDefault: "stream",
|
||||
},
|
||||
resolveDefaultThinkingLevel,
|
||||
});
|
||||
|
||||
expect(result.currentReasoningLevel).toBe("stream");
|
||||
});
|
||||
|
||||
it("skips agent reasoningDefault when thinking is active", async () => {
|
||||
const resolveDefaultThinkingLevel = vi.fn().mockResolvedValue("low");
|
||||
|
||||
const result = await resolveCurrentDirectiveLevels({
|
||||
sessionEntry: {},
|
||||
agentEntry: {
|
||||
reasoningDefault: "stream",
|
||||
},
|
||||
resolveDefaultThinkingLevel,
|
||||
});
|
||||
|
||||
expect(result.currentReasoningLevel).toBe("off");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,10 @@ export async function resolveCurrentDirectiveLevels(params: {
|
||||
reasoningLevel?: unknown;
|
||||
elevatedLevel?: unknown;
|
||||
};
|
||||
agentEntry?: {
|
||||
fastModeDefault?: unknown;
|
||||
reasoningDefault?: unknown;
|
||||
};
|
||||
agentCfg?: {
|
||||
thinkingDefault?: unknown;
|
||||
verboseDefault?: unknown;
|
||||
@@ -27,12 +31,20 @@ export async function resolveCurrentDirectiveLevels(params: {
|
||||
(params.agentCfg?.thinkingDefault as ThinkLevel | undefined);
|
||||
const currentThinkLevel = resolvedDefaultThinkLevel;
|
||||
const currentFastMode =
|
||||
typeof params.sessionEntry?.fastMode === "boolean" ? params.sessionEntry.fastMode : undefined;
|
||||
typeof params.sessionEntry?.fastMode === "boolean"
|
||||
? params.sessionEntry.fastMode
|
||||
: typeof params.agentEntry?.fastModeDefault === "boolean"
|
||||
? params.agentEntry.fastModeDefault
|
||||
: undefined;
|
||||
const currentVerboseLevel =
|
||||
(params.sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
|
||||
(params.agentCfg?.verboseDefault as VerboseLevel | undefined);
|
||||
const sessionReasoningLevel = params.sessionEntry?.reasoningLevel as ReasoningLevel | undefined;
|
||||
const currentReasoningLevel =
|
||||
(params.sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ?? "off";
|
||||
sessionReasoningLevel ??
|
||||
(currentThinkLevel === "off"
|
||||
? ((params.agentEntry?.reasoningDefault as ReasoningLevel | undefined) ?? "off")
|
||||
: "off");
|
||||
const currentElevatedLevel =
|
||||
(params.sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
|
||||
(params.agentCfg?.elevatedDefault as ElevatedLevel | undefined);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry, SessionScope } from "../../config/sessions/types.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import type { ElevatedLevel } from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
@@ -11,6 +12,7 @@ import type { createModelSelectionState } from "./model-selection.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
|
||||
type AgentDefaults = NonNullable<OpenClawConfig["agents"]>["defaults"];
|
||||
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
|
||||
|
||||
let commandsStatusPromise: Promise<typeof import("./commands-status.runtime.js")> | null = null;
|
||||
let directiveLevelsPromise: Promise<typeof import("./directive-handling.levels.js")> | null = null;
|
||||
@@ -69,6 +71,7 @@ export async function applyInlineDirectiveOverrides(params: {
|
||||
agentId: string;
|
||||
agentDir: string;
|
||||
agentCfg: AgentDefaults;
|
||||
agentEntry?: AgentEntry;
|
||||
sessionEntry: SessionEntry;
|
||||
sessionStore: Record<string, SessionEntry>;
|
||||
sessionKey: string;
|
||||
@@ -102,6 +105,7 @@ export async function applyInlineDirectiveOverrides(params: {
|
||||
agentId,
|
||||
agentDir,
|
||||
agentCfg,
|
||||
agentEntry,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -156,6 +160,16 @@ export async function applyInlineDirectiveOverrides(params: {
|
||||
|
||||
let directiveAck: ReplyPayload | undefined;
|
||||
|
||||
if (modelState.resetModelOverride) {
|
||||
enqueueSystemEvent(
|
||||
`Model override not allowed for this agent; reverted to ${initialModelLabel}.`,
|
||||
{
|
||||
sessionKey,
|
||||
contextKey: `model:reset:${initialModelLabel}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!command.isAuthorizedSender) {
|
||||
directives = clearInlineDirectives(directives.cleaned);
|
||||
}
|
||||
@@ -184,6 +198,7 @@ export async function applyInlineDirectiveOverrides(params: {
|
||||
await loadDirectiveLevels()
|
||||
).resolveCurrentDirectiveLevels({
|
||||
sessionEntry,
|
||||
agentEntry,
|
||||
agentCfg,
|
||||
resolveDefaultThinkingLevel: () => modelState.resolveDefaultThinkingLevel(),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { listAgentEntries } from "../../agents/agent-scope.js";
|
||||
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
||||
import { resolveFastModeState } from "../../agents/fast-mode.js";
|
||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||
@@ -5,6 +6,7 @@ import { resolveSandboxRuntimeStatus } from "../../agents/sandbox/runtime-status
|
||||
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||
import { shouldHandleTextCommands } from "../commands-text-routing.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
||||
@@ -167,6 +169,9 @@ export async function resolveReplyDirectives(params: {
|
||||
opts,
|
||||
skillFilter,
|
||||
} = params;
|
||||
const agentEntry = listAgentEntries(cfg).find(
|
||||
(entry) => normalizeAgentId(entry.id) === normalizeAgentId(agentId),
|
||||
);
|
||||
let provider = initialProvider;
|
||||
let model = initialModel;
|
||||
|
||||
@@ -387,6 +392,7 @@ export async function resolveReplyDirectives(params: {
|
||||
cfg,
|
||||
provider,
|
||||
model,
|
||||
agentId,
|
||||
sessionEntry,
|
||||
}).enabled;
|
||||
|
||||
@@ -398,6 +404,8 @@ export async function resolveReplyDirectives(params: {
|
||||
directives.reasoningLevel ??
|
||||
(sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ??
|
||||
"off";
|
||||
const agentReasoningDefault = agentEntry?.reasoningDefault as ReasoningLevel | undefined;
|
||||
const hasAgentReasoningDefault = agentReasoningDefault !== undefined;
|
||||
const resolvedElevatedLevel = elevatedAllowed
|
||||
? (directives.elevatedLevel ??
|
||||
(sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
|
||||
@@ -458,7 +466,11 @@ export async function resolveReplyDirectives(params: {
|
||||
(sessionEntry?.reasoningLevel !== undefined && sessionEntry?.reasoningLevel !== null);
|
||||
const thinkingActive = resolvedThinkLevelWithDefault !== "off";
|
||||
if (!reasoningExplicitlySet && resolvedReasoningLevel === "off" && !thinkingActive) {
|
||||
resolvedReasoningLevel = await modelState.resolveDefaultReasoningLevel();
|
||||
if (hasAgentReasoningDefault) {
|
||||
resolvedReasoningLevel = agentReasoningDefault;
|
||||
} else {
|
||||
resolvedReasoningLevel = await modelState.resolveDefaultReasoningLevel();
|
||||
}
|
||||
}
|
||||
logDirectiveStage("reasoning-default-resolved", `reasoning=${resolvedReasoningLevel}`);
|
||||
|
||||
@@ -483,6 +495,7 @@ export async function resolveReplyDirectives(params: {
|
||||
agentId,
|
||||
agentDir,
|
||||
agentCfg,
|
||||
agentEntry,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
|
||||
@@ -517,6 +517,7 @@ export async function runPreparedReply(
|
||||
cfg,
|
||||
provider,
|
||||
model,
|
||||
agentId,
|
||||
sessionEntry,
|
||||
}).enabled,
|
||||
verboseLevel: resolvedVerboseLevel,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { loadModelCatalog } from "../../agents/model-catalog.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { createModelSelectionState } from "./model-selection.js";
|
||||
|
||||
vi.mock("../../agents/model-catalog.js", () => ({
|
||||
@@ -64,6 +65,41 @@ describe("createModelSelectionState catalog loading", () => {
|
||||
expect(loadModelCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prefers per-agent thinkingDefault over model and global defaults", async () => {
|
||||
vi.mocked(loadModelCatalog).mockClear();
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
thinkingDefault: "low",
|
||||
models: {
|
||||
"openai-codex/gpt-5.4": {
|
||||
params: { thinking: "high" },
|
||||
},
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "alpha",
|
||||
thinkingDefault: "minimal",
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentId: "alpha",
|
||||
agentCfg: cfg.agents?.defaults,
|
||||
defaultProvider: "openai-codex",
|
||||
defaultModel: "gpt-5.4",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.4",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
await expect(state.resolveDefaultThinkingLevel()).resolves.toBe("minimal");
|
||||
});
|
||||
|
||||
it("loads the full catalog for explicit model directives", async () => {
|
||||
vi.mocked(loadModelCatalog).mockClear();
|
||||
const cfg = {
|
||||
@@ -90,7 +126,7 @@ describe("createModelSelectionState catalog loading", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const makeEntry = (overrides: Record<string, unknown> = {}) => ({
|
||||
const makeEntry = (overrides: Partial<SessionEntry> = {}): SessionEntry => ({
|
||||
sessionId: "session-id",
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
@@ -380,6 +416,42 @@ describe("createModelSelectionState respects session model override", () => {
|
||||
expect(state.model).toBe("grok-4.20-reasoning");
|
||||
expect(state.resetModelOverride).toBe(false);
|
||||
});
|
||||
|
||||
it("clears disallowed model overrides and falls back to the default", async () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4o" },
|
||||
models: {
|
||||
"openai/gpt-4o": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const sessionKey = "agent:main:telegram:direct:1";
|
||||
const sessionEntry = makeEntry({
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-4o-mini",
|
||||
});
|
||||
const sessionStore = { [sessionKey]: sessionEntry };
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentCfg: cfg.agents?.defaults,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4o",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
expect(state.resetModelOverride).toBe(true);
|
||||
expect(sessionStore[sessionKey]?.modelOverride).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.providerOverride).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createModelSelectionState resolveDefaultReasoningLevel", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||
import { clearSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
@@ -343,6 +344,7 @@ export async function createModelSelectionState(params: {
|
||||
let allowedModelCatalog: ModelCatalog = configuredModelCatalog;
|
||||
let modelCatalog: ModelCatalog | null = null;
|
||||
let resetModelOverride = false;
|
||||
const agentEntry = params.agentId ? resolveAgentConfig(cfg, params.agentId) : undefined;
|
||||
|
||||
if (needsModelCatalog) {
|
||||
modelCatalog = await (await loadModelCatalogRuntime()).loadModelCatalog({ config: cfg });
|
||||
@@ -461,8 +463,12 @@ export async function createModelSelectionState(params: {
|
||||
model,
|
||||
catalog: catalogForThinking,
|
||||
});
|
||||
const agentThinkingDefault = agentEntry?.thinkingDefault as ThinkLevel | undefined;
|
||||
defaultThinkingLevel =
|
||||
resolved ?? (agentCfg?.thinkingDefault as ThinkLevel | undefined) ?? "off";
|
||||
agentThinkingDefault ??
|
||||
resolved ??
|
||||
(agentCfg?.thinkingDefault as ThinkLevel | undefined) ??
|
||||
"off";
|
||||
return defaultThinkingLevel;
|
||||
};
|
||||
|
||||
|
||||
@@ -214,6 +214,12 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Shared default settings inherited by agents unless overridden per entry in agents.list. Use defaults to enforce consistent baseline behavior and reduce duplicated per-agent configuration.",
|
||||
"agents.list":
|
||||
"Explicit list of configured agents with IDs and optional overrides for model, tools, identity, and workspace. Keep IDs stable over time so bindings, approvals, and session routing remain deterministic.",
|
||||
"agents.list[].thinkingDefault":
|
||||
"Optional per-agent default thinking level. Overrides agents.defaults.thinkingDefault for this agent when no per-message or session override is set.",
|
||||
"agents.list[].reasoningDefault":
|
||||
"Optional per-agent default reasoning visibility (on|off|stream). Applies when no per-message or session reasoning override is set.",
|
||||
"agents.list[].fastModeDefault":
|
||||
"Optional per-agent default for fast mode. Applies when no per-message or session fast-mode override is set.",
|
||||
"agents.list[].runtime":
|
||||
"Optional runtime descriptor for this agent. Use embedded for default OpenClaw execution or acp for external ACP harness defaults.",
|
||||
"agents.list[].runtime.type":
|
||||
|
||||
@@ -63,6 +63,9 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.list[].runtime.acp.backend": "Agent ACP Backend",
|
||||
"agents.list[].runtime.acp.mode": "Agent ACP Mode",
|
||||
"agents.list[].runtime.acp.cwd": "Agent ACP Working Directory",
|
||||
"agents.list[].thinkingDefault": "Agent Thinking Default",
|
||||
"agents.list[].reasoningDefault": "Agent Reasoning Default",
|
||||
"agents.list[].fastModeDefault": "Agent Fast Mode Default",
|
||||
agents: "Agents",
|
||||
"agents.defaults": "Agent Defaults",
|
||||
"agents.list": "Agent List",
|
||||
|
||||
@@ -65,6 +65,12 @@ export type AgentConfig = {
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: AgentModelConfig;
|
||||
/** Optional per-agent default thinking level (overrides agents.defaults.thinkingDefault). */
|
||||
thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
|
||||
/** Optional per-agent default reasoning visibility. */
|
||||
reasoningDefault?: "on" | "off" | "stream";
|
||||
/** Optional per-agent default for fast mode. */
|
||||
fastModeDefault?: boolean;
|
||||
/** Optional allowlist of skills for this agent (omit = all skills; empty = none). */
|
||||
skills?: string[];
|
||||
memorySearch?: MemorySearchConfig;
|
||||
|
||||
@@ -767,6 +767,11 @@ export const AgentEntrySchema = z
|
||||
workspace: z.string().optional(),
|
||||
agentDir: z.string().optional(),
|
||||
model: AgentModelSchema.optional(),
|
||||
thinkingDefault: z
|
||||
.enum(["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"])
|
||||
.optional(),
|
||||
reasoningDefault: z.enum(["on", "off", "stream"]).optional(),
|
||||
fastModeDefault: z.boolean().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
memorySearch: MemorySearchSchema,
|
||||
humanDelay: HumanDelaySchema.optional(),
|
||||
|
||||
@@ -521,6 +521,7 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
cfg: cfgWithAgentDefaults,
|
||||
provider: providerOverride,
|
||||
model: modelOverride,
|
||||
agentId,
|
||||
sessionEntry: cronSession.sessionEntry,
|
||||
}).enabled,
|
||||
verboseLevel: resolvedVerboseLevel,
|
||||
|
||||
Reference in New Issue
Block a user