From 29a446f88625b99fb98adf035d0e26ace998eb39 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 8 Aug 2026 13:55:57 -0700 Subject: [PATCH] fix(agents): apply global fast mode to implicit agent (#120697) --- docs/automation/cron-jobs.md | 2 +- docs/cli/cron.md | 2 +- docs/gateway/config-agents.md | 4 +- src/agents/agent-scope-config.test.ts | 9 +++ src/agents/agent-scope-config.ts | 4 +- src/agents/agent-scope.test.ts | 6 -- src/agents/fast-mode.test.ts | 55 +++++++++++++-- .../server.sessions.list-changed.test.ts | 67 +++++++++++++++++++ 8 files changed, 133 insertions(+), 16 deletions(-) diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 49f4387c5e21..71dd162421fe 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -228,7 +228,7 @@ Model-selection precedence for isolated jobs, highest first: 3. User-selected stored automation-session model override 4. Agent/default model selection -Fast mode follows the resolved live selection. If the selected model config has `params.fastMode`, isolated automation runs use it by default; a stored session `fastMode` override (then an agent `fastModeDefault`) still wins over model config either direction. Auto mode uses the model's `params.fastAutoOnSeconds` cutoff, defaulting to 60 seconds. +Fast mode follows the resolved live selection. Isolated automation resolves it in this order: stored session `fastMode`, per-agent `agents.entries.*.fastModeDefault`, global `agents.defaults.fastModeDefault`, then selected-model `params.fastMode`. Auto mode uses the model's `params.fastAutoOnSeconds` cutoff, defaulting to 60 seconds. If a run hits a live model-switch handoff, the scheduler retries with the switched provider/model and persists that selection (and any new auth profile) for the active run. Retries are bounded: after the initial attempt plus 2 switch retries, the scheduler aborts instead of looping. diff --git a/docs/cli/cron.md b/docs/cli/cron.md index 7f447533d1d0..e0fcf17c75c2 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -188,7 +188,7 @@ Isolated automation runs resolve the active model in this order: ### Fast mode -Isolated automation fast mode follows the resolved live model selection. Model config `params.fastMode` applies by default, but a stored session `fastMode` override still wins over config. When the resolved mode is `auto`, the cutoff uses the selected model's `params.fastAutoOnSeconds` value, defaulting to 60 seconds. +Isolated automation fast mode follows the resolved live model selection. It resolves stored session `fastMode`, per-agent `agents.entries.*.fastModeDefault`, global `agents.defaults.fastModeDefault`, then selected-model `params.fastMode`. When the resolved mode is `auto`, the cutoff uses the selected model's `params.fastAutoOnSeconds` value, defaulting to 60 seconds. ### Live model switch retries diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index 47e613e6b678..0c80f542fa69 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -373,6 +373,7 @@ date context. Falls back to the host timezone. pdfMaxMb: 10, pdfMaxPages: 20, thinkingDefault: "low", + fastModeDefault: false, verboseDefault: "off", toolProgressDetail: "explain", reasoningDefault: "off", @@ -415,6 +416,7 @@ date context. Falls back to the host timezone. - If omitted, the PDF tool falls back to `imageModel`, then to the resolved session/default model. - `pdfMaxMb`: default PDF size limit for the `pdf` tool when `maxBytesMb` is not passed at call time. - `pdfMaxPages`: default maximum pages considered by extraction fallback mode in the `pdf` tool. +- `fastModeDefault`: default fast mode for agents. Values: `"auto"`, `true`, `false`. Per-agent `agents.entries.*.fastModeDefault` overrides it when no per-message or session fast-mode override is set. - `verboseDefault`: default verbose level for agents. Values: `"off"`, `"on"`, `"full"`. Default: `"off"`. - `toolProgressDetail`: detail mode for `/verbose` tool summaries and progress-draft tool lines. Values: `"explain"` (default, compact human labels) or `"raw"` (append raw command/detail when available). Per-agent `agents.entries.*.toolProgressDetail` overrides this default. - `reasoningDefault`: default reasoning visibility for agents. Values: `"off"`, `"on"`, `"stream"`. Per-agent `agents.entries.*.reasoningDefault` overrides this default. Configured reasoning defaults are only applied for owners, authorized senders, or operator-admin gateway contexts when no per-message or session reasoning override is set. @@ -1029,7 +1031,7 @@ for provider examples and precedence. - `skills`: optional per-agent skill allowlist. If omitted, the agent inherits `agents.defaults.skills` when set; an explicit list replaces defaults instead of merging, and `[]` means no skills. - `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive | max`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. The selected provider/model profile controls which values are valid; for Google Gemini, `adaptive` keeps provider-owned dynamic thinking (`thinkingLevel` omitted on Gemini 3/3.1, `thinkingBudget: -1` on Gemini 2.5). - `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Overrides `agents.defaults.reasoningDefault` for this agent when no per-message or session reasoning override is set. -- `fastModeDefault`: optional per-agent default for fast mode (`"auto" | true | false`). Applies when no per-message or session fast-mode override is set. +- `fastModeDefault`: optional per-agent default for fast mode (`"auto" | true | false`). Overrides `agents.defaults.fastModeDefault` for this agent when no per-message or session fast-mode override is set. - `models`: optional per-agent model catalog/runtime overrides keyed by full `provider/model` ids. Use `models["provider/model"].agentRuntime` for per-agent runtime exceptions. - `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. diff --git a/src/agents/agent-scope-config.test.ts b/src/agents/agent-scope-config.test.ts index b6e42985fa04..ce4788598757 100644 --- a/src/agents/agent-scope-config.test.ts +++ b/src/agents/agent-scope-config.test.ts @@ -47,6 +47,15 @@ describe("agent roster resolution", () => { expect(tryResolveDefaultAgentId(duplicateDefaults)).toBeUndefined(); }); + it("resolves defaults only for the rosterless implicit main agent", () => { + const defaults = { fastModeDefault: "auto" as const }; + + expect(resolveAgentConfig({ agents: { defaults } }, "main")?.fastModeDefault).toBe("auto"); + expect(resolveAgentConfig({ agents: { defaults } }, "work")).toBeUndefined(); + expect(resolveAgentConfig({ agents: { defaults, entries: {} } }, "main")).toBeUndefined(); + expect(resolveAgentConfig({ agents: { defaults, list: [] } }, "main")).toBeUndefined(); + }); + it("offers a non-throwing diagnostic lookup for malformed rosters", () => { expect(tryResolveDefaultAgentId({ agents: { list: [{ id: "alpha" }] } })).toBeUndefined(); for (const marker of ["false", 1]) { diff --git a/src/agents/agent-scope-config.ts b/src/agents/agent-scope-config.ts index 5f0ac9876acb..6378cf43f7c6 100644 --- a/src/agents/agent-scope-config.ts +++ b/src/agents/agent-scope-config.ts @@ -196,7 +196,9 @@ export function resolveAgentConfig( agentId: string, ): ResolvedAgentConfig | undefined { const id = normalizeAgentId(agentId); - const entry = resolveAgentEntry(cfg, id); + const entry: AgentEntry | undefined = + resolveAgentEntry(cfg, id) ?? + (!hasAgentRosterProperty(cfg) && id === LEGACY_IMPLICIT_AGENT_ID ? { id } : undefined); if (!entry) { return undefined; } diff --git a/src/agents/agent-scope.test.ts b/src/agents/agent-scope.test.ts index 67628e4f1ac6..d0bf606ef2cc 100644 --- a/src/agents/agent-scope.test.ts +++ b/src/agents/agent-scope.test.ts @@ -29,12 +29,6 @@ import { } from "./agent-scope.js"; describe("resolveAgentConfig", () => { - it("should return undefined when no agents config exists", () => { - const cfg: OpenClawConfig = {}; - const result = resolveAgentConfig(cfg, "main"); - expect(result).toBeUndefined(); - }); - it("should return undefined when agent id does not exist", () => { const cfg: OpenClawConfig = { agents: { diff --git a/src/agents/fast-mode.test.ts b/src/agents/fast-mode.test.ts index 2d249a88c79c..1328e5887654 100644 --- a/src/agents/fast-mode.test.ts +++ b/src/agents/fast-mode.test.ts @@ -12,11 +12,17 @@ import { } from "./fast-mode.js"; describe("resolveFastModeState", () => { - it("prefers session overrides", () => { + it("prefers session overrides over per-agent and global defaults", () => { const state = resolveFastModeState({ - cfg: {} as OpenClawConfig, + cfg: { + agents: { + defaults: { fastModeDefault: "auto" }, + list: [{ id: "main", fastModeDefault: false }], + }, + } as OpenClawConfig, provider: "openai", model: "gpt-4o", + agentId: "main", sessionEntry: { fastMode: true }, }); @@ -37,10 +43,47 @@ describe("resolveFastModeState", () => { expect(state.enabled).toBe(true); }); - it("uses agent fastModeDefault when present", () => { + it.each([ + [true, false], + [false, true], + ["auto", false], + ] as const)( + "uses rosterless global fastModeDefault %s over model config", + (fastModeDefault, modelFastMode) => { + const cfg = { + agents: { + defaults: { + fastModeDefault, + models: { + "openai/gpt-4o": { params: { fastMode: modelFastMode } }, + }, + }, + }, + } as OpenClawConfig; + + const state = resolveFastModeState({ + cfg, + provider: "openai", + model: "gpt-4o", + agentId: "main", + }); + + expect(state.mode).toBe(fastModeDefault); + expect(state.enabled).toBe(fastModeDefault === "auto" ? true : fastModeDefault); + expect(state.source).toBe("agent"); + }, + ); + + it("prefers per-agent fastModeDefault over the global default", () => { const cfg = { agents: { - list: [{ id: "alpha", fastModeDefault: true }], + defaults: { + fastModeDefault: true, + models: { + "openai/gpt-4o": { params: { fastMode: true } }, + }, + }, + list: [{ id: "main", fastModeDefault: false }], }, } as OpenClawConfig; @@ -48,10 +91,10 @@ describe("resolveFastModeState", () => { cfg, provider: "openai", model: "gpt-4o", - agentId: "alpha", + agentId: "main", }); - expect(state.enabled).toBe(true); + expect(state.mode).toBe(false); expect(state.source).toBe("agent"); }); diff --git a/src/gateway/server.sessions.list-changed.test.ts b/src/gateway/server.sessions.list-changed.test.ts index 08ce3214a595..dd4f78a6ae1c 100644 --- a/src/gateway/server.sessions.list-changed.test.ts +++ b/src/gateway/server.sessions.list-changed.test.ts @@ -564,6 +564,73 @@ test("sessions.list exposes effective fast auto defaults from the selected model }); }); +test.each([ + { + label: "rosterless global default", + agents: { + defaults: { + fastModeDefault: true, + models: { "openai/gpt-5.5": { params: { fastMode: false } } }, + }, + }, + expectedFastMode: undefined, + expectedEffectiveFastMode: true, + expectedSource: "agent", + }, + { + label: "per-agent default", + agents: { + defaults: { + fastModeDefault: true, + models: { "openai/gpt-5.5": { params: { fastMode: true } } }, + }, + entries: { main: { fastModeDefault: false } }, + }, + expectedFastMode: undefined, + expectedEffectiveFastMode: false, + expectedSource: "agent", + }, + { + label: "session override", + agents: { + defaults: { + fastModeDefault: false, + models: { "openai/gpt-5.5": { params: { fastMode: false } } }, + }, + entries: { main: { fastModeDefault: false } }, + }, + sessionFastMode: "auto" as const, + expectedFastMode: "auto", + expectedEffectiveFastMode: "auto", + expectedSource: "session", + }, +])("sessions.list projects $label fast-mode precedence", async (scenario) => { + await writeMainSessionStore({ + modelProvider: "openai", + model: "gpt-5.5", + ...(scenario.sessionFastMode === undefined ? {} : { fastMode: scenario.sessionFastMode }), + }); + const storePath = expectDefined(testState.sessionStorePath, "session store path"); + + const { respond } = await invokeSessionsList({ + requestId: `req-sessions-list-fast-${scenario.label.replaceAll(" ", "-")}`, + context: { + getRuntimeConfig: () => ({ + agents: scenario.agents, + session: { store: storePath }, + }), + }, + }); + + const payload = expectRespondPayload(respond); + const session = findSession(payload, "agent:main:main"); + expectFields(session, { + fastMode: scenario.expectedFastMode, + effectiveFastMode: scenario.expectedEffectiveFastMode, + effectiveFastModeSource: scenario.expectedSource, + }); +}); + test("sessions.list resolves effective fast metadata from the raw runtime provider", async () => { testState.agentConfig = { model: { primary: "openai-codex/gpt-5.5" },