Skip to content

Commit bd62996

Browse files
committed
feat: add subagent execution control and improve HookMatcher docs
- Add SubagentExecutionConfig for controlling parallel vs sequential subagent execution when multiple Task tools are invoked in same turn - Add execution_mode parameter to AgentDefinition for per-agent control - Add subagent_execution field to ClaudeAgentOptions for global config - Improve HookMatcher docstring with comprehensive examples including MCP tool matching patterns (mcp__server__tool format) - Add new type aliases: SubagentExecutionMode, MultiInvocationMode, SubagentErrorHandling - Add comprehensive tests for all new types Addresses issues #2 (Critical: subagent execution mode) and #3 (Medium: HookMatcher documentation).
1 parent ccff8dd commit bd62996

File tree

3 files changed

+270
-10
lines changed

3 files changed

+270
-10
lines changed

src/claude_agent_sdk/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
McpSdkServerConfig,
3131
McpServerConfig,
3232
Message,
33+
MultiInvocationMode,
3334
PermissionMode,
3435
PermissionResult,
3536
PermissionResultAllow,
@@ -46,6 +47,9 @@
4647
SdkPluginConfig,
4748
SettingSource,
4849
StopHookInput,
50+
SubagentErrorHandling,
51+
SubagentExecutionConfig,
52+
SubagentExecutionMode,
4953
SubagentStopHookInput,
5054
SystemMessage,
5155
TextBlock,
@@ -344,6 +348,11 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
344348
# Agent support
345349
"AgentDefinition",
346350
"SettingSource",
351+
# Subagent execution support
352+
"SubagentExecutionConfig",
353+
"SubagentExecutionMode",
354+
"MultiInvocationMode",
355+
"SubagentErrorHandling",
347356
# Plugin support
348357
"SdkPluginConfig",
349358
# Beta support

src/claude_agent_sdk/types.py

Lines changed: 130 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,88 @@ class ToolsPreset(TypedDict):
3939
preset: Literal["claude_code"]
4040

4141

42+
# Subagent execution modes
43+
SubagentExecutionMode = Literal["sequential", "parallel", "auto"]
44+
45+
# Multi-invocation handling modes
46+
MultiInvocationMode = Literal["sequential", "parallel", "error"]
47+
48+
# Error handling modes for subagent execution
49+
SubagentErrorHandling = Literal["fail_fast", "continue"]
50+
51+
52+
@dataclass
53+
class SubagentExecutionConfig:
54+
"""Configuration for subagent execution behavior.
55+
56+
This controls how the SDK handles multiple Task tool calls (subagent invocations)
57+
when they occur in the same turn.
58+
59+
Attributes:
60+
multi_invocation: How to handle multiple Task tool calls in the same turn.
61+
- "sequential": Execute subagents one at a time in order (default)
62+
- "parallel": Execute subagents concurrently
63+
- "error": Raise an error if multiple Task tools are invoked in same turn
64+
max_concurrent: Maximum number of subagents to run concurrently when
65+
multi_invocation is "parallel". Default is 3.
66+
error_handling: How to handle errors from individual subagents.
67+
- "fail_fast": Stop execution on first subagent error
68+
- "continue": Continue executing remaining subagents on error (default)
69+
70+
Example:
71+
>>> config = SubagentExecutionConfig(
72+
... multi_invocation="parallel",
73+
... max_concurrent=5,
74+
... error_handling="fail_fast"
75+
... )
76+
>>> options = ClaudeAgentOptions(subagent_execution=config)
77+
"""
78+
79+
multi_invocation: MultiInvocationMode = "sequential"
80+
max_concurrent: int = 3
81+
error_handling: SubagentErrorHandling = "continue"
82+
83+
4284
@dataclass
4385
class AgentDefinition:
44-
"""Agent definition configuration."""
86+
"""Agent definition configuration.
87+
88+
Defines a custom agent (subagent) that can be invoked via the Task tool.
89+
90+
Attributes:
91+
description: A short description of what this agent does. This is shown
92+
to the parent agent to help it decide when to use this subagent.
93+
prompt: The system prompt or instructions for this agent.
94+
tools: Optional list of tool names this agent can use. If None, inherits
95+
from parent agent.
96+
model: Optional model override for this agent.
97+
- "sonnet": Use Claude Sonnet
98+
- "opus": Use Claude Opus
99+
- "haiku": Use Claude Haiku (faster, lower cost)
100+
- "inherit": Use the same model as the parent agent
101+
- None: Use default model
102+
execution_mode: How this agent should be executed when invoked alongside
103+
other subagents in the same turn.
104+
- "sequential": This agent must complete before the next starts
105+
- "parallel": This agent can run concurrently with others
106+
- "auto": SDK decides based on global SubagentExecutionConfig (default)
107+
- None: Same as "auto"
108+
109+
Example:
110+
>>> agent = AgentDefinition(
111+
... description="Analyzes code for security issues",
112+
... prompt="You are a security analyst...",
113+
... tools=["Read", "Grep", "Glob"],
114+
... model="haiku",
115+
... execution_mode="parallel"
116+
... )
117+
"""
45118

46119
description: str
47120
prompt: str
48121
tools: list[str] | None = None
49122
model: Literal["sonnet", "opus", "haiku", "inherit"] | None = None
123+
execution_mode: SubagentExecutionMode | None = None
50124

51125

52126
# Permission Update types (matching TypeScript SDK)
@@ -368,18 +442,62 @@ class HookContext(TypedDict):
368442
# Hook matcher configuration
369443
@dataclass
370444
class HookMatcher:
371-
"""Hook matcher configuration."""
445+
"""Hook matcher configuration for matching tool invocations.
372446
373-
# See https://docs.anthropic.com/en/docs/claude-code/hooks#structure for the
374-
# expected string value. For example, for PreToolUse, the matcher can be
375-
# a tool name like "Bash" or a combination of tool names like
376-
# "Write|MultiEdit|Edit".
377-
matcher: str | None = None
447+
The matcher field supports regex patterns for filtering which tools trigger
448+
the associated hooks. This is particularly useful for PreToolUse and
449+
PostToolUse hook events.
378450
379-
# A list of Python functions with function signature HookCallback
380-
hooks: list[HookCallback] = field(default_factory=list)
451+
Attributes:
452+
matcher: Regex pattern for matching tool names. If None, matches all tools.
453+
hooks: List of async callback functions to execute when a tool matches.
454+
timeout: Timeout in seconds for all hooks in this matcher (default: 60).
455+
456+
Pattern Examples:
457+
Built-in tools:
458+
- "Bash" - Match only the Bash tool
459+
- "Read" - Match only the Read tool
460+
- "Write|Edit" - Match Write OR Edit tools
461+
- "Write|MultiEdit|Edit" - Match any file writing tool
462+
463+
MCP tools (format: mcp__<server>__<tool>):
464+
- "mcp__.*" - Match all MCP tools from any server
465+
- "mcp__slack__.*" - Match all tools from the slack MCP server
466+
- "mcp__github__create_issue" - Match specific github tool
467+
- "mcp__.*__delete.*" - Match any MCP tool with "delete" in the name
468+
- "mcp__db__.*write.*" - Match db server tools containing "write"
469+
470+
Combined patterns:
471+
- "Bash|mcp__.*__execute.*" - Match Bash or any MCP execute tools
472+
- None - Match all tools (universal matcher)
381473
382-
# Timeout in seconds for all hooks in this matcher (default: 60)
474+
Example:
475+
>>> from claude_agent_sdk import HookMatcher, HookCallback
476+
>>>
477+
>>> async def log_mcp_calls(input, tool_use_id, context):
478+
... print(f"MCP tool called: {input['tool_name']}")
479+
... return {"continue_": True}
480+
>>>
481+
>>> # Match all MCP tools
482+
>>> mcp_matcher = HookMatcher(
483+
... matcher="mcp__.*",
484+
... hooks=[log_mcp_calls],
485+
... timeout=30.0
486+
... )
487+
>>>
488+
>>> # Match dangerous operations
489+
>>> dangerous_matcher = HookMatcher(
490+
... matcher="mcp__.*__delete.*|mcp__.*__drop.*",
491+
... hooks=[confirm_dangerous_operation],
492+
... timeout=60.0
493+
... )
494+
495+
See Also:
496+
https://docs.anthropic.com/en/docs/claude-code/hooks#structure
497+
"""
498+
499+
matcher: str | None = None
500+
hooks: list[HookCallback] = field(default_factory=list)
383501
timeout: float | None = None
384502

385503

@@ -660,6 +778,8 @@ class ClaudeAgentOptions:
660778
fork_session: bool = False
661779
# Agent definitions for custom agents
662780
agents: dict[str, AgentDefinition] | None = None
781+
# Subagent execution configuration (controls parallel vs sequential execution)
782+
subagent_execution: SubagentExecutionConfig | None = None
663783
# Setting sources to load (user, project, local)
664784
setting_sources: list[SettingSource] | None = None
665785
# Sandbox configuration for bash command isolation.

tests/test_types.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""Tests for Claude SDK type definitions."""
22

33
from claude_agent_sdk import (
4+
AgentDefinition,
45
AssistantMessage,
56
ClaudeAgentOptions,
7+
HookMatcher,
68
ResultMessage,
9+
SubagentExecutionConfig,
710
)
811
from claude_agent_sdk.types import (
912
TextBlock,
@@ -149,3 +152,131 @@ def test_claude_code_options_with_model_specification(self):
149152
)
150153
assert options.model == "claude-sonnet-4-5"
151154
assert options.permission_prompt_tool_name == "CustomTool"
155+
156+
def test_claude_code_options_with_subagent_execution(self):
157+
"""Test Options with subagent execution configuration."""
158+
config = SubagentExecutionConfig(
159+
multi_invocation="parallel",
160+
max_concurrent=5,
161+
error_handling="fail_fast",
162+
)
163+
options = ClaudeAgentOptions(subagent_execution=config)
164+
assert options.subagent_execution is not None
165+
assert options.subagent_execution.multi_invocation == "parallel"
166+
assert options.subagent_execution.max_concurrent == 5
167+
assert options.subagent_execution.error_handling == "fail_fast"
168+
169+
170+
class TestSubagentExecutionConfig:
171+
"""Test SubagentExecutionConfig configuration."""
172+
173+
def test_default_values(self):
174+
"""Test SubagentExecutionConfig with default values."""
175+
config = SubagentExecutionConfig()
176+
assert config.multi_invocation == "sequential"
177+
assert config.max_concurrent == 3
178+
assert config.error_handling == "continue"
179+
180+
def test_parallel_mode(self):
181+
"""Test SubagentExecutionConfig with parallel mode."""
182+
config = SubagentExecutionConfig(
183+
multi_invocation="parallel",
184+
max_concurrent=10,
185+
)
186+
assert config.multi_invocation == "parallel"
187+
assert config.max_concurrent == 10
188+
189+
def test_error_mode(self):
190+
"""Test SubagentExecutionConfig with error mode for multi-invocation."""
191+
config = SubagentExecutionConfig(
192+
multi_invocation="error",
193+
error_handling="fail_fast",
194+
)
195+
assert config.multi_invocation == "error"
196+
assert config.error_handling == "fail_fast"
197+
198+
199+
class TestAgentDefinition:
200+
"""Test AgentDefinition configuration."""
201+
202+
def test_basic_agent_definition(self):
203+
"""Test creating a basic AgentDefinition."""
204+
agent = AgentDefinition(
205+
description="Test agent",
206+
prompt="You are a test agent.",
207+
)
208+
assert agent.description == "Test agent"
209+
assert agent.prompt == "You are a test agent."
210+
assert agent.tools is None
211+
assert agent.model is None
212+
assert agent.execution_mode is None
213+
214+
def test_agent_definition_with_all_fields(self):
215+
"""Test AgentDefinition with all fields specified."""
216+
agent = AgentDefinition(
217+
description="Security analyzer",
218+
prompt="Analyze code for security issues.",
219+
tools=["Read", "Grep", "Glob"],
220+
model="haiku",
221+
execution_mode="parallel",
222+
)
223+
assert agent.description == "Security analyzer"
224+
assert agent.prompt == "Analyze code for security issues."
225+
assert agent.tools == ["Read", "Grep", "Glob"]
226+
assert agent.model == "haiku"
227+
assert agent.execution_mode == "parallel"
228+
229+
def test_agent_definition_sequential_mode(self):
230+
"""Test AgentDefinition with sequential execution mode."""
231+
agent = AgentDefinition(
232+
description="Sequential agent",
233+
prompt="Run sequentially.",
234+
execution_mode="sequential",
235+
)
236+
assert agent.execution_mode == "sequential"
237+
238+
def test_agent_definition_auto_mode(self):
239+
"""Test AgentDefinition with auto execution mode."""
240+
agent = AgentDefinition(
241+
description="Auto agent",
242+
prompt="SDK decides execution mode.",
243+
execution_mode="auto",
244+
)
245+
assert agent.execution_mode == "auto"
246+
247+
248+
class TestHookMatcher:
249+
"""Test HookMatcher configuration."""
250+
251+
def test_default_hook_matcher(self):
252+
"""Test HookMatcher with default values."""
253+
matcher = HookMatcher()
254+
assert matcher.matcher is None
255+
assert matcher.hooks == []
256+
assert matcher.timeout is None
257+
258+
def test_hook_matcher_with_simple_tool(self):
259+
"""Test HookMatcher matching a single tool."""
260+
matcher = HookMatcher(matcher="Bash", timeout=30.0)
261+
assert matcher.matcher == "Bash"
262+
assert matcher.timeout == 30.0
263+
264+
def test_hook_matcher_with_multiple_tools(self):
265+
"""Test HookMatcher matching multiple tools."""
266+
matcher = HookMatcher(matcher="Write|Edit|MultiEdit")
267+
assert matcher.matcher == "Write|Edit|MultiEdit"
268+
269+
def test_hook_matcher_with_mcp_pattern(self):
270+
"""Test HookMatcher with MCP tool pattern."""
271+
matcher = HookMatcher(matcher="mcp__slack__.*")
272+
assert matcher.matcher == "mcp__slack__.*"
273+
274+
def test_hook_matcher_with_mcp_delete_pattern(self):
275+
"""Test HookMatcher matching all MCP delete operations."""
276+
matcher = HookMatcher(matcher="mcp__.*__delete.*")
277+
assert matcher.matcher == "mcp__.*__delete.*"
278+
279+
def test_hook_matcher_with_combined_pattern(self):
280+
"""Test HookMatcher with combined built-in and MCP patterns."""
281+
matcher = HookMatcher(matcher="Bash|mcp__.*__execute.*")
282+
assert matcher.matcher == "Bash|mcp__.*__execute.*"

0 commit comments

Comments
 (0)