Skip to content

Commit 5ee0685

Browse files
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 #3410 Co-authored-by: Copilot <[email protected]> * 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 7f606a2 commit 5ee0685

90 files changed

Lines changed: 631 additions & 707 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

python/CODING_STANDARD.md

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,105 @@ The package follows a flat import structure:
165165
from agent_framework.azure import AzureOpenAIChatClient
166166
```
167167

168+
## Exception Hierarchy
169+
170+
The Agent Framework defines a structured exception hierarchy rooted at `AgentFrameworkException`. Every AF-specific
171+
exception inherits from this base, so callers can catch `AgentFrameworkException` as a broad fallback. The hierarchy
172+
is organized into domain-specific L1 branches, each with a consistent set of leaf exceptions where applicable.
173+
174+
### Design Principles
175+
176+
- **Domain-scoped branches**: Exceptions are grouped by the subsystem that raises them (agent, chat client,
177+
integration, workflow, content, tool, middleware), not by HTTP status code or generic error category.
178+
- **Consistent suberror pattern**: The `AgentException`, `ChatClientException`, and `IntegrationException` branches
179+
share a parallel set of leaf exceptions (`InvalidAuth`, `InvalidRequest`, `InvalidResponse`, `ContentFilter`) so
180+
that callers can handle the same failure mode uniformly across domains.
181+
- **Built-ins for validation**: Configuration/parameter validation errors use Python built-in exceptions
182+
(`ValueError`, `TypeError`, `RuntimeError`) rather than AF-specific classes. AF exceptions are reserved for
183+
domain-level failures that callers may want to catch and handle distinctly from programming errors.
184+
- **No compatibility aliases**: When exceptions are renamed or removed, the old names are not kept as aliases.
185+
This is a deliberate trade-off for hierarchy clarity over backward compatibility.
186+
- **Suffix convention**: L1 branch classes use `...Exception` (e.g., `AgentException`). Leaf classes may use
187+
either `...Exception` or `...Error` depending on the domain convention (e.g., `ContentError`,
188+
`WorkflowValidationError`). Within a branch, the suffix is consistent.
189+
190+
### Full Hierarchy
191+
192+
```
193+
AgentFrameworkException # Base for all AF exceptions
194+
├── AgentException # Agent-scoped failures
195+
│ ├── AgentInvalidAuthException # Agent auth failures
196+
│ ├── AgentInvalidRequestException # Invalid request to agent (e.g., agent not found, bad input)
197+
│ ├── AgentInvalidResponseException # Invalid/unexpected response from agent
198+
│ └── AgentContentFilterException # Agent content filter triggered
199+
200+
├── ChatClientException # Chat client lifecycle and communication failures
201+
│ ├── ChatClientInvalidAuthException # Chat client auth failures
202+
│ ├── ChatClientInvalidRequestException # Invalid request to chat client
203+
│ ├── ChatClientInvalidResponseException # Invalid/unexpected response from chat client
204+
│ └── ChatClientContentFilterException # Chat client content filter triggered
205+
206+
├── IntegrationException # External service/dependency integration failures
207+
│ ├── IntegrationInitializationError # Wrapped dependency lifecycle failure during setup
208+
│ ├── IntegrationInvalidAuthException # Integration auth failures (e.g., 401/403)
209+
│ ├── IntegrationInvalidRequestException # Invalid request to integration
210+
│ ├── IntegrationInvalidResponseException # Invalid/unexpected response from integration
211+
│ └── IntegrationContentFilterException # Integration content filter triggered
212+
213+
├── ContentError # Content processing/validation failures
214+
│ └── AdditionItemMismatch # Type mismatch when merging content items
215+
216+
├── WorkflowException # Workflow engine failures
217+
│ ├── WorkflowRunnerException # Runtime execution failures
218+
│ │ ├── WorkflowConvergenceException # Runner exceeded max iterations
219+
│ │ └── WorkflowCheckpointException # Checkpoint save/restore/decode failures
220+
│ ├── WorkflowValidationError # Graph validation errors
221+
│ │ ├── EdgeDuplicationError # Duplicate edge in workflow graph
222+
│ │ ├── TypeCompatibilityError # Type mismatch between connected executors
223+
│ │ └── GraphConnectivityError # Graph connectivity issues
224+
│ ├── WorkflowActionError # User-level error from declarative ThrowException action
225+
│ └── DeclarativeWorkflowError # Declarative workflow definition/YAML errors
226+
227+
├── ToolException # Tool-related failures
228+
│ └── ToolExecutionException # Failure during tool execution
229+
230+
├── MiddlewareException # Middleware failures
231+
│ └── MiddlewareTermination # Control-flow: early middleware termination
232+
233+
└── SettingNotFoundError # Required setting not resolved from any source
234+
```
235+
236+
### When to Use AF Exceptions vs Built-ins
237+
238+
| Scenario | Exception to use |
239+
|---|---|
240+
| Missing or invalid constructor argument (e.g., `api_key` is `None`) | `ValueError` or `TypeError` |
241+
| Object in wrong state (e.g., client not initialized) | `RuntimeError` |
242+
| External service returns 401/403 | `IntegrationInvalidAuthException` (or `ChatClient`/`Agent` variant) |
243+
| External service returns unexpected response | `IntegrationInvalidResponseException` (or variant) |
244+
| Content filter blocks a request | `IntegrationContentFilterException` (or variant) |
245+
| Request validation fails before sending to service | `IntegrationInvalidRequestException` (or variant) |
246+
| Agent not found in registry | `AgentInvalidRequestException` |
247+
| Agent returned no/bad response | `AgentInvalidResponseException` |
248+
| Workflow runner exceeds max iterations | `WorkflowConvergenceException` |
249+
| Checkpoint serialization/deserialization failure | `WorkflowCheckpointException` |
250+
| Workflow graph has invalid structure | `WorkflowValidationError` (or specific subclass) |
251+
| Declarative YAML definition error | `DeclarativeWorkflowError` |
252+
| Tool execution failure | `ToolExecutionException` |
253+
| Content merge type mismatch | `AdditionItemMismatch` |
254+
255+
### Choosing Between Agent, ChatClient, and Integration Branches
256+
257+
- **`AgentException`**: The failure is scoped to agent-level logic — agent lookup, agent response handling,
258+
agent content filtering. Use when the agent itself is the source of the problem.
259+
- **`ChatClientException`**: The failure is scoped to the chat client (the LLM provider connection) — auth with
260+
the LLM provider, request/response format issues specific to the chat protocol, chat-level content filtering.
261+
- **`IntegrationException`**: The failure is in a non-chat external dependency — search services, vector stores,
262+
Purview, custom APIs, or any service that is not the primary LLM chat provider.
263+
264+
When in doubt: if the code is in a chat client constructor or method, use `ChatClient*`. If it's in an agent
265+
method, use `Agent*`. If it's talking to an external service that isn't the chat LLM, use `Integration*`.
266+
168267
## Package Structure
169268

170269
The project uses a monorepo structure with separate packages for each connector/extension:
@@ -299,7 +398,7 @@ They should contain:
299398
- Returns are specified after a header called `Returns:` or `Yields:`, with the return type and explanation of the return value.
300399
- Keyword arguments are specified after a header called `Keyword Args:`, with each argument being specified in the same format as `Args:`.
301400
- A header for exceptions can be added, called `Raises:`, following these guidelines:
302-
- **Always document** Agent Framework specific exceptions (e.g., `AgentInitializationError`, `AgentExecutionException`)
401+
- **Always document** Agent Framework specific exceptions (e.g., `AgentInvalidRequestException`, `IntegrationInvalidAuthException`)
303402
- **Only document** standard Python exceptions (TypeError, ValueError, KeyError, etc.) when the condition is non-obvious or provides value to API users
304403
- Format: `ExceptionType`: Explanation of the exception.
305404
- If a longer explanation is needed, it should be placed on the next line, indented by 4 spaces.

python/packages/ag-ui/agent_framework_ag_ui/_run.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
normalize_function_invocation_configuration,
4141
)
4242
from agent_framework._types import ResponseStream
43-
from agent_framework.exceptions import AgentExecutionException
43+
from agent_framework.exceptions import AgentInvalidResponseException
4444

4545
from ._message_adapters import normalize_agui_input_messages
4646
from ._orchestration._predictive_state import PredictiveStateHandler
@@ -207,7 +207,7 @@ async def _normalize_response_stream(response_stream: Any) -> AsyncIterable[Any]
207207
if isinstance(resolved_stream, AsyncIterable):
208208
return cast(AsyncIterable[Any], resolved_stream)
209209
resolved_type = f"{type(resolved_stream).__module__}.{type(resolved_stream).__name__}"
210-
raise AgentExecutionException(
210+
raise AgentInvalidResponseException(
211211
"Agent did not return a streaming AsyncIterable response. "
212212
f"Awaitable resolved to unsupported type: {resolved_type}."
213213
)
@@ -220,7 +220,7 @@ async def _normalize_response_stream(response_stream: Any) -> AsyncIterable[Any]
220220
return cast(AsyncIterable[Any], response_stream)
221221

222222
stream_type = f"{type(response_stream).__module__}.{type(response_stream).__name__}"
223-
raise AgentExecutionException(
223+
raise AgentInvalidResponseException(
224224
f"Agent did not return a streaming AsyncIterable response. Received unsupported type: {stream_type}."
225225
)
226226

python/packages/ag-ui/tests/ag_ui/test_run.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
TextMessageStartEvent,
99
)
1010
from agent_framework import AgentResponseUpdate, Content, Message, ResponseStream
11-
from agent_framework.exceptions import AgentExecutionException
11+
from agent_framework.exceptions import AgentInvalidResponseException
1212

1313
from agent_framework_ag_ui._run import (
1414
FlowState,
@@ -226,7 +226,7 @@ async def _resolve():
226226

227227
async def test_rejects_non_stream_values(self):
228228
"""Reject unsupported stream return values."""
229-
with pytest.raises(AgentExecutionException):
229+
with pytest.raises(AgentInvalidResponseException):
230230
await _normalize_response_stream("not-a-stream")
231231

232232

python/packages/anthropic/agent_framework_anthropic/_chat_client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
)
2929
from agent_framework._settings import SecretString, load_settings
3030
from agent_framework._types import _get_data_bytes_as_str # type: ignore
31-
from agent_framework.exceptions import ServiceInitializationError
3231
from agent_framework.observability import ChatTelemetryLayer
3332
from anthropic import AsyncAnthropic
3433
from anthropic.types.beta import (
@@ -303,7 +302,7 @@ class MyOptions(AnthropicChatOptions, total=False):
303302

304303
if anthropic_client is None:
305304
if not anthropic_settings["api_key"]:
306-
raise ServiceInitializationError(
305+
raise ValueError(
307306
"Anthropic API key is required. Set via 'api_key' parameter "
308307
"or 'ANTHROPIC_API_KEY' environment variable."
309308
)

python/packages/anthropic/tests/test_anthropic_client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
tool,
1515
)
1616
from agent_framework._settings import load_settings
17-
from agent_framework.exceptions import ServiceInitializationError
1817
from anthropic.types.beta import (
1918
BetaMessage,
2019
BetaTextBlock,
@@ -128,7 +127,7 @@ def test_anthropic_client_init_missing_api_key() -> None:
128127
with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load:
129128
mock_load.return_value = {"api_key": None, "chat_model_id": "claude-3-5-sonnet-20241022"}
130129

131-
with pytest.raises(ServiceInitializationError, match="Anthropic API key is required"):
130+
with pytest.raises(ValueError, match="Anthropic API key is required"):
132131
AnthropicClient()
133132

134133

python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext
1818
from agent_framework._settings import SecretString, load_settings
1919
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
20-
from agent_framework.exceptions import ServiceInitializationError
2120
from azure.core.credentials import AzureKeyCredential
2221
from azure.core.credentials_async import AsyncTokenCredential
2322
from azure.core.exceptions import ResourceNotFoundError
@@ -219,7 +218,7 @@ def __init__(
219218
)
220219

221220
if mode == "agentic" and settings.get("index_name") and not model_deployment_name:
222-
raise ServiceInitializationError(
221+
raise ValueError(
223222
"model_deployment_name is required for agentic mode when creating Knowledge Base from index."
224223
)
225224

@@ -231,7 +230,7 @@ def __init__(
231230
elif settings.get("api_key"):
232231
resolved_credential = AzureKeyCredential(settings["api_key"].get_secret_value()) # type: ignore[union-attr]
233232
else:
234-
raise ServiceInitializationError(
233+
raise ValueError(
235234
"Azure credential is required. Provide 'api_key' or 'credential' parameter "
236235
"or set 'AZURE_SEARCH_API_KEY' environment variable."
237236
)

python/packages/azure-ai-search/tests/test_aisearch_context_provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99
from agent_framework import Message
1010
from agent_framework._sessions import AgentSession, SessionContext
11-
from agent_framework.exceptions import ServiceInitializationError, SettingNotFoundError
11+
from agent_framework.exceptions import SettingNotFoundError
1212
from azure.core.credentials import AzureKeyCredential
1313

1414
from agent_framework_azure_ai_search._context_provider import AzureAISearchContextProvider
@@ -180,7 +180,7 @@ def test_azure_key_credential_passed_through(self) -> None:
180180
assert provider.credential is akc
181181

182182
def test_no_credential_raises(self) -> None:
183-
with pytest.raises(ServiceInitializationError, match="Azure credential is required"):
183+
with pytest.raises(ValueError, match="Azure credential is required"):
184184
AzureAISearchContextProvider(
185185
endpoint="https://test.search.windows.net",
186186
index_name="idx",
@@ -216,7 +216,7 @@ def test_neither_index_nor_kb_raises(self) -> None:
216216
)
217217

218218
def test_missing_model_deployment_name_raises(self) -> None:
219-
with pytest.raises(ServiceInitializationError, match="model_deployment_name"):
219+
with pytest.raises(ValueError, match="model_deployment_name"):
220220
AzureAISearchContextProvider(
221221
source_id="s",
222222
endpoint="https://test.search.windows.net",

python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from agent_framework._settings import load_settings
1919
from agent_framework._tools import ToolTypes
2020
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
21-
from agent_framework.exceptions import ServiceInitializationError
2221
from azure.ai.agents.aio import AgentsClient
2322
from azure.ai.agents.models import Agent as AzureAgent
2423
from azure.ai.agents.models import ResponseFormatJsonSchema, ResponseFormatJsonSchemaType
@@ -113,7 +112,7 @@ def __init__(
113112
env_file_encoding: Encoding of the .env file.
114113
115114
Raises:
116-
ServiceInitializationError: If required parameters are missing or invalid.
115+
ValueError: If required parameters are missing or invalid.
117116
"""
118117
self._settings = load_settings(
119118
AzureAISettings,
@@ -130,12 +129,12 @@ def __init__(
130129
else:
131130
resolved_endpoint = self._settings.get("project_endpoint")
132131
if not resolved_endpoint:
133-
raise ServiceInitializationError(
132+
raise ValueError(
134133
"Azure AI project endpoint is required. Provide 'project_endpoint' parameter "
135134
"or set 'AZURE_AI_PROJECT_ENDPOINT' environment variable."
136135
)
137136
if not credential:
138-
raise ServiceInitializationError("Azure credential is required when agents_client is not provided.")
137+
raise ValueError("Azure credential is required when agents_client is not provided.")
139138
self._agents_client = AgentsClient(
140139
endpoint=resolved_endpoint,
141140
credential=credential, # type: ignore[arg-type]
@@ -199,7 +198,7 @@ async def create_agent(
199198
Agent: A Agent instance configured with the created agent.
200199
201200
Raises:
202-
ServiceInitializationError: If model deployment name is not available.
201+
ValueError: If model deployment name is not available.
203202
204203
Examples:
205204
.. code-block:: python
@@ -212,7 +211,7 @@ async def create_agent(
212211
"""
213212
resolved_model = model or self._settings.get("model_deployment_name")
214213
if not resolved_model:
215-
raise ServiceInitializationError(
214+
raise ValueError(
216215
"Model deployment name is required. Provide 'model' parameter "
217216
"or set 'AZURE_AI_MODEL_DEPLOYMENT_NAME' environment variable."
218217
)
@@ -290,7 +289,7 @@ async def get_agent(
290289
Agent: A Agent instance configured with the retrieved agent.
291290
292291
Raises:
293-
ServiceInitializationError: If required function tools are not provided.
292+
ValueError: If required function tools are not provided.
294293
295294
Examples:
296295
.. code-block:: python
@@ -340,7 +339,7 @@ def as_agent(
340339
Agent: A Agent instance configured with the agent.
341340
342341
Raises:
343-
ServiceInitializationError: If required function tools are not provided.
342+
ValueError: If required function tools are not provided.
344343
345344
Examples:
346345
.. code-block:: python
@@ -449,7 +448,7 @@ def _validate_function_tools(
449448
"""Validate that required function tools are provided.
450449
451450
Raises:
452-
ServiceInitializationError: If agent has function tools but user
451+
ValueError: If agent has function tools but user
453452
didn't provide implementations.
454453
"""
455454
if not agent_tools:
@@ -483,7 +482,7 @@ def _validate_function_tools(
483482
# Check for missing implementations
484483
missing = function_tool_names - provided_names
485484
if missing:
486-
raise ServiceInitializationError(
485+
raise ValueError(
487486
f"Agent has function tools that require implementations: {missing}. "
488487
"Provide these functions via the 'tools' parameter."
489488
)

0 commit comments

Comments
 (0)