Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
9f3e5d1
WIP
eavanvalkenburg Jan 20, 2026
390aa0c
big update to new ResponseStream model
eavanvalkenburg Jan 22, 2026
d79285e
fixed tests and typing
eavanvalkenburg Jan 23, 2026
61b9cad
fixed tests and typing
eavanvalkenburg Feb 4, 2026
f083d53
fixed tools typevar import
eavanvalkenburg Jan 23, 2026
bd57450
fix
eavanvalkenburg Jan 23, 2026
d0a82a8
mypy fix
eavanvalkenburg Jan 23, 2026
d41944d
mypy fixes and some cleanup
eavanvalkenburg Jan 23, 2026
185812c
fix missing quoted names
eavanvalkenburg Feb 4, 2026
1f14d14
and client
eavanvalkenburg Jan 23, 2026
ca74f76
fix imports agui
eavanvalkenburg Jan 23, 2026
03fdb1e
fix anthropic override
eavanvalkenburg Jan 23, 2026
fa166de
fix agui
eavanvalkenburg Jan 23, 2026
cd526f0
fix ag ui
eavanvalkenburg Feb 4, 2026
cb0d757
fix import
eavanvalkenburg Jan 23, 2026
4d35f47
fix anthropic types
eavanvalkenburg Jan 23, 2026
fdbe3a1
fix mypy
eavanvalkenburg Jan 23, 2026
18d1c8d
refactoring
eavanvalkenburg Jan 23, 2026
9f4f2db
updated typing
eavanvalkenburg Jan 29, 2026
4d7f852
fix 3.11
eavanvalkenburg Jan 29, 2026
c24d994
fixes
eavanvalkenburg Feb 4, 2026
806ab90
redid layering of chat clients and agents
eavanvalkenburg Feb 4, 2026
7b384ac
redid layering of chat clients and agents
eavanvalkenburg Feb 4, 2026
144d368
Fix lint, type, and test issues after rebase
eavanvalkenburg Jan 30, 2026
89dba9f
Fix AgentExecutionException import error in test_agents.py
eavanvalkenburg Jan 30, 2026
40f67ec
Fix test import and asyncio deprecation issues
eavanvalkenburg Jan 30, 2026
019afbf
Fix azure-ai test failures
eavanvalkenburg Jan 30, 2026
73eae0c
Convert ag-ui utils_test_ag_ui.py to conftest.py
eavanvalkenburg Jan 30, 2026
7818c50
fix: use relative imports for ag-ui test utilities
eavanvalkenburg Jan 30, 2026
7b81568
fix agui
eavanvalkenburg Jan 30, 2026
ace11f8
Rename Bare*Client to Raw*Client and BaseChatClient
eavanvalkenburg Jan 30, 2026
e45a0d5
Fix layer ordering: FunctionInvocationLayer before ChatTelemetryLayer
eavanvalkenburg Jan 30, 2026
df011a9
Remove run_stream usage
eavanvalkenburg Feb 4, 2026
4802a7d
Fix conversation_id propagation
eavanvalkenburg Feb 4, 2026
fc795d1
Update uv.lock with latest dependencies
eavanvalkenburg Jan 30, 2026
827565e
Python: Add BaseAgent implementation for Claude Agent SDK (#3509)
dmytrostruk Jan 30, 2026
cdb9c10
Update Claude agent connector layering
eavanvalkenburg Feb 4, 2026
6e1888a
fix test and plugin
eavanvalkenburg Feb 1, 2026
33238c8
Store function middleware in invocation layer
eavanvalkenburg Feb 2, 2026
0f65c24
Fix telemetry streaming and ag-ui tests
eavanvalkenburg Feb 2, 2026
98fa1a5
Remove legacy ag-ui tests folder
eavanvalkenburg Feb 4, 2026
6410c1b
updates
eavanvalkenburg Feb 4, 2026
effca46
Remove terminate flag from FunctionInvocationContext, use MiddlewareT…
eavanvalkenburg Feb 3, 2026
4377bdd
fix: remove references to removed terminate flag in purview tests, ad…
eavanvalkenburg Feb 3, 2026
749cce8
fix: move _test_utils.py from package to test folder
eavanvalkenburg Feb 3, 2026
cc83c9c
fix: call get_final_response() to trigger context provider notificati…
eavanvalkenburg Feb 3, 2026
d0f802c
fix: correct broken links in tools README
eavanvalkenburg Feb 3, 2026
63e151d
docs: clarify default middleware behavior in summary table
eavanvalkenburg Feb 3, 2026
40c6e06
fix: ensure inner stream result hooks are called when using map()/fro…
eavanvalkenburg Feb 3, 2026
4e60766
Fix mypy type errors
eavanvalkenburg Feb 4, 2026
3ddc226
Address PR review comments on observability.py
eavanvalkenburg Feb 3, 2026
ed4446d
Remove gen_ai.client.operation.duration from span attributes
eavanvalkenburg Feb 3, 2026
f433f87
Remove duration from _get_response_attributes, pass directly to _capt…
eavanvalkenburg Feb 3, 2026
d0ef476
Remove redundant _close_span cleanup hook in AgentTelemetryLayer
eavanvalkenburg Feb 3, 2026
7bc1a0e
Use weakref.finalize to close span when stream is garbage collected
eavanvalkenburg Feb 3, 2026
2cd1362
Fix _get_finalizers_from_stream to use _result_hooks attribute
eavanvalkenburg Feb 4, 2026
c602cdc
Add missing asyncio import in test_request_info_mixin.py
eavanvalkenburg Feb 4, 2026
d3e8568
Fix leftover merge conflict marker in image_generation sample
eavanvalkenburg Feb 4, 2026
5165b60
Update integration tests
eavanvalkenburg Feb 4, 2026
499c1a5
Fix integration tests: increase max_iterations from 1 to 2
eavanvalkenburg Feb 4, 2026
23a82af
Fix duplicate function call error in conversation-based APIs
eavanvalkenburg Feb 4, 2026
c3b120f
Add regression test for conversation_id propagation between tool iter…
eavanvalkenburg Feb 4, 2026
f7bc4d3
Fix tool_choice=required to return after tool execution
eavanvalkenburg Feb 4, 2026
3b01600
Document tool_choice behavior in tools README
eavanvalkenburg Feb 4, 2026
e4b005e
Fix tool_choice=None behavior - don't default to 'auto'
eavanvalkenburg Feb 4, 2026
13a5e03
Fix tool_choice=none should not remove tools
eavanvalkenburg Feb 4, 2026
0f416c0
Add test for tool_choice=none preserving tools
eavanvalkenburg Feb 4, 2026
db3adfd
Fix tool_choice=none should not remove tools in all clients
eavanvalkenburg Feb 4, 2026
f9f5bd7
Keep tool_choice even when tools is None
eavanvalkenburg Feb 4, 2026
a9807ad
Update test to match new parallel_tool_calls behavior
eavanvalkenburg Feb 4, 2026
7e739d1
Fix ChatMessage API and Role enum usage after rebase
eavanvalkenburg Feb 4, 2026
d3daf0f
Fix additional ChatMessage API and method name changes
eavanvalkenburg Feb 4, 2026
c824cc3
Fix remaining ChatMessage API usage in test files
eavanvalkenburg Feb 4, 2026
11bd057
Fix more ChatMessage and Role API changes in source and test files
eavanvalkenburg Feb 4, 2026
23b22c0
Fix ChatMessage and Role API changes across packages
eavanvalkenburg Feb 4, 2026
32aa605
Fix ChatMessage and Role API changes in github_copilot tests
eavanvalkenburg Feb 4, 2026
22c78e5
Fix ChatMessage and Role API changes in redis and github_copilot pack…
eavanvalkenburg Feb 4, 2026
0e56ecf
Fix ChatMessage and Role API changes in devui package
eavanvalkenburg Feb 4, 2026
9ebb1e3
Fix ChatMessage and Role API changes in a2a and lab packages
eavanvalkenburg Feb 4, 2026
4cefbc0
Remove duplicate test files from ag-ui/tests (tests are in ag_ui_tests)
eavanvalkenburg Feb 4, 2026
18f650a
Fix ChatMessage and Role API changes across packages
eavanvalkenburg Feb 4, 2026
1d93a19
Fix mypy errors for ChatMessage and Role API changes
eavanvalkenburg Feb 4, 2026
d42c470
Improve CI test timeout configuration
eavanvalkenburg Feb 4, 2026
6d1f1ac
Fix ChatMessage API usage in docstrings and source
eavanvalkenburg Feb 4, 2026
008e327
Revert tool_choice/parallel_tool_calls changes - must be removed when…
eavanvalkenburg Feb 4, 2026
dfb121f
fixed issue in tests
eavanvalkenburg Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 3 additions & 4 deletions .github/workflows/python-merge-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ jobs:
uses: ./.github/actions/azure-functions-integration-setup
id: azure-functions-setup
- name: Test with pytest
timeout-minutes: 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 900 --retries 3 --retry-delay 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
working-directory: ./python
- name: Test core samples
timeout-minutes: 10
Expand Down Expand Up @@ -153,8 +152,8 @@ jobs:
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
timeout-minutes: 10
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
timeout-minutes: 15
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
working-directory: ./python
- name: Test Azure AI samples
timeout-minutes: 10
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/0012-python-typeddict-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,4 @@ response = await client.get_response(

Chosen option: **"Option 2: TypedDict with Generic Type Parameters"**, because it provides full type safety, excellent IDE support with autocompletion, and allows users to extend provider-specific options for their use cases. Extended this Generic to ChatAgents in order to also properly type the options used in agent construction and run methods.

See [typed_options.py](../../python/samples/getting_started/chat_client/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
See [typed_options.py](../../python/samples/concepts/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
2 changes: 2 additions & 0 deletions python/.cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"endregion",
"entra",
"faiss",
"finalizer",
"finalizers",
"genai",
"generativeai",
"hnsw",
Expand Down
2 changes: 1 addition & 1 deletion python/.github/instructions/python.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ applyTo: '**/agent-framework/python/**'
- Do not use `Optional`; use `Type | None` instead.
- Before running any commands to execute or test the code, ensure that all problems, compilation errors, and warnings are resolved.
- When formatting files, format only the files you changed or are currently working on; do not format the entire codebase.
- Do not mark new tests with `@pytest.mark.asyncio`.
- Do not mark new tests with `@pytest.mark.asyncio`, they are marked automatically, so you can just set the test to `async def`.
- If you need debug information to understand an issue, use print statements as needed and remove them when testing is complete.
- Avoid adding excessive comments.
- When working with samples, make sure to update the associated README files with the latest information. These files are usually located in the same folder as the sample or in one of its parent folders.
Expand Down
101 changes: 75 additions & 26 deletions python/packages/a2a/agent_framework_a2a/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import json
import re
import uuid
from collections.abc import AsyncIterable, Sequence
from typing import Any, Final, cast
from collections.abc import AsyncIterable, Awaitable, Sequence
from typing import Any, Final, Literal, cast, overload

import httpx
from a2a.client import Client, ClientConfig, ClientFactory, minimal_agent_card
Expand All @@ -32,10 +32,12 @@
BaseAgent,
ChatMessage,
Content,
ResponseStream,
Role,
normalize_messages,
prepend_agent_framework_to_user_agent,
)
from agent_framework.observability import use_agent_instrumentation
from agent_framework.observability import AgentTelemetryLayer

__all__ = ["A2AAgent"]

Expand All @@ -56,8 +58,7 @@ def _get_uri_data(uri: str) -> str:
return match.group("base64_data")


@use_agent_instrumentation
class A2AAgent(BaseAgent):
class A2AAgent(AgentTelemetryLayer, BaseAgent):
"""Agent2Agent (A2A) protocol implementation.

Wraps an A2A Client to connect the Agent Framework with external A2A-compliant agents
Expand Down Expand Up @@ -184,44 +185,92 @@ async def __aexit__(
if self._http_client is not None and self._close_http_client:
await self._http_client.aclose()

async def run(
@overload
def run(
self,
messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
stream: Literal[False] = ...,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AgentResponse:
) -> Awaitable[AgentResponse[Any]]: ...

@overload
def run(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
stream: Literal[True],
thread: AgentThread | None = None,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...

def run(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
stream: bool = False,
thread: AgentThread | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
"""Get a response from the agent.

This method returns the final result of the agent's execution
as a single AgentResponse object. The caller is blocked until
the final result is available.
as a single AgentResponse object when stream=False. When stream=True,
it returns a ResponseStream that yields AgentResponseUpdate objects.

Args:
messages: The message(s) to send to the agent.

Keyword Args:
stream: Whether to stream the response. Defaults to False.
thread: The conversation thread associated with the message(s).
kwargs: Additional keyword arguments.

Returns:
An agent response item.
When stream=False: An Awaitable[AgentResponse].
When stream=True: A ResponseStream of AgentResponseUpdate items.
"""
if stream:
return self._run_stream_impl(messages=messages, thread=thread, **kwargs)
return self._run_impl(messages=messages, thread=thread, **kwargs)

async def _run_impl(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AgentResponse[Any]:
"""Non-streaming implementation of run."""
# Collect all updates and use framework to consolidate updates into response
updates = [update async for update in self.run_stream(messages, thread=thread, **kwargs)]
return AgentResponse.from_updates(updates)
updates: list[AgentResponseUpdate] = []
async for update in self._stream_updates(messages, thread=thread, **kwargs):
updates.append(update)
return AgentResponse.from_agent_run_response_updates(updates)

async def run_stream(
def _run_stream_impl(
self,
messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentResponseUpdate]:
"""Run the agent as a stream.
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
"""Streaming implementation of run."""

def _finalize(updates: Sequence[AgentResponseUpdate]) -> AgentResponse[Any]:
return AgentResponse.from_agent_run_response_updates(list(updates))

return ResponseStream(self._stream_updates(messages, thread=thread, **kwargs), finalizer=_finalize)

This method will return the intermediate steps and final results of the
agent's execution as a stream of AgentResponseUpdate objects to the caller.
async def _stream_updates(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentResponseUpdate]:
"""Internal method to stream updates from the A2A agent.

Args:
messages: The message(s) to send to the agent.
Expand All @@ -231,10 +280,10 @@ async def run_stream(
kwargs: Additional keyword arguments.

Yields:
An agent response item.
AgentResponseUpdate items from the A2A agent.
"""
messages = normalize_messages(messages)
a2a_message = self._prepare_message_for_a2a(messages[-1])
normalized_messages = normalize_messages(messages)
a2a_message = self._prepare_message_for_a2a(normalized_messages[-1])

response_stream = self.client.send_message(a2a_message)

Expand All @@ -244,7 +293,7 @@ async def run_stream(
contents = self._parse_contents_from_a2a(item.parts)
yield AgentResponseUpdate(
contents=contents,
role="assistant" if item.role == A2ARole.agent else "user",
role=Role.ASSISTANT if item.role == A2ARole.agent else Role.USER,
response_id=str(getattr(item, "message_id", uuid.uuid4())),
raw_representation=item,
)
Expand All @@ -268,7 +317,7 @@ async def run_stream(
# Empty task
yield AgentResponseUpdate(
contents=[],
role="assistant",
role=Role.ASSISTANT,
response_id=task.id,
raw_representation=task,
)
Expand Down Expand Up @@ -420,7 +469,7 @@ def _parse_messages_from_task(self, task: Task) -> list[ChatMessage]:
contents = self._parse_contents_from_a2a(history_item.parts)
messages.append(
ChatMessage(
role="assistant" if history_item.role == A2ARole.agent else "user",
role=Role.ASSISTANT if history_item.role == A2ARole.agent else Role.USER,
contents=contents,
raw_representation=history_item,
)
Expand All @@ -432,7 +481,7 @@ def _parse_message_from_artifact(self, artifact: Artifact) -> ChatMessage:
"""Parse A2A Artifact into ChatMessage using part contents."""
contents = self._parse_contents_from_a2a(artifact.parts)
return ChatMessage(
role="assistant",
role=Role.ASSISTANT,
contents=contents,
raw_representation=artifact,
)
26 changes: 13 additions & 13 deletions python/packages/a2a/tests/test_a2a_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ async def test_run_with_message_response(a2a_agent: A2AAgent, mock_a2a_client: M

assert isinstance(response, AgentResponse)
assert len(response.messages) == 1
assert response.messages[0].role == "assistant"
assert response.messages[0].role.value == "assistant"
assert response.messages[0].text == "Hello from agent!"
assert response.response_id == "msg-123"
assert mock_a2a_client.call_count == 1
Expand All @@ -143,7 +143,7 @@ async def test_run_with_task_response_single_artifact(a2a_agent: A2AAgent, mock_

assert isinstance(response, AgentResponse)
assert len(response.messages) == 1
assert response.messages[0].role == "assistant"
assert response.messages[0].role.value == "assistant"
assert response.messages[0].text == "Generated report content"
assert response.response_id == "task-456"
assert mock_a2a_client.call_count == 1
Expand All @@ -169,7 +169,7 @@ async def test_run_with_task_response_multiple_artifacts(a2a_agent: A2AAgent, mo

# All should be assistant messages
for message in response.messages:
assert message.role == "assistant"
assert message.role.value == "assistant"

assert response.response_id == "task-789"

Expand Down Expand Up @@ -232,7 +232,7 @@ def test_parse_messages_from_task_with_artifacts(a2a_agent: A2AAgent) -> None:
assert len(result) == 2
assert result[0].text == "Content 1"
assert result[1].text == "Content 2"
assert all(msg.role == "assistant" for msg in result)
assert all(msg.role.value == "assistant" for msg in result)


def test_parse_message_from_artifact(a2a_agent: A2AAgent) -> None:
Expand All @@ -251,7 +251,7 @@ def test_parse_message_from_artifact(a2a_agent: A2AAgent) -> None:
result = a2a_agent._parse_message_from_artifact(artifact)

assert isinstance(result, ChatMessage)
assert result.role == "assistant"
assert result.role.value == "assistant"
assert result.text == "Artifact content"
assert result.raw_representation == artifact

Expand Down Expand Up @@ -295,7 +295,7 @@ def test_prepare_message_for_a2a_with_error_content(a2a_agent: A2AAgent) -> None

# Create ChatMessage with ErrorContent
error_content = Content.from_error(message="Test error message")
message = ChatMessage("user", [error_content])
message = ChatMessage(role="user", contents=[error_content])

# Convert to A2A message
a2a_message = a2a_agent._prepare_message_for_a2a(message)
Expand All @@ -310,7 +310,7 @@ def test_prepare_message_for_a2a_with_uri_content(a2a_agent: A2AAgent) -> None:

# Create ChatMessage with UriContent
uri_content = Content.from_uri(uri="http://example.com/file.pdf", media_type="application/pdf")
message = ChatMessage("user", [uri_content])
message = ChatMessage(role="user", contents=[uri_content])

# Convert to A2A message
a2a_message = a2a_agent._prepare_message_for_a2a(message)
Expand All @@ -326,7 +326,7 @@ def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None:

# Create ChatMessage with DataContent (base64 data URI)
data_content = Content.from_uri(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain")
message = ChatMessage("user", [data_content])
message = ChatMessage(role="user", contents=[data_content])

# Convert to A2A message
a2a_message = a2a_agent._prepare_message_for_a2a(message)
Expand All @@ -340,26 +340,26 @@ def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None:
def test_prepare_message_for_a2a_empty_contents_raises_error(a2a_agent: A2AAgent) -> None:
"""Test _prepare_message_for_a2a with empty contents raises ValueError."""
# Create ChatMessage with no contents
message = ChatMessage("user", [])
message = ChatMessage(role="user", contents=[])

# Should raise ValueError for empty contents
with raises(ValueError, match="ChatMessage.contents is empty"):
a2a_agent._prepare_message_for_a2a(message)


async def test_run_stream_with_message_response(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None:
"""Test run_stream() method with immediate Message response."""
async def test_run_streaming_with_message_response(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None:
"""Test run(stream=True) method with immediate Message response."""
mock_a2a_client.add_message_response("msg-stream-123", "Streaming response from agent!", "agent")

# Collect streaming updates
updates: list[AgentResponseUpdate] = []
async for update in a2a_agent.run_stream("Hello agent"):
async for update in a2a_agent.run("Hello agent", stream=True):
updates.append(update)

# Verify streaming response
assert len(updates) == 1
assert isinstance(updates[0], AgentResponseUpdate)
assert updates[0].role == "assistant"
assert updates[0].role.value == "assistant"
assert len(updates[0].contents) == 1

content = updates[0].contents[0]
Expand Down
2 changes: 1 addition & 1 deletion python/packages/ag-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ from agent_framework.ag_ui import AGUIChatClient
async def main():
async with AGUIChatClient(endpoint="http://localhost:8000/") as client:
# Stream responses
async for update in client.get_streaming_response("Hello!"):
async for update in client.get_response("Hello!", stream=True):
for content in update.contents:
if isinstance(content, TextContent):
print(content.text, end="", flush=True)
Expand Down
1 change: 1 addition & 0 deletions python/packages/ag-ui/ag_ui_tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
Loading
Loading