feat(custom-agents): add filesystem-based loading of Claude subagents from .claude/agents/*.md#1212
feat(custom-agents): add filesystem-based loading of Claude subagents from .claude/agents/*.md#1212mcljot wants to merge 3 commits intoambient-code:mainfrom
Conversation
The Claude Agent SDK supports custom subagent types via the `agents` parameter on ClaudeAgentOptions, but the runner never populated it. This meant sessions could only use the 4 built-in agent types (general-purpose, Explore, Plan, statusline-setup) — custom agents defined as .md files in .claude/agents/ were invisible to the SDK. Add a new agents.py utility that scans .claude/agents/*.md, parses frontmatter (name, description, tools, model, skills) and the markdown body (prompt), and constructs AgentDefinition objects. These are loaded during _setup_platform() and passed through the options dict in _ensure_adapter(), where build_options() already merges them into ClaudeAgentOptions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughLoads workspace-local Claude agent definitions from Changes
Sequence Diagram(s)sequenceDiagram
participant Bridge as ClaudeBridge
participant Loader as load_agents_from_directory
participant FS as File System
participant Parser as Frontmatter Parser
participant Factory as AgentDefinition Factory
participant Adapter as ClaudeAgentAdapter
Bridge->>+Loader: load_agents_from_directory(agents_dir)
Loader->>+FS: list *.md in .claude/agents
FS-->>-Loader: file list
loop each file
Loader->>Parser: parse file content (frontmatter → metadata, body)
Parser-->>Loader: metadata, body
Loader->>Factory: build AgentDefinition(metadata, body)
Factory-->>Loader: AgentDefinition
end
Loader-->>-Bridge: dict[name → AgentDefinition]
Bridge->>Adapter: provide options (including agents)
Adapter-->>Bridge: adapter initialized with custom agents
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py`:
- Around line 27-30: The current except OSError block that returns {}, "" allows
unreadable files to proceed and later creates an AgentDefinition with an empty
prompt; change the error handling so unreadable files are skipped: in the
file-reading code (the try around file_path.read_text and its except OSError)
return a sentinel (e.g., None) or raise so the caller can filter them out, and
update the agent-building logic that constructs AgentDefinition with the
fallback name/prompt (the AgentDefinition construction path) to ignore any
None/missing inputs instead of building an empty agent.
- Around line 32-40: The frontmatter parsing only looks for Unix newlines (using
content.startswith("---\n") and content.find("\n---", 4)), so files with CRLF
("\r\n") line endings won't match; normalize line endings or make the boundary
detection CRLF-aware before extracting frontmatter. Modify the code that checks
content.startswith, content.find, and computes frontmatter_raw/body (symbols:
content.startswith, content.find, frontmatter_raw, body) to first normalize
content (e.g., replace "\r\n" with "\n") or use a regex/split that accepts
"\r?\n" so name/tools/model/skills metadata are correctly extracted regardless
of CRLF vs LF.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 50c86e0e-77e2-4a75-a6f4-a4722275b564
📒 Files selected for processing (3)
components/runners/ambient-runner/ambient_runner/bridges/claude/agents.pycomponents/runners/ambient-runner/ambient_runner/bridges/claude/bridge.pycomponents/runners/ambient-runner/tests/test_custom_agents.py
components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py
Outdated
Show resolved
Hide resolved
components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py
Show resolved
Hide resolved
Re-raise OSError from _parse_agent_file so unreadable files are skipped instead of silently creating agents with empty prompts. Normalise CRLF to LF before frontmatter parsing so Windows-origin files work correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py`:
- Around line 106-113: Resolve and validate the agent name before inserting into
the agents dict: retrieve and trim the name (replace name = metadata.get("name",
md_file.stem) with something like resolved_name = (metadata.get("name") or
md_file.stem).strip()), ensure resolved_name is not empty (fallback to
md_file.stem if it is), and then check agents for an existing key (e.g., if
resolved_name in agents) and either raise/return an explicit error or log and
skip to avoid silent overwrites; finally use resolved_name when constructing the
AgentDefinition and inserting into agents.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ea53e688-2d04-4798-83d7-55b280a0c795
📒 Files selected for processing (1)
components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py
| name = metadata.get("name", md_file.stem) | ||
| description = metadata.get("description", f"Agent: {name}") | ||
| tools = _parse_string_list(metadata.get("tools")) | ||
| skills = _parse_string_list(metadata.get("skills")) | ||
| model = metadata.get("model") | ||
|
|
||
| agents[name] = AgentDefinition( | ||
| description=description, |
There was a problem hiding this comment.
Validate resolved agent names and prevent silent overwrites.
At Line 106, an empty frontmatter name (e.g., name:) is accepted, and at Line 112 duplicate names overwrite previously loaded agents without warning. This can silently drop intended subagent registrations.
Suggested fix
- name = metadata.get("name", md_file.stem)
+ name = (metadata.get("name") or md_file.stem).strip()
+ if not name:
+ logger.warning(
+ "Skipping agent from %s: resolved name is empty", md_file.name
+ )
+ continue
+ if name in agents:
+ logger.warning(
+ "Skipping duplicate custom agent '%s' from %s",
+ name,
+ md_file.name,
+ )
+ continue
description = metadata.get("description", f"Agent: {name}")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py`
around lines 106 - 113, Resolve and validate the agent name before inserting
into the agents dict: retrieve and trim the name (replace name =
metadata.get("name", md_file.stem) with something like resolved_name =
(metadata.get("name") or md_file.stem).strip()), ensure resolved_name is not
empty (fallback to md_file.stem if it is), and then check agents for an existing
key (e.g., if resolved_name in agents) and either raise/return an explicit error
or log and skip to avoid silent overwrites; finally use resolved_name when
constructing the AgentDefinition and inserting into agents.
Summary
ACP sessions can only dispatch the 4 built-in agent types (
general-purpose,Explore,Plan,statusline-setup). Custom agents defined as.claude/agents/*.mdfiles in a project's repo are ignored because the runner never loads them into the SDK — even thoughClaudeAgentOptions.agentssupports exactly this viadict[str, AgentDefinition].This PR adds filesystem-based custom agent loading to the Claude bridge so that
.claude/agents/*.mdfiles are parsed at session startup and passed to the SDK as registered subagent types.Changes
agents.pymodule — scans.claude/agents/*.md, parses frontmatter (name,description,tools,model,skills) and markdown body(
prompt), returnsdict[str, AgentDefinition]bridge.py— loads custom agents in_setup_platform()after workspace path resolution, passes them viaoptions["agents"]in_ensure_adapter()Rationale
The
.claude/agents/filesystem loading is a Claude Code CLI feature — the CLI reads those files and converts them toAgentDefinitionobjects before passing them to the SDK. The ACP runner never did this conversion. The runner already parses these files for the workflow metadata endpoint (content.py), but only to surface names to the frontend UI — it never constructed SDKAgentDefinitionobjects for actual session dispatch.The
build_options()method inadapter.pyalready merges the full options dict intoClaudeAgentOptions(**merged_kwargs), so adding"agents"to the dict passes through to the SDK with no adapter changes needed.Testing
cd components/runners/ambient-runner && python -m pytest tests/test_custom_agents.py -v.claude/agents/*.mdfilesAgent(subagent_type="<custom-agent-name>")dispatches successfully"Loaded N custom agent(s)"messagesNotes
content.py(no pyyaml)AgentDefinitionis from the existingclaude-agent-sdk>=0.1.50dependencysilently dropped when combined system prompt + agent definitions exceed an undocumented token limit. This is tracked upstream.
Summary by CodeRabbit
New Features
.claude/agentsdirectory. Agent metadata (name, description, model, tools, skills) and prompt body are parsed, with sensible fallbacks when fields are missing.Tests