diff --git a/src/claude_agent_sdk/__init__.py b/src/claude_agent_sdk/__init__.py index 4898bc0..bb2d4b1 100644 --- a/src/claude_agent_sdk/__init__.py +++ b/src/claude_agent_sdk/__init__.py @@ -30,6 +30,7 @@ from .types import ( McpSdkServerConfig, McpServerConfig, Message, + MultiInvocationMode, PermissionMode, PermissionResult, PermissionResultAllow, @@ -46,6 +47,9 @@ from .types import ( SdkPluginConfig, SettingSource, StopHookInput, + SubagentErrorHandling, + SubagentExecutionConfig, + SubagentExecutionMode, SubagentStopHookInput, SystemMessage, TextBlock, @@ -344,6 +348,11 @@ __all__ = [ # Agent support "AgentDefinition", "SettingSource", + # Subagent execution support + "SubagentExecutionConfig", + "SubagentExecutionMode", + "MultiInvocationMode", + "SubagentErrorHandling", # Plugin support "SdkPluginConfig", # Beta support diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index fa6ca35..acc94d8 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -39,14 +39,88 @@ class ToolsPreset(TypedDict): preset: Literal["claude_code"] +# Subagent execution modes +SubagentExecutionMode = Literal["sequential", "parallel", "auto"] + +# Multi-invocation handling modes +MultiInvocationMode = Literal["sequential", "parallel", "error"] + +# Error handling modes for subagent execution +SubagentErrorHandling = Literal["fail_fast", "continue"] + + +@dataclass +class SubagentExecutionConfig: + """Configuration for subagent execution behavior. + + This controls how the SDK handles multiple Task tool calls (subagent invocations) + when they occur in the same turn. + + Attributes: + multi_invocation: How to handle multiple Task tool calls in the same turn. + - "sequential": Execute subagents one at a time in order (default) + - "parallel": Execute subagents concurrently + - "error": Raise an error if multiple Task tools are invoked in same turn + max_concurrent: Maximum number of subagents to run concurrently when + multi_invocation is "parallel". Default is 3. + error_handling: How to handle errors from individual subagents. + - "fail_fast": Stop execution on first subagent error + - "continue": Continue executing remaining subagents on error (default) + + Example: + >>> config = SubagentExecutionConfig( + ... multi_invocation="parallel", + ... max_concurrent=5, + ... error_handling="fail_fast" + ... ) + >>> options = ClaudeAgentOptions(subagent_execution=config) + """ + + multi_invocation: MultiInvocationMode = "sequential" + max_concurrent: int = 3 + error_handling: SubagentErrorHandling = "continue" + + @dataclass class AgentDefinition: - """Agent definition configuration.""" + """Agent definition configuration. + + Defines a custom agent (subagent) that can be invoked via the Task tool. + + Attributes: + description: A short description of what this agent does. This is shown + to the parent agent to help it decide when to use this subagent. + prompt: The system prompt or instructions for this agent. + tools: Optional list of tool names this agent can use. If None, inherits + from parent agent. + model: Optional model override for this agent. + - "sonnet": Use Claude Sonnet + - "opus": Use Claude Opus + - "haiku": Use Claude Haiku (faster, lower cost) + - "inherit": Use the same model as the parent agent + - None: Use default model + execution_mode: How this agent should be executed when invoked alongside + other subagents in the same turn. + - "sequential": This agent must complete before the next starts + - "parallel": This agent can run concurrently with others + - "auto": SDK decides based on global SubagentExecutionConfig (default) + - None: Same as "auto" + + Example: + >>> agent = AgentDefinition( + ... description="Analyzes code for security issues", + ... prompt="You are a security analyst...", + ... tools=["Read", "Grep", "Glob"], + ... model="haiku", + ... execution_mode="parallel" + ... ) + """ description: str prompt: str tools: list[str] | None = None model: Literal["sonnet", "opus", "haiku", "inherit"] | None = None + execution_mode: SubagentExecutionMode | None = None # Permission Update types (matching TypeScript SDK) @@ -368,18 +442,62 @@ HookCallback = Callable[ # Hook matcher configuration @dataclass class HookMatcher: - """Hook matcher configuration.""" + """Hook matcher configuration for matching tool invocations. + + The matcher field supports regex patterns for filtering which tools trigger + the associated hooks. This is particularly useful for PreToolUse and + PostToolUse hook events. + + Attributes: + matcher: Regex pattern for matching tool names. If None, matches all tools. + hooks: List of async callback functions to execute when a tool matches. + timeout: Timeout in seconds for all hooks in this matcher (default: 60). + + Pattern Examples: + Built-in tools: + - "Bash" - Match only the Bash tool + - "Read" - Match only the Read tool + - "Write|Edit" - Match Write OR Edit tools + - "Write|MultiEdit|Edit" - Match any file writing tool + + MCP tools (format: mcp____): + - "mcp__.*" - Match all MCP tools from any server + - "mcp__slack__.*" - Match all tools from the slack MCP server + - "mcp__github__create_issue" - Match specific github tool + - "mcp__.*__delete.*" - Match any MCP tool with "delete" in the name + - "mcp__db__.*write.*" - Match db server tools containing "write" + + Combined patterns: + - "Bash|mcp__.*__execute.*" - Match Bash or any MCP execute tools + - None - Match all tools (universal matcher) + + Example: + >>> from claude_agent_sdk import HookMatcher, HookCallback + >>> + >>> async def log_mcp_calls(input, tool_use_id, context): + ... print(f"MCP tool called: {input['tool_name']}") + ... return {"continue_": True} + >>> + >>> # Match all MCP tools + >>> mcp_matcher = HookMatcher( + ... matcher="mcp__.*", + ... hooks=[log_mcp_calls], + ... timeout=30.0 + ... ) + >>> + >>> # Match dangerous operations + >>> dangerous_matcher = HookMatcher( + ... matcher="mcp__.*__delete.*|mcp__.*__drop.*", + ... hooks=[confirm_dangerous_operation], + ... timeout=60.0 + ... ) + + See Also: + https://docs.anthropic.com/en/docs/claude-code/hooks#structure + """ - # See https://docs.anthropic.com/en/docs/claude-code/hooks#structure for the - # expected string value. For example, for PreToolUse, the matcher can be - # a tool name like "Bash" or a combination of tool names like - # "Write|MultiEdit|Edit". matcher: str | None = None - - # A list of Python functions with function signature HookCallback hooks: list[HookCallback] = field(default_factory=list) - - # Timeout in seconds for all hooks in this matcher (default: 60) timeout: float | None = None @@ -660,6 +778,8 @@ class ClaudeAgentOptions: fork_session: bool = False # Agent definitions for custom agents agents: dict[str, AgentDefinition] | None = None + # Subagent execution configuration (controls parallel vs sequential execution) + subagent_execution: SubagentExecutionConfig | None = None # Setting sources to load (user, project, local) setting_sources: list[SettingSource] | None = None # Sandbox configuration for bash command isolation. diff --git a/tests/test_types.py b/tests/test_types.py index 21a84da..117a128 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,9 +1,12 @@ """Tests for Claude SDK type definitions.""" from claude_agent_sdk import ( + AgentDefinition, AssistantMessage, ClaudeAgentOptions, + HookMatcher, ResultMessage, + SubagentExecutionConfig, ) from claude_agent_sdk.types import ( TextBlock, @@ -149,3 +152,131 @@ class TestOptions: ) assert options.model == "claude-sonnet-4-5" assert options.permission_prompt_tool_name == "CustomTool" + + def test_claude_code_options_with_subagent_execution(self): + """Test Options with subagent execution configuration.""" + config = SubagentExecutionConfig( + multi_invocation="parallel", + max_concurrent=5, + error_handling="fail_fast", + ) + options = ClaudeAgentOptions(subagent_execution=config) + assert options.subagent_execution is not None + assert options.subagent_execution.multi_invocation == "parallel" + assert options.subagent_execution.max_concurrent == 5 + assert options.subagent_execution.error_handling == "fail_fast" + + +class TestSubagentExecutionConfig: + """Test SubagentExecutionConfig configuration.""" + + def test_default_values(self): + """Test SubagentExecutionConfig with default values.""" + config = SubagentExecutionConfig() + assert config.multi_invocation == "sequential" + assert config.max_concurrent == 3 + assert config.error_handling == "continue" + + def test_parallel_mode(self): + """Test SubagentExecutionConfig with parallel mode.""" + config = SubagentExecutionConfig( + multi_invocation="parallel", + max_concurrent=10, + ) + assert config.multi_invocation == "parallel" + assert config.max_concurrent == 10 + + def test_error_mode(self): + """Test SubagentExecutionConfig with error mode for multi-invocation.""" + config = SubagentExecutionConfig( + multi_invocation="error", + error_handling="fail_fast", + ) + assert config.multi_invocation == "error" + assert config.error_handling == "fail_fast" + + +class TestAgentDefinition: + """Test AgentDefinition configuration.""" + + def test_basic_agent_definition(self): + """Test creating a basic AgentDefinition.""" + agent = AgentDefinition( + description="Test agent", + prompt="You are a test agent.", + ) + assert agent.description == "Test agent" + assert agent.prompt == "You are a test agent." + assert agent.tools is None + assert agent.model is None + assert agent.execution_mode is None + + def test_agent_definition_with_all_fields(self): + """Test AgentDefinition with all fields specified.""" + agent = AgentDefinition( + description="Security analyzer", + prompt="Analyze code for security issues.", + tools=["Read", "Grep", "Glob"], + model="haiku", + execution_mode="parallel", + ) + assert agent.description == "Security analyzer" + assert agent.prompt == "Analyze code for security issues." + assert agent.tools == ["Read", "Grep", "Glob"] + assert agent.model == "haiku" + assert agent.execution_mode == "parallel" + + def test_agent_definition_sequential_mode(self): + """Test AgentDefinition with sequential execution mode.""" + agent = AgentDefinition( + description="Sequential agent", + prompt="Run sequentially.", + execution_mode="sequential", + ) + assert agent.execution_mode == "sequential" + + def test_agent_definition_auto_mode(self): + """Test AgentDefinition with auto execution mode.""" + agent = AgentDefinition( + description="Auto agent", + prompt="SDK decides execution mode.", + execution_mode="auto", + ) + assert agent.execution_mode == "auto" + + +class TestHookMatcher: + """Test HookMatcher configuration.""" + + def test_default_hook_matcher(self): + """Test HookMatcher with default values.""" + matcher = HookMatcher() + assert matcher.matcher is None + assert matcher.hooks == [] + assert matcher.timeout is None + + def test_hook_matcher_with_simple_tool(self): + """Test HookMatcher matching a single tool.""" + matcher = HookMatcher(matcher="Bash", timeout=30.0) + assert matcher.matcher == "Bash" + assert matcher.timeout == 30.0 + + def test_hook_matcher_with_multiple_tools(self): + """Test HookMatcher matching multiple tools.""" + matcher = HookMatcher(matcher="Write|Edit|MultiEdit") + assert matcher.matcher == "Write|Edit|MultiEdit" + + def test_hook_matcher_with_mcp_pattern(self): + """Test HookMatcher with MCP tool pattern.""" + matcher = HookMatcher(matcher="mcp__slack__.*") + assert matcher.matcher == "mcp__slack__.*" + + def test_hook_matcher_with_mcp_delete_pattern(self): + """Test HookMatcher matching all MCP delete operations.""" + matcher = HookMatcher(matcher="mcp__.*__delete.*") + assert matcher.matcher == "mcp__.*__delete.*" + + def test_hook_matcher_with_combined_pattern(self): + """Test HookMatcher with combined built-in and MCP patterns.""" + matcher = HookMatcher(matcher="Bash|mcp__.*__execute.*") + assert matcher.matcher == "Bash|mcp__.*__execute.*"