fix(agents): preserve Codex for reasoning capability metadata (#130752)

* fix(agents): preserve Codex for reasoning capability metadata

Keep native-compatible reasoning metadata out of authored transport overrides.
Share effective harness availability across execution, session metadata, and
thinking controls while preserving exact-request fallback and locked ownership.

Fixes #130665.

* fix(agents): keep cold harness registry reads side-effect free
This commit is contained in:
Peter Steinberger
2026-08-27 00:41:18 -07:00
committed by GitHub
parent 36856401d4
commit 625f3cf7ea
19 changed files with 704 additions and 365 deletions
+1 -1
View File
@@ -1908,7 +1908,7 @@ src/agents/harness/lifecycle-hook-helpers.ts 1
src/agents/harness/native-hook-relay-client.ts 2
src/agents/harness/native-hook-relay-codec.ts 2
src/agents/harness/native-hook-relay-state.ts 1
src/agents/harness/selection.ts 6
src/agents/harness/selection.ts 5
src/agents/harness/support.ts 1
src/agents/harness/tool-result-middleware.ts 1
src/agents/identity.ts 8
+26 -11
View File
@@ -143,16 +143,28 @@ this order:
`openclaw` as the compatibility runtime. Use an explicit runtime id when
the run must be strict.
Whole-session and whole-agent runtime pins are ignored: `OPENCLAW_AGENT_RUNTIME`,
session `agentHarnessId`/`agentRuntimeOverride` state, `agents.defaults.agentRuntime`,
and `agents.entries.*.agentRuntime`. Run `openclaw doctor --fix` to remove stale
whole-agent runtime config and convert legacy runtime model refs where intent
can be preserved.
Historical `agentHarnessId` records which runtime produced the transcript; it
does not pin the next turn. Locked native transcripts retain their owner, and
compatible explicit session runtime overrides take precedence over configured
policy. ACP sessions retain their ACP backend. Legacy whole-agent runtime
config and `OPENCLAW_AGENT_RUNTIME` are ignored; use `openclaw doctor --fix`
to remove stale config and repair legacy model refs.
Explicit provider/model plugin runtimes fail closed: `agentRuntime.id: "codex"`
on a provider or model means Codex, or a clear selection/runtime error - it is
never silently routed back to OpenClaw. Only `auto` may route an unmatched
turn to OpenClaw.
Explicit provider/model plugin runtimes fail closed when the harness is missing
or cannot support the route or authentication. There is one selection-time
exception: a harness may declare that OpenClaw can reproduce the exact request.
Codex uses this fallback for authored request overrides such as headers, request
parameters, timeouts, or payload compatibility switches. It preserves those
settings instead of silently dropping them. Once a harness starts executing,
its failures are not replayed through another runtime.
Affirmative `compat.supportsReasoningEffort: true` and a nonempty
`compat.supportedReasoningEfforts` list containing only `minimal`, `low`,
`medium`, `high`, `xhigh`, `max`, or `ultra` describe native reasoning
capabilities; they do not opt an otherwise compatible route out of Codex.
Disabling reasoning, custom effort labels, and other compatibility switches
remain request behavior. Model-level runtime controls such as `fastMode` and
`thinking` also preserve native selection when their values are valid.
CLI backend aliases differ from embedded harness ids. Preferred Claude CLI form:
@@ -187,7 +199,7 @@ harness. Explicit OpenClaw runtime config remains an opt-in compatibility
route for `openai/*` agent turns; when paired with a selected `openai` OAuth
profile, OpenClaw routes that path internally through the Codex-auth
transport while keeping the public model ref as `openai/*`. Stale OpenAI
runtime session pins are ignored by runtime selection and can be cleaned with
historical producer fields do not pin the next turn and can be cleaned with
`openclaw doctor --fix`.
If `openclaw doctor` warns that the `codex` plugin is enabled while legacy
@@ -251,7 +263,10 @@ diagnostics, not provider names:
- A channel label such as Telegram or Discord is where the conversation is happening.
If a run shows an unexpected runtime, inspect the selected provider/model
runtime policy first. Legacy session runtime pins no longer decide routing.
runtime policy first. Next-turn runtime metadata includes declared fallback
when the registered harness can determine it from the configured route. It does
not probe credentials or start a runtime; final route/auth preparation can still
reject the turn. The completed result records the runtime that actually ran.
## Related
+23 -15
View File
@@ -4,7 +4,7 @@ title: "Codex harness"
read_when:
- You want to use the official Codex app-server harness
- You need Codex harness config examples
- You want Codex-only deployments to fail instead of falling back to OpenClaw
- You need explicit Codex runtime policy and fallback rules
---
The official `codex` plugin runs embedded OpenAI agent turns through Codex
@@ -735,13 +735,13 @@ Keep provider refs and runtime policy separate:
| Send Codex feedback only | `/codex diagnostics [note]` |
| Start an ACP/acpx task | ACP/acpx session commands, not `/codex` |
| Use case | Configure | Verify | Notes |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------- |
| Eligible OpenAI route with native Codex runtime | Exact official HTTPS Responses/ChatGPT route with no authored provider request override, plus enabled `codex` plugin | `/status` shows `Runtime: OpenAI Codex` | Valid Fast runtime controls do not disqualify this path |
| Fail closed if Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | Turn fails instead of embedded fallback | Use for Codex-only deployments |
| Direct OpenAI API-key traffic through OpenClaw | Provider or model `agentRuntime.id: "openclaw"` and normal OpenAI auth | `/status` shows OpenClaw runtime | Use only when OpenClaw is intentional |
| Legacy config | legacy Codex GPT refs | `openclaw doctor --fix` rewrites it | Do not write new config this way |
| ACP/acpx Codex adapter | ACP `sessions_spawn({ runtime: "acp" })` | ACP task/session status | Separate from native Codex harness |
| Use case | Configure | Verify | Notes |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------- |
| Eligible OpenAI route with native Codex runtime | Exact official HTTPS Responses/ChatGPT route with no authored provider request override, plus enabled `codex` plugin | `/status` shows `Runtime: OpenAI Codex` | Valid Fast runtime controls do not disqualify this path |
| Fail closed if Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | Missing harness fails the turn | Authored request overrides may still use declared fallback |
| Direct OpenAI API-key traffic through OpenClaw | Provider or model `agentRuntime.id: "openclaw"` and normal OpenAI auth | `/status` shows OpenClaw runtime | Use only when OpenClaw is intentional |
| Legacy config | legacy Codex GPT refs | `openclaw doctor --fix` rewrites it | Do not write new config this way |
| ACP/acpx Codex adapter | ACP `sessions_spawn({ runtime: "acp" })` | ACP task/session status | Separate from native Codex harness |
`agents.defaults.imageModel` follows the same prefix split. Use `openai/gpt-*`
for the normal OpenAI route and `codex/gpt-*` only when image understanding
@@ -837,9 +837,12 @@ fail-closed rule:
}
```
With Codex forced, OpenClaw fails early if the effective route is not declared
Codex-compatible, the plugin is disabled, the app-server is too old, or the
app-server cannot start.
With Codex forced, OpenClaw fails early if the plugin is disabled, the app-server
is too old or cannot start, or route/auth support is rejected without a declared
fallback. Authored request overrides may instead use the
[selection-time OpenClaw fallback](/concepts/agent-runtimes#runtime-selection)
that preserves the exact request. Once Codex starts, its failures are not replayed
through OpenClaw.
## App-server policy
@@ -1501,10 +1504,15 @@ configs. Select an `openai/gpt-*` model, enable
**OpenClaw uses the built-in harness instead of Codex:** confirm the effective
route is an exact official HTTPS Platform Responses or ChatGPT Responses route,
has no authored provider request override, and that the Codex plugin is installed and
enabled. The `openai/gpt-*` prefix alone is not enough. For strict proof while
testing, set provider or model `agentRuntime.id: "codex"`; forced Codex fails
instead of falling back when the route or harness is incompatible.
has no authored provider request override, and that the Codex plugin is installed
and enabled. Affirmative reasoning support and native reasoning-effort metadata
do not count as request overrides. Headers, request parameters, timeouts, and
payload compatibility switches still do: Codex declares an OpenClaw fallback
that preserves the exact request, including for explicit runtime selections.
Other unsupported routes/authentication and missing explicit harnesses fail
closed. The `openai/gpt-*` prefix and `agentRuntime.id: "codex"` alone are not
execution proof; inspect the actual harness in the completed result. See
[Runtime selection](/concepts/agent-runtimes#runtime-selection).
**OpenAI Codex runtime falls back to the API-key path:** collect a redacted
gateway excerpt that shows the model, runtime, selected provider, and
+14 -12
View File
@@ -280,11 +280,13 @@ error. Do not use harness scope after a request or tool action may have produced
side effects.
Configured runtime policy remains authoritative about the desired runtime. A
persisted session `agentHarnessId` keeps ownership of its native transcript
locked session `agentHarnessId` keeps ownership of its native transcript
while route/auth preparation is still pending. Neither makes an incompatible
route compatible: once prepared facts exist, the selected or pinned harness
must support them or the run fails closed. `/status` shows the effective runtime
selected from policy, persisted ownership, and route support.
must support them, declare the exact-request OpenClaw fallback, or the run fails
closed. Next-turn metadata uses the same registered support decision and retains
its model/provider/session source. An unlocked historical producer does not pin
the next turn. Projection never loads a harness or reads credentials.
Prepared status is explicit: missing `runtimePolicy` stays undeclared instead
of being inferred from whichever transport fields happen to be present.
When harness-owned auth leaves multiple physical routes unresolved, the
@@ -492,10 +494,10 @@ model refs remain compatibility aliases for the native harness.
When this mode runs, Codex owns the native thread id, resume behavior,
compaction, and app-server execution. OpenClaw still owns the chat channel,
visible transcript mirror, tool policy, approvals, media delivery, and session
selection. Use provider/model `agentRuntime.id: "codex"` when you need to
prove that only the Codex app-server path can claim the run. Explicit plugin
runtimes fail closed; Codex app-server selection failures and runtime failures
are not retried through another runtime.
selection. Use provider/model `agentRuntime.id: "codex"` to require a registered
Codex harness. Unsupported routes/auth fail closed unless the harness declares
an exact-request fallback before execution. Codex runtime failures are not
retried through another runtime.
## Runtime strictness
@@ -509,7 +511,7 @@ incompatible route compatible. Selected plugin harness failures always fail
hard. This does not block an explicit provider/model
`agentRuntime.id: "openclaw"`.
For Codex-only embedded runs:
To request Codex for embedded runs:
```json
{
@@ -585,10 +587,10 @@ Legacy whole-agent runtime examples like this are ignored:
```
With an explicit plugin runtime, a session fails early when the requested
harness is not registered, does not support the resolved provider/model, or
fails before producing turn side effects. That is intentional for Codex-only
deployments and for live tests that must prove the Codex app-server path is
actually in use.
harness is not registered or rejects the resolved provider/model without a
declared fallback. An authored transport override may select OpenClaw through
that fallback even with an explicit runtime. To prove native execution, inspect
the actual harness in the completed result; configured intent alone is not proof.
This setting only controls the embedded agent harness. It does not disable
image, video, music, TTS, PDF, or other provider-specific model routing.
+14 -12
View File
@@ -80,16 +80,17 @@ endpoint and adapter:
| Explicit exact official endpoint using HTTP | Rejected |
| Route with an authored provider/model request override | OpenClaw |
Valid model-scoped `params.fastMode` / `params.fast_mode` values and valid
cutoff keys are typed agent-runtime controls, not authored provider request
params. They do not disqualify implicit Codex selection or select a runtime by
themselves. Pin `agentRuntime.id: "openclaw"` or `agentRuntime.id: "codex"`
when a recipe depends on one runtime.
Valid model-scoped `params.fastMode` / `params.fast_mode`, cutoff, and `thinking`
values are typed agent-runtime controls, not authored provider request params.
Affirmative reasoning support and native reasoning-effort metadata also preserve
Codex selection. See [Runtime selection](/concepts/agent-runtimes#runtime-selection)
for the supported capability values and the request overrides that remain protected.
An explicit non-default provider/model `agentRuntime.id` remains authoritative.
For example, `agentRuntime.id: "openclaw"` keeps an otherwise Codex-eligible
route on OpenClaw, while `agentRuntime.id: "codex"` requires Codex and fails
closed when the effective route is not declared Codex-compatible.
An explicit `agentRuntime.id: "openclaw"` keeps a Codex-eligible route on
OpenClaw. Explicit `agentRuntime.id: "codex"` requires a registered Codex harness;
unsupported routes/auth fail closed, except that authored request overrides may
use Codex's declared exact-request OpenClaw fallback before execution. Inspect
the completed result's actual harness when a recipe depends on native execution.
Runtime selection does not change credential type or billing: Platform API-key
auth and ChatGPT/Codex subscription auth remain distinct.
@@ -1307,9 +1308,10 @@ request settings, so an otherwise eligible `auto` route stays on OpenClaw
instead of selecting Codex implicitly. Valid `fastMode` / `fast_mode` values
and valid cutoff keys are typed agent-runtime controls and do not select a
runtime. Runtime-specific examples therefore pin `agentRuntime.id` explicitly.
The native Codex app-server harness owns its own transport and request settings;
explicit `agentRuntime.id: "codex"` fails closed when the effective route is
not declared Codex-compatible.
The native Codex app-server harness owns its transport and request settings.
Authored embedded-provider settings can therefore select the declared OpenClaw
fallback even with explicit `agentRuntime.id: "codex"`; see
[Runtime selection](/concepts/agent-runtimes#runtime-selection).
<AccordionGroup>
<Accordion title="Transport (WebSocket vs SSE)">
+13 -5
View File
@@ -2,7 +2,7 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { applyAcpRuntimeOverlay, type AgentRuntimeMetadata } from "./acp-runtime-overlay.js";
import { isDefaultAgentRuntimeId } from "./agent-runtime-id.js";
import { resolveAgentHarnessPolicy } from "./harness/policy.js";
import { resolveAvailableAgentHarnessPolicy } from "./harness/availability.js";
import { resolveDefaultModelForAgent } from "./model-selection.js";
import {
resolvePersistedSessionRuntimeId,
@@ -20,6 +20,7 @@ type ModelAgentRuntimeMetadataParams = {
acpRuntime?: boolean;
/** Persisted ACP backend id, falling back to acpx when absent. */
acpBackend?: string;
agentHarnessRuntimeOverride?: string;
};
/** Resolves the runtime id/source that should be reported for a model-backed agent session. */
@@ -39,12 +40,14 @@ export function resolveModelAgentRuntimeMetadata(
params.provider && params.model
? { provider: params.provider, model: params.model }
: resolveDefaultModelForAgent({ cfg: params.cfg, agentId: params.agentId });
const policy = resolveAgentHarnessPolicy({
const policy = resolveAvailableAgentHarnessPolicy({
mode: "projection",
provider: resolved.provider,
modelId: resolved.model,
config: params.cfg,
agentId: params.agentId,
sessionKey: params.sessionKey,
agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride,
});
const meta: AgentRuntimeMetadata = {
id: policy.runtime,
@@ -58,17 +61,22 @@ export function resolveCurrentSessionAgentRuntimeMetadata(
params: ModelAgentRuntimeMetadataParams,
): AgentRuntimeMetadata {
const { sessionEntry, ...configuredParams } = params;
const configuredRuntime = resolveModelAgentRuntimeMetadata(configuredParams);
const sessionRuntime = resolveSessionRuntimeOverrideForProvider({
provider: params.provider,
entry: sessionEntry,
cfg: params.cfg,
});
if (params.acpRuntime || !sessionRuntime) {
return configuredRuntime;
return resolveModelAgentRuntimeMetadata(configuredParams);
}
const runtime = resolveModelAgentRuntimeMetadata({
...configuredParams,
...(sessionEntry?.modelSelectionLocked === true
? { sessionEntry }
: { agentHarnessRuntimeOverride: sessionRuntime }),
});
return {
id: sessionRuntime,
id: runtime.id,
source: sessionEntry?.modelSelectionLocked === true ? "session" : "session-key",
};
}
+88
View File
@@ -0,0 +1,88 @@
/** Lightweight runtime availability shared by execution and next-turn projections. */
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import { resolveAgentHarnessPolicy, type AgentHarnessPolicy } from "./policy.js";
import { getRegisteredAgentHarness } from "./registry.js";
import { buildAgentHarnessSupportContext } from "./support.js";
import type { AgentHarnessSupport, AgentHarnessSupportContext } from "./types.js";
type AgentHarnessAvailabilityParams = Parameters<typeof resolveAgentHarnessPolicy>[0] & {
modelProvider?: AgentHarnessSupportContext["modelProvider"];
/** Projection can inspect loaded support, but absence before registration is not a fallback. */
mode?: "execution" | "projection";
agentHarnessId?: string;
agentHarnessRuntimeOverride?: string;
preparedModelProvider?: boolean;
/** Execution supplies ownership lazily; read-only projections never discover plugins. */
resolveProviderOwnership?: () => Parameters<
typeof buildAgentHarnessSupportContext
>[0]["providerOwnership"];
};
type AgentHarnessAvailabilityDecision = {
kind: "available" | "implicit-unavailable" | "implicit-unsupported" | "declared-fallback";
policy: AgentHarnessPolicy;
/** Reuse the exact support decision during execution, without another probe. */
support?: AgentHarnessSupport;
};
export function resolveAvailableAgentHarnessPolicy(
params: AgentHarnessAvailabilityParams,
): AgentHarnessPolicy {
return resolveAgentHarnessAvailabilityDecision(params).policy;
}
export function resolveAgentHarnessAvailabilityDecision(
params: AgentHarnessAvailabilityParams,
): AgentHarnessAvailabilityDecision {
const configured = resolveAgentHarnessPolicy({
...params,
modelApi: params.modelProvider?.api ?? params.modelApi,
modelBaseUrl: params.modelProvider?.baseUrl ?? params.modelBaseUrl,
requestTransportOverrides:
params.modelProvider?.requestTransportOverrides ?? params.requestTransportOverrides,
});
const pinnedHarnessId = normalizeOptionalAgentRuntimeId(params.agentHarnessId);
const runtimeOverride =
pinnedHarnessId ?? normalizeOptionalAgentRuntimeId(params.agentHarnessRuntimeOverride);
const policy: AgentHarnessPolicy =
runtimeOverride && !isDefaultAgentRuntimeId(runtimeOverride)
? { ...configured, runtime: runtimeOverride, runtimeSource: "model" }
: configured;
const implicit = policy.runtime === "codex" && policy.runtimeSource === "implicit";
if (policy.runtime === "auto" || policy.runtime === "openclaw") {
return { kind: "available", policy };
}
const registered = getRegisteredAgentHarness(policy.runtime);
if (!registered) {
return implicit && params.mode !== "projection"
? { kind: "implicit-unavailable", policy: { ...policy, runtime: "openclaw" } }
: { kind: "available", policy };
}
// A pinned native transcript owns early selection. Final prepared routes must
// revalidate that owner; historical producer metadata is not a next-turn pin.
if (pinnedHarnessId === policy.runtime && !params.preparedModelProvider) {
return { kind: "available", policy };
}
const provider = params.provider?.trim() ?? "";
if (params.provider === undefined) {
return { kind: "available", policy };
}
const support = registered.harness.supports(
buildAgentHarnessSupportContext({
...params,
provider,
requestedRuntime: policy.runtime,
providerOwnership: params.resolveProviderOwnership?.(),
}),
);
if (!support.supported && !policy.forcedByEnvironment) {
if (implicit || support.fallbackRuntime === "openclaw") {
return {
kind: implicit ? "implicit-unsupported" : "declared-fallback",
policy: { ...policy, runtime: "openclaw" },
support,
};
}
}
return { kind: "available", policy, support };
}
+28
View File
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js";
import { withPluginRegistrationContext } from "../../plugins/runtime.js";
import { withPluginRuntimeRegistryScope } from "../../plugins/runtime/gateway-request-scope.js";
import {
clearAgentHarnesses,
disposeRegisteredAgentHarnesses,
@@ -161,6 +162,33 @@ describe("agent harness registry", () => {
expect(building.agentHarnesses[1]?.pluginId).toBe("builder-plugin");
});
it("keeps harness reads in registration, request, then active registry order", () => {
registerAgentHarness(makeHarness("shared"), { ownerPluginId: "active-plugin" });
const request = createEmptyPluginRegistry();
const building = createEmptyPluginRegistry();
const expectOwner = (ownerPluginId: string) => {
expect(getRegisteredAgentHarness("shared")?.ownerPluginId).toBe(ownerPluginId);
expect(listRegisteredAgentHarnesses().map((entry) => entry.ownerPluginId)).toEqual([
ownerPluginId,
]);
};
withPluginRuntimeRegistryScope(request, () => {
expect(getRegisteredAgentHarness("shared")).toBeUndefined();
expect(listRegisteredAgentHarnesses()).toEqual([]);
registerAgentHarness(makeHarness("shared"), { ownerPluginId: "request-plugin" });
expectOwner("request-plugin");
withPluginRegistrationContext(building, "builder-plugin", () => {
expect(getRegisteredAgentHarness("shared")).toBeUndefined();
expect(listRegisteredAgentHarnesses()).toEqual([]);
registerAgentHarness(makeHarness("shared"));
expectOwner("builder-plugin");
});
expectOwner("request-plugin");
});
expectOwner("active-plugin");
});
it("dispatches generic session reset to registered harnesses", async () => {
const resets: unknown[] = [];
registerAgentHarness({
+3 -2
View File
@@ -4,6 +4,7 @@
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
assertDirectPluginRegistrationReplacement,
getPluginRegistryForContext,
requireActivePluginRegistry,
resolveDirectPluginRegistrationOwner,
} from "../../plugins/runtime.js";
@@ -19,7 +20,7 @@ const log = createSubsystemLogger("agents/harness");
const CODEX_NATIVE_COMPACTION_OWNER_ID = "codex";
function getAgentHarnesses() {
return requireActivePluginRegistry().agentHarnesses;
return getPluginRegistryForContext()?.agentHarnesses ?? [];
}
/** Registers or replaces an agent harness under its trimmed id. */
@@ -28,7 +29,7 @@ export function registerAgentHarness(
options?: AgentHarnessRegistrationOptions & { ownerPluginId?: string },
): void {
const id = harness.id.trim();
const harnesses = getAgentHarnesses();
const harnesses = requireActivePluginRegistry().agentHarnesses;
const pluginId = resolveDirectPluginRegistrationOwner(options?.ownerPluginId) ?? "core";
if (id === "openclaw") {
throw new Error('agent harness id "openclaw" is reserved for the built-in runtime');
+178 -68
View File
@@ -1643,6 +1643,30 @@ describe("runAgentHarnessAttempt", () => {
});
describe("selectAgentHarness", () => {
it("rejects a harness replaced during its support probe", () => {
const replacement: AgentHarness = {
id: "codex",
label: "Replacement",
supports: () => ({ supported: true }),
runAttempt: async () => createAttemptResult("replacement"),
};
registerAgentHarness({
...replacement,
supports: () => {
registerAgentHarness(replacement);
return { supported: true };
},
});
expect(() =>
selectAgentHarness({
provider: "openai",
modelId: "gpt-5.6-sol",
agentHarnessRuntimeOverride: "codex",
}),
).toThrow("changed during owner resolution");
});
it("does not select Codex from a non-OpenAI model name", () => {
registerSuccessfulCodexHarness();
@@ -2140,82 +2164,168 @@ describe("selectAgentHarness", () => {
},
);
it("keeps native model run controls compatible with Codex", () => {
expect(
buildAgentHarnessSupportContext({
provider: "openai",
modelId: "gpt-5.6-sol",
modelProvider: {
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
requestTransportOverrides: "none",
},
requestedRuntime: "codex",
config: {
agents: {
defaults: {
models: {
"openai/gpt-5.6-sol": {
params: { thinking: "xhigh", fastMode: true, fastAutoOnSeconds: 30 },
it.each([
["Platform", "openai-responses", "https://api.openai.com/v1"],
["ChatGPT", "openai-chatgpt-responses", "https://chatgpt.com/backend-api/codex"],
] as const)(
"keeps authored reasoning metadata and native controls on %s Codex",
(_label, api, baseUrl) => {
const config: OpenClawConfig = {
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [
{
id: "gpt-5.6-sol",
name: "Sol",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
maxTokens: 8192,
api,
baseUrl,
compat: {
supportsReasoningEffort: true,
supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"],
},
},
},
],
},
},
} as OpenClawConfig,
}).modelProvider,
).toMatchObject({
requestTransportOverrides: "none",
runtimePolicy: { compatibleIds: ["openclaw", "codex"] },
});
});
it("uses a harness-declared OpenClaw fallback for explicit request params", () => {
const supports = vi.fn((ctx: Parameters<AgentHarness["supports"]>[0]) =>
ctx.modelProvider?.requestTransportOverrides === "present"
? {
supported: false as const,
reason: "authored request params are unsupported",
fallbackRuntime: "openclaw" as const,
}
: { supported: true as const },
);
registerAgentHarness({
id: "codex",
label: "Codex",
supports,
runAttempt: vi.fn(async () => createAttemptResult("codex")),
});
expect(
selectAgentHarness({
provider: "openai",
modelId: "gpt-5.6-sol",
modelProvider: {
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
requestTransportOverrides: "none",
runtimePolicy: { compatibleIds: ["openclaw", "codex"] },
},
config: {
agents: {
defaults: {
models: {
"openai/gpt-5.6-sol": {
params: { responsesServerCompaction: true },
agentRuntime: { id: "codex" },
},
agents: {
defaults: {
models: {
"openai/gpt-5.6-sol": {
params: { thinking: "xhigh", fastMode: true, fastAutoOnSeconds: 30 },
},
},
},
},
}).id,
).toBe("openclaw");
expect(supports).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: expect.objectContaining({ requestTransportOverrides: "present" }),
}),
);
});
};
registerAgentHarness({
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.modelProvider?.requestTransportOverrides === "present"
? { supported: false, fallbackRuntime: "openclaw" }
: { supported: true },
runAttempt: async () => createAttemptResult("codex"),
});
for (const runtime of [undefined, "codex", "openclaw"]) {
expect(
selectAgentHarness({
provider: "openai",
modelId: "gpt-5.6-sol",
config,
agentHarnessRuntimeOverride: runtime,
}).id,
).toBe(runtime ?? "codex");
}
expect(
buildAgentHarnessSupportContext({
provider: "openai",
modelId: "gpt-5.6-sol",
requestedRuntime: "codex",
config,
}).modelProvider,
).toMatchObject({
requestTransportOverrides: "none",
runtimePolicy: { compatibleIds: ["openclaw", "codex"] },
});
},
);
it.each([
["model request params", {}, {}, { responsesServerCompaction: true }],
["provider headers", { headers: { "x-route": "required" } }, {}, {}],
["provider params", { params: { store: false } }, {}, {}],
["provider timeout", { timeoutSeconds: 90 }, {}, {}],
["model headers", {}, { headers: { "x-route": "required" } }, {}],
["model params", {}, { params: { store: false } }, {}],
["store compatibility", {}, { compat: { supportsStore: false } }, {}],
[
"mixed compatibility",
{},
{
compat: {
supportsReasoningEffort: true,
supportedReasoningEfforts: ["high"],
supportsStore: false,
},
},
{},
],
])(
"uses a harness-declared fallback preserving %s",
(_label, providerPatch, modelPatch, params) => {
const supports = vi.fn((ctx: Parameters<AgentHarness["supports"]>[0]) =>
ctx.modelProvider?.requestTransportOverrides === "present"
? {
supported: false as const,
reason: "authored request params are unsupported",
fallbackRuntime: "openclaw" as const,
}
: { supported: true as const },
);
registerAgentHarness({
id: "codex",
label: "Codex",
supports,
runAttempt: vi.fn(async () => createAttemptResult("codex")),
});
expect(
selectAgentHarness({
provider: "openai",
modelId: "gpt-5.6-sol",
modelProvider: {
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
requestTransportOverrides: "none",
runtimePolicy: { compatibleIds: ["openclaw", "codex"] },
},
config: {
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
...providerPatch,
models: [
{
id: "gpt-5.6-sol",
name: "Sol",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
maxTokens: 8192,
...modelPatch,
},
],
},
},
},
agents: {
defaults: {
models: {
"openai/gpt-5.6-sol": {
params,
agentRuntime: { id: "codex" },
},
},
},
},
},
}).id,
).toBe("openclaw");
expect(supports).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: expect.objectContaining({ requestTransportOverrides: "present" }),
}),
);
},
);
it("keeps a private-QA forced runtime despite a plugin-declared fallback", () => {
vi.stubEnv("OPENCLAW_BUILD_PRIVATE_QA", "1");
+26 -164
View File
@@ -12,7 +12,6 @@ import {
import { formatErrorMessage } from "../../infra/errors.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { resolveProviderRefOwnership } from "../../plugins/providers.js";
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import { resolveGroupToolPolicy } from "../agent-tools.policy.js";
import {
isHostScopedAgentToolActive,
@@ -39,6 +38,7 @@ import {
} from "../tool-policy.js";
import type { SystemAgentToolOptions } from "../tools/system-agent-tool.js";
import { resolveAgentHarnessAutoSelectionHint } from "./auto-selection.js";
import { resolveAgentHarnessAvailabilityDecision } from "./availability.js";
import { createOpenClawAgentHarness, isBuiltInOpenClawAgentHarness } from "./builtin-openclaw.js";
import { selectContextEngineForTranscriptHost } from "./context-engine-logical-turn.js";
import { drainPendingContextEngineTurnsBeforeRun } from "./context-engine-turn-attempt.js";
@@ -48,15 +48,8 @@ import {
runAgentHarnessLifecycleAttempt,
runAgentHarnessLifecycleFinalization,
} from "./lifecycle.js";
import {
resolveAgentHarnessPolicy as resolveConfiguredAgentHarnessPolicy,
type AgentHarnessPolicy,
} from "./policy.js";
import {
getRegisteredAgentHarness,
listRegisteredAgentHarnesses,
resolveAgentHarnessOwnerPluginId,
} from "./registry.js";
import type { AgentHarnessPolicy } from "./policy.js";
import { listRegisteredAgentHarnesses, resolveAgentHarnessOwnerPluginId } from "./registry.js";
import {
buildAgentHarnessSupportContext,
compareHarnessSupport,
@@ -67,17 +60,7 @@ import type { AgentHarness, AgentHarnessSupport, AgentHarnessSupportContext } fr
const log = createSubsystemLogger("agents/harness");
export { resolveAgentHarnessPolicy } from "./policy.js";
type AgentHarnessAvailabilityParams = {
provider?: string;
modelId?: string;
modelProvider?: AgentHarnessSupportContext["modelProvider"];
config?: OpenClawConfig;
agentId?: string;
sessionKey?: string;
env?: NodeJS.ProcessEnv;
preparedModelProvider?: boolean;
};
export { resolveAvailableAgentHarnessPolicy } from "./availability.js";
type AgentHarnessSelectionParams = {
provider: string;
@@ -99,11 +82,6 @@ export type AgentHarnessPreparedModelProvider = NonNullable<
AgentHarnessSupportContext["modelProvider"]
>;
type AgentHarnessAvailabilityDecision =
| { kind: "available"; policy: AgentHarnessPolicy }
| { kind: "implicit-unavailable"; policy: AgentHarnessPolicy }
| { kind: "implicit-unsupported"; policy: AgentHarnessPolicy };
const PLUGIN_HARNESS_SENDER_DENY_ALL_PROMPT =
"Tool and file actions are disabled for this sender by chat policy. If asked to edit files or use tools, say this sender is not allowed by policy; do not imply retrying will help.";
const PLUGIN_HARNESS_GROUP_DENY_ALL_PROMPT =
@@ -191,56 +169,6 @@ function listPluginAgentHarnesses(): AgentHarness[] {
return listRegisteredAgentHarnesses().map((entry) => entry.harness);
}
export function resolveAvailableAgentHarnessPolicy(
params: AgentHarnessAvailabilityParams,
): AgentHarnessPolicy {
return resolveAgentHarnessAvailabilityDecision(params).policy;
}
function resolveAgentHarnessAvailabilityDecision(
params: AgentHarnessAvailabilityParams,
): AgentHarnessAvailabilityDecision {
const policy = resolveConfiguredAgentHarnessPolicy({
...params,
modelApi: params.modelProvider?.api,
modelBaseUrl: params.modelProvider?.baseUrl,
requestTransportOverrides: params.modelProvider?.requestTransportOverrides,
});
if (policy.runtime !== "codex" || policy.runtimeSource !== "implicit") {
return { kind: "available", policy };
}
const codexHarness = getRegisteredAgentHarness("codex");
if (!codexHarness) {
return {
kind: "implicit-unavailable",
policy: { ...policy, runtime: "openclaw" },
};
}
const provider = params.provider?.trim();
if (!provider) {
return { kind: "available", policy };
}
const support = codexHarness.harness.supports(
buildAgentHarnessSupportContext({
provider,
modelId: params.modelId,
modelProvider: params.modelProvider,
requestedRuntime: policy.runtime,
config: params.config,
agentId: params.agentId,
sessionKey: params.sessionKey,
preparedModelProvider: params.preparedModelProvider,
}),
);
if (support.supported) {
return { kind: "available", policy };
}
return {
kind: "implicit-unsupported",
policy: { ...policy, runtime: "openclaw" },
};
}
export function selectAgentHarness(params: AgentHarnessSelectionParams): AgentHarness {
return selectAgentHarnessDecision(params).harness;
}
@@ -290,47 +218,30 @@ export function agentHarnessExposesOpenClawTools(harnessId: string): boolean {
function selectAgentHarnessDecision(
params: AgentHarnessSelectionDecisionParams,
): AgentHarnessSelectionDecision {
const pinnedHarnessId = normalizeOptionalAgentRuntimeId(params.agentHarnessId);
const runtimeOverride = normalizeOptionalAgentRuntimeId(params.agentHarnessRuntimeOverride);
const requestedRuntimeOverride = pinnedHarnessId ?? runtimeOverride;
const selectedRuntimeOverride =
requestedRuntimeOverride && !isDefaultAgentRuntimeId(requestedRuntimeOverride)
? requestedRuntimeOverride
: undefined;
// Persisted ownership and explicit model policy are already authoritative.
// Avoid probing implicit harness support before those overrides are applied.
const availability: AgentHarnessAvailabilityDecision = selectedRuntimeOverride
? {
kind: "available",
policy: resolveConfiguredAgentHarnessPolicy({
...params,
modelApi: params.modelProvider?.api,
modelBaseUrl: params.modelProvider?.baseUrl,
requestTransportOverrides: params.modelProvider?.requestTransportOverrides,
}),
}
: resolveAgentHarnessAvailabilityDecision(params);
const resolvedPolicy = availability.policy;
const policy = selectedRuntimeOverride
? ({
...resolvedPolicy,
runtime: selectedRuntimeOverride,
runtimeSource: "model",
} as AgentHarnessPolicy)
: resolvedPolicy;
// Keep the probed instance: owner validation must reject replacement during supports().
const pluginHarnesses = listPluginAgentHarnesses();
const availability = resolveAgentHarnessAvailabilityDecision({
...params,
resolveProviderOwnership: () =>
resolveProviderRefOwnership({
provider: params.provider,
config: params.config,
}),
});
const policy = availability.policy;
// OpenClaw's built-in harness is intentionally not part of the plugin candidate list. Explicit plugin
// runtimes fail closed unless the selected plugin declares OpenClaw as a lossless fallback.
const pluginHarnesses = listPluginAgentHarnesses();
const openClawHarness = createOpenClawAgentHarness();
const runtime = policy.runtime;
if (runtime === "openclaw") {
const selectedReason = selectedRuntimeOverride
? "forced_openclaw"
: availability.kind === "implicit-unavailable"
const selectedReason =
availability.kind === "implicit-unavailable"
? "implicit_plugin_unavailable_openclaw"
: availability.kind === "implicit-unsupported"
? "implicit_plugin_unsupported_openclaw"
: "forced_openclaw";
: availability.kind === "declared-fallback"
? "plugin_declared_fallback_openclaw"
: "forced_openclaw";
return buildSelectionDecision({
harness: openClawHarness,
policy,
@@ -341,55 +252,17 @@ function selectAgentHarnessDecision(
if (runtime !== "auto") {
const forced = pluginHarnesses.find((entry) => entry.id === runtime);
if (forced) {
// A persisted harness owns the native transcript before route/auth preparation. The
// finalized entrypoint sets preparedModelProvider and must always revalidate that owner.
if (pinnedHarnessId === runtime && !params.preparedModelProvider) {
return buildSelectionDecision({
harness: forced,
policy,
selectedReason: "forced_plugin",
candidates: listHarnessCandidates(pluginHarnesses),
});
}
const supportContext = buildAgentHarnessSupportContext({
provider: params.provider,
modelId: params.modelId,
modelProvider: params.modelProvider,
requestedRuntime: runtime,
config: params.config,
agentId: params.agentId,
sessionKey: params.sessionKey,
preparedModelProvider: params.preparedModelProvider,
providerOwnership: resolveProviderRefOwnership({
provider: params.provider,
config: params.config,
}),
});
const support = forced.supports(supportContext);
if (support.supported) {
return buildSelectionDecision({
harness: forced,
policy,
selectedReason: "forced_plugin",
candidates: listHarnessCandidates(pluginHarnesses),
});
}
if (support.fallbackRuntime === "openclaw") {
if (policy.forcedByEnvironment) {
const support = availability.support;
if (!support || support.supported || support.fallbackRuntime === "openclaw") {
if (support && !support.supported) {
log.info(
`agent harness selected requested=${runtime} selected=${forced.id} reason=private_qa_forced_runtime`,
);
return buildSelectionDecision({
harness: forced,
policy,
selectedReason: "forced_plugin",
candidates: listHarnessCandidates(pluginHarnesses),
});
}
return buildSelectionDecision({
harness: openClawHarness,
policy: { ...policy, runtime: "openclaw" },
selectedReason: "plugin_declared_fallback_openclaw",
harness: forced,
policy,
selectedReason: "forced_plugin",
candidates: listHarnessCandidates(pluginHarnesses),
});
}
@@ -410,17 +283,6 @@ function selectAgentHarnessDecision(
}.`,
);
}
if (runtime === "codex" && policy.runtimeSource === "implicit") {
return buildSelectionDecision({
harness: openClawHarness,
policy: {
...policy,
runtime: "openclaw",
},
selectedReason: "implicit_plugin_unavailable_openclaw",
candidates: listHarnessCandidates(pluginHarnesses),
});
}
if (
isCliRuntimeAliasForProvider({
runtime,
+23 -11
View File
@@ -148,17 +148,29 @@ describe("resolveEffectiveAgentRuntime", () => {
expect(supports).not.toHaveBeenCalled();
});
it("prefers explicit session overrides", () => {
const cfg = openAIConfig("openclaw");
expect(
resolveEffectiveAgentRuntime({
cfg,
provider: "openai",
modelId: "gpt-5.6-luna",
sessionEntry: { agentRuntimeOverride: "codex", agentHarnessId: "openclaw" },
}),
).toBe("codex");
});
it.each([false, true])(
"projects explicit session overrides with declared fallback=%s",
(fallback) => {
registerAgentHarness({
id: "codex",
label: "Codex",
supports: () =>
fallback ? { supported: false, fallbackRuntime: "openclaw" } : { supported: true },
runAttempt: async () => {
throw new Error("projection must not execute");
},
});
const cfg = openAIConfig("openclaw");
expect(
resolveEffectiveAgentRuntime({
cfg,
provider: "openai",
modelId: "gpt-5.6-luna",
sessionEntry: { agentRuntimeOverride: "codex", agentHarnessId: "openclaw" },
}),
).toBe(fallback ? "openclaw" : "codex");
},
);
it("ignores legacy harness ids when choosing a runtime", () => {
const cfg = openAIConfig("openclaw");
+16 -13
View File
@@ -9,7 +9,7 @@ import {
/** Resolves the concrete harness runtime that owns the next agent turn. */
import type { SessionEntry } from "../config/sessions.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveAgentHarnessPolicy } from "./harness/policy.js";
import { resolveAvailableAgentHarnessPolicy } from "./harness/availability.js";
import { resolveAutoAgentHarnessId } from "./harness/support.js";
import { resolveSessionRuntimeOverrideForProvider } from "./session-runtime-compat.js";
@@ -65,24 +65,27 @@ export function resolveEffectiveAgentRuntime(params: {
modelBaseUrl?: unknown;
agentId?: string;
sessionKey?: string;
sessionEntry?: Pick<SessionEntry, "agentHarnessId" | "agentRuntimeOverride">;
sessionEntry?: Pick<
SessionEntry,
"agentHarnessId" | "agentRuntimeOverride" | "modelSelectionLocked"
>;
}): string {
const sessionRuntime = resolveSessionRuntimeOverrideForProvider({
provider: params.provider,
entry: params.sessionEntry,
cfg: params.cfg,
});
const runtime =
sessionRuntime ??
resolveAgentHarnessPolicy({
provider: params.provider,
modelId: params.modelId,
modelApi: params.modelApi,
modelBaseUrl: params.modelBaseUrl,
config: params.cfg,
agentId: params.agentId,
sessionKey: params.sessionKey,
}).runtime;
const runtime = resolveAvailableAgentHarnessPolicy({
...params,
mode: "projection",
config: params.cfg,
modelProvider: {
api: params.modelApi ?? undefined,
baseUrl: normalizeOptionalString(params.modelBaseUrl),
},
agentHarnessId: params.sessionEntry?.modelSelectionLocked ? sessionRuntime : undefined,
agentHarnessRuntimeOverride: sessionRuntime,
}).runtime;
if (runtime === "auto") {
// Reuse the loaded harness registry without triggering plugin discovery.
// This keeps thinking policy aligned with the harness that would own the turn.
@@ -209,7 +209,7 @@ describe("node-hosting preconditions", () => {
).toEqual([]);
});
it("does not activate plugins or reject an unknown external runtime", () => {
it.each(["codex", "auto"])("does not activate plugins or reject a cold %s runtime", (runtime) => {
resetPluginRuntimeStateForTest();
expect(
@@ -218,7 +218,7 @@ describe("node-hosting preconditions", () => {
agents: {
defaults: {
model: "openai/gpt-5.6-sol",
models: { "openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } } },
models: { "openai/gpt-5.6-sol": { agentRuntime: { id: runtime } } },
},
},
}),
@@ -1,9 +1,15 @@
// Sessions ACP runtime metadata tests cover session-owned runtime overlays.
import { describe, expect, it } from "vitest";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
resolveCurrentSessionAgentRuntimeMetadata,
resolveModelAgentRuntimeMetadata,
} from "../agents/agent-runtime-metadata.js";
import {
clearAgentHarnesses,
listRegisteredAgentHarnesses,
registerAgentHarness,
} from "../agents/harness/registry.js";
import { restoreRegisteredAgentHarnesses } from "../agents/harness/registry.test-support.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
@@ -36,7 +42,87 @@ function computeSessionAgentRuntime(params: {
});
}
const registeredHarnesses = listRegisteredAgentHarnesses();
beforeEach(() => clearAgentHarnesses());
afterAll(() => restoreRegisteredAgentHarnesses(registeredHarnesses));
describe("session ACP runtime metadata", () => {
it.each(["model", "provider", "session-key", "implicit"] as const)(
"projects a declared fallback for the next turn while retaining %s attribution",
(source) => {
const supports = vi.fn((_context: unknown) => ({
supported: false as const,
fallbackRuntime: "openclaw" as const,
}));
registerAgentHarness({
id: "codex",
label: "Codex",
supports,
runAttempt: async () => {
throw new Error("projection must not execute");
},
});
const cfg: OpenClawConfig = {
models: {
providers: {
openai: {
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
...(source === "provider" ? { agentRuntime: { id: "codex" } } : {}),
models: [
{
id: "gpt-5.6-sol",
name: "Sol",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 8192,
compat: { supportsStore: false },
},
],
},
},
},
agents: {
defaults: {
models: {
"openai/gpt-5.6-sol": source === "model" ? { agentRuntime: { id: "codex" } } : {},
},
},
},
};
const params = {
cfg,
agentId: "main",
provider: "openai",
model: "gpt-5.6-sol",
sessionKey: NON_ACP_SESSION_KEY,
sessionEntry: {
agentHarnessId: "codex",
...(source === "session-key" ? { agentRuntimeOverride: "codex" } : {}),
},
};
expect(resolveCurrentSessionAgentRuntimeMetadata(params)).toEqual({ id: "openclaw", source });
expect(
resolveCurrentSessionAgentRuntimeMetadata({
...params,
sessionEntry: { ...params.sessionEntry, modelSelectionLocked: true },
}),
).toEqual({ id: "codex", source: "session" });
expect(
resolveCurrentSessionAgentRuntimeMetadata({
...params,
sessionKey: "agent:main:acp:runtime-test",
acpRuntime: true,
}),
).toEqual({ id: "acpx", source: "session-key" });
// Projection consumes registered support only; it never discovers provider ownership.
for (const [context] of supports.mock.calls) {
expect(context).not.toHaveProperty("providerOwnerStatus");
}
},
);
it("prefers an explicit ACP backend", () => {
const agentRuntime = computeSessionAgentRuntime({
cfg: buildConfigWithoutAgentRuntimePolicy(),
@@ -97,28 +183,31 @@ describe("session ACP runtime metadata", () => {
expect(agentRuntime).toEqual({ id: "codex", source: "session" });
});
it("reports current model policy instead of an unlocked historical producer", () => {
const agentRuntime = resolveCurrentSessionAgentRuntimeMetadata({
cfg: {
agents: {
defaults: {
models: {
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
it.each([undefined, "codex"])(
"reports current %s policy instead of an unlocked historical producer",
(runtime) => {
const agentRuntime = resolveCurrentSessionAgentRuntimeMetadata({
cfg: {
agents: {
defaults: {
models: {
"openai/gpt-5.6-sol": runtime ? { agentRuntime: { id: runtime } } : {},
},
},
},
} as OpenClawConfig,
agentId: "main",
provider: "openai",
model: "gpt-5.6-sol",
sessionKey: NON_ACP_SESSION_KEY,
sessionEntry: {
agentHarnessId: "openclaw",
},
} as OpenClawConfig,
agentId: "main",
provider: "openai",
model: "gpt-5.6-sol",
sessionKey: NON_ACP_SESSION_KEY,
sessionEntry: {
agentHarnessId: "openclaw",
},
});
});
expect(agentRuntime).toEqual({ id: "codex", source: "model" });
});
expect(agentRuntime).toEqual({ id: "codex", source: runtime ? "model" : "implicit" });
},
);
it("keeps an explicit compatible runtime override", () => {
const agentRuntime = resolveCurrentSessionAgentRuntimeMetadata({
+29 -14
View File
@@ -50,15 +50,37 @@ describe("resolveMergedModelProviderModels", () => {
});
describe("resolveModelProviderRouteOverridePresence", () => {
it("treats authored model compatibility as request behavior", () => {
it.each([
["empty metadata", {}, "none"],
["affirmative reasoning support", { supportsReasoningEffort: true }, "none"],
[
"native reasoning efforts",
{ supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"] },
"none",
],
[
"combined reasoning metadata",
{ supportsReasoningEffort: true, supportedReasoningEfforts: ["low", "high"] },
"none",
],
["disabled reasoning", { supportsReasoningEffort: false }, "present"],
["malformed reasoning support", { supportsReasoningEffort: "true" }, "present"],
["empty effort list", { supportedReasoningEfforts: [] }, "present"],
["non-native effort", { supportedReasoningEfforts: ["high", "custom"] }, "present"],
["disabled effort", { supportedReasoningEfforts: ["none"] }, "present"],
["malformed effort", { supportedReasoningEfforts: ["high", false] }, "present"],
["store behavior", { supportsStore: false }, "present"],
[
"mixed metadata and behavior",
{ supportsReasoningEffort: true, supportedReasoningEfforts: ["high"], supportsStore: false },
"present",
],
])("classifies %s without discarding request behavior", (_label, compat, expected) => {
const config = {
models: {
providers: {
openai: {
models: [
{ id: "gpt-5.5", compat: { supportsStore: false } },
{ id: "gpt-5.5-empty", compat: {} },
],
models: [{ id: "gpt-5.6-sol", compat }],
},
},
},
@@ -67,17 +89,10 @@ describe("resolveModelProviderRouteOverridePresence", () => {
expect(
resolveModelProviderRouteOverridePresence({
provider: "openai",
modelId: "gpt-5.5",
modelId: "gpt-5.6-sol",
authoredConfig: config,
}),
).toBe("present");
expect(
resolveModelProviderRouteOverridePresence({
provider: "openai",
modelId: "gpt-5.5-empty",
authoredConfig: config,
}),
).toBe("none");
).toBe(expected);
});
it("treats a provider request timeout as authored behavior", () => {
+23 -1
View File
@@ -129,6 +129,28 @@ function hasNonEmptyRecord(value: unknown): boolean {
return record !== undefined && Object.keys(record).length > 0;
}
function hasRequestCompatOverrides(compat: ModelDefinitionConfig["compat"]): boolean {
return Object.entries(compat ?? {}).some(([key, value]) => {
// Native runtimes consume affirmative reasoning capabilities as turn controls.
// Disabling reasoning, custom labels, and payload shaping still require the authored adapter.
if (key === "supportsReasoningEffort") {
return value !== true;
}
if (key === "supportedReasoningEfforts") {
return !(
Array.isArray(value) &&
value.length > 0 &&
value.every(
(effort) =>
typeof effort === "string" &&
/^(minimal|low|medium|high|xhigh|max|ultra)$/u.test(effort),
)
);
}
return true;
});
}
/** Projects authored request behavior without exposing values or local commands. */
export function resolveModelProviderRouteOverridePresence(params: {
provider: string;
@@ -166,7 +188,7 @@ export function resolveModelProviderRouteOverridePresence(params: {
return configuredModel &&
(hasNonEmptyRecord(configuredModel.headers) ||
hasNonEmptyRecord(configuredModel.params) ||
hasNonEmptyRecord(configuredModel.compat))
hasRequestCompatOverrides(configuredModel.compat))
? "present"
: "none";
}
@@ -1,5 +1,11 @@
// Session model projection tests verify ACP metadata reads preserve row ownership.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearAgentHarnesses,
listRegisteredAgentHarnesses,
registerAgentHarness,
} from "../agents/harness/registry.js";
import { restoreRegisteredAgentHarnesses } from "../agents/harness/registry.test-support.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
const readAcpSessionMeta = vi.hoisted(() => vi.fn(() => undefined));
@@ -9,9 +15,73 @@ vi.mock("../acp/runtime/session-meta.js", () => ({ readAcpSessionMeta }));
import { resolveGatewaySessionThinkingProjectionInternal } from "./session-utils-model.js";
describe("resolveGatewaySessionThinkingProjectionInternal", () => {
const registeredHarnesses = listRegisteredAgentHarnesses();
beforeEach(() => {
clearAgentHarnesses();
readAcpSessionMeta.mockClear();
});
afterAll(() => restoreRegisteredAgentHarnesses(registeredHarnesses));
it.each([false, true])(
"projects the effective model runtime with authored transport=%s",
(transportOverride) => {
registerAgentHarness({
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.modelProvider?.requestTransportOverrides === "present"
? { supported: false, fallbackRuntime: "openclaw" }
: { supported: true },
runAttempt: async () => {
throw new Error("projection must not execute");
},
});
const cfg: OpenClawConfig = {
agents: {
defaults: { models: { "openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } } } },
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
api: "openai-responses",
models: [
{
id: "gpt-5.6-sol",
name: "Sol",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
maxTokens: 8192,
compat: {
supportsReasoningEffort: true,
supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"],
...(transportOverride ? { supportsStore: false } : {}),
},
},
],
},
},
},
};
const projection = resolveGatewaySessionThinkingProjectionInternal({
cfg,
agentId: "main",
provider: "openai",
model: "gpt-5.6-sol",
sessionKey: "agent:main:main",
entry: {
sessionId: "runtime-projection",
updatedAt: 1,
agentHarnessId: transportOverride ? "codex" : "openclaw",
},
});
expect(projection.agentRuntime).toEqual({
id: transportOverride ? "openclaw" : "codex",
source: "model",
});
},
);
it("reads bare-key ACP metadata under the resolved row owner", () => {
const cfg: OpenClawConfig = {
+18 -14
View File
@@ -259,22 +259,26 @@ export function getActivePluginRegistryWorkspaceDir(): string | undefined {
return state.workspaceDir ?? undefined;
}
/** Reads registration/request/active registry precedence without initializing a cold runtime. */
export function getPluginRegistryForContext(): PluginRegistry | null {
return (
state.registrationContext?.registry ??
getPluginRuntimeGatewayRequestScope()?.pluginRegistry ??
getActivePluginRegistry()
);
}
export function requireActivePluginRegistry(): PluginRegistry {
if (state.registrationContext) {
return state.registrationContext.registry;
const registry = getPluginRegistryForContext();
if (registry) {
return registry;
}
const scopedRegistry = getPluginRuntimeGatewayRequestScope()?.pluginRegistry;
if (scopedRegistry) {
return scopedRegistry;
}
if (!state.activeRegistry) {
state.activeRegistry = createEmptyPluginRegistry();
markPluginRegistryActive(state.activeRegistry);
state.activeVersion += 1;
settlePreparedMessageToolCatalog(state.activeRegistry, state.activeVersion);
syncPluginAgentEventBridge();
}
return asPluginRegistry(state.activeRegistry)!;
state.activeRegistry = createEmptyPluginRegistry();
markPluginRegistryActive(state.activeRegistry);
state.activeVersion += 1;
settlePreparedMessageToolCatalog(state.activeRegistry, state.activeVersion);
syncPluginAgentEventBridge();
return state.activeRegistry;
}
/** Binds unchanged direct SDK facades to the registry currently running synchronous register(). */