mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:20:44 +00:00
fix: prefer native codex app-server controls
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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 |
|
||||
| ------------- | ------------------------------------- | ---------------------------------- |
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.',
|
||||
|
||||
93
src/agents/tools/agents-list-tool.test.ts
Normal file
93
src/agents/tools/agents-list-tool.test.ts
Normal 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" },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user