diff --git a/src/claude_agent_sdk/__init__.py b/src/claude_agent_sdk/__init__.py index 3e60d5bd..bd40b168 100644 --- a/src/claude_agent_sdk/__init__.py +++ b/src/claude_agent_sdk/__init__.py @@ -56,6 +56,7 @@ from .client import ClaudeSDKClient from .query import query from .types import ( + AdvisorToolConfig, AgentDefinition, AssistantMessage, BaseHookInput, @@ -594,6 +595,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any: "HookJSONOutput", "HookMatcher", # Agent support + "AdvisorToolConfig", "AgentDefinition", "SettingSource", # Plugin support diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index b4860270..af3299fa 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -78,6 +78,32 @@ class ToolsPreset(TypedDict): preset: Literal["claude_code"] +class AdvisorToolConfig(TypedDict, total=False): + """Per-agent configuration for the ``advisor_20260301`` server-side tool. + + Mirrors the API's ``BetaAdvisorTool20260301Param`` shape. All fields are + optional at the SDK layer; the CLI fills in sensible defaults for any + field that is omitted (e.g., picks a sibling-tier advisor model when + ``model`` is not set). + + See https://docs.anthropic.com/en/api/beta-headers for the + ``advisor-tool-2026-03-01`` beta. + """ + + model: str + """Advisor model id (e.g., ``"claude-opus-4-7"`` for a Sonnet executor).""" + + max_uses: int + """Maximum number of advisor consultations allowed per request.""" + + caching: dict[str, Any] + """Ephemeral cache breakpoint config for the advisor's own prompt + (e.g., ``{"type": "ephemeral", "ttl": "5m"}``).""" + + allowed_callers: list[str] + """Restrict which callers can invoke the advisor (e.g., ``["direct"]``).""" + + @dataclass class AgentDefinition: """Agent definition configuration.""" @@ -97,6 +123,24 @@ class AgentDefinition: background: bool | None = None effort: Literal["low", "medium", "high", "max"] | int | None = None permissionMode: PermissionMode | None = None # noqa: N815 + advisor: bool | AdvisorToolConfig | None = None + """Attach the ``advisor_20260301`` server-side tool to this sub-agent. + + - ``None`` (default): no advisor; field omitted from the wire payload. + - ``True``: enable the advisor with CLI-default config (the CLI picks an + advisor model and adds the ``advisor-tool-2026-03-01`` beta header). + - ``AdvisorToolConfig`` dict: enable with explicit per-field config + (``model``, ``max_uses``, ``caching``, ...). Forwarded verbatim. + + Useful for the executor/advisor pattern: a Sonnet sub-agent runs the + main loop while consulting an Opus advisor at decision points. + + Requires a Claude Code CLI build that wires advisor configuration + through to per-sub-agent API calls; older CLIs silently ignore the + field (matching how unknown ``AgentDefinition`` fields are handled). + The advisor tool itself is a server-side beta — see the API docs for + current rollout status. + """ # Permission Update types (matching TypeScript SDK) diff --git a/tests/test_types.py b/tests/test_types.py index fbd07509..f0445bd9 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -619,3 +619,74 @@ def test_new_fields_omitted_when_none(self): assert "background" not in payload assert "effort" not in payload assert "permissionMode" not in payload + + def test_advisor_field_default_none_omitted(self): + """``advisor`` defaults to None and must not appear in the wire payload.""" + from claude_agent_sdk import AgentDefinition + + agent = AgentDefinition(description="test", prompt="p") + payload = self._serialize(agent) + + assert "advisor" not in payload + + def test_advisor_field_true_serializes_as_bool(self): + """``advisor=True`` opts the sub-agent into the CLI's default advisor config.""" + from claude_agent_sdk import AgentDefinition + + agent = AgentDefinition( + description="test", + prompt="p", + advisor=True, + ) + payload = self._serialize(agent) + + assert payload["advisor"] is True + + def test_advisor_field_false_serializes_as_bool(self): + """``advisor=False`` is sent verbatim so callers can explicitly disable + an advisor that a parent profile or default would otherwise enable.""" + from claude_agent_sdk import AgentDefinition + + agent = AgentDefinition( + description="test", + prompt="p", + advisor=False, + ) + payload = self._serialize(agent) + + assert payload["advisor"] is False + + def test_advisor_field_dict_passes_through_verbatim(self): + """An ``AdvisorToolConfig`` dict must reach the CLI unmodified — + snake_case keys, no key rewriting — so the CLI can forward it + straight to the API as ``BetaAdvisorTool20260301Param``.""" + from claude_agent_sdk import AgentDefinition + + config: dict = { + "model": "claude-opus-4-7", + "max_uses": 5, + "caching": {"type": "ephemeral", "ttl": "5m"}, + "allowed_callers": ["direct"], + } + agent = AgentDefinition( + description="test", + prompt="p", + advisor=config, + ) + payload = self._serialize(agent) + + # asdict on a dict-typed field must not deep-copy/transform it. + assert payload["advisor"] == config + assert payload["advisor"]["model"] == "claude-opus-4-7" + assert payload["advisor"]["max_uses"] == 5 + assert payload["advisor"]["caching"]["ttl"] == "5m" + assert payload["advisor"]["allowed_callers"] == ["direct"] + + def test_advisor_config_typed_dict_is_exported(self): + """``AdvisorToolConfig`` must be importable from the package root so + callers can type their config dicts.""" + from claude_agent_sdk import AdvisorToolConfig + + # TypedDict is a dict at runtime; the import above is the contract. + config: AdvisorToolConfig = {"model": "claude-opus-4-7"} + assert config["model"] == "claude-opus-4-7"