diff --git a/CHANGELOG.md b/CHANGELOG.md index 9170e17cd3d..0a9f39aefdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/concepts/agent-runtimes.md b/docs/concepts/agent-runtimes.md index 56e1130e796..2dd067f5b8e 100644 --- a/docs/concepts/agent-runtimes.md +++ b/docs/concepts/agent-runtimes.md @@ -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). diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index f2847597c6b..a0b0ca8487f 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -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: diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index 3ce29df5aad..977713904d4 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -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 ``." @@ -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 | | ------------- | ------------------------------------- | ---------------------------------- | diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index 9c9132c965e..a77512a6ae2 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -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: diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 6b8b1fee398..5c2fefd28e4 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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, diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/pi-embedded-runner/system-prompt.ts index 8a9b8df7f43..cc0ec414779 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/pi-embedded-runner/system-prompt.ts @@ -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, diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index cc2d08452e3..95a87b0591e 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -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, }); diff --git a/src/agents/subagent-system-prompt.ts b/src/agents/subagent-system-prompt.ts index f06358ed184..8ca92350356 100644 --- a/src/agents/subagent-system-prompt.ts +++ b/src/agents/subagent-system-prompt.ts @@ -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.", diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index c874339a2f0..41c24574985 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -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", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 9d624b8bff2..e53b1798746 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -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(); 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=).`, "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.', diff --git a/src/agents/tools/agents-list-tool.test.ts b/src/agents/tools/agents-list-tool.test.ts new file mode 100644 index 00000000000..067fac08983 --- /dev/null +++ b/src/agents/tools/agents-list-tool.test.ts @@ -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("../../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" }, + }, + ], + }); + }); +}); diff --git a/src/agents/tools/agents-list-tool.ts b/src/agents/tools/agents-list-tool.ts index 63e17338d11..c5ba9f93698 100644 --- a/src/agents/tools/agents-list-tool.ts +++ b/src/agents/tools/agents-list-tool.ts @@ -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, + 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({ diff --git a/src/auto-reply/reply/commands-system-prompt.ts b/src/auto-reply/reply/commands-system-prompt.ts index ee61da42cf4..c79d25384f3 100644 --- a/src/auto-reply/reply/commands-system-prompt.ts +++ b/src/auto-reply/reply/commands-system-prompt.ts @@ -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,