diff --git a/docs/.generated/plugin-sdk-api-baseline.jsonl b/docs/.generated/plugin-sdk-api-baseline.jsonl index 2455a7c1ea41..c32ff4fa6b52 100644 --- a/docs/.generated/plugin-sdk-api-baseline.jsonl +++ b/docs/.generated/plugin-sdk-api-baseline.jsonl @@ -53,7 +53,7 @@ {"closureHash":"ff3c4616cd6212a6d698831a8b287ad87e3968c8663f4090d095bb30ec40fc1c","declaration":"export function abortAndDrainAgentHarnessRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }): Promise;","entrypoint":"agent-harness","exportName":"abortAndDrainAgentHarnessRun","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"58989e3fa91eecf2e05202e36548e6e64a3b9219e83154bd99866c851604e34e","declaration":"export function createAgentToolResultMiddlewareRunner(ctx: AgentToolResultMiddlewareContext, handlers?: AgentToolResultMiddleware[]): { applyToolResultMiddleware(event: AgentToolResultMiddlewareEvent): Promise; };","entrypoint":"agent-harness","exportName":"createAgentToolResultMiddlewareRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"b8f6355d7ad1700aceecddaa6e9ccf43091a792d5b481a1ee3f55294a881a67d","declaration":"export function createCodexAppServerToolResultExtensionRunner(ctx: CodexAppServerExtensionContext, factories?: CodexAppServerExtensionFactory[]): { applyToolResultExtensions(event: CodexAppServerToolResultEvent): Promise>; };","entrypoint":"agent-harness","exportName":"createCodexAppServerToolResultExtensionRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} -{"closureHash":"c3b6f83fc89b5682067b75004eb8fe76a780e34c0bd017a3ebb104781c6694e7","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} +{"closureHash":"0511dbfbe051bdcd4302b9923203989e9222f9ed91209cff658eb41679ca316d","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"13649ee853485319e7449fda25c106b3f3f53d3090e53cdbb28e220a8529eb80","declaration":"export function disposeRegisteredAgentHarnesses(): Promise;","entrypoint":"agent-harness","exportName":"disposeRegisteredAgentHarnesses","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"f255a91162ece4c239bb8fa745efa14a1054bc024a291e9954f75f239b2b87c3","declaration":"export function resolveActiveEmbeddedRunSessionId(sessionKey: string): string | undefined;","entrypoint":"agent-harness","exportName":"resolveActiveEmbeddedRunSessionId","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"27d9f61df9ccf68615cb2002a6da891e36ab0c07623bc37c4d07aa980b446881","declaration":"export function resolveWebSearchToolPolicy(params: WebSearchToolPolicyParams): WebSearchToolPolicyResolution;","entrypoint":"agent-harness","exportName":"resolveWebSearchToolPolicy","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} diff --git a/src/gateway/server-methods/system-agent.test.ts b/src/gateway/server-methods/system-agent.test.ts index 41a2aec49733..dc01a28f5c81 100644 --- a/src/gateway/server-methods/system-agent.test.ts +++ b/src/gateway/server-methods/system-agent.test.ts @@ -15,7 +15,6 @@ import { getActiveGatewayRootWorkCount } from "../../process/gateway-work-admiss import { CommandLane } from "../../process/lanes.js"; import { defaultRuntime } from "../../runtime.js"; import { SystemAgentChatEngine } from "../../system-agent/chat-engine.js"; -import { SystemAgentInferenceUnavailableError } from "../../system-agent/inference-error.js"; import { createSystemAgentVerifiedInferenceTestFixture, installSystemAgentPluginMetadataTestSnapshot, @@ -934,10 +933,14 @@ describe("openclaw.chat", () => { it("reuses a live session, then requires fresh fallback verification after failure", async () => { stubEngineOverview(); - const engine = makeVerifiedEngine(); - vi.spyOn(engine, "handle").mockRejectedValue( - new SystemAgentInferenceUnavailableError("conversation"), - ); + const engine = new SystemAgentChatEngine({ + verifiedInference: requireVerifiedInferenceFixture(), + runAgentTurn: async () => { + throw new Error("workspace owner openclaw is missing from the roster"); + }, + planWithAssistant: async () => null, + deps: requireVerifiedInferenceDeps(), + }); const dispose = vi.spyOn(engine, "dispose").mockResolvedValue(); const sessions = new Map([["s1", seededSession({ engine })]]); const context = makeContext(sessions); @@ -948,7 +951,7 @@ describe("openclaw.chat", () => { ok: false, error: { code: "UNAVAILABLE", - message: expect.stringContaining("working inference"), + message: expect.stringContaining("workspace owner openclaw is missing from the roster"), details: { code: "system_agent_session_invalidated" }, }, }); diff --git a/src/system-agent/chat-engine.test.ts b/src/system-agent/chat-engine.test.ts index 5b01beea7436..c6cfb200905c 100644 --- a/src/system-agent/chat-engine.test.ts +++ b/src/system-agent/chat-engine.test.ts @@ -55,6 +55,7 @@ const mocks = vi.hoisted(() => ({ runSetupMemoryImportStep: vi.fn(), writeWizardConfigFile: vi.fn(), runCollectedChannelOnboardingPostWriteHooks: vi.fn(async () => {}), + chatWarn: vi.fn(), sharedVerifiedInference: undefined as SystemAgentVerifiedInferenceBinding | undefined, })); @@ -65,6 +66,17 @@ vi.mock("../config/config.js", async (importOriginal) => ({ readConfigFileSnapshot: mocks.readConfigFileSnapshot, })); +vi.mock("../logging/subsystem.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createSubsystemLogger: (subsystem: string) => + subsystem === "system-agent/chat-engine" + ? ({ warn: mocks.chatWarn } as unknown as ReturnType) + : actual.createSubsystemLogger(subsystem), + }; +}); + vi.mock("../wizard/setup.shared.js", async (importOriginal) => ({ ...(await importOriginal()), readSetupConfigFileSnapshot: mocks.readSetupConfigFileSnapshot, @@ -174,7 +186,9 @@ function testHarnessBinding(route: SystemAgentConfiguredRoute) { return { auth: {}, deps: {} }; } const agentHarnessId = - route.agentHarnessRuntimeOverride === "auto" ? "openclaw" : route.agentHarnessRuntimeOverride; + route.agentHarnessRuntimeOverride === "auto" + ? "openclaw" + : (route.agentHarnessRuntimeOverride ?? "codex"); if (agentHarnessId === "openclaw") { return { auth: { agentHarnessId }, deps: {} }; } @@ -3148,13 +3162,15 @@ describe("SystemAgentChatEngine", () => { it("fails closed when neither inference path is usable", async () => { const planner = vi.fn(async () => null); const engine = new SystemAgentChatEngine({ - runAgentTurn: async () => null, + runAgentTurn: async () => { + throw new Error("workspace owner openclaw is missing from the roster"); + }, planWithAssistant: planner, deps: { loadOverview: fakeOverviewLoader() }, }); - await expect(engine.handle("please make everything nice")).rejects.toBeInstanceOf( - SystemAgentInferenceUnavailableError, + await expect(engine.handle("please make everything nice")).rejects.toThrow( + "workspace owner openclaw is missing from the roster", ); }); }); @@ -3272,6 +3288,7 @@ describe("OpenClaw agent loop backends", () => { expect(runCliAgent).toHaveBeenCalledOnce(); expect(reply.text).toContain("planner fallback reply"); + expect(mocks.chatWarn).toHaveBeenCalledWith(expect.stringContaining("claude exploded")); }); }); diff --git a/src/system-agent/chat-engine.ts b/src/system-agent/chat-engine.ts index a3f4dd575a2d..cdc8d0d69fc7 100644 --- a/src/system-agent/chat-engine.ts +++ b/src/system-agent/chat-engine.ts @@ -1136,6 +1136,7 @@ export class SystemAgentChatEngine { session: this.agentSession, }); } catch (error) { + log.warn(`agent turn failed before planner fallback: ${formatErrorMessage(error)}`); agentFailure = error; loopReply = null; } diff --git a/src/system-agent/inference-error.ts b/src/system-agent/inference-error.ts index fa6f2539c96e..165425cc3c1d 100644 --- a/src/system-agent/inference-error.ts +++ b/src/system-agent/inference-error.ts @@ -1,5 +1,24 @@ +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; +import { formatErrorMessage } from "../infra/errors.js"; + type SystemAgentInferenceStage = "agent-turn" | "planner" | "conversation"; +const INFERENCE_UNAVAILABLE_MESSAGE = + "OpenClaw could not reach working inference. Run `openclaw onboard` on the machine running OpenClaw to reconnect — it live-tests the route before saving it. Then try again."; +const INFERENCE_FAILURE_SUMMARY_MAX_CHARS = 300; + +function inferenceUnavailableMessage(failures: readonly unknown[]): string { + const detail = failures.length > 0 ? formatErrorMessage(failures[0]).trim() : ""; + if (!detail) { + return INFERENCE_UNAVAILABLE_MESSAGE; + } + const summary = + detail.length > INFERENCE_FAILURE_SUMMARY_MAX_CHARS + ? `${truncateUtf16Safe(detail, INFERENCE_FAILURE_SUMMARY_MAX_CHARS - 1)}…` + : detail; + return `${INFERENCE_UNAVAILABLE_MESSAGE} Cause: ${summary}`; +} + /** Safe public error for an OpenClaw turn that could not complete with intelligence. */ export class SystemAgentInferenceUnavailableError extends Error { readonly code = "SYSTEM_AGENT_INFERENCE_UNAVAILABLE"; @@ -8,9 +27,7 @@ export class SystemAgentInferenceUnavailableError extends Error { readonly stage: SystemAgentInferenceStage, readonly failures: readonly unknown[] = [], ) { - super( - "OpenClaw could not reach working inference. Run `openclaw onboard` on the machine running OpenClaw to reconnect — it live-tests the route before saving it. Then try again.", - ); + super(inferenceUnavailableMessage(failures)); this.name = "SystemAgentInferenceUnavailableError"; } } diff --git a/src/system-agent/inference-route.test.ts b/src/system-agent/inference-route.test.ts new file mode 100644 index 000000000000..3f605f23b945 --- /dev/null +++ b/src/system-agent/inference-route.test.ts @@ -0,0 +1,110 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { clearAgentHarnesses, registerAgentHarness } from "../agents/harness/registry.js"; +import { selectAgentHarness } from "../agents/harness/selection.js"; +import { resolveRunWorkspaceDir } from "../agents/workspace-run.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { SYSTEM_AGENT_ID } from "./agent-id.js"; +import { resolveSystemAgentConfiguredRouteFromConfig } from "./inference-route.js"; + +function devConfig(agentRuntime?: string): OpenClawConfig { + return { + agents: { + defaults: { model: "openai/gpt-5.5" }, + entries: { + dev: { default: true, workspace: "/tmp/x" }, + }, + }, + models: { + providers: { + openai: { + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + ...(agentRuntime ? { agentRuntime: { id: agentRuntime } } : {}), + models: [ + { + id: "gpt-5.5", + name: "GPT-5.5", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 8_192, + }, + ], + }, + }, + }, + }; +} + +afterEach(() => { + clearAgentHarnesses(); +}); + +describe("resolveSystemAgentConfiguredRouteFromConfig", () => { + it("admits the reserved execution agent for a dev-shaped roster", async () => { + const route = await resolveSystemAgentConfiguredRouteFromConfig(devConfig()); + + expect(route).not.toBeNull(); + expect(() => + resolveRunWorkspaceDir({ + workspaceDir: "/tmp/x", + agentId: SYSTEM_AGENT_ID, + config: route!.runConfig, + }), + ).not.toThrow(); + }); + + it("keeps implicit harness selection fallible while forcing explicit policy", async () => { + const supports = vi.fn((ctx: { modelProvider?: { requestTransportOverrides?: string } }) => + ctx.modelProvider?.requestTransportOverrides === "present" + ? { supported: false as const, reason: "authored request transport overrides" } + : { supported: true as const, priority: 100 }, + ); + registerAgentHarness({ + id: "codex", + label: "Codex", + supports: supports as never, + runAttempt: vi.fn() as never, + }); + const preparedModelProvider = { + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + requestTransportOverrides: "present" as const, + }; + + const implicitRoute = await resolveSystemAgentConfiguredRouteFromConfig(devConfig()); + expect(implicitRoute).toMatchObject({ runner: "embedded" }); + expect(implicitRoute).not.toHaveProperty("agentHarnessRuntimeOverride"); + expect( + selectAgentHarness({ + provider: "openai", + modelId: "gpt-5.5", + modelProvider: preparedModelProvider, + config: implicitRoute!.runConfig, + agentHarnessRuntimeOverride: + implicitRoute!.runner === "embedded" + ? implicitRoute!.agentHarnessRuntimeOverride + : undefined, + }).id, + ).toBe("openclaw"); + + const explicitRoute = await resolveSystemAgentConfiguredRouteFromConfig(devConfig("codex")); + expect(explicitRoute).toMatchObject({ + runner: "embedded", + agentHarnessRuntimeOverride: "codex", + }); + expect(() => + selectAgentHarness({ + provider: "openai", + modelId: "gpt-5.5", + modelProvider: preparedModelProvider, + config: explicitRoute!.runConfig, + agentHarnessRuntimeOverride: + explicitRoute!.runner === "embedded" + ? explicitRoute!.agentHarnessRuntimeOverride + : undefined, + }), + ).toThrow("authored request transport overrides"); + }); +}); diff --git a/src/system-agent/inference-route.ts b/src/system-agent/inference-route.ts index a417be61c324..ec6febd7eeed 100644 --- a/src/system-agent/inference-route.ts +++ b/src/system-agent/inference-route.ts @@ -14,6 +14,7 @@ import { import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js"; import { normalizeAgentId } from "../routing/session-key.js"; +import { SYSTEM_AGENT_ID } from "./agent-id.js"; export type SystemAgentConfiguredRoute = { runConfig: OpenClawConfig; @@ -27,7 +28,7 @@ export type SystemAgentConfiguredRoute = { | { runner: "cli" } | { runner: "embedded"; - agentHarnessRuntimeOverride: string; + agentHarnessRuntimeOverride?: string; } ); @@ -70,42 +71,20 @@ export type DefaultInferenceRouteProjection = { tools: OpenClawConfig["tools"]; }; -const SYSTEM_AGENT_EXECUTION_AGENT_ID = "openclaw"; - function projectSystemAgentExecutionConfig( config: OpenClawConfig, routeAgentId: string, ): OpenClawConfig { const agents = listAgentEntries(config); - if (agents.length === 0) { - return config; - } - const routeAgent = - routeAgentId === SYSTEM_AGENT_EXECUTION_AGENT_ID - ? undefined - : agents.find((agent) => normalizeAgentId(agent.id) === routeAgentId); - const retainedAgents = agents.filter( - (agent) => normalizeAgentId(agent.id) !== SYSTEM_AGENT_EXECUTION_AGENT_ID, - ); - const hasProjectedSettings = routeAgent?.params !== undefined || routeAgent?.tools !== undefined; - if (retainedAgents.length === agents.length && !hasProjectedSettings) { - return config; - } + const routeAgent = agents.find((agent) => normalizeAgentId(agent.id) === routeAgentId); + const retainedAgents = agents.filter((agent) => normalizeAgentId(agent.id) !== SYSTEM_AGENT_ID); const projectedAgents = [ ...retainedAgents, - ...(hasProjectedSettings - ? [ - { - id: SYSTEM_AGENT_EXECUTION_AGENT_ID, - ...(routeAgent?.params !== undefined - ? { params: structuredClone(routeAgent.params) } - : {}), - ...(routeAgent?.tools !== undefined - ? { tools: structuredClone(routeAgent.tools) } - : {}), - }, - ] - : []), + { + id: SYSTEM_AGENT_ID, + ...(routeAgent?.params !== undefined ? { params: structuredClone(routeAgent.params) } : {}), + ...(routeAgent?.tools !== undefined ? { tools: structuredClone(routeAgent.tools) } : {}), + }, ]; const { list: _legacyList, ...agentsConfig } = config.agents ?? {}; return { @@ -195,13 +174,17 @@ export async function resolveSystemAgentConfiguredRouteFromConfig( if (isCliRoute) { return { runner: "cli", ...base }; } - const runtime = harnessPolicy.resolveAgentHarnessPolicy({ + const policy = harnessPolicy.resolveAgentHarnessPolicy({ config: runConfig, agentId: modelOwnerAgentId, provider: selection.provider, modelId: selection.modelId, - }).runtime; - return { runner: "embedded", agentHarnessRuntimeOverride: runtime, ...base }; + }); + return { + runner: "embedded", + ...(policy.runtimeSource === "implicit" ? {} : { agentHarnessRuntimeOverride: policy.runtime }), + ...base, + }; } function projectRelevantModelMap(params: { @@ -248,7 +231,7 @@ export async function projectInferenceRoute( const list = listAgentEntries(config); const agent = list.find((entry) => normalizeAgentId(entry.id) === routeAgentId); const executionAgent = listAgentEntries(route?.runConfig ?? {}).find( - (entry) => normalizeAgentId(entry.id) === SYSTEM_AGENT_EXECUTION_AGENT_ID, + (entry) => normalizeAgentId(entry.id) === SYSTEM_AGENT_ID, ); const defaults = config.agents?.defaults; const logicalProvider = normalizeProviderId(route?.modelLabel.split("/", 1)[0] ?? ""); @@ -344,7 +327,7 @@ export async function projectInferenceRoute( ...(executionAgent ? { executionAgent: { - id: SYSTEM_AGENT_EXECUTION_AGENT_ID, + id: SYSTEM_AGENT_ID, params: structuredClone(executionAgent.params), tools: structuredClone(executionAgent.tools), }, diff --git a/src/system-agent/revalidate-inference-owner.ts b/src/system-agent/revalidate-inference-owner.ts index a01f39d646e4..9eccc570f3d7 100644 --- a/src/system-agent/revalidate-inference-owner.ts +++ b/src/system-agent/revalidate-inference-owner.ts @@ -22,7 +22,7 @@ export async function revalidateSetupInferenceOwner(params: { }): Promise { const configuredHarnessId = params.route.runner === "embedded" - ? params.route.agentHarnessRuntimeOverride.trim() + ? params.route.agentHarnessRuntimeOverride?.trim() : undefined; const successfulHarnessId = params.auth.agentHarnessId?.trim() || diff --git a/src/system-agent/setup-inference-activate.ts b/src/system-agent/setup-inference-activate.ts index 75ad4f74d1fc..640085bc153d 100644 --- a/src/system-agent/setup-inference-activate.ts +++ b/src/system-agent/setup-inference-activate.ts @@ -361,14 +361,16 @@ async function activateSetupInferenceUnredacted( if (testPlan.runner === "embedded" && stagedRoute.runner === "embedded") { testPlan = { ...testPlan, - config: stagedExecutionRoute.runConfig, + executionConfig: stagedExecutionRoute.runConfig, agentDir: hasPreparedAuthProfiles ? testAgentDir : stagedRoute.agentDir, - agentHarnessRuntimeOverride: stagedRoute.agentHarnessRuntimeOverride, + ...(stagedRoute.agentHarnessRuntimeOverride + ? { agentHarnessRuntimeOverride: stagedRoute.agentHarnessRuntimeOverride } + : {}), }; } else { testPlan = { ...testPlan, - config: stagedExecutionRoute.runConfig, + executionConfig: stagedExecutionRoute.runConfig, ...(!hasPreparedAuthProfiles ? { agentDir: stagedRoute.agentDir } : {}), }; } @@ -479,10 +481,12 @@ async function activateSetupInferenceUnredacted( } if (testPlan.runner === "embedded") { const successfulHarnessId = test.auth.agentHarnessId?.trim(); + const configuredHarnessId = testPlan.agentHarnessRuntimeOverride?.trim(); if ( !successfulHarnessId || - (testPlan.agentHarnessRuntimeOverride !== "auto" && - successfulHarnessId !== testPlan.agentHarnessRuntimeOverride) + (configuredHarnessId !== undefined && + configuredHarnessId !== "auto" && + successfulHarnessId !== configuredHarnessId) ) { return { ok: false, diff --git a/src/system-agent/setup-inference-persist.ts b/src/system-agent/setup-inference-persist.ts index b5a821ec7f84..9a46be6cf1c0 100644 --- a/src/system-agent/setup-inference-persist.ts +++ b/src/system-agent/setup-inference-persist.ts @@ -513,7 +513,7 @@ export async function runSetupInferenceTest(params: { sessionFile, workspaceDir: tempDir, ...(plan.agentDir ? { agentDir: plan.agentDir } : {}), - config: plan.config, + config: plan.executionConfig ?? plan.config, prompt: params.prompt ?? SETUP_INFERENCE_TEST_PROMPT, provider: plan.provider, model: plan.model, @@ -543,7 +543,7 @@ export async function runSetupInferenceTest(params: { sessionFile, workspaceDir: tempDir, ...(plan.agentDir ? { agentDir: plan.agentDir } : {}), - config: plan.config, + config: plan.executionConfig ?? plan.config, prompt: params.prompt ?? SETUP_INFERENCE_TEST_PROMPT, provider: plan.provider, model: plan.model, diff --git a/src/system-agent/setup-inference-plan-helpers.ts b/src/system-agent/setup-inference-plan-helpers.ts index ec9f37f3f5e1..89938318fdc4 100644 --- a/src/system-agent/setup-inference-plan-helpers.ts +++ b/src/system-agent/setup-inference-plan-helpers.ts @@ -28,7 +28,10 @@ export type SetupInferenceTestPlan = { provider: string; model: string; modelRef: string; + /** Authored/staged config used for route, auth, and persistence decisions. */ config: OpenClawConfig; + /** Execution-only projection that admits the reserved OpenClaw agent. */ + executionConfig?: OpenClawConfig; /** Execution identity used by the real OpenClaw turn. */ agentId?: string; /** Default-agent owner whose model/runtime config is being selected. */ diff --git a/src/system-agent/setup-inference-plan.ts b/src/system-agent/setup-inference-plan.ts index 2accebb55366..add39aa6883f 100644 --- a/src/system-agent/setup-inference-plan.ts +++ b/src/system-agent/setup-inference-plan.ts @@ -242,11 +242,12 @@ export async function buildTestPlan(params: { provider: route.provider, model: route.model, modelRef: route.modelLabel, - config: route.runConfig, + config: cfg, + executionConfig: route.runConfig, agentId: "openclaw", routeAgentId: route.agentId, agentDir: route.agentDir, - ...(route.runner === "embedded" + ...(route.runner === "embedded" && route.agentHarnessRuntimeOverride ? { agentHarnessRuntimeOverride: route.agentHarnessRuntimeOverride } : {}), ...(route.authProfileId ? { authProfileId: route.authProfileId } : {}), diff --git a/src/system-agent/system-agent.test-helpers.ts b/src/system-agent/system-agent.test-helpers.ts index 42b747d93a7d..8ffbdcfa1fbe 100644 --- a/src/system-agent/system-agent.test-helpers.ts +++ b/src/system-agent/system-agent.test-helpers.ts @@ -297,7 +297,7 @@ export async function createSystemAgentVerifiedInferenceTestFixture( const agentHarnessId = configuredRoute.agentHarnessRuntimeOverride === "auto" ? "openclaw" - : configuredRoute.agentHarnessRuntimeOverride; + : (configuredRoute.agentHarnessRuntimeOverride ?? "codex"); const authFingerprint = profileId && agentHarnessId !== "openclaw" ? fingerprintResolvedAuthProfileCredential({ profileId, credential, resolvedAuth }) diff --git a/src/system-agent/tui-backend.test.ts b/src/system-agent/tui-backend.test.ts index f7f4ed788ffa..4fd77f45d5b0 100644 --- a/src/system-agent/tui-backend.test.ts +++ b/src/system-agent/tui-backend.test.ts @@ -8,10 +8,10 @@ import type { SystemAgentCommandDeps, SystemAgentOperation } from "./operations. import type { SystemAgentOverview } from "./overview.js"; import { createSystemAgentVerifiedInferenceTestFixture } from "./system-agent.test-helpers.js"; import { runSystemAgentTui, type SystemAgentTuiOptions } from "./tui-backend.js"; -import { resolveSystemAgentVerifiedInferenceRoute } from "./verified-inference.js"; +import { resolveSystemAgentVerifiedInferenceState } from "./verified-inference.js"; const verifiedInferenceMocks = vi.hoisted(() => ({ - preparedBindings: new WeakSet(), + preparedBindings: new WeakMap(), })); vi.mock("../plugins/providers.js", () => ({ @@ -30,11 +30,12 @@ vi.mock("./verified-inference.js", async (importOriginal) => { const original = await importOriginal(); return { ...original, - resolveSystemAgentVerifiedInferenceRoute: vi.fn(async (binding, deps) => - verifiedInferenceMocks.preparedBindings.has(binding) - ? binding.execution - : await original.resolveSystemAgentVerifiedInferenceRoute(binding, deps), - ), + resolveSystemAgentVerifiedInferenceState: vi.fn(async (binding, deps) => { + const config = verifiedInferenceMocks.preparedBindings.get(binding); + return config + ? { config, route: binding.execution } + : await original.resolveSystemAgentVerifiedInferenceState(binding, deps); + }), }; }); @@ -106,7 +107,7 @@ async function createVerifiedTuiOptions( ? sharedVerifiedFixture : await createSystemAgentVerifiedInferenceTestFixture(config); if (!useRealVerification) { - verifiedInferenceMocks.preparedBindings.add(fixture.binding); + verifiedInferenceMocks.preparedBindings.set(fixture.binding, config); } return { verifiedInference: fixture.binding, @@ -162,8 +163,8 @@ describe("runSystemAgentTui", () => { verifiedConfig, true, ); - const resolveVerifiedRoute = vi.mocked(resolveSystemAgentVerifiedInferenceRoute); - resolveVerifiedRoute.mockClear(); + const resolveVerifiedState = vi.mocked(resolveSystemAgentVerifiedInferenceState); + resolveVerifiedState.mockClear(); const runTui = vi.fn( async (opts: Parameters>[0]) => { runTuiCalls += 1; @@ -181,9 +182,9 @@ describe("runSystemAgentTui", () => { ); expect(runTuiCalls).toBe(1); - expect(resolveVerifiedRoute).toHaveBeenCalledOnce(); - expect(resolveVerifiedRoute).toHaveBeenCalledWith(verified.verifiedInference, verified.deps); - const [resolveOrder] = resolveVerifiedRoute.mock.invocationCallOrder; + expect(resolveVerifiedState).toHaveBeenCalledOnce(); + expect(resolveVerifiedState).toHaveBeenCalledWith(verified.verifiedInference, verified.deps); + const [resolveOrder] = resolveVerifiedState.mock.invocationCallOrder; const [runTuiOrder] = runTui.mock.invocationCallOrder; if (resolveOrder === undefined || runTuiOrder === undefined) { throw new Error("expected verified route resolution before TUI startup"); diff --git a/src/system-agent/tui-backend.ts b/src/system-agent/tui-backend.ts index 74aa5a748e62..2cf0c0caf3b3 100644 --- a/src/system-agent/tui-backend.ts +++ b/src/system-agent/tui-backend.ts @@ -37,7 +37,7 @@ import { } from "./operations.js"; import { formatSystemAgentStartupMessage, loadSystemAgentOverview } from "./overview.js"; import { - resolveSystemAgentVerifiedInferenceRoute, + resolveSystemAgentVerifiedInferenceState, type SystemAgentVerifiedInferenceBinding, } from "./verified-inference.js"; @@ -602,8 +602,9 @@ async function requireTuiVerifiedInference( throw new SystemAgentInferenceUnavailableError("conversation"); } try { - const route = await resolveSystemAgentVerifiedInferenceRoute(binding, opts.deps); - if (route) { + const verified = await resolveSystemAgentVerifiedInferenceState(binding, opts.deps); + if (verified) { + const { config, route } = verified; const [{ getPreparedModelCatalogSnapshot }, { resolveThinkingDefault }] = await Promise.all([ import("../agents/prepared-model-catalog.js"), import("../agents/model-thinking-default.js"), @@ -611,7 +612,7 @@ async function requireTuiVerifiedInference( // Catalog metadata improves the label but must not become a new startup // dependency after this exact inference route has already been verified. const catalog = getPreparedModelCatalogSnapshot({ - config: route.runConfig, + config, agentId: route.agentId, agentDir: route.agentDir, readOnly: true, diff --git a/src/system-agent/verified-inference.test.ts b/src/system-agent/verified-inference.test.ts index 4ee13ec00f1d..3b2a9d9381c1 100644 --- a/src/system-agent/verified-inference.test.ts +++ b/src/system-agent/verified-inference.test.ts @@ -222,7 +222,7 @@ async function bindingFor( route.runner === "embedded" ? route.agentHarnessRuntimeOverride === "auto" ? "openclaw" - : route.agentHarnessRuntimeOverride + : (route.agentHarnessRuntimeOverride ?? "codex") : undefined; return createBinding( route, @@ -868,7 +868,7 @@ describe("verified OpenClaw inference binding", () => { const fingerprint = () => fingerprintAwsSdkRuntimeOwner({ provider: route.provider, - backendId: route.agentHarnessRuntimeOverride, + backendId: route.agentHarnessRuntimeOverride ?? "openclaw", auth, }); try { @@ -1047,7 +1047,8 @@ describe("verified OpenClaw inference binding", () => { const route = await revalidate(binding, changed, authDeps()); expect(route).toBe(binding.execution); - expect(route?.runConfig).toEqual(baseConfig); + expect(route?.runConfig).toMatchObject(baseConfig); + expect(route?.runConfig.agents?.entries).toEqual({ openclaw: {} }); expect(route?.runConfig).not.toBe(baseConfig); }); diff --git a/src/system-agent/verified-inference.ts b/src/system-agent/verified-inference.ts index 3ab1934e271e..277ced0b3d08 100644 --- a/src/system-agent/verified-inference.ts +++ b/src/system-agent/verified-inference.ts @@ -48,6 +48,16 @@ type SystemAgentConfiguredRouteIdentity = DistributiveOmit< SystemAgentConfiguredRoute, "runConfig" | "authProfileId" >; +type SystemAgentVerifiedExecutionRoute = + | Extract + | (Extract & { + agentHarnessRuntimeOverride: string; + }); + +type SystemAgentVerifiedInferenceState = Readonly<{ + config: OpenClawConfig; + route: SystemAgentVerifiedExecutionRoute; +}>; type SystemAgentVerifiedExecutionFingerprint = { route: unknown; @@ -106,7 +116,7 @@ type SystemAgentOwnerPluginRegistryLoader = (params: { /** Server-local proof returned only after the exact route completes a live turn. */ export type SystemAgentVerifiedInferenceBinding = Readonly<{ configuredRoute: SystemAgentConfiguredRouteIdentity; - execution: SystemAgentConfiguredRoute; + execution: SystemAgentVerifiedExecutionRoute; executionFingerprint: SystemAgentVerifiedExecutionFingerprint; ownerPluginIds: readonly string[]; ownerPluginArtifacts: readonly SystemAgentOwnerPluginArtifactIdentity[]; @@ -212,7 +222,7 @@ function systemAgentRouteIdentity( } async function resolveCurrentRuntimeOwnerFingerprint(params: { - route: SystemAgentConfiguredRoute; + route: SystemAgentVerifiedExecutionRoute; kind: OpaqueRuntimeOwnerKind; runtimeOwnerId: string; authProfileId?: string; @@ -400,7 +410,7 @@ function projectOwnerPluginArtifacts(params: { } async function projectVerifiedExecutionFingerprint( config: OpenClawConfig, - route: SystemAgentConfiguredRoute, + route: SystemAgentVerifiedExecutionRoute, ownerPluginIds: readonly string[], deps: SystemAgentVerifiedInferenceDeps, ): Promise { @@ -431,7 +441,11 @@ function resolveRouteHarnessOwnerPluginIds( config: OpenClawConfig, route: SystemAgentConfiguredRoute, ): string[] { - if (route.runner !== "embedded" || route.agentHarnessRuntimeOverride === "openclaw") { + if ( + route.runner !== "embedded" || + !route.agentHarnessRuntimeOverride || + route.agentHarnessRuntimeOverride === "openclaw" + ) { return []; } const workspaceDir = resolveAgentWorkspaceDir(config, route.agentId, process.env); @@ -487,7 +501,7 @@ export function captureSystemAgentOwnerPluginArtifacts(params: { } async function resolveCurrentAuthFingerprint(params: { - route: SystemAgentConfiguredRoute; + route: SystemAgentVerifiedExecutionRoute; authProfileId?: string; modelId?: string; modelApi?: string; @@ -645,12 +659,15 @@ export async function createSystemAgentVerifiedInferenceBinding(params: { }): Promise { const deps = params.deps ?? {}; const runConfig = structuredClone(params.executionRoute.runConfig); - const execution = { ...params.executionRoute, runConfig } as SystemAgentConfiguredRoute; - const authProfileId = params.auth.authProfileId ?? execution.authProfileId; + const configuredExecution = { + ...params.executionRoute, + runConfig, + } as SystemAgentConfiguredRoute; + const authProfileId = params.auth.authProfileId ?? configuredExecution.authProfileId; const modelId = params.auth.modelId?.trim(); const modelApi = params.auth.modelApi?.trim(); if (authProfileId) { - execution.authProfileId = authProfileId; + configuredExecution.authProfileId = authProfileId; } const proofKind = params.auth.runtimeOwnerFingerprint ? "runtime-owner" : "credential"; if ( @@ -664,17 +681,15 @@ export async function createSystemAgentVerifiedInferenceBinding(params: { throw new Error("The successful inference run did not report its exact runtime owner."); } let successfulHarnessId: string | undefined; - if (execution.runner === "embedded") { - const configuredHarnessId = execution.agentHarnessRuntimeOverride.trim(); + if (configuredExecution.runner === "embedded") { + const configuredHarnessId = configuredExecution.agentHarnessRuntimeOverride?.trim(); const reportedHarnessId = params.auth.agentHarnessId?.trim(); - if (!configuredHarnessId) { - throw new Error("The configured inference route did not select an agent harness."); - } - if (configuredHarnessId === "auto" && !reportedHarnessId) { + if ((!configuredHarnessId || configuredHarnessId === "auto") && !reportedHarnessId) { throw new Error("The successful inference run did not report its exact agent harness."); } if ( reportedHarnessId && + configuredHarnessId && configuredHarnessId !== "auto" && reportedHarnessId !== configuredHarnessId ) { @@ -683,8 +698,14 @@ export async function createSystemAgentVerifiedInferenceBinding(params: { ); } successfulHarnessId = reportedHarnessId ?? configuredHarnessId; - execution.agentHarnessRuntimeOverride = successfulHarnessId; } + const execution = + configuredExecution.runner === "embedded" + ? ({ + ...configuredExecution, + agentHarnessRuntimeOverride: successfulHarnessId!, + } satisfies SystemAgentVerifiedExecutionRoute) + : configuredExecution; let currentRuntimeArtifactFingerprint: string | undefined; if (execution.runner === "cli") { if (!params.auth.runtimeArtifactFingerprint || !params.auth.runtimeArtifactId?.trim()) { @@ -837,10 +858,10 @@ export async function hasCurrentSystemAgentOwnerPluginArtifacts( * switch this frozen run, while relevant runtime plugin membership and the * actual selected credential are checked explicitly. */ -export async function resolveSystemAgentVerifiedInferenceRoute( +async function resolveSystemAgentVerifiedInferenceStateInternal( binding: SystemAgentVerifiedInferenceBinding, deps: SystemAgentVerifiedInferenceDeps = {}, -): Promise { +): Promise { const readSnapshot = deps.readConfigFileSnapshot ?? (await import("../config/config.js")).readConfigFileSnapshot; const snapshot = await readSnapshot(); @@ -861,7 +882,7 @@ export async function resolveSystemAgentVerifiedInferenceRoute( } // Keep the live-tested runner/harness selection frozen, but reproject its // owner through current config so policy/backend changes cannot reuse proof. - const currentExecution: SystemAgentConfiguredRoute = { + const currentExecution: SystemAgentVerifiedExecutionRoute = { ...binding.execution, runConfig: currentRoute.runConfig, }; @@ -949,6 +970,20 @@ export async function resolveSystemAgentVerifiedInferenceRoute( if (currentAuthFingerprint !== binding.auth.authFingerprint) { return null; } - return binding.execution; + return { config, route: binding.execution }; +} + +export async function resolveSystemAgentVerifiedInferenceState( + binding: SystemAgentVerifiedInferenceBinding, + deps: SystemAgentVerifiedInferenceDeps = {}, +): Promise { + return resolveSystemAgentVerifiedInferenceStateInternal(binding, deps); +} + +export async function resolveSystemAgentVerifiedInferenceRoute( + binding: SystemAgentVerifiedInferenceBinding, + deps: SystemAgentVerifiedInferenceDeps = {}, +): Promise { + return (await resolveSystemAgentVerifiedInferenceStateInternal(binding, deps))?.route ?? null; } /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */