Python: Add BaseAgent implementation for Claude Agent SDK (#3509)

* Added ClaudeAgent implementation

* Updated streaming logic

* Small updates

* Small update

* Fixes

* Small fix

* Naming improvements

* Updated imports

* Addressed comments

* Updated package versions
This commit is contained in:
Dmytro Struk
2026-01-30 10:11:41 -08:00
committed by GitHub
Unverified
parent 0fcf075ea7
commit 8b475afe17
39 changed files with 2180 additions and 52 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
version = "1.0.0b260128"
version = "1.0.0b260130"
description = "AG-UI protocol integration for Agent Framework"
readme = "README.md"
license-files = ["LICENSE"]
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
+11
View File
@@ -0,0 +1,11 @@
# Get Started with Microsoft Agent Framework Claude
Please install this package via pip:
```bash
pip install agent-framework-claude --pre
```
## Claude Agent
The Claude agent enables integration with Claude Agent SDK, allowing you to interact with Claude's agentic capabilities through the Agent Framework.
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib.metadata
from ._agent import ClaudeAgent, ClaudeAgentOptions
from ._settings import ClaudeAgentSettings
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
__all__ = [
"ClaudeAgent",
"ClaudeAgentOptions",
"ClaudeAgentSettings",
"__version__",
]
@@ -0,0 +1,645 @@
# Copyright (c) Microsoft. All rights reserved.
import contextlib
import sys
from collections.abc import AsyncIterable, Callable, MutableMapping, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Generic
from agent_framework import (
AgentMiddlewareTypes,
AgentResponse,
AgentResponseUpdate,
AgentThread,
BaseAgent,
ChatMessage,
Content,
ContextProvider,
FunctionTool,
Role,
ToolProtocol,
get_logger,
normalize_messages,
)
from agent_framework._types import normalize_tools
from agent_framework.exceptions import ServiceException, ServiceInitializationError
from claude_agent_sdk import (
ClaudeAgentOptions as SDKOptions,
)
from claude_agent_sdk import (
ClaudeSDKClient,
ResultMessage,
SdkMcpTool,
create_sdk_mcp_server,
)
from claude_agent_sdk.types import StreamEvent
from pydantic import ValidationError
from ._settings import ClaudeAgentSettings
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
else:
from typing_extensions import TypeVar # type: ignore # pragma: no cover
if sys.version_info >= (3, 11):
from typing import TypedDict # pragma: no cover
else:
from typing_extensions import TypedDict # pragma: no cover
if TYPE_CHECKING:
from claude_agent_sdk import (
AgentDefinition,
CanUseTool,
HookMatcher,
McpServerConfig,
PermissionMode,
SandboxSettings,
SdkBeta,
)
__all__ = ["ClaudeAgent", "ClaudeAgentOptions"]
logger = get_logger("agent_framework.claude")
# Name of the in-process MCP server that hosts Agent Framework tools.
# FunctionTool instances are converted to SDK MCP tools and served
# through this server, as Claude Code CLI only supports tools via MCP.
TOOLS_MCP_SERVER_NAME = "_agent_framework_tools"
class ClaudeAgentOptions(TypedDict, total=False):
"""Claude Agent-specific options."""
system_prompt: str
"""System prompt for the agent."""
cli_path: str | Path
"""Path to Claude CLI executable. Default: auto-detected."""
cwd: str | Path
"""Working directory for Claude CLI. Default: current working directory."""
env: dict[str, str]
"""Environment variables to pass to CLI."""
settings: str
"""Path to Claude settings file."""
model: str
"""Model to use ("sonnet", "opus", "haiku"). Default: "sonnet"."""
fallback_model: str
"""Fallback model if primary fails."""
max_thinking_tokens: int
"""Maximum tokens for thinking blocks."""
allowed_tools: list[str]
"""Allowlist of tools. If set, Claude can ONLY use tools in this list."""
disallowed_tools: list[str]
"""Blocklist of tools. Claude cannot use these tools."""
mcp_servers: dict[str, "McpServerConfig"]
"""MCP server configurations for external tools."""
permission_mode: "PermissionMode"
"""Permission handling mode ("default", "acceptEdits", "plan", "bypassPermissions")."""
can_use_tool: "CanUseTool"
"""Permission callback for tool use."""
max_turns: int
"""Maximum conversation turns."""
max_budget_usd: float
"""Budget limit in USD."""
hooks: dict[str, list["HookMatcher"]]
"""Pre/post tool hooks."""
add_dirs: list[str | Path]
"""Additional directories to add to context."""
sandbox: "SandboxSettings"
"""Sandbox configuration for bash isolation."""
agents: dict[str, "AgentDefinition"]
"""Custom agent definitions."""
output_format: dict[str, Any]
"""Structured output format (JSON schema)."""
enable_file_checkpointing: bool
"""Enable file checkpointing for rewind."""
betas: list["SdkBeta"]
"""Beta features to enable."""
TOptions = TypeVar(
"TOptions",
bound=TypedDict, # type: ignore[valid-type]
default="ClaudeAgentOptions",
covariant=True,
)
class ClaudeAgent(BaseAgent, Generic[TOptions]):
"""Claude Agent using Claude Code CLI.
Wraps the Claude Agent SDK to provide agentic capabilities including
tool use, session management, and streaming responses.
This agent communicates with Claude through the Claude Code CLI,
enabling access to Claude's full agentic capabilities like file
editing, code execution, and tool use.
The agent can be used as an async context manager to ensure proper cleanup:
Examples:
Basic usage with context manager:
.. code-block:: python
from agent_framework_claude import ClaudeAgent
async with ClaudeAgent(
instructions="You are a helpful assistant.",
) as agent:
response = await agent.run("Hello!")
print(response.text)
With streaming:
.. code-block:: python
async with ClaudeAgent() as agent:
async for update in agent.run_stream("Write a poem"):
print(update.text, end="", flush=True)
With session management:
.. code-block:: python
async with ClaudeAgent() as agent:
thread = agent.get_new_thread()
await agent.run("Remember my name is Alice", thread=thread)
response = await agent.run("What's my name?", thread=thread)
# Claude will remember "Alice" from the same session
With Agent Framework tools:
.. code-block:: python
from agent_framework import tool
@tool
def greet(name: str) -> str:
\"\"\"Greet someone by name.\"\"\"
return f"Hello, {name}!"
async with ClaudeAgent(tools=[greet]) as agent:
response = await agent.run("Greet Alice")
"""
AGENT_PROVIDER_NAME: ClassVar[str] = "anthropic.claude"
def __init__(
self,
instructions: str | None = None,
*,
client: ClaudeSDKClient | None = None,
id: str | None = None,
name: str | None = None,
description: str | None = None,
context_provider: ContextProvider | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
tools: ToolProtocol
| Callable[..., Any]
| MutableMapping[str, Any]
| str
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str]
| None = None,
default_options: TOptions | MutableMapping[str, Any] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize a ClaudeAgent instance.
Args:
instructions: System prompt for the agent.
Keyword Args:
client: Optional pre-configured ClaudeSDKClient instance. If not provided,
a new client will be created using the other parameters.
id: Unique identifier for the agent.
name: Name of the agent.
description: Description of the agent.
context_provider: Context provider for the agent.
middleware: List of middleware.
tools: Tools for the agent. Can be:
- Strings for built-in tools (e.g., "Read", "Write", "Bash", "Glob")
- Functions or ToolProtocol instances for custom tools
default_options: Default ClaudeAgentOptions including system_prompt, model, etc.
env_file_path: Path to .env file.
env_file_encoding: Encoding of .env file.
"""
super().__init__(
id=id,
name=name,
description=description,
context_provider=context_provider,
middleware=middleware,
)
self._client = client
self._owns_client = client is None
# Parse options
opts: dict[str, Any] = dict(default_options) if default_options else {}
# Handle instructions parameter - set as system_prompt in options
if instructions is not None:
opts["system_prompt"] = instructions
cli_path = opts.pop("cli_path", None)
model = opts.pop("model", None)
cwd = opts.pop("cwd", None)
permission_mode = opts.pop("permission_mode", None)
max_turns = opts.pop("max_turns", None)
max_budget_usd = opts.pop("max_budget_usd", None)
self._mcp_servers: dict[str, Any] = opts.pop("mcp_servers", None) or {}
# Load settings from environment and options
try:
self._settings = ClaudeAgentSettings(
cli_path=cli_path,
model=model,
cwd=cwd,
permission_mode=permission_mode,
max_turns=max_turns,
max_budget_usd=max_budget_usd,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Claude Agent settings.", ex) from ex
# Separate built-in tools (strings) from custom tools (callables/ToolProtocol)
self._builtin_tools: list[str] = []
self._custom_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
self._normalize_tools(tools)
self._default_options = opts
self._started = False
self._current_session_id: str | None = None
def _normalize_tools(
self,
tools: ToolProtocol
| Callable[..., Any]
| MutableMapping[str, Any]
| str
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str]
| None,
) -> None:
"""Separate built-in tools (strings) from custom tools.
Args:
tools: Mixed list of tool names and custom tools.
"""
if tools is None:
return
# Normalize to sequence
if isinstance(tools, str):
tools_list: Sequence[Any] = [tools]
elif isinstance(tools, (ToolProtocol, MutableMapping)) or callable(tools):
tools_list = [tools]
else:
tools_list = list(tools)
for tool in tools_list:
if isinstance(tool, str):
self._builtin_tools.append(tool)
else:
# Use normalize_tools for custom tools
normalized = normalize_tools(tool)
self._custom_tools.extend(normalized)
async def __aenter__(self) -> "ClaudeAgent[TOptions]":
"""Start the agent when entering async context."""
await self.start()
return self
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
"""Stop the agent when exiting async context."""
await self.stop()
async def start(self) -> None:
"""Start the Claude SDK client.
This method initializes the Claude SDK client and establishes a connection
to the Claude Code CLI. It is called automatically when using the agent
as an async context manager.
Raises:
ServiceException: If the client fails to start.
"""
await self._ensure_session()
async def stop(self) -> None:
"""Stop the Claude SDK client and clean up resources.
Stops the client if owned by this agent. Called automatically when
using the agent as an async context manager.
"""
if self._client and self._owns_client:
with contextlib.suppress(Exception):
await self._client.disconnect()
self._started = False
self._current_session_id = None
async def _ensure_session(self, session_id: str | None = None) -> None:
"""Ensure the client is connected for the specified session.
If the requested session differs from the current one, recreates the client.
Args:
session_id: The session ID to use, or None for a new session.
"""
needs_new_client = (
not self._started or self._client is None or (session_id and session_id != self._current_session_id)
)
if needs_new_client:
# Stop existing client if any
if self._client and self._owns_client:
with contextlib.suppress(Exception):
await self._client.disconnect()
self._started = False
# Create new client with resume option if needed
opts = self._prepare_client_options(resume_session_id=session_id)
self._client = ClaudeSDKClient(options=opts)
self._owns_client = True
try:
await self._client.connect()
self._started = True
self._current_session_id = session_id
except Exception as ex:
self._client = None
raise ServiceException(f"Failed to start Claude SDK client: {ex}") from ex
def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOptions:
"""Prepare SDK options for client initialization.
Args:
resume_session_id: Optional session ID to resume.
Returns:
SDKOptions instance configured for the client.
"""
opts: dict[str, Any] = {}
# Set resume option if provided
if resume_session_id:
opts["resume"] = resume_session_id
# Apply settings from environment
if self._settings.cli_path:
opts["cli_path"] = self._settings.cli_path
if self._settings.model:
opts["model"] = self._settings.model
if self._settings.cwd:
opts["cwd"] = self._settings.cwd
if self._settings.permission_mode:
opts["permission_mode"] = self._settings.permission_mode
if self._settings.max_turns:
opts["max_turns"] = self._settings.max_turns
if self._settings.max_budget_usd:
opts["max_budget_usd"] = self._settings.max_budget_usd
# Apply default options
for key, value in self._default_options.items():
if value is not None:
opts[key] = value
# Add built-in tools (strings like "Read", "Write", "Bash")
if self._builtin_tools:
opts["tools"] = self._builtin_tools
# Prepare custom tools (FunctionTool instances)
custom_tools_server, custom_tool_names = (
self._prepare_tools(self._custom_tools) if self._custom_tools else (None, [])
)
# MCP servers - merge user-provided servers with custom tools server
mcp_servers = dict(self._mcp_servers) if self._mcp_servers else {}
if custom_tools_server:
mcp_servers[TOOLS_MCP_SERVER_NAME] = custom_tools_server
if mcp_servers:
opts["mcp_servers"] = mcp_servers
# Add custom tools to allowed_tools so they can be executed
if custom_tool_names:
existing_allowed = opts.get("allowed_tools", [])
opts["allowed_tools"] = list(existing_allowed) + custom_tool_names
# Always enable partial messages for streaming support
opts["include_partial_messages"] = True
return SDKOptions(**opts)
def _prepare_tools(
self,
tools: list[ToolProtocol | MutableMapping[str, Any]],
) -> tuple[Any, list[str]]:
"""Convert Agent Framework tools to SDK MCP server.
Args:
tools: List of Agent Framework tools.
Returns:
Tuple of (MCP server config, list of allowed tool names).
"""
sdk_tools: list[SdkMcpTool[Any]] = []
tool_names: list[str] = []
for tool in tools:
if isinstance(tool, FunctionTool):
sdk_tools.append(self._function_tool_to_sdk_mcp_tool(tool))
# Claude Agent SDK convention: MCP tools use format "mcp__{server}__{tool}"
tool_names.append(f"mcp__{TOOLS_MCP_SERVER_NAME}__{tool.name}")
elif isinstance(tool, ToolProtocol):
logger.debug(f"Unsupported tool type: {type(tool)}")
if not sdk_tools:
return None, []
return create_sdk_mcp_server(name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools), tool_names
def _function_tool_to_sdk_mcp_tool(self, func_tool: FunctionTool[Any, Any]) -> SdkMcpTool[Any]:
"""Convert a FunctionTool to an SDK MCP tool.
Args:
func_tool: The FunctionTool to convert.
Returns:
An SdkMcpTool instance.
"""
async def handler(args: dict[str, Any]) -> dict[str, Any]:
"""Handler that invokes the FunctionTool."""
try:
if func_tool.input_model:
args_instance = func_tool.input_model(**args)
result = await func_tool.invoke(arguments=args_instance)
else:
result = await func_tool.invoke(arguments=args)
return {"content": [{"type": "text", "text": str(result)}]}
except Exception as e:
return {"content": [{"type": "text", "text": f"Error: {e}"}]}
# Get JSON schema from pydantic model
schema: dict[str, Any] = func_tool.input_model.model_json_schema() if func_tool.input_model else {}
input_schema: dict[str, Any] = {
"type": "object",
"properties": schema.get("properties", {}),
"required": schema.get("required", []),
}
return SdkMcpTool(
name=func_tool.name,
description=func_tool.description,
input_schema=input_schema,
handler=handler,
)
async def _apply_runtime_options(self, options: dict[str, Any] | None) -> None:
"""Apply runtime options that can be changed dynamically.
The Claude SDK supports changing model and permission_mode after connection.
Args:
options: Runtime options to apply.
"""
if not options or not self._client:
return
if "model" in options:
await self._client.set_model(options["model"])
if "permission_mode" in options:
await self._client.set_permission_mode(options["permission_mode"])
def _format_prompt(self, messages: list[ChatMessage] | None) -> str:
"""Format messages into a prompt string.
Args:
messages: List of chat messages.
Returns:
Formatted prompt string.
"""
if not messages:
return ""
return "\n".join([msg.text or "" for msg in messages])
async def run(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
options: TOptions | MutableMapping[str, Any] | None = None,
**kwargs: Any,
) -> AgentResponse[Any]:
"""Run the agent with the given messages.
Args:
messages: The messages to process.
Keyword Args:
thread: The conversation thread. If thread has service_thread_id set,
the agent will resume that session.
options: Runtime options (model, permission_mode can be changed per-request).
kwargs: Additional keyword arguments.
Returns:
AgentResponse with the agent's response.
"""
thread = thread or self.get_new_thread()
return await AgentResponse.from_agent_response_generator(
self.run_stream(messages, thread=thread, options=options, **kwargs)
)
async def run_stream(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
options: TOptions | MutableMapping[str, Any] | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentResponseUpdate]:
"""Stream the agent's response.
Args:
messages: The messages to process.
Keyword Args:
thread: The conversation thread. If thread has service_thread_id set,
the agent will resume that session.
options: Runtime options (model, permission_mode can be changed per-request).
kwargs: Additional keyword arguments.
Yields:
AgentResponseUpdate objects containing chunks of the response.
"""
thread = thread or self.get_new_thread()
# Ensure we're connected to the right session
await self._ensure_session(thread.service_thread_id)
if not self._client:
raise ServiceException("Claude SDK client not initialized.")
prompt = self._format_prompt(normalize_messages(messages))
# Apply runtime options (model, permission_mode)
await self._apply_runtime_options(dict(options) if options else None)
session_id: str | None = None
await self._client.query(prompt)
async for message in self._client.receive_response():
if isinstance(message, StreamEvent):
# Handle streaming events - extract text/thinking deltas
event = message.event
if event.get("type") == "content_block_delta":
delta = event.get("delta", {})
delta_type = delta.get("type")
if delta_type == "text_delta":
text = delta.get("text", "")
if text:
yield AgentResponseUpdate(
role=Role.ASSISTANT,
contents=[Content.from_text(text=text, raw_representation=message)],
raw_representation=message,
)
elif delta_type == "thinking_delta":
thinking = delta.get("thinking", "")
if thinking:
yield AgentResponseUpdate(
role=Role.ASSISTANT,
contents=[Content.from_text_reasoning(text=thinking, raw_representation=message)],
raw_representation=message,
)
elif isinstance(message, ResultMessage):
session_id = message.session_id
# Update thread with session ID
if session_id:
thread.service_thread_id = session_id
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from agent_framework._pydantic import AFBaseSettings
__all__ = ["ClaudeAgentSettings"]
class ClaudeAgentSettings(AFBaseSettings):
"""Claude Agent settings.
The settings are first loaded from environment variables with the prefix 'CLAUDE_AGENT_'.
If the environment variables are not found, the settings can be loaded from a .env file
with the encoding 'utf-8'. If the settings are not found in the .env file, the settings
are ignored; however, validation will fail alerting that the settings are missing.
Keyword Args:
cli_path: The path to Claude CLI executable.
model: The model to use (sonnet, opus, haiku).
cwd: The working directory for Claude CLI.
permission_mode: Permission mode (default, acceptEdits, plan, bypassPermissions).
max_turns: Maximum number of conversation turns.
max_budget_usd: Maximum budget in USD.
env_file_path: If provided, the .env settings are read from this file path location.
env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
Examples:
.. code-block:: python
from agent_framework.anthropic import ClaudeAgentSettings
# Using environment variables
# Set CLAUDE_AGENT_MODEL=sonnet
# CLAUDE_AGENT_PERMISSION_MODE=default
# Or passing parameters directly
settings = ClaudeAgentSettings(model="sonnet")
# Or loading from a .env file
settings = ClaudeAgentSettings(env_file_path="path/to/.env")
"""
env_prefix: ClassVar[str] = "CLAUDE_AGENT_"
cli_path: str | None = None
model: str | None = None
cwd: str | None = None
permission_mode: str | None = None
max_turns: int | None = None
max_budget_usd: float | None = None
+89
View File
@@ -0,0 +1,89 @@
[project]
name = "agent-framework-claude"
description = "Claude Agent SDK integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
urls.issues = "https://github.com/microsoft/agent-framework/issues"
classifiers = [
"License :: OSI Approved :: MIT License",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
"agent-framework-core",
"claude-agent-sdk>=0.1.25",
]
[tool.uv]
prerelease = "if-necessary-or-explicit"
environments = [
"sys_platform == 'darwin'",
"sys_platform == 'linux'",
"sys_platform == 'win32'"
]
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
[tool.ruff]
extend = "../../pyproject.toml"
[tool.coverage.run]
omit = [
"**/__init__.py"
]
[tool.pyright]
extends = "../../pyproject.toml"
exclude = ['tests']
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
python_version = "3.10"
ignore_missing_imports = true
disallow_untyped_defs = true
no_implicit_optional = true
check_untyped_defs = true
warn_return_any = true
show_error_codes = true
warn_unused_ignores = false
disallow_incomplete_defs = true
disallow_untyped_decorators = true
[tool.bandit]
targets = ["agent_framework_claude"]
exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude"
test = "pytest --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests"
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
build-backend = "flit_core.buildapi"
+1
View File
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,714 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import AgentResponseUpdate, AgentThread, ChatMessage, Content, Role, tool
from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings
from agent_framework_claude._agent import TOOLS_MCP_SERVER_NAME
# region Test ClaudeAgentSettings
class TestClaudeAgentSettings:
"""Tests for ClaudeAgentSettings."""
def test_env_prefix(self) -> None:
"""Test that env_prefix is correctly set."""
assert ClaudeAgentSettings.env_prefix == "CLAUDE_AGENT_"
def test_default_values(self) -> None:
"""Test default values are None."""
settings = ClaudeAgentSettings()
assert settings.cli_path is None
assert settings.model is None
assert settings.cwd is None
assert settings.permission_mode is None
assert settings.max_turns is None
assert settings.max_budget_usd is None
def test_explicit_values(self) -> None:
"""Test explicit values override defaults."""
settings = ClaudeAgentSettings(
cli_path="/usr/local/bin/claude",
model="sonnet",
cwd="/home/user/project",
permission_mode="default",
max_turns=10,
max_budget_usd=5.0,
)
assert settings.cli_path == "/usr/local/bin/claude"
assert settings.model == "sonnet"
assert settings.cwd == "/home/user/project"
assert settings.permission_mode == "default"
assert settings.max_turns == 10
assert settings.max_budget_usd == 5.0
def test_env_variable_loading(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test loading from environment variables."""
monkeypatch.setenv("CLAUDE_AGENT_MODEL", "opus")
monkeypatch.setenv("CLAUDE_AGENT_MAX_TURNS", "20")
settings = ClaudeAgentSettings()
assert settings.model == "opus"
assert settings.max_turns == 20
# region Test ClaudeAgent Initialization
class TestClaudeAgentInit:
"""Tests for ClaudeAgent initialization."""
def test_default_initialization(self) -> None:
"""Test agent initializes with defaults."""
agent = ClaudeAgent()
assert agent.id is not None
assert agent.name is None
assert agent.description is None
def test_with_name_and_description(self) -> None:
"""Test agent with name and description."""
agent = ClaudeAgent(name="test-agent", description="A test agent")
assert agent.name == "test-agent"
assert agent.description == "A test agent"
def test_with_instructions_parameter(self) -> None:
"""Test agent with instructions parameter."""
agent = ClaudeAgent(instructions="You are a helpful assistant.")
assert agent._default_options.get("system_prompt") == "You are a helpful assistant." # type: ignore[reportPrivateUsage]
def test_with_system_prompt_in_options(self) -> None:
"""Test agent with system_prompt in options."""
options: ClaudeAgentOptions = {
"system_prompt": "You are a helpful assistant.",
}
agent = ClaudeAgent(default_options=options)
assert agent._default_options.get("system_prompt") == "You are a helpful assistant." # type: ignore[reportPrivateUsage]
def test_with_default_options(self) -> None:
"""Test agent with default options."""
options: ClaudeAgentOptions = {
"model": "sonnet",
"permission_mode": "default",
"max_turns": 10,
}
agent = ClaudeAgent(default_options=options)
assert agent._settings.model == "sonnet" # type: ignore[reportPrivateUsage]
assert agent._settings.permission_mode == "default" # type: ignore[reportPrivateUsage]
assert agent._settings.max_turns == 10 # type: ignore[reportPrivateUsage]
def test_with_function_tool(self) -> None:
"""Test agent with function tool."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=[greet])
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
def test_with_single_tool(self) -> None:
"""Test agent with single tool (not in list)."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=greet)
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
def test_with_builtin_tools(self) -> None:
"""Test agent with built-in tool names."""
agent = ClaudeAgent(tools=["Read", "Write", "Bash"])
assert agent._builtin_tools == ["Read", "Write", "Bash"] # type: ignore[reportPrivateUsage]
assert agent._custom_tools == [] # type: ignore[reportPrivateUsage]
def test_with_mixed_tools(self) -> None:
"""Test agent with both built-in and custom tools."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=["Read", greet, "Bash"])
assert agent._builtin_tools == ["Read", "Bash"] # type: ignore[reportPrivateUsage]
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
# region Test ClaudeAgent Lifecycle
class TestClaudeAgentLifecycle:
"""Tests for ClaudeAgent tool initialization."""
def test_custom_tools_stored_from_constructor(self) -> None:
"""Test that custom tools from constructor are stored."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=[greet])
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
def test_multiple_custom_tools(self) -> None:
"""Test agent with multiple custom tools."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
@tool
def farewell(name: str) -> str:
"""Say goodbye."""
return f"Goodbye, {name}!"
agent = ClaudeAgent(tools=[greet, farewell])
assert len(agent._custom_tools) == 2 # type: ignore[reportPrivateUsage]
def test_no_tools(self) -> None:
"""Test agent without tools."""
agent = ClaudeAgent()
assert agent._custom_tools == [] # type: ignore[reportPrivateUsage]
assert agent._builtin_tools == [] # type: ignore[reportPrivateUsage]
# region Test ClaudeAgent Run
class TestClaudeAgentRun:
"""Tests for ClaudeAgent run method."""
@staticmethod
async def _create_async_generator(items: list[Any]) -> Any:
"""Helper to create async generator from list."""
for item in items:
yield item
def _create_mock_client(self, messages: list[Any]) -> MagicMock:
"""Create a mock ClaudeSDKClient that yields given messages."""
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.query = AsyncMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages))
return mock_client
async def test_run_with_string_message(self) -> None:
"""Test run with string message."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
messages = [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Hello!"},
},
uuid="event-1",
session_id="session-123",
),
AssistantMessage(
content=[TextBlock(text="Hello!")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="session-123",
),
]
mock_client = self._create_mock_client(messages)
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
response = await agent.run("Hello")
assert response.text == "Hello!"
async def test_run_captures_session_id(self) -> None:
"""Test that session ID is captured from ResultMessage."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
messages = [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Response"},
},
uuid="event-1",
session_id="test-session-id",
),
AssistantMessage(
content=[TextBlock(text="Response")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="test-session-id",
),
]
mock_client = self._create_mock_client(messages)
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
thread = agent.get_new_thread()
await agent.run("Hello", thread=thread)
assert thread.service_thread_id == "test-session-id"
async def test_run_with_thread(self) -> None:
"""Test run with existing thread."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
messages = [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Response"},
},
uuid="event-1",
session_id="session-123",
),
AssistantMessage(
content=[TextBlock(text="Response")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="session-123",
),
]
mock_client = self._create_mock_client(messages)
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
thread = agent.get_new_thread()
thread.service_thread_id = "existing-session"
await agent.run("Hello", thread=thread)
# region Test ClaudeAgent Run Stream
class TestClaudeAgentRunStream:
"""Tests for ClaudeAgent run_stream method."""
@staticmethod
async def _create_async_generator(items: list[Any]) -> Any:
"""Helper to create async generator from list."""
for item in items:
yield item
def _create_mock_client(self, messages: list[Any]) -> MagicMock:
"""Create a mock ClaudeSDKClient that yields given messages."""
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.query = AsyncMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages))
return mock_client
async def test_run_stream_yields_updates(self) -> None:
"""Test run_stream yields AgentResponseUpdate objects."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
messages = [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Streaming "},
},
uuid="event-1",
session_id="stream-session",
),
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "response"},
},
uuid="event-2",
session_id="stream-session",
),
AssistantMessage(
content=[TextBlock(text="Streaming response")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="stream-session",
),
]
mock_client = self._create_mock_client(messages)
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
updates: list[AgentResponseUpdate] = []
async for update in agent.run_stream("Hello"):
updates.append(update)
# StreamEvent yields text deltas
assert len(updates) == 2
assert updates[0].role == Role.ASSISTANT
assert updates[0].text == "Streaming "
assert updates[1].text == "response"
# region Test ClaudeAgent Session Management
class TestClaudeAgentSessionManagement:
"""Tests for ClaudeAgent session management."""
def test_get_new_thread(self) -> None:
"""Test get_new_thread creates a new thread."""
agent = ClaudeAgent()
thread = agent.get_new_thread()
assert isinstance(thread, AgentThread)
assert thread.service_thread_id is None
def test_get_new_thread_with_service_thread_id(self) -> None:
"""Test get_new_thread with existing service_thread_id."""
agent = ClaudeAgent()
thread = agent.get_new_thread(service_thread_id="existing-session-123")
assert isinstance(thread, AgentThread)
assert thread.service_thread_id == "existing-session-123"
def test_thread_inherits_context_provider(self) -> None:
"""Test that thread inherits context provider."""
mock_provider = MagicMock()
agent = ClaudeAgent(context_provider=mock_provider)
thread = agent.get_new_thread()
assert thread.context_provider == mock_provider
async def test_ensure_session_creates_client(self) -> None:
"""Test _ensure_session creates client when not started."""
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client_class.return_value = mock_client
agent = ClaudeAgent()
await agent._ensure_session(None) # type: ignore[reportPrivateUsage]
assert agent._started # type: ignore[reportPrivateUsage]
mock_client.connect.assert_called_once()
async def test_ensure_session_recreates_for_different_session(self) -> None:
"""Test _ensure_session recreates client for different session ID."""
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
mock_client1 = MagicMock()
mock_client1.connect = AsyncMock()
mock_client1.disconnect = AsyncMock()
mock_client2 = MagicMock()
mock_client2.connect = AsyncMock()
mock_client_class.side_effect = [mock_client1, mock_client2]
agent = ClaudeAgent()
# First session
await agent._ensure_session(None) # type: ignore[reportPrivateUsage]
assert agent._started # type: ignore[reportPrivateUsage]
# Different session should recreate client
await agent._ensure_session("new-session-id") # type: ignore[reportPrivateUsage]
assert agent._current_session_id == "new-session-id" # type: ignore[reportPrivateUsage]
mock_client1.disconnect.assert_called_once()
async def test_ensure_session_reuses_for_same_session(self) -> None:
"""Test _ensure_session reuses client for same session ID."""
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client_class.return_value = mock_client
agent = ClaudeAgent()
# First call
await agent._ensure_session("session-123") # type: ignore[reportPrivateUsage]
# Same session should not recreate
await agent._ensure_session("session-123") # type: ignore[reportPrivateUsage]
# Only called once
assert mock_client_class.call_count == 1
# region Test ClaudeAgent Tool Conversion
class TestClaudeAgentToolConversion:
"""Tests for ClaudeAgent tool conversion."""
def test_prepare_tools_creates_mcp_server(self) -> None:
"""Test _prepare_tools creates MCP server for AF tools."""
@tool
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
agent = ClaudeAgent(tools=[add])
server, tool_names = agent._prepare_tools(agent._custom_tools) # type: ignore[reportPrivateUsage]
assert server is not None
assert len(tool_names) == 1
assert tool_names[0] == f"mcp__{TOOLS_MCP_SERVER_NAME}__add"
def test_function_tool_to_sdk_mcp_tool(self) -> None:
"""Test converting FunctionTool to SDK MCP tool."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(greet) # type: ignore[reportPrivateUsage]
assert sdk_tool.name == "greet"
assert sdk_tool.description == "Greet someone."
assert sdk_tool.input_schema is not None
assert "properties" in sdk_tool.input_schema # type: ignore[operator]
async def test_tool_handler_success(self) -> None:
"""Test tool handler executes successfully."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(greet) # type: ignore[reportPrivateUsage]
result = await sdk_tool.handler({"name": "World"})
assert result["content"][0]["text"] == "Hello, World!"
async def test_tool_handler_error(self) -> None:
"""Test tool handler handles errors."""
@tool
def failing_tool() -> str:
"""A tool that fails."""
raise ValueError("Something went wrong")
agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(failing_tool) # type: ignore[reportPrivateUsage]
result = await sdk_tool.handler({})
assert "Error:" in result["content"][0]["text"]
assert "Something went wrong" in result["content"][0]["text"]
# region Test ClaudeAgent Permissions
class TestClaudeAgentPermissions:
"""Tests for ClaudeAgent permission handling."""
def test_default_permission_mode(self) -> None:
"""Test default permission mode."""
agent = ClaudeAgent()
assert agent._settings.permission_mode is None # type: ignore[reportPrivateUsage]
def test_permission_mode_from_settings(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test permission mode from environment settings."""
monkeypatch.setenv("CLAUDE_AGENT_PERMISSION_MODE", "acceptEdits")
settings = ClaudeAgentSettings()
assert settings.permission_mode == "acceptEdits"
def test_permission_mode_in_options(self) -> None:
"""Test permission mode in options."""
options: ClaudeAgentOptions = {
"permission_mode": "bypassPermissions",
}
agent = ClaudeAgent(default_options=options)
assert agent._settings.permission_mode == "bypassPermissions" # type: ignore[reportPrivateUsage]
# region Test ClaudeAgent Error Handling
class TestClaudeAgentErrorHandling:
"""Tests for ClaudeAgent error handling."""
@staticmethod
async def _empty_gen() -> Any:
"""Empty async generator."""
if False:
yield
async def test_handles_empty_response(self) -> None:
"""Test handling of empty response."""
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.query = AsyncMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
mock_client.receive_response = MagicMock(return_value=self._empty_gen())
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
response = await agent.run("Hello")
assert response.messages == []
# region Test Format Prompt
class TestFormatPrompt:
"""Tests for _format_prompt method."""
def test_format_empty_messages(self) -> None:
"""Test formatting empty messages."""
agent = ClaudeAgent()
result = agent._format_prompt([]) # type: ignore[reportPrivateUsage]
assert result == ""
def test_format_none_messages(self) -> None:
"""Test formatting None messages."""
agent = ClaudeAgent()
result = agent._format_prompt(None) # type: ignore[reportPrivateUsage]
assert result == ""
def test_format_user_message(self) -> None:
"""Test formatting user message."""
agent = ClaudeAgent()
msg = ChatMessage(
role=Role.USER,
contents=[Content.from_text(text="Hello")],
)
result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage]
assert "Hello" in result
def test_format_multiple_messages(self) -> None:
"""Test formatting multiple messages."""
agent = ClaudeAgent()
messages = [
ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hi")]),
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello!")]),
ChatMessage(role=Role.USER, contents=[Content.from_text(text="How are you?")]),
]
result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage]
assert "Hi" in result
assert "Hello!" in result
assert "How are you?" in result
# region Test Build Options
class TestPrepareClientOptions:
"""Tests for _prepare_client_options method."""
def test_prepare_client_options_with_settings(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test preparing options with settings."""
monkeypatch.setenv("CLAUDE_AGENT_MODEL", "opus")
monkeypatch.setenv("CLAUDE_AGENT_MAX_TURNS", "15")
agent = ClaudeAgent()
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
mock_opts.return_value = MagicMock()
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
call_kwargs = mock_opts.call_args[1]
assert call_kwargs.get("model") == "opus"
assert call_kwargs.get("max_turns") == 15
def test_prepare_client_options_with_instructions(self) -> None:
"""Test building options with instructions parameter."""
agent = ClaudeAgent(instructions="Be helpful")
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
mock_opts.return_value = MagicMock()
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
call_kwargs = mock_opts.call_args[1]
assert call_kwargs.get("system_prompt") == "Be helpful"
def test_prepare_client_options_includes_custom_tools(self) -> None:
"""Test that _prepare_client_options includes custom tools MCP server."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=[greet])
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
mock_opts.return_value = MagicMock()
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
call_kwargs = mock_opts.call_args[1]
assert "mcp_servers" in call_kwargs
assert TOOLS_MCP_SERVER_NAME in call_kwargs["mcp_servers"]
class TestApplyRuntimeOptions:
"""Tests for _apply_runtime_options method."""
async def test_apply_runtime_model(self) -> None:
"""Test applying runtime model option."""
mock_client = MagicMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
agent = ClaudeAgent()
agent._client = mock_client # type: ignore[reportPrivateUsage]
await agent._apply_runtime_options({"model": "opus"}) # type: ignore[reportPrivateUsage]
mock_client.set_model.assert_called_once_with("opus")
async def test_apply_runtime_permission_mode(self) -> None:
"""Test applying runtime permission_mode option."""
mock_client = MagicMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
agent = ClaudeAgent()
agent._client = mock_client # type: ignore[reportPrivateUsage]
await agent._apply_runtime_options({"permission_mode": "acceptEdits"}) # type: ignore[reportPrivateUsage]
mock_client.set_permission_mode.assert_called_once_with("acceptEdits")
async def test_apply_runtime_options_none(self) -> None:
"""Test applying None options does nothing."""
mock_client = MagicMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
agent = ClaudeAgent()
agent._client = mock_client # type: ignore[reportPrivateUsage]
await agent._apply_runtime_options(None) # type: ignore[reportPrivateUsage]
mock_client.set_model.assert_not_called()
mock_client.set_permission_mode.assert_not_called()
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"