feat: expose harness selection decisions (#70760)

Codex harness selection now keeps the decision helper internal, logs debug-only selection reasons and candidates, and documents `/status` as the primary user-facing signal.

Thanks @100yenadmin.

Co-authored-by: Eva <eva@100yen.org>
This commit is contained in:
EVA
2026-04-24 03:57:09 +07:00
committed by GitHub
parent 235e17a08f
commit 137dbc71f0
5 changed files with 227 additions and 17 deletions

View File

@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Agents/subagents: add optional forked context for native `sessions_spawn` runs so agents can let a child inherit the requester transcript when needed, while keeping clean isolated sessions as the default; includes prompt guidance, context-engine hook metadata, docs, and QA coverage.
- Codex harness: add structured debug logging for embedded harness selection decisions so `/status` stays simple while gateway logs explain auto-selection and Pi fallback reasons. (#70760) Thanks @100yenadmin.
- Providers/OpenAI: add forward-compatible `gpt-5.5` and `gpt-5.5-pro` support for OpenAI API keys, OpenAI Codex OAuth, and the Codex CLI default model.
- Providers/OpenAI Codex: add image generation and reference-image editing through Codex OAuth, so `openai-codex/gpt-image-2` works without an `OPENAI_API_KEY`. Fixes #70703.

View File

@@ -47,6 +47,12 @@ OpenClaw now keeps OpenAI GPT model refs canonical as `openai/*`:
Legacy `openai-codex/gpt-*` and `codex/gpt-*` refs remain accepted as
compatibility aliases, but new docs/config examples should use `openai/gpt-*`.
Use `/status` to confirm the effective harness for the current session. If the
selection is surprising, enable debug logging for the `agents/harness` subsystem
and inspect the gateway's structured `agent harness selected` record. It
includes the selected harness id, selection reason, runtime/fallback policy, and,
in `auto` mode, each plugin candidate's support result.
Harness selection is not a live session control. When an embedded turn runs,
OpenClaw records the selected harness id on that session and keeps using it for
later turns in the same session id. Change `embeddedHarness` config or

View File

@@ -106,6 +106,10 @@ Legacy sessions created before harness pins are treated as PI-pinned once they
have transcript history. Use a new/reset session when changing between PI and a
native plugin harness. `/status` shows non-default harness ids such as `codex`
next to `Fast`; PI stays hidden because it is the default compatibility path.
If the selected harness is surprising, enable `agents/harness` debug logging and
inspect the gateway's structured `agent harness selected` record. It includes
the selected harness id, selection reason, runtime/fallback policy, and, in
`auto` mode, each plugin candidate's support result.
The bundled Codex plugin registers `codex` as its harness id. Core treats that
as an ordinary plugin harness id; Codex-specific aliases belong in the plugin

View File

@@ -152,6 +152,80 @@ describe("runAgentHarnessAttemptWithFallback", () => {
});
describe("selectAgentHarness", () => {
it("auto-selects the highest-priority plugin harness without duplicate support probes", () => {
process.env.OPENCLAW_AGENT_RUNTIME = "auto";
const lowPrioritySupports = vi.fn(() => ({
supported: true as const,
priority: 10,
reason: "generic codex support",
}));
const highPrioritySupports = vi.fn(() => ({
supported: true as const,
priority: 100,
reason: "native codex app-server",
}));
const unsupportedSupports = vi.fn(() => ({
supported: false as const,
reason: "provider mismatch",
}));
registerAgentHarness(
{
id: "codex-low",
label: "Low Codex",
supports: lowPrioritySupports,
runAttempt: vi.fn(async () => createAttemptResult("codex-low")),
},
{ ownerPluginId: "codex-low" },
);
registerAgentHarness(
{
id: "codex-high",
label: "High Codex",
supports: highPrioritySupports,
runAttempt: vi.fn(async () => createAttemptResult("codex-high")),
},
{ ownerPluginId: "codex-high" },
);
registerAgentHarness(
{
id: "other",
label: "Other Harness",
supports: unsupportedSupports,
runAttempt: vi.fn(async () => createAttemptResult("other")),
},
{ ownerPluginId: "other" },
);
const harness = selectAgentHarness({
provider: "codex",
modelId: "gpt-5.4",
});
expect(harness.id).toBe("codex-high");
expect(lowPrioritySupports).toHaveBeenCalledTimes(1);
expect(highPrioritySupports).toHaveBeenCalledTimes(1);
expect(unsupportedSupports).toHaveBeenCalledTimes(1);
});
it("keeps pinned PI selection from probing plugin support", () => {
const supports = vi.fn(() => ({ supported: true as const, priority: 100 }));
registerAgentHarness({
id: "codex",
label: "Codex",
supports,
runAttempt: vi.fn(async () => createAttemptResult("codex")),
});
const harness = selectAgentHarness({
provider: "codex",
modelId: "gpt-5.4",
agentHarnessId: "pi",
});
expect(harness.id).toBe("pi");
expect(supports).not.toHaveBeenCalled();
});
it("fails instead of choosing PI when no plugin harness matches and fallback is none", () => {
expect(() =>
selectAgentHarness({

View File

@@ -28,6 +28,31 @@ type AgentHarnessPolicy = {
fallback: EmbeddedAgentHarnessFallback;
};
type AgentHarnessSelectionCandidate = {
id: string;
label: string;
pluginId?: string;
supported?: boolean;
priority?: number;
reason?: string;
};
type AgentHarnessSelectionDecision = {
harness: AgentHarness;
policy: AgentHarnessPolicy;
selectedHarnessId: string;
selectedReason:
| "pinned"
| "forced_pi"
| "forced_plugin"
| "forced_plugin_fallback_to_pi"
// Auto mode chose a registered plugin harness that supports the provider/model.
| "auto_plugin"
// Auto mode found no supporting plugin harness, so PI handled the run.
| "auto_pi_fallback";
candidates: AgentHarnessSelectionCandidate[];
};
function listPluginAgentHarnesses(): AgentHarness[] {
return listRegisteredAgentHarnesses().map((entry) => entry.harness);
}
@@ -51,20 +76,41 @@ export function selectAgentHarness(params: {
sessionKey?: string;
agentHarnessId?: string;
}): AgentHarness {
const policy =
resolvePinnedAgentHarnessPolicy(params.agentHarnessId) ?? resolveAgentHarnessPolicy(params);
return selectAgentHarnessDecision(params).harness;
}
function selectAgentHarnessDecision(params: {
provider: string;
modelId?: string;
config?: OpenClawConfig;
agentId?: string;
sessionKey?: string;
agentHarnessId?: string;
}): AgentHarnessSelectionDecision {
const pinnedPolicy = resolvePinnedAgentHarnessPolicy(params.agentHarnessId);
const policy = pinnedPolicy ?? resolveAgentHarnessPolicy(params);
// PI is intentionally not part of the plugin candidate list. It is the legacy
// fallback path, so `fallback: "none"` can prove that only plugin harnesses run.
const pluginHarnesses = listPluginAgentHarnesses();
const piHarness = createPiAgentHarness();
const runtime = policy.runtime;
if (runtime === "pi") {
return piHarness;
return buildSelectionDecision({
harness: piHarness,
policy,
selectedReason: pinnedPolicy ? "pinned" : "forced_pi",
candidates: listHarnessCandidates(pluginHarnesses),
});
}
if (runtime !== "auto") {
const forced = pluginHarnesses.find((entry) => entry.id === runtime);
if (forced) {
return forced;
return buildSelectionDecision({
harness: forced,
policy,
selectedReason: pinnedPolicy ? "pinned" : "forced_plugin",
candidates: listHarnessCandidates(pluginHarnesses),
});
}
if (policy.fallback === "none") {
throw new Error(
@@ -74,18 +120,23 @@ export function selectAgentHarness(params: {
log.warn("requested agent harness is not registered; falling back to embedded PI backend", {
requestedRuntime: runtime,
});
return piHarness;
return buildSelectionDecision({
harness: piHarness,
policy,
selectedReason: "forced_plugin_fallback_to_pi",
candidates: listHarnessCandidates(pluginHarnesses),
});
}
const supported = pluginHarnesses
.map((harness) => ({
harness,
support: harness.supports({
provider: params.provider,
modelId: params.modelId,
requestedRuntime: runtime,
}),
}))
const candidates = pluginHarnesses.map((harness) => ({
harness,
support: harness.supports({
provider: params.provider,
modelId: params.modelId,
requestedRuntime: runtime,
}),
}));
const supported = candidates
.filter(
(
entry,
@@ -98,20 +149,30 @@ export function selectAgentHarness(params: {
const selected = supported[0]?.harness;
if (selected) {
return selected;
return buildSelectionDecision({
harness: selected,
policy,
selectedReason: "auto_plugin",
candidates: candidates.map(toSelectionCandidate),
});
}
if (policy.fallback === "none") {
throw new Error(
`No registered agent harness supports ${formatProviderModel(params)} and PI fallback is disabled.`,
);
}
return piHarness;
return buildSelectionDecision({
harness: piHarness,
policy,
selectedReason: "auto_pi_fallback",
candidates: candidates.map(toSelectionCandidate),
});
}
export async function runAgentHarnessAttemptWithFallback(
params: EmbeddedRunAttemptParams,
): Promise<EmbeddedRunAttemptResult> {
const harness = selectAgentHarness({
const selection = selectAgentHarnessDecision({
provider: params.provider,
modelId: params.modelId,
config: params.config,
@@ -119,6 +180,13 @@ export async function runAgentHarnessAttemptWithFallback(
sessionKey: params.sessionKey,
agentHarnessId: params.agentHarnessId,
});
const harness = selection.harness;
logAgentHarnessSelection(selection, {
provider: params.provider,
modelId: params.modelId,
sessionKey: params.sessionKey,
agentId: params.agentId,
});
if (harness.id === "pi") {
const result = await harness.runAttempt(params);
return { ...result, agentHarnessId: harness.id };
@@ -138,6 +206,63 @@ export async function runAgentHarnessAttemptWithFallback(
}
}
function listHarnessCandidates(harnesses: AgentHarness[]): AgentHarnessSelectionCandidate[] {
return harnesses.map((harness) => ({
id: harness.id,
label: harness.label,
pluginId: harness.pluginId,
}));
}
function toSelectionCandidate(entry: {
harness: AgentHarness;
support: AgentHarnessSupport;
}): AgentHarnessSelectionCandidate {
return {
id: entry.harness.id,
label: entry.harness.label,
pluginId: entry.harness.pluginId,
supported: entry.support.supported,
priority: entry.support.supported ? entry.support.priority : undefined,
reason: entry.support.reason,
};
}
function buildSelectionDecision(params: {
harness: AgentHarness;
policy: AgentHarnessPolicy;
selectedReason: AgentHarnessSelectionDecision["selectedReason"];
candidates: AgentHarnessSelectionCandidate[];
}): AgentHarnessSelectionDecision {
return {
harness: params.harness,
policy: params.policy,
selectedHarnessId: params.harness.id,
selectedReason: params.selectedReason,
candidates: params.candidates,
};
}
function logAgentHarnessSelection(
selection: AgentHarnessSelectionDecision,
params: { provider: string; modelId?: string; sessionKey?: string; agentId?: string },
) {
if (!log.isEnabled("debug")) {
return;
}
log.debug("agent harness selected", {
provider: params.provider,
modelId: params.modelId,
sessionKey: params.sessionKey,
agentId: params.agentId,
selectedHarnessId: selection.selectedHarnessId,
selectedReason: selection.selectedReason,
runtime: selection.policy.runtime,
fallback: selection.policy.fallback,
candidates: selection.candidates,
});
}
function resolvePinnedAgentHarnessPolicy(
agentHarnessId: string | undefined,
): AgentHarnessPolicy | undefined {