fix: prefer native codex app-server controls

This commit is contained in:
Peter Steinberger
2026-04-26 00:58:54 +01:00
parent 77d04a39d8
commit 8e12c24d17
14 changed files with 246 additions and 25 deletions

View File

@@ -59,6 +59,7 @@ Docs: https://docs.openclaw.ai
- Android/Talk Mode: expose Talk Mode in the Voice tab with runtime-owned voice capture modes and microphone foreground-service escalation. Thanks @alex-latitude.
- Providers/LiteLLM: register `litellm` as an image-generation provider so `image_generate model=litellm/...` calls and `agents.defaults.imageGenerationModel.fallbacks` entries resolve through the LiteLLM proxy. Thanks @zqchris.
- Codex harness: require Codex app-server `0.125.0` or newer and cover native MCP `PreToolUse`, `PostToolUse`, and `PermissionRequest` payloads through the OpenClaw hook relay.
- Agents/Codex: teach prompts and `agents_list` to surface native Codex app-server availability so agents prefer `/codex ...` over Codex ACP unless ACP/acpx is explicit. Thanks @vincentkoc.
### Fixes

View File

@@ -46,6 +46,20 @@ That means OpenClaw selects an OpenAI model ref, then asks the Codex app-server
runtime to run the embedded agent turn. It does not mean the channel, model
provider catalog, or OpenClaw session store becomes Codex.
When the bundled `codex` plugin is enabled, natural-language Codex control
should use the native `/codex` command surface (`/codex bind`, `/codex threads`,
`/codex resume`, `/codex steer`, `/codex stop`) instead of ACP. Use ACP for
Codex only when the user explicitly asks for ACP/acpx or is testing the ACP
adapter path. Claude Code, Gemini CLI, OpenCode, Cursor, and similar external
harnesses still use ACP.
| You mean... | Use... |
| --------------------------------------- | -------------------------------------------- |
| Codex app-server chat/thread control | `/codex ...` from the bundled `codex` plugin |
| Codex app-server embedded agent runtime | `embeddedHarness.runtime: "codex"` |
| OpenAI Codex OAuth on the PI runner | `openai-codex/*` model refs |
| Claude Code or other external harness | ACP/acpx |
For the OpenAI-family prefix split, see [OpenAI](/providers/openai) and
[Model providers](/concepts/model-providers). For the Codex runtime support
contract, see [Codex harness](/plugins/codex-harness#v1-support-contract).

View File

@@ -20,6 +20,12 @@ If you are trying to orient yourself, start with
`openai/gpt-5.5` is the model ref, `codex` is the runtime, and Telegram,
Discord, Slack, or another channel remains the communication surface.
The same plugin also owns the native `/codex` chat-control command surface. If
the plugin is enabled and the user asks to bind, resume, steer, stop, or inspect
Codex threads from chat, agents should prefer `/codex ...` over ACP. ACP remains
the explicit fallback when the user asks for ACP/acpx or is testing the ACP
Codex adapter.
Native Codex turns keep OpenClaw plugin hooks as the public compatibility layer.
These are in-process OpenClaw hooks, not Codex `hooks.json` command hooks:

View File

@@ -12,7 +12,7 @@ title: "ACP agents"
[Agent Client Protocol (ACP)](https://agentclientprotocol.com/) sessions let OpenClaw run external coding harnesses (for example Pi, Claude Code, Cursor, Copilot, OpenClaw ACP, OpenCode, Gemini CLI, and other supported ACPX harnesses) through an ACP backend plugin.
If you ask OpenClaw in plain language to bind or control Codex in the current conversation, OpenClaw should use the native Codex app-server plugin (`/codex bind`, `/codex threads`, `/codex resume`). If you ask for `/acp`, ACP, acpx, or a Codex background child session, OpenClaw can still route Codex through ACP. Each ACP session spawn is tracked as a [background task](/automation/tasks).
If you ask OpenClaw in plain language to bind or control Codex in the current conversation and the bundled `codex` plugin is enabled, OpenClaw should use the native Codex app-server plugin (`/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, `/codex stop`) instead of ACP. If you ask for `/acp`, ACP, acpx, or an ACP adapter test explicitly, OpenClaw can still route Codex through ACP. Each ACP session spawn is tracked as a [background task](/automation/tasks).
If you ask OpenClaw in plain language to "start Claude Code in a thread" or use another external harness, OpenClaw should route that request to the ACP runtime (not the native sub-agent runtime).
@@ -24,12 +24,12 @@ instead of ACP.
There are three nearby surfaces that are easy to confuse:
| You want to... | Use this | Notes |
| ----------------------------------------------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bind or control Codex in the current conversation | `/codex bind`, `/codex threads` | Native Codex app-server path; includes bound chat replies, image forwarding, model/fast/permissions, stop, and steer controls. ACP is an explicit fallback |
| Run Claude Code, Gemini CLI, explicit Codex ACP, or another external harness _through_ OpenClaw | This page: ACP agents | Chat-bound sessions, `/acp spawn`, `sessions_spawn({ runtime: "acp" })`, background tasks, runtime controls |
| Expose an OpenClaw Gateway session _as_ an ACP server for an editor or client | [`openclaw acp`](/cli/acp) | Bridge mode. IDE/client talks ACP to OpenClaw over stdio/WebSocket |
| Reuse a local AI CLI as a text-only fallback model | [CLI Backends](/gateway/cli-backends) | Not ACP. No OpenClaw tools, no ACP controls, no harness runtime |
| You want to... | Use this | Notes |
| ----------------------------------------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bind or control Codex in the current conversation | `/codex bind`, `/codex threads` | Native Codex app-server path when the `codex` plugin is enabled; includes bound chat replies, image forwarding, model/fast/permissions, stop, and steer controls. ACP is an explicit fallback |
| Run Claude Code, Gemini CLI, explicit Codex ACP, or another external harness _through_ OpenClaw | This page: ACP agents | Chat-bound sessions, `/acp spawn`, `sessions_spawn({ runtime: "acp" })`, background tasks, runtime controls |
| Expose an OpenClaw Gateway session _as_ an ACP server for an editor or client | [`openclaw acp`](/cli/acp) | Bridge mode. IDE/client talks ACP to OpenClaw over stdio/WebSocket |
| Reuse a local AI CLI as a text-only fallback model | [CLI Backends](/gateway/cli-backends) | Not ACP. No OpenClaw tools, no ACP controls, no harness runtime |
## Does this work out of the box?
@@ -53,7 +53,8 @@ Quick `/acp` flow from chat:
5. **Steer** without replacing context — `/acp steer tighten logging and continue`
6. **Stop**`/acp cancel` (current turn) or `/acp close` (session + bindings)
Natural-language triggers that should route to the native Codex plugin:
Natural-language triggers that should route to the native Codex plugin when it
is enabled:
- "Bind this Discord channel to Codex."
- "Attach this chat to Codex thread `<id>`."
@@ -77,7 +78,7 @@ Natural-language triggers that should route to the ACP runtime:
- "Use Gemini CLI for this task in a thread, then keep follow-ups in that same thread."
- "Run Codex through ACP in a background thread."
OpenClaw picks `runtime: "acp"`, resolves the harness `agentId`, binds to the current conversation or thread when supported, and routes follow-ups to that session until close/expiry. Codex only follows this path when ACP is explicit or the requested background runtime still needs ACP.
OpenClaw picks `runtime: "acp"`, resolves the harness `agentId`, binds to the current conversation or thread when supported, and routes follow-ups to that session until close/expiry. Codex only follows this path when ACP/acpx is explicit or the native Codex plugin is unavailable for the requested operation.
For `sessions_spawn`, `runtime: "acp"` is advertised only when ACP is enabled,
the requester is not sandboxed, and an ACP runtime backend is loaded. It targets
@@ -90,7 +91,7 @@ harness id.
## ACP versus sub-agents
Use ACP when you want an external harness runtime. Use native Codex app-server for Codex conversation binding/control. Use sub-agents when you want OpenClaw-native delegated runs.
Use ACP when you want an external harness runtime. Use native Codex app-server for Codex conversation binding/control when the `codex` plugin is enabled. Use sub-agents when you want OpenClaw-native delegated runs.
| Area | ACP session | Sub-agent run |
| ------------- | ------------------------------------- | ---------------------------------- |

View File

@@ -60,7 +60,7 @@ transcript path on disk when you need the raw full transcript.
- `--model` and `--thinking` override defaults for that specific run.
- Use `info`/`log` to inspect details and output after completion.
- `/subagents spawn` is one-shot mode (`mode: "run"`). For persistent thread-bound sessions, use `sessions_spawn` with `thread: true` and `mode: "session"`.
- For ACP harness sessions (Codex, Claude Code, Gemini CLI, OpenCode), use `sessions_spawn` with `runtime: "acp"` when the tool advertises that runtime, and see [ACP Agents](/tools/acp-agents), especially the [ACP delivery model](/tools/acp-agents#delivery-model) when debugging completions or agent-to-agent loops. OpenClaw hides `runtime: "acp"` until ACP is enabled, the requester is not sandboxed, and a backend plugin such as `acpx` is loaded. `runtime: "acp"` expects an external ACP harness id, or an `agents.list[]` entry with `runtime.type="acp"`; use the default sub-agent runtime for normal OpenClaw config agents from `agents_list`.
- For ACP harness sessions (Claude Code, Gemini CLI, OpenCode, or explicit Codex ACP/acpx), use `sessions_spawn` with `runtime: "acp"` when the tool advertises that runtime, and see [ACP Agents](/tools/acp-agents), especially the [ACP delivery model](/tools/acp-agents#delivery-model) when debugging completions or agent-to-agent loops. When the `codex` plugin is enabled, Codex chat/thread control should prefer `/codex ...` over ACP unless the user explicitly asks for ACP/acpx. OpenClaw hides `runtime: "acp"` until ACP is enabled, the requester is not sandboxed, and a backend plugin such as `acpx` is loaded. `runtime: "acp"` expects an external ACP harness id, or an `agents.list[]` entry with `runtime.type="acp"`; use the default sub-agent runtime for normal OpenClaw config agents from `agents_list`.
Primary goals:
@@ -103,7 +103,7 @@ Tool params:
- `task` (required)
- `label?` (optional)
- `agentId?` (optional; spawn under another agent id if allowed)
- `runtime?` (`subagent|acp`, default `subagent`; `acp` is only for external ACP harnesses such as `codex`, `claude`, `gemini`, or `opencode`, or for `agents.list[]` entries whose `runtime.type` is `acp`)
- `runtime?` (`subagent|acp`, default `subagent`; `acp` is only for external ACP harnesses such as `claude`, `gemini`, `opencode`, or explicitly requested Codex ACP/acpx, or for `agents.list[]` entries whose `runtime.type` is `acp`)
- `model?` (optional; overrides the sub-agent model; invalid values are skipped and the sub-agent runs on the default model with a warning in the tool result)
- `thinking?` (optional; overrides thinking level for the sub-agent run)
- `runTimeoutSeconds?` (defaults to `agents.defaults.subagents.runTimeoutSeconds` when set, otherwise `0`; when set, the sub-agent run is aborted after N seconds)
@@ -159,7 +159,7 @@ Allowlist:
Discovery:
- Use `agents_list` to see which agent ids are currently allowed for `sessions_spawn`.
- Use `agents_list` to see which agent ids are currently allowed for `sessions_spawn`. The response includes each listed agent's effective model and embedded runtime metadata so callers can distinguish PI, Codex app-server, and other configured native runtimes.
Auto-archive:

View File

@@ -21,6 +21,7 @@ import { formatErrorMessage } from "../../../infra/errors.js";
import { resolveHeartbeatSummaryForAgent } from "../../../infra/heartbeat-summary.js";
import { getMachineDisplayName } from "../../../infra/machine-name.js";
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
import { listRegisteredPluginCommands } from "../../../plugins/command-registry-state.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import {
extractModelCompat,
@@ -1125,6 +1126,7 @@ export async function runEmbeddedAttempt(
config: params.config,
sandboxed: sandboxInfo?.enabled === true,
}),
nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name),
runtimeInfo,
messageToolHints,
sandboxInfo,

View File

@@ -32,6 +32,8 @@ export function buildEmbeddedSystemPrompt(params: {
promptMode?: PromptMode;
/** Whether ACP-specific routing guidance should be included. Defaults to true. */
acpEnabled?: boolean;
/** Registered runtime slash/native command names such as `codex`. */
nativeCommandNames?: string[];
runtimeInfo: {
agentId?: string;
host: string;
@@ -76,6 +78,7 @@ export function buildEmbeddedSystemPrompt(params: {
reactionGuidance: params.reactionGuidance,
promptMode: params.promptMode,
acpEnabled: params.acpEnabled,
nativeCommandNames: params.nativeCommandNames,
runtimeInfo: params.runtimeInfo,
messageToolHints: params.messageToolHints,
sandboxInfo: params.sandboxInfo,

View File

@@ -5,6 +5,7 @@ import { isAcpRuntimeSpawnAvailable } from "../acp/runtime/availability.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { SubagentSpawnPreparation } from "../context-engine/types.js";
import { listRegisteredPluginCommands } from "../plugins/command-registry-state.js";
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
import { isValidAgentId, normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import {
@@ -868,6 +869,7 @@ export async function spawnSubagentDirect(
config: cfg,
sandboxed: childRuntime.sandboxed,
}),
nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name),
childDepth,
maxSpawnDepth,
});

View File

@@ -9,6 +9,8 @@ export function buildSubagentSystemPrompt(params: {
task?: string;
/** Whether ACP-specific routing guidance should be included. Defaults to true. */
acpEnabled?: boolean;
/** Registered runtime slash/native command names such as `codex`. */
nativeCommandNames?: string[];
/** Depth of the child being spawned (1 = sub-agent, 2 = sub-sub-agent). */
childDepth?: number;
/** Config value: max allowed spawn depth. */
@@ -24,6 +26,9 @@ export function buildSubagentSystemPrompt(params: {
? params.maxSpawnDepth
: DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH;
const acpEnabled = params.acpEnabled !== false;
const nativeCodexCommandAvailable = (params.nativeCommandNames ?? []).some(
(name) => name.trim().replace(/^\/+/, "").toLowerCase() === "codex",
);
const canSpawn = childDepth < maxSpawnDepth;
const parentLabel = childDepth >= 2 ? "parent orchestrator" : "main agent";
@@ -75,7 +80,12 @@ export function buildSubagentSystemPrompt(params: {
"Coordinate their work and synthesize results before reporting back.",
...(acpEnabled
? [
'For ACP harness sessions (codex/claudecode/gemini), use `sessions_spawn` with `runtime: "acp"` (set `agentId` unless `acp.defaultAgent` is configured).',
...(nativeCodexCommandAvailable
? [
"Native Codex app-server plugin is available (`/codex ...`). Prefer that path for Codex bind/control/thread/resume/steer/stop requests; use Codex ACP only when explicitly requested.",
]
: []),
'For ACP harness sessions (claudecode/gemini/opencode, or Codex only when explicit ACP/acpx), use `sessions_spawn` with `runtime: "acp"` (set `agentId` unless `acp.defaultAgent` is configured).',
'`agents_list` and `subagents` apply to OpenClaw sub-agents (`runtime: "subagent"`); ACP harness ids are controlled by `acp.allowedAgents`.',
"Do not ask users to run slash commands or CLI when `sessions_spawn` can do it directly.",
"Do not use `exec` (`openclaw ...`, `acpx ...`) to spawn ACP sessions.",

View File

@@ -317,13 +317,14 @@ describe("buildAgentSystemPrompt", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
toolNames: ["sessions_spawn", "subagents", "agents_list", "exec"],
nativeCommandNames: ["codex"],
});
expect(prompt).toContain("Native Codex app-server plugin is available");
expect(prompt).toContain("prefer `/codex bind`, `/codex threads`, `/codex resume`");
expect(prompt).toContain("Use ACP for Codex only when the user explicitly asks for ACP/acpx");
expect(prompt).toContain(
'For requests like "do this in claude code/cursor/gemini" or similar ACP harnesses, treat it as ACP harness intent',
);
expect(prompt).toContain(
"For Codex conversation binding/control, prefer the native Codex app-server plugin path",
'For requests like "do this in claude code/cursor/gemini/opencode" or similar ACP harnesses, treat it as ACP harness intent',
);
expect(prompt).toContain(
'On Discord, default ACP harness requests to thread-bound persistent sessions (`thread: true`, `mode: "session"`)',
@@ -344,8 +345,9 @@ describe("buildAgentSystemPrompt", () => {
});
expect(prompt).not.toContain(
'For requests like "do this in codex/claude code/gemini", treat it as ACP harness intent',
'For requests like "do this in claude code/cursor/gemini/opencode" or similar ACP harnesses, treat it as ACP harness intent',
);
expect(prompt).not.toContain("Native Codex app-server plugin is available");
expect(prompt).not.toContain('runtime="acp" requires `agentId`');
expect(prompt).not.toContain("not ACP harness ids");
expect(prompt).toContain("- sessions_spawn: Spawn an isolated sub-agent session");
@@ -364,7 +366,7 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).not.toContain('runtime="acp" requires `agentId`');
expect(prompt).not.toContain("ACP harness ids follow acp.allowedAgents");
expect(prompt).not.toContain(
'For requests like "do this in codex/claude code/gemini", treat it as ACP harness intent',
'For requests like "do this in claude code/cursor/gemini/opencode" or similar ACP harnesses, treat it as ACP harness intent',
);
expect(prompt).not.toContain(
'do not call `message` with `action=thread-create`; use `sessions_spawn` (`runtime: "acp"`, `thread: true`) as the single thread creation path',
@@ -941,7 +943,7 @@ describe("buildSubagentSystemPrompt", () => {
);
expect(prompt).toContain("sessions_spawn");
expect(prompt).toContain('runtime: "acp"');
expect(prompt).toContain("For ACP harness sessions (codex/claudecode/gemini)");
expect(prompt).toContain("For ACP harness sessions (claudecode/gemini/opencode");
expect(prompt).toContain("set `agentId` unless `acp.defaultAgent` is configured");
expect(prompt).toContain("Do not ask users to run slash commands or CLI");
expect(prompt).toContain("Do not use `exec` (`openclaw ...`, `acpx ...`)");
@@ -974,11 +976,24 @@ describe("buildSubagentSystemPrompt", () => {
});
expect(prompt).not.toContain('runtime: "acp"');
expect(prompt).not.toContain("For ACP harness sessions (codex/claudecode/gemini)");
expect(prompt).not.toContain("For ACP harness sessions (claudecode/gemini/opencode");
expect(prompt).not.toContain("set `agentId` unless `acp.defaultAgent` is configured");
expect(prompt).toContain("You CAN spawn your own sub-agents");
});
it("prefers native Codex commands over Codex ACP when available", () => {
const prompt = buildSubagentSystemPrompt({
childSessionKey: "agent:main:subagent:abc",
task: "research task",
childDepth: 1,
maxSpawnDepth: 2,
nativeCommandNames: ["codex"],
});
expect(prompt).toContain("Native Codex app-server plugin is available");
expect(prompt).toContain("use Codex ACP only when explicitly requested");
});
it("renders depth-2 leaf guidance with parent orchestrator labels", () => {
const prompt = buildSubagentSystemPrompt({
childSessionKey: "agent:main:subagent:abc:subagent:def",

View File

@@ -381,6 +381,14 @@ function buildMessagingSection(params: {
];
}
function hasNativeCommand(params: { nativeCommandNames?: string[]; command: string }): boolean {
const target = normalizeLowercaseStringOrEmpty(params.command);
return (params.nativeCommandNames ?? []).some((name) => {
const normalized = normalizeLowercaseStringOrEmpty(name).replace(/^\/+/, "");
return normalized === target;
});
}
function buildVoiceSection(params: { isMinimal: boolean; ttsHint?: string }) {
if (params.isMinimal) {
return [];
@@ -461,6 +469,8 @@ export function buildAgentSystemPrompt(params: {
promptMode?: PromptMode;
/** Whether ACP-specific routing guidance should be included. Defaults to true. */
acpEnabled?: boolean;
/** Registered runtime slash/native command names such as `codex`. */
nativeCommandNames?: string[];
runtimeInfo?: {
agentId?: string;
host?: string;
@@ -569,6 +579,10 @@ export function buildAgentSystemPrompt(params: {
const availableTools = new Set(normalizedTools);
const hasSessionsSpawn = availableTools.has("sessions_spawn");
const acpHarnessSpawnAllowed = hasSessionsSpawn && acpSpawnRuntimeEnabled;
const nativeCodexCommandAvailable = hasNativeCommand({
nativeCommandNames: params.nativeCommandNames,
command: "codex",
});
const externalToolSummaries = new Map<string, string>();
for (const [key, value] of Object.entries(params.toolSummaries ?? {})) {
const normalized = key.trim().toLowerCase();
@@ -718,10 +732,15 @@ export function buildAgentSystemPrompt(params: {
`For long waits, avoid rapid poll loops: use ${execToolName} with enough yieldMs or ${processToolName}(action=poll, timeout=<ms>).`,
"If a task is more complex or takes longer, spawn a sub-agent. Completion is push-based: it will auto-announce when done.",
'Sub-agents start isolated by default. Use `sessions_spawn` with `context:"fork"` only when the child needs the current transcript context; otherwise omit `context` or use `context:"isolated"`.',
...(nativeCodexCommandAvailable
? [
"Native Codex app-server plugin is available (`/codex ...`). For Codex bind/control/thread/resume/steer/stop requests, prefer `/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, and `/codex stop` over ACP.",
"Use ACP for Codex only when the user explicitly asks for ACP/acpx or wants to test the ACP path.",
]
: []),
...(acpHarnessSpawnAllowed
? [
'For requests like "do this in claude code/cursor/gemini" or similar ACP harnesses, treat it as ACP harness intent and call `sessions_spawn` with `runtime: "acp"`.',
"For Codex conversation binding/control, prefer the native Codex app-server plugin path (`/codex bind`, `/codex threads`, `/codex resume`). Use ACP for Codex only when the user explicitly asks for ACP/`/acp`, or for background child sessions where native Codex runtime spawn is not exposed.",
'For requests like "do this in claude code/cursor/gemini/opencode" or similar ACP harnesses, treat it as ACP harness intent and call `sessions_spawn` with `runtime: "acp"`.',
'On Discord, default ACP harness requests to thread-bound persistent sessions (`thread: true`, `mode: "session"`) unless the user asks otherwise.',
"Set `agentId` explicitly unless `acp.defaultAgent` is configured, and do not route ACP harness requests through `subagents`/`agents_list` or local PTY exec flows.",
'For ACP harness thread spawns, do not call `message` with `action=thread-create`; use `sessions_spawn` (`runtime: "acp"`, `thread: true`) as the single thread creation path.',

View File

@@ -0,0 +1,93 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
const loadConfigMock = vi.fn<() => OpenClawConfig>();
vi.mock("../../config/config.js", async () => {
const actual =
await vi.importActual<typeof import("../../config/config.js")>("../../config/config.js");
return {
...actual,
loadConfig: () => loadConfigMock(),
};
});
describe("agents_list tool", () => {
beforeEach(() => {
vi.unstubAllEnvs();
loadConfigMock.mockReset();
});
it("returns model and embedded harness metadata for allowed agents", async () => {
loadConfigMock.mockReturnValue({
agents: {
defaults: {
model: "anthropic/claude-opus-4.5",
embeddedHarness: { runtime: "pi", fallback: "pi" },
subagents: { allowAgents: ["codex"] },
},
list: [
{ id: "main", default: true },
{
id: "codex",
name: "Codex",
model: "openai/gpt-5.5",
embeddedHarness: { runtime: "codex", fallback: "none" },
},
],
},
} satisfies OpenClawConfig);
const { createAgentsListTool } = await import("./agents-list-tool.js");
const result = await createAgentsListTool({ agentSessionKey: "agent:main:main" }).execute(
"call",
{},
);
expect(result.details).toMatchObject({
requester: "main",
agents: [
{
id: "main",
configured: true,
model: "anthropic/claude-opus-4.5",
embeddedHarness: { runtime: "pi", source: "defaults" },
},
{
id: "codex",
name: "Codex",
configured: true,
model: "openai/gpt-5.5",
embeddedHarness: { runtime: "codex", fallback: "none", source: "agent" },
},
],
});
});
it("marks OPENCLAW_AGENT_RUNTIME as the effective runtime source", async () => {
vi.stubEnv("OPENCLAW_AGENT_RUNTIME", "codex");
loadConfigMock.mockReturnValue({
agents: {
defaults: {
model: "openai/gpt-5.5",
},
list: [{ id: "main", default: true }],
},
} satisfies OpenClawConfig);
const { createAgentsListTool } = await import("./agents-list-tool.js");
const result = await createAgentsListTool({ agentSessionKey: "agent:main:main" }).execute(
"call",
{},
);
expect(result.details).toMatchObject({
agents: [
{
id: "main",
embeddedHarness: { runtime: "codex", source: "env" },
},
],
});
});
});

View File

@@ -5,7 +5,11 @@ import {
normalizeAgentId,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { resolveAgentConfig } from "../agent-scope.js";
import {
listAgentEntries,
resolveAgentConfig,
resolveAgentEffectiveModelPrimary,
} from "../agent-scope.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult } from "./common.js";
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
@@ -16,8 +20,55 @@ type AgentListEntry = {
id: string;
name?: string;
configured: boolean;
model?: string;
embeddedHarness?: {
runtime: string;
fallback?: "pi" | "none";
source: "env" | "agent" | "defaults" | "implicit";
};
};
function normalizeRuntimeValue(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
}
function resolveAgentEmbeddedHarnessMetadata(
cfg: ReturnType<typeof loadConfig>,
agentId: string,
): AgentListEntry["embeddedHarness"] {
const envRuntime = normalizeRuntimeValue(process.env.OPENCLAW_AGENT_RUNTIME);
if (envRuntime) {
return {
runtime: envRuntime,
source: "env",
};
}
const agentEntry = listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === agentId);
const agentRuntime = normalizeRuntimeValue(agentEntry?.embeddedHarness?.runtime);
if (agentRuntime) {
return {
runtime: agentRuntime,
fallback: agentEntry?.embeddedHarness?.fallback,
source: "agent",
};
}
const defaultsRuntime = normalizeRuntimeValue(cfg.agents?.defaults?.embeddedHarness?.runtime);
if (defaultsRuntime) {
return {
runtime: defaultsRuntime,
fallback: cfg.agents?.defaults?.embeddedHarness?.fallback,
source: "defaults",
};
}
return {
runtime: "pi",
source: "implicit",
};
}
export function createAgentsListTool(opts?: {
agentSessionKey?: string;
/** Explicit agent ID override for cron/hook sessions. */
@@ -89,6 +140,8 @@ export function createAgentsListTool(opts?: {
id,
name: configuredNameMap.get(id),
configured: configuredIds.includes(id),
model: resolveAgentEffectiveModelPrimary(cfg, id),
embeddedHarness: resolveAgentEmbeddedHarnessMetadata(cfg, id),
}));
return jsonResult({

View File

@@ -14,6 +14,7 @@ import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
import type { WorkspaceBootstrapFile } from "../../agents/workspace.js";
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
import { listRegisteredPluginCommands } from "../../plugins/command-registry-state.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import type { HandleCommandsParams } from "./commands-types.js";
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
@@ -167,6 +168,7 @@ export async function resolveCommandsSystemPromptBundle(
config: params.cfg,
sandboxed: sandboxRuntime.sandboxed,
}),
nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name),
runtimeInfo,
sandboxInfo,
memoryCitationsMode: params.cfg?.memory?.citations,