Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/agent-profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Define the agent's role, responsibilities, and behavior here.
- `model` (string): AI model to use
- `permissionMode` (string, `claude_code` only): One of `"default"`, `"acceptEdits"`, `"plan"`, `"auto"`, `"bypassPermissions"`. When set, the `claude_code` provider passes `--permission-mode <value>` instead of `--dangerously-skip-permissions`. `cao launch --yolo` overrides this and forces bypass. See [Claude Code permission modes](https://code.claude.com/docs/en/permission-modes).
- `native_agent` (string, `claude_code` only): Name of a native Claude Code agent (`~/.claude/agents/`). When set, the provider passes `--agent <name>` directly and skips system prompt / MCP config decomposition (thin-wrapper mode). See [Claude Code native agent routing](claude-code.md#native-agent-routing).
- `claudeConfig` (object, `claude_code` only): Per-agent Claude Code knobs mapped to CLI flags at launch. `{"effort": "<low|medium|high|xhigh>"}` adds `--effort <level>` and `{"fallback_model": "<model>"}` adds `--fallback-model <model>`. Lets a profile set per-agent reasoning effort without relying on the machine-global `effortLevel` in `~/.claude/settings.json`. The Claude analog of `codexConfig`; the top-level `model` field still maps to `--model`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context: codexConfig is introduced by the sibling PR #278 (feat/codex-agent-config). These two are a pair — per-agent provider config: codexConfig for the codex provider, claudeConfig for claude_code — so the cross-reference is intentional and resolves once both land. If you'd prefer each PR to read as fully self-contained regardless of merge order, I'm happy to drop the codexConfig mention here.

- `hermesProfile` (string, `hermes` only): Optional Hermes profile wrapper command CAO should launch instead of the default `hermes`, for example one created with `hermes profile alias test-worker`. This is intentionally separate from `codexProfile`: Codex consumes profile names via `codex --profile <name>`, while Hermes aliases are executable commands launched directly as `<alias> chat ...`. See [Hermes Provider](hermes.md).
- `prompt` (string): Additional prompt text

Expand Down
9 changes: 9 additions & 0 deletions src/cli_agent_orchestrator/models/agent_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ class AgentProfile(BaseModel):
# example one created by `hermes profile alias <profile>`). When omitted,
# the Hermes provider launches the default `hermes` command.
hermesProfile: Optional[str] = Field(default=None, min_length=1)

# Claude Code-only. Per-agent Claude Code knobs mapped to CLI flags at
# launch: {"effort": "<low|medium|high|xhigh>"} -> `--effort <level>` and
# {"fallback_model": "<model>"} -> `--fallback-model <model>`. Lets a
# profile set per-agent reasoning effort without relying on the
# machine-global `effortLevel` in ~/.claude/settings.json. This is the
# Claude analog of codexConfig for the codex provider; the top-level
# `model` field still maps to `--model`.
Comment on lines +55 to +61

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context: codexConfig is added by the sibling PR #278 (feat/codex-agent-config). claudeConfig here is the deliberate claude_code analog of that field, so this comment cross-references it on purpose; it becomes accurate once both PRs merge. If you'd rather keep this PR independent of merge order, I can drop the codexConfig reference and just describe claudeConfig on its own.

claudeConfig: Optional[Dict[str, Any]] = None
15 changes: 15 additions & 0 deletions src/cli_agent_orchestrator/providers/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ def _build_claude_command(self) -> str:
if profile.model:
command_parts.extend(["--model", profile.model])

# Apply Claude Code-only per-agent knobs from claudeConfig:
# effort -> --effort <level>
# fallback_model -> --fallback-model <model>
# Claude analog of codexConfig: per-agent reasoning effort without
# depending on the machine-global effortLevel in
# ~/.claude/settings.json.
Comment on lines +123 to +128

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context: codexConfig is introduced by the sibling PR #278 (feat/codex-agent-config). This launch-override comment intentionally mirrors that field — claudeConfig is the claude_code analog — so it resolves once both PRs merge. If reviewers prefer this PR to stand alone regardless of merge order, I'm happy to drop the codexConfig mention and describe the behavior on its own.

claude_config = getattr(profile, "claudeConfig", None)
if isinstance(claude_config, dict):
effort = claude_config.get("effort")
if effort:
command_parts.extend(["--effort", str(effort)])
fallback_model = claude_config.get("fallback_model")
if fallback_model:
command_parts.extend(["--fallback-model", str(fallback_model)])

# Add system prompt - escape newlines to prevent tmux chunking issues
system_prompt = profile.system_prompt if profile.system_prompt is not None else ""
system_prompt = self._apply_skill_prompt(system_prompt)
Expand Down
49 changes: 49 additions & 0 deletions test/providers/test_claude_code_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,55 @@ def test_build_command_omits_model_when_unset(self, mock_load):
assert "--model" not in command


class TestClaudeCodeProviderClaudeConfig:
"""Tests that profile.claudeConfig maps to Claude Code CLI flags."""

@patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile")
def test_build_command_appends_effort_from_claude_config(self, mock_load):
mock_profile = MagicMock()
mock_profile.model = None
mock_profile.system_prompt = None
mock_profile.mcpServers = None
mock_profile.permissionMode = None
mock_profile.claudeConfig = {"effort": "xhigh"}
mock_load.return_value = mock_profile

provider = ClaudeCodeProvider("tid", "sess", "win", "agent")
command = provider._build_claude_command()

assert "--effort xhigh" in command

@patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile")
def test_build_command_appends_fallback_model_from_claude_config(self, mock_load):
mock_profile = MagicMock()
mock_profile.model = None
mock_profile.system_prompt = None
mock_profile.mcpServers = None
mock_profile.permissionMode = None
mock_profile.claudeConfig = {"fallback_model": "sonnet"}
mock_load.return_value = mock_profile

provider = ClaudeCodeProvider("tid", "sess", "win", "agent")
command = provider._build_claude_command()

assert "--fallback-model sonnet" in command

@patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile")
def test_build_command_omits_effort_when_claude_config_absent(self, mock_load):
mock_profile = MagicMock()
mock_profile.model = None
mock_profile.system_prompt = None
mock_profile.mcpServers = None
mock_profile.permissionMode = None
mock_profile.claudeConfig = None
mock_load.return_value = mock_profile

provider = ClaudeCodeProvider("tid", "sess", "win", "agent")
command = provider._build_claude_command()

assert "--effort" not in command


class TestClaudeCodeProviderPermissionMode:

@patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile")
Expand Down
Loading