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
2 changes: 2 additions & 0 deletions src/claude_agent_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
from .client import ClaudeSDKClient
from .query import query
from .types import (
AdvisorToolConfig,
AgentDefinition,
AssistantMessage,
BaseHookInput,
Expand Down Expand Up @@ -594,6 +595,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
"HookJSONOutput",
"HookMatcher",
# Agent support
"AdvisorToolConfig",
"AgentDefinition",
"SettingSource",
# Plugin support
Expand Down
44 changes: 44 additions & 0 deletions src/claude_agent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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)
Expand Down
71 changes: 71 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"