mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 12:25:44 +00:00
fix: apply ACP spawn model defaults
This commit is contained in:
parent
fdf6092494
commit
ff22b1e9e6
6 changed files with 281 additions and 24 deletions
|
|
@ -560,6 +560,10 @@ Two ways to start an ACP session:
|
|||
normalize OpenAI refs such as `openai/gpt-5.4` to Codex ACP startup
|
||||
config before `session/new`; slash forms such as `openai/gpt-5.4/high`
|
||||
also set Codex ACP reasoning effort.
|
||||
When omitted, `sessions_spawn({ runtime: "acp" })` uses existing
|
||||
subagent model defaults (`agents.defaults.subagents.model` or
|
||||
`agents.list[].subagents.model`) when configured; otherwise it lets the
|
||||
ACP harness use its own default model.
|
||||
Other harnesses must advertise ACP `models` and support
|
||||
`session/set_model`; otherwise OpenClaw/acpx fails clearly instead of
|
||||
silently falling back to the target agent default.
|
||||
|
|
@ -568,6 +572,9 @@ Two ways to start an ACP session:
|
|||
Explicit thinking/reasoning effort. For Codex ACP, `minimal` maps to
|
||||
low effort, `low`/`medium`/`high`/`xhigh` map directly, and `off`
|
||||
omits the reasoning-effort startup override.
|
||||
When omitted, ACP spawns use existing subagent thinking defaults and
|
||||
per-model `agents.defaults.models["provider/model"].params.thinking`
|
||||
for the selected model.
|
||||
</ParamField>
|
||||
|
||||
## Spawn bind and thread modes
|
||||
|
|
|
|||
|
|
@ -139,8 +139,8 @@ session to confirm the effective tool list.
|
|||
|
||||
**Defaults:**
|
||||
|
||||
- **Model:** inherits the caller unless you set `agents.defaults.subagents.model` (or per-agent `agents.list[].subagents.model`); an explicit `sessions_spawn.model` still wins.
|
||||
- **Thinking:** inherits the caller unless you set `agents.defaults.subagents.thinking` (or per-agent `agents.list[].subagents.thinking`); an explicit `sessions_spawn.thinking` still wins.
|
||||
- **Model:** native sub-agents inherit the caller unless you set `agents.defaults.subagents.model` (or per-agent `agents.list[].subagents.model`). ACP runtime spawns use the same configured subagent model when present; otherwise the ACP harness keeps its own default. An explicit `sessions_spawn.model` still wins.
|
||||
- **Thinking:** native sub-agents inherit the caller unless you set `agents.defaults.subagents.thinking` (or per-agent `agents.list[].subagents.thinking`). ACP runtime spawns also apply `agents.defaults.models["provider/model"].params.thinking` for the selected model. An explicit `sessions_spawn.thinking` still wins.
|
||||
- **Run timeout:** if `sessions_spawn.runTimeoutSeconds` is omitted, OpenClaw uses `agents.defaults.subagents.runTimeoutSeconds` when set; otherwise it falls back to `0` (no timeout).
|
||||
- **Task delivery:** native sub-agents receive the delegated task in their first visible `[Subagent Task]` message. The sub-agent system prompt carries runtime rules and routing context, not a hidden duplicate of the task.
|
||||
|
||||
|
|
|
|||
|
|
@ -929,6 +929,129 @@ describe("spawnAcpDirect", () => {
|
|||
expect(initInput.sessionKey).toMatch(/^agent:codex:acp:/);
|
||||
});
|
||||
|
||||
it("applies existing subagent model and model-profile thinking defaults to ACP runtime options", async () => {
|
||||
replaceSpawnConfig({
|
||||
...createDefaultSpawnConfig(),
|
||||
agents: {
|
||||
defaults: {
|
||||
subagents: {
|
||||
allowAgents: ["codex"],
|
||||
maxSpawnDepth: 2,
|
||||
model: "openai/gpt-5.4",
|
||||
},
|
||||
models: {
|
||||
"openai/gpt-5.4": {
|
||||
params: { thinking: "high" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
task: "Investigate flaky tests",
|
||||
agentId: "codex",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
},
|
||||
);
|
||||
|
||||
expectAcceptedSpawn(result);
|
||||
expectInitializeSessionFields({
|
||||
agent: "codex",
|
||||
runtimeOptions: {
|
||||
model: "openai/gpt-5.4",
|
||||
thinking: "high",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("uses configured runtime=acp agent defaults before launching the external ACP agent", async () => {
|
||||
replaceSpawnConfig({
|
||||
...createDefaultSpawnConfig(),
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "codex-acp",
|
||||
runtime: {
|
||||
type: "acp",
|
||||
acp: { agent: "codex" },
|
||||
},
|
||||
subagents: {
|
||||
model: "openai/gpt-5.5",
|
||||
thinking: "low",
|
||||
},
|
||||
},
|
||||
],
|
||||
defaults: {
|
||||
subagents: {
|
||||
allowAgents: ["codex"],
|
||||
maxSpawnDepth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
task: "Investigate flaky tests",
|
||||
agentId: "codex-acp",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
},
|
||||
);
|
||||
|
||||
expectAcceptedSpawn(result);
|
||||
expectInitializeSessionFields({
|
||||
agent: "codex",
|
||||
runtimeOptions: {
|
||||
model: "openai/gpt-5.5",
|
||||
thinking: "low",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat a configured runtime=acp agent primary model as an ACP startup model", async () => {
|
||||
replaceSpawnConfig({
|
||||
...createDefaultSpawnConfig(),
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "codex-acp",
|
||||
runtime: {
|
||||
type: "acp",
|
||||
acp: { agent: "codex" },
|
||||
},
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
],
|
||||
defaults: {
|
||||
subagents: {
|
||||
allowAgents: ["codex"],
|
||||
maxSpawnDepth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
task: "Investigate flaky tests",
|
||||
agentId: "codex-acp",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
},
|
||||
);
|
||||
|
||||
expectAcceptedSpawn(result);
|
||||
const initInput = expectInitializeSessionFields({ agent: "codex" });
|
||||
expect(initInput.runtimeOptions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("applies ACP spawn run timeout to runtime options and dispatch", async () => {
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from "../acp/control-plane/spawn.js";
|
||||
import { isAcpEnabledByPolicy, resolveAcpAgentPolicyError } from "../acp/policy.js";
|
||||
import { DEFAULT_HEARTBEAT_EVERY } from "../auto-reply/heartbeat.js";
|
||||
import { formatThinkingLevels } from "../auto-reply/thinking.js";
|
||||
import {
|
||||
resolveChannelDefaultBindingPlacement,
|
||||
resolveInboundConversationResolution,
|
||||
|
|
@ -82,6 +83,10 @@ import {
|
|||
inheritedToolDenyPatch,
|
||||
} from "./inherited-tool-deny.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
|
||||
import {
|
||||
resolveConfiguredSubagentSpawnModelSelection,
|
||||
resolveThinkingDefault,
|
||||
} from "./model-selection.js";
|
||||
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
|
||||
import { resolveRequesterOriginForChild } from "./spawn-requester-origin.js";
|
||||
import { resolveSpawnedWorkspaceInheritance } from "./spawned-context.js";
|
||||
|
|
@ -93,6 +98,8 @@ import {
|
|||
} from "./subagent-capabilities.js";
|
||||
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||
import { countActiveRunsForSession, getSubagentRunByChildSessionKey } from "./subagent-registry.js";
|
||||
import { splitModelRef } from "./subagent-spawn-plan.js";
|
||||
import { resolveSubagentThinkingOverride } from "./subagent-spawn-thinking.js";
|
||||
import { resolveSubagentTargetPolicy } from "./subagent-target-policy.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./tools/sessions-helpers.js";
|
||||
|
||||
|
|
@ -437,7 +444,7 @@ function hasSessionLocalHeartbeatRelayRoute(params: {
|
|||
function resolveTargetAcpAgentId(params: {
|
||||
requestedAgentId?: string;
|
||||
cfg: OpenClawConfig;
|
||||
}): { ok: true; agentId: string } | { ok: false; error: string } {
|
||||
}): { ok: true; agentId: string; configAgentId?: string } | { ok: false; error: string } {
|
||||
const requested = normalizeOptionalAgentId(params.requestedAgentId);
|
||||
if (requested) {
|
||||
const configuredAgent = params.cfg.agents?.list?.find(
|
||||
|
|
@ -447,6 +454,7 @@ function resolveTargetAcpAgentId(params: {
|
|||
return {
|
||||
ok: true,
|
||||
agentId: normalizeOptionalAgentId(configuredAgent.runtime.acp?.agent) ?? requested,
|
||||
configAgentId: requested,
|
||||
};
|
||||
}
|
||||
if (configuredAgent && !isExplicitlyAllowedAcpAgent(params.cfg, requested)) {
|
||||
|
|
@ -458,7 +466,11 @@ function resolveTargetAcpAgentId(params: {
|
|||
'Use runtime="acp" only with external ACP harness ids such as codex, claude, droid, gemini, or opencode, or configure agents.list[].runtime.type="acp" with runtime.acp.agent.',
|
||||
};
|
||||
}
|
||||
return { ok: true, agentId: requested };
|
||||
return {
|
||||
ok: true,
|
||||
agentId: requested,
|
||||
...(configuredAgent ? { configAgentId: requested } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const configuredDefault = normalizeOptionalAgentId(params.cfg.acp?.defaultAgent);
|
||||
|
|
@ -975,15 +987,71 @@ function validateAcpResumeSessionOwnership(params: {
|
|||
};
|
||||
}
|
||||
|
||||
type AcpSpawnRuntimeOptions = {
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
timeoutSeconds?: number;
|
||||
};
|
||||
|
||||
function resolveAcpSpawnRuntimeOptions(params: {
|
||||
cfg: OpenClawConfig;
|
||||
targetAgentId: string;
|
||||
configAgentId?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
runTimeoutSeconds?: number;
|
||||
}): { ok: true; runtimeOptions?: AcpSpawnRuntimeOptions } | { ok: false; error: string } {
|
||||
const policyAgentId = params.configAgentId ?? params.targetAgentId;
|
||||
const model = resolveConfiguredSubagentSpawnModelSelection({
|
||||
cfg: params.cfg,
|
||||
agentId: policyAgentId,
|
||||
modelOverride: params.model,
|
||||
includeAgentPrimary: false,
|
||||
});
|
||||
const targetAgentConfig = resolveAgentConfig(params.cfg, policyAgentId);
|
||||
const thinkingPlan = resolveSubagentThinkingOverride({
|
||||
cfg: params.cfg,
|
||||
targetAgentConfig,
|
||||
thinkingOverrideRaw: params.thinking,
|
||||
});
|
||||
if (thinkingPlan.status === "error") {
|
||||
const { provider, model: modelId } = splitModelRef(model);
|
||||
return {
|
||||
ok: false,
|
||||
error: `Invalid thinking level "${thinkingPlan.thinkingCandidateRaw}". Use one of: ${formatThinkingLevels(provider, modelId)}.`,
|
||||
};
|
||||
}
|
||||
|
||||
let thinking = thinkingPlan.thinkingOverride;
|
||||
if (!thinking && model) {
|
||||
const { provider, model: modelId } = splitModelRef(model);
|
||||
if (provider && modelId) {
|
||||
thinking = resolveThinkingDefault({
|
||||
cfg: params.cfg,
|
||||
provider,
|
||||
model: modelId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeOptions =
|
||||
model || thinking || params.runTimeoutSeconds
|
||||
? {
|
||||
...(model ? { model } : {}),
|
||||
...(thinking ? { thinking } : {}),
|
||||
...(params.runTimeoutSeconds ? { timeoutSeconds: params.runTimeoutSeconds } : {}),
|
||||
}
|
||||
: undefined;
|
||||
return { ok: true, runtimeOptions };
|
||||
}
|
||||
|
||||
async function initializeAcpSpawnRuntime(params: {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey: string;
|
||||
targetAgentId: string;
|
||||
runtimeMode: AcpRuntimeSessionMode;
|
||||
resumeSessionId?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
runTimeoutSeconds?: number;
|
||||
runtimeOptions?: AcpSpawnRuntimeOptions;
|
||||
cwd?: string;
|
||||
}): Promise<AcpSpawnInitializedRuntime> {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.targetAgentId });
|
||||
|
|
@ -1008,14 +1076,7 @@ async function initializeAcpSpawnRuntime(params: {
|
|||
agent: params.targetAgentId,
|
||||
mode: params.runtimeMode,
|
||||
resumeSessionId: params.resumeSessionId,
|
||||
runtimeOptions:
|
||||
params.model || params.thinking || params.runTimeoutSeconds
|
||||
? {
|
||||
...(params.model ? { model: params.model } : {}),
|
||||
...(params.thinking ? { thinking: params.thinking } : {}),
|
||||
...(params.runTimeoutSeconds ? { timeoutSeconds: params.runTimeoutSeconds } : {}),
|
||||
}
|
||||
: undefined,
|
||||
runtimeOptions: params.runtimeOptions,
|
||||
cwd: params.cwd,
|
||||
backendId: params.cfg.acp?.backend,
|
||||
});
|
||||
|
|
@ -1320,6 +1381,21 @@ export async function spawnAcpDirect(
|
|||
error: resumeAuthorization.error,
|
||||
});
|
||||
}
|
||||
const runtimeOptionsResult = resolveAcpSpawnRuntimeOptions({
|
||||
cfg,
|
||||
targetAgentId,
|
||||
configAgentId: targetAgentResult.configAgentId,
|
||||
model: params.model,
|
||||
thinking: params.thinking,
|
||||
runTimeoutSeconds: params.runTimeoutSeconds,
|
||||
});
|
||||
if (!runtimeOptionsResult.ok) {
|
||||
return createAcpSpawnFailure({
|
||||
status: "error",
|
||||
errorCode: "spawn_failed",
|
||||
error: runtimeOptionsResult.error,
|
||||
});
|
||||
}
|
||||
const { effectiveStreamToParent } = resolveAcpSpawnStreamPlan({
|
||||
spawnMode,
|
||||
requestThreadBinding,
|
||||
|
|
@ -1392,9 +1468,7 @@ export async function spawnAcpDirect(
|
|||
targetAgentId,
|
||||
runtimeMode,
|
||||
resumeSessionId: params.resumeSessionId,
|
||||
model: params.model,
|
||||
thinking: params.thinking,
|
||||
runTimeoutSeconds: params.runTimeoutSeconds,
|
||||
runtimeOptions: runtimeOptionsResult.runtimeOptions,
|
||||
cwd: runtimeCwd,
|
||||
});
|
||||
initializedRuntime = initializedSession.runtimeCloseHandle;
|
||||
|
|
|
|||
|
|
@ -323,12 +323,13 @@ function resolveAllowedFallbacks(params: { cfg: OpenClawConfig; agentId?: string
|
|||
export function resolveSubagentConfiguredModelSelection(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
includeAgentPrimary?: boolean;
|
||||
}): string | undefined {
|
||||
const agentConfig = resolveAgentConfig(params.cfg, params.agentId);
|
||||
return (
|
||||
normalizeModelSelection(agentConfig?.subagents?.model) ??
|
||||
normalizeModelSelection(params.cfg.agents?.defaults?.subagents?.model) ??
|
||||
normalizeModelSelection(agentConfig?.model)
|
||||
(params.includeAgentPrimary === false ? undefined : normalizeModelSelection(agentConfig?.model))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -361,12 +362,16 @@ export function resolveSubagentSpawnModelSelection(params: {
|
|||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const configured = resolveConfiguredSubagentSpawnModelSelection({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
modelOverride: params.modelOverride,
|
||||
defaultProvider: runtimeDefault.provider,
|
||||
});
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
const raw =
|
||||
normalizeModelSelection(params.modelOverride) ??
|
||||
resolveSubagentConfiguredModelSelection({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
}) ??
|
||||
normalizeModelSelection(resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)) ??
|
||||
`${runtimeDefault.provider}/${runtimeDefault.model}`;
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
|
|
@ -376,6 +381,36 @@ export function resolveSubagentSpawnModelSelection(params: {
|
|||
return resolveModelThroughAliases(raw, aliasIndex);
|
||||
}
|
||||
|
||||
export function resolveConfiguredSubagentSpawnModelSelection(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
modelOverride?: unknown;
|
||||
defaultProvider?: string;
|
||||
includeAgentPrimary?: boolean;
|
||||
}): string | undefined {
|
||||
const raw =
|
||||
normalizeModelSelection(params.modelOverride) ??
|
||||
resolveSubagentConfiguredModelSelection({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
includeAgentPrimary: params.includeAgentPrimary,
|
||||
});
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const defaultProvider =
|
||||
normalizeOptionalString(params.defaultProvider) ??
|
||||
resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
}).provider;
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg,
|
||||
defaultProvider,
|
||||
});
|
||||
return resolveModelThroughAliases(raw, aliasIndex);
|
||||
}
|
||||
|
||||
export function buildAllowedModelSet(
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import { resolveConfiguredSubagentSpawnModelSelection } from "./model-selection.js";
|
||||
import {
|
||||
resolveConfiguredSubagentRunTimeoutSeconds,
|
||||
resolveSubagentModelAndThinkingPlan,
|
||||
|
|
@ -98,6 +99,23 @@ describe("subagent spawn model + thinking plan", () => {
|
|||
expect(plan.initialSessionPatch.modelOverrideSource).toBe("auto");
|
||||
});
|
||||
|
||||
it("can resolve only explicit or configured subagent model selections", () => {
|
||||
expect(
|
||||
resolveConfiguredSubagentSpawnModelSelection({
|
||||
cfg: createConfig(),
|
||||
agentId: "research",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
resolveConfiguredSubagentSpawnModelSelection({
|
||||
cfg: createConfig({
|
||||
agents: { defaults: { subagents: { model: "minimax/MiniMax-M2.7" } } },
|
||||
}),
|
||||
agentId: "research",
|
||||
}),
|
||||
).toBe("minimax/MiniMax-M2.7");
|
||||
});
|
||||
|
||||
it("prefers per-agent subagent model over defaults", () => {
|
||||
const cfg = createConfig({
|
||||
agents: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue