mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
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:
committed by
GitHub
parent
36856401d4
commit
625f3cf7ea
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)">
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
@@ -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(). */
|
||||
|
||||
Reference in New Issue
Block a user