Python: [BREAKING] Redesign Python exception hierarchy (#4082)

* [BREAKING] Redesign Python exception hierarchy

Replace the flat ServiceException family with domain-scoped branches:
- AgentException (with InvalidAuth, InvalidRequest, InvalidResponse, ContentFilter)
- ChatClientException (same consistent suberrors)
- IntegrationException (same + InitializationError)
- WorkflowException (Runner, Convergence, Checkpoint, Validation, Action, Declarative)
- ContentError (AdditionItemMismatch)
- ToolException / ToolExecutionException (unchanged)
- MiddlewareException / MiddlewareTermination (unchanged)

Key changes:
- All Service* exceptions removed (ServiceException, ServiceInitializationError, etc.)
- AgentExecutionException split into AgentInvalidRequest/ResponseException
- AgentInvocationError removed, split into AgentInvalidRequest/ResponseException
- Workflow exceptions moved from _workflows/_exceptions.py into main exceptions.py
- _workflows/__init__.py emptied; main __init__.py imports directly from submodules
- Purview exceptions re-parented under IntegrationException hierarchy
- Init validation errors use built-in ValueError/TypeError instead of custom exceptions
- CODING_STANDARD.md updated with hierarchy design and rationale

Fixes microsoft/agent-framework#3410

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify ToolException vs ToolExecutionException docstrings

ToolException: base class for all tool-related exceptions (preconditions,
connection/init failures).
ToolExecutionException: runtime call failures (tool call failed, reconnect
failed, MCP errors).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix remaining stale imports from agent_framework._workflows

- azurefunctions: _context.py, _app.py, _serialization.py, test_func_utils.py
  used 'from agent_framework._workflows import X' which broke after
  emptying _workflows/__init__.py; changed to direct submodule imports
- azure-ai-search: test still referenced ServiceInitializationError;
  updated to ValueError to match production code

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-02-19 18:58:14 +01:00
committed by GitHub
Unverified
parent 7f606a2e3a
commit 5ee06853a1
90 changed files with 642 additions and 718 deletions
@@ -14,7 +14,6 @@ from typing import TYPE_CHECKING, Any, ClassVar
from agent_framework import Message
from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext
from agent_framework.exceptions import ServiceInitializationError
from mem0 import AsyncMemory, AsyncMemoryClient
if sys.version_info >= (3, 11):
@@ -172,9 +171,7 @@ class Mem0ContextProvider(BaseContextProvider):
def _validate_filters(self) -> None:
"""Validates that at least one filter is provided."""
if not self.agent_id and not self.user_id and not self.application_id:
raise ServiceInitializationError(
"At least one of the filters: agent_id, user_id, or application_id is required."
)
raise ValueError("At least one of the filters: agent_id, user_id, or application_id is required.")
def _build_filters(self) -> dict[str, Any]:
"""Build search filters from initialization parameters."""
@@ -8,7 +8,6 @@ from unittest.mock import AsyncMock, patch
import pytest
from agent_framework import AgentResponse, Message
from agent_framework._sessions import AgentSession, SessionContext
from agent_framework.exceptions import ServiceInitializationError
from agent_framework_mem0._context_provider import Mem0ContextProvider
@@ -136,15 +135,13 @@ class TestBeforeRun:
assert "mem0" not in ctx.context_messages
async def test_validates_filters_before_search(self, mock_mem0_client: AsyncMock) -> None:
"""Raises ServiceInitializationError when no filters."""
"""Raises ValueError when no filters."""
provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client)
session = AgentSession(session_id="test-session")
ctx = SessionContext(input_messages=[Message(role="user", text="test")], session_id="s1")
with pytest.raises(ServiceInitializationError, match="At least one of the filters"):
await provider.before_run(
agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {})
) # type: ignore[arg-type]
with pytest.raises(ValueError, match="At least one of the filters"):
await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type]
async def test_v1_1_response_format(self, mock_mem0_client: AsyncMock) -> None:
"""Search response in v1.1 dict format with 'results' key."""
@@ -312,16 +309,14 @@ class TestAfterRun:
assert "run_id" not in mock_mem0_client.add.call_args.kwargs
async def test_validates_filters(self, mock_mem0_client: AsyncMock) -> None:
"""Raises ServiceInitializationError when no filters."""
"""Raises ValueError when no filters."""
provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client)
session = AgentSession(session_id="test-session")
ctx = SessionContext(input_messages=[Message(role="user", text="hi")], session_id="s1")
ctx._response = AgentResponse(messages=[Message(role="assistant", text="hey")])
with pytest.raises(ServiceInitializationError, match="At least one of the filters"):
await provider.after_run(
agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {})
) # type: ignore[arg-type]
with pytest.raises(ValueError, match="At least one of the filters"):
await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type]
async def test_stores_with_application_id_metadata(self, mock_mem0_client: AsyncMock) -> None:
"""application_id is passed in metadata."""
@@ -347,7 +342,7 @@ class TestValidateFilters:
def test_raises_when_no_filters(self, mock_mem0_client: AsyncMock) -> None:
provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client)
with pytest.raises(ServiceInitializationError, match="At least one of the filters"):
with pytest.raises(ValueError, match="At least one of the filters"):
provider._validate_filters()
def test_passes_with_user_id(self, mock_mem0_client: AsyncMock) -> None: