mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 04:20:44 +00:00
fix(subagents): preserve default fallbacks on model overrides
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
|
||||
- Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.
|
||||
- TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.
|
||||
- TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.
|
||||
|
||||
@@ -7,10 +7,9 @@ import { loadConfig } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { normalizeDeliveryContext } from "../../utils/delivery-context.js";
|
||||
import { resolveAgentConfig, resolveAgentModelFallbacksOverride } from "../agent-scope.js";
|
||||
import { resolveAgentConfig } from "../agent-scope.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
|
||||
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
||||
import { runWithModelFallback } from "../model-fallback.js";
|
||||
import { optionalStringEnum } from "../schema/typebox.js";
|
||||
import { buildSubagentSystemPrompt } from "../subagent-announce.js";
|
||||
import { getSubagentDepthFromSessionStore } from "../subagent-depth.js";
|
||||
@@ -268,23 +267,9 @@ export function createSessionsSpawnTool(opts?: {
|
||||
maxSpawnDepth,
|
||||
});
|
||||
|
||||
// Get fallbacks for this agent
|
||||
const fallbacks = resolveAgentModelFallbacksOverride(cfg, targetAgentId);
|
||||
|
||||
// Generate idempotency key upfront
|
||||
const childIdem = crypto.randomUUID();
|
||||
let childRunId: string = childIdem;
|
||||
|
||||
// Build the run function that will be executed with fallback support
|
||||
const runSubagentWithModel = async (modelProvider: string, modelName: string) => {
|
||||
// Patch the session with the current model
|
||||
await callGateway({
|
||||
method: "sessions.patch",
|
||||
params: { key: childSessionKey, model: `${modelProvider}/${modelName}` },
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
|
||||
// Start the agent
|
||||
try {
|
||||
const response = await callGateway<{ runId: string }>({
|
||||
method: "agent",
|
||||
params: {
|
||||
@@ -309,47 +294,9 @@ export function createSessionsSpawnTool(opts?: {
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
// Extract provider and model from resolvedModel
|
||||
const { provider: primaryProvider, model: primaryModel } = splitModelRef(resolvedModel);
|
||||
|
||||
try {
|
||||
// Run with fallback support
|
||||
const fallbackResult = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: primaryProvider ?? "unknown",
|
||||
model: primaryModel ?? "default",
|
||||
agentDir: undefined,
|
||||
fallbacksOverride: fallbacks,
|
||||
run: async (provider, model) => {
|
||||
// Patch session depth first
|
||||
await callGateway({
|
||||
method: "sessions.patch",
|
||||
params: { key: childSessionKey, spawnDepth: childDepth },
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
// Patch thinking if set
|
||||
if (thinkingOverride !== undefined) {
|
||||
await callGateway({
|
||||
method: "sessions.patch",
|
||||
params: {
|
||||
key: childSessionKey,
|
||||
thinkingLevel: thinkingOverride === "off" ? null : thinkingOverride,
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
}
|
||||
return runSubagentWithModel(provider, model);
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof fallbackResult.result?.runId === "string" && fallbackResult.result.runId) {
|
||||
childRunId = fallbackResult.result.runId;
|
||||
if (typeof response?.runId === "string" && response.runId) {
|
||||
childRunId = response.runId;
|
||||
}
|
||||
modelApplied = true;
|
||||
} catch (err) {
|
||||
const messageText =
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
||||
|
||||
@@ -196,6 +196,71 @@ describe("agentCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses default fallback list for session model overrides", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
fs.mkdirSync(path.dirname(store), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
store,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:subagent:test": {
|
||||
sessionId: "session-subagent",
|
||||
updatedAt: Date.now(),
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-opus-4-5",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
mockConfig(home, store, {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
fallbacks: ["openai/gpt-5.2"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5.2": {},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce([
|
||||
{ id: "claude-opus-4-5", name: "Opus", provider: "anthropic" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2", provider: "openai" },
|
||||
]);
|
||||
vi.mocked(runEmbeddedPiAgent)
|
||||
.mockRejectedValueOnce(Object.assign(new Error("rate limited"), { status: 429 }))
|
||||
.mockResolvedValueOnce({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: {
|
||||
durationMs: 5,
|
||||
agentMeta: { sessionId: "session-subagent", provider: "openai", model: "gpt-5.2" },
|
||||
},
|
||||
});
|
||||
|
||||
await agentCommand(
|
||||
{
|
||||
message: "hi",
|
||||
sessionKey: "agent:main:subagent:test",
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const attempts = vi
|
||||
.mocked(runEmbeddedPiAgent)
|
||||
.mock.calls.map((call) => ({ provider: call[0]?.provider, model: call[0]?.model }));
|
||||
expect(attempts).toEqual([
|
||||
{ provider: "anthropic", model: "claude-opus-4-5" },
|
||||
{ provider: "openai", model: "gpt-5.2" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps explicit sessionKey even when sessionId exists elsewhere", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
|
||||
@@ -396,13 +396,17 @@ export async function agentCommand(
|
||||
opts.replyChannel ?? opts.channel,
|
||||
);
|
||||
const spawnedBy = opts.spawnedBy ?? sessionEntry?.spawnedBy;
|
||||
// When a session has an explicit model override, prevent the fallback logic
|
||||
// from silently appending the global primary model as a backstop. Passing an
|
||||
// empty array (instead of undefined) tells resolveFallbackCandidates to skip
|
||||
// the implicit primary append, so the session stays on its overridden model.
|
||||
// When a session has an explicit model override, keep the candidate chain
|
||||
// anchored to that override (no implicit configured-primary append), while
|
||||
// still preserving configured fallback lists unless the agent explicitly
|
||||
// overrides fallbacks with its own list (including an empty list to disable).
|
||||
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(cfg, sessionAgentId);
|
||||
const defaultFallbacks =
|
||||
typeof cfg.agents?.defaults?.model === "object"
|
||||
? (cfg.agents.defaults.model.fallbacks ?? [])
|
||||
: [];
|
||||
const effectiveFallbacksOverride = storedModelOverride
|
||||
? (agentFallbacksOverride ?? [])
|
||||
? (agentFallbacksOverride ?? defaultFallbacks)
|
||||
: agentFallbacksOverride;
|
||||
|
||||
// Track model fallback attempts so retries on an existing session don't
|
||||
|
||||
Reference in New Issue
Block a user