Skip to content

Commit 1aca2a9

Browse files
Python: [BREAKING] restore agent middleware API contract (microsoft#7918)
* Python: restore agent middleware API contract Remove the agent-hooks extra from the core package and restore sequence-only middleware inputs for agents and the harness.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Python: validate middleware sequences consistently Use one strict sequence validator across agent entry points and align client and Foundry factory signatures with the runtime contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac1d6000-fc1a-4891-95f4-8d77d4420e31 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac1d6000-fc1a-4891-95f4-8d77d4420e31
1 parent edfe115 commit 1aca2a9

15 files changed

Lines changed: 110 additions & 114 deletions

File tree

‎python/PACKAGE_STATUS.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ listed below.
7070
`create_agent_hooks_middleware_from_emitter` from `agent_framework/_agent_hooks.py`,
7171
the AGENT-HOOKS-0.1 enforcement middleware bundle, and the `MiddlewareBundle`
7272
container from `agent_framework/_middleware.py` that both factories produce
73-
(`MiddlewareBundle` itself needs no extra). Requires the opt-in
74-
`agent-framework-core[agent-hooks]` extra (`agent-hooks-sdk`), which is deliberately
75-
not part of `agent-framework-core[all]`. Known limitation: service-side (hosted) tool
73+
(`MiddlewareBundle` itself needs no optional dependency). Requires installing the
74+
`agent-hooks-sdk` package separately. Known limitation: service-side (hosted) tool
7675
execution never passes through the framework's function-invocation seam, so the
7776
`pre_tool_call`/`post_tool_call` points cannot intercept it; hosted tool calls and
7877
outputs are surfaced in the `post_model_call` content projection instead.

‎python/packages/core/agent_framework/_agent_hooks.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
9494
The ``agent-hooks-sdk`` dependency is optional: importing this module (and the lazy root
9595
exports) works without it, and the factories raise a descriptive ``ModuleNotFoundError``
96-
when the SDK is missing. Install it via ``pip install agent-framework-core[agent-hooks]``.
96+
when the SDK is missing. Install it via ``pip install agent-hooks-sdk``.
9797
"""
9898

9999
from __future__ import annotations
@@ -162,7 +162,7 @@
162162

163163
_SDK_MISSING_MESSAGE = (
164164
"The agent-hooks middleware requires the optional `agent-hooks-sdk` package. "
165-
"Please install `agent-framework-core[agent-hooks]` (or `agent-hooks-sdk`)."
165+
"Please install it with `pip install agent-hooks-sdk`."
166166
)
167167

168168
_TRIO_REQUIRED_MESSAGE = (
@@ -191,7 +191,7 @@ def _require_sdk() -> None:
191191
"""Import the SDK surface this module uses at runtime, with a helpful install hint.
192192
193193
Only a genuinely missing ``agent_hooks`` package is translated into the
194-
install-the-extra message; anything else (a broken installation, an incompatible
194+
SDK installation message; anything else (a broken installation, an incompatible
195195
SDK version missing symbols, a failing transitive import) propagates unchanged so
196196
real breakage is not masked as a missing extra.
197197
"""

‎python/packages/core/agent_framework/_agents.py‎

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
FunctionInvocationContext,
3232
MiddlewareTypes,
3333
_as_middleware_list, # pyright: ignore[reportPrivateUsage]
34+
_copy_middleware_sequence, # pyright: ignore[reportPrivateUsage]
3435
categorize_middleware,
3536
)
3637
from ._serialization import SerializationMixin
@@ -439,7 +440,7 @@ def __init__(
439440
name: str | None = None,
440441
description: str | None = None,
441442
context_providers: Sequence[ContextProvider] | None = None,
442-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
443+
middleware: Sequence[MiddlewareTypes] | None = None,
443444
additional_properties: MutableMapping[str, Any] | None = None,
444445
) -> None:
445446
"""Initialize a BaseAgent instance.
@@ -450,10 +451,8 @@ def __init__(
450451
name: The name of the agent, can be None.
451452
description: The description of the agent.
452453
context_providers: Context providers to include during agent invocation.
453-
middleware: List of middleware, or a single middleware object (including a
454-
``MiddlewareBundle``) which is treated as a one-element list. The
455-
constructor copies the sequence; assign to or mutate the
456-
``middleware`` attribute for post-construction changes.
454+
middleware: List of middleware. The constructor copies the sequence; assign
455+
to or mutate the ``middleware`` attribute for post-construction changes.
457456
additional_properties: Additional properties set on the agent.
458457
"""
459458
if id is None:
@@ -462,11 +461,8 @@ def __init__(
462461
self.name = name
463462
self.description = description
464463
self.context_providers: list[ContextProvider] = list(context_providers or [])
465-
# Canonicalize storage: the bare-source rule (a single middleware object or a
466-
# MiddlewareBundle is one element) is owned by _as_middleware_list; storing a
467-
# normalized list keeps the declared attribute type honest.
468464
self.middleware: list[MiddlewareTypes] | None = (
469-
_as_middleware_list(middleware) if middleware is not None else None
465+
_copy_middleware_sequence(middleware) if middleware is not None else None
470466
)
471467
self.additional_properties: dict[str, Any] = cast(dict[str, Any], additional_properties or {})
472468

@@ -819,7 +815,7 @@ def __init__(
819815
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
820816
default_options: OptionsCoT | None = None,
821817
context_providers: Sequence[ContextProvider] | None = None,
822-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
818+
middleware: Sequence[MiddlewareTypes] | None = None,
823819
require_per_service_call_history_persistence: bool = False,
824820
compaction_strategy: CompactionStrategy | None = None,
825821
tokenizer: TokenizerProtocol | None = None,
@@ -838,8 +834,6 @@ def __init__(
838834
description: A brief description of the agent's purpose.
839835
context_providers: Context providers to include during agent invocation.
840836
middleware: List of middleware to intercept agent and function invocations.
841-
A single middleware object (including a ``MiddlewareBundle``) is
842-
treated as a one-element list.
843837
require_per_service_call_history_persistence: When True (and a HistoryProvider is
844838
present), the provider always persists history via per-service-call middleware,
845839
regardless of whether the client stores history server-side. If the client does
@@ -1808,7 +1802,7 @@ def run(
18081802
*,
18091803
stream: Literal[False] = ...,
18101804
session: AgentSession | None = None,
1811-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1805+
middleware: Sequence[MiddlewareTypes] | None = None,
18121806
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
18131807
options: ChatOptions[ResponseModelBoundT],
18141808
compaction_strategy: CompactionStrategy | None = None,
@@ -1824,7 +1818,7 @@ def run(
18241818
*,
18251819
stream: Literal[False] = ...,
18261820
session: AgentSession | None = None,
1827-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1821+
middleware: Sequence[MiddlewareTypes] | None = None,
18281822
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
18291823
options: OptionsCoT | ChatOptions[None] | None = None,
18301824
compaction_strategy: CompactionStrategy | None = None,
@@ -1840,7 +1834,7 @@ def run(
18401834
*,
18411835
stream: Literal[True],
18421836
session: AgentSession | None = None,
1843-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1837+
middleware: Sequence[MiddlewareTypes] | None = None,
18441838
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
18451839
options: OptionsCoT | ChatOptions[Any] | None = None,
18461840
compaction_strategy: CompactionStrategy | None = None,
@@ -1855,7 +1849,7 @@ def run(
18551849
*,
18561850
stream: bool = False,
18571851
session: AgentSession | None = None,
1858-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1852+
middleware: Sequence[MiddlewareTypes] | None = None,
18591853
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
18601854
options: OptionsCoT | ChatOptions[Any] | None = None,
18611855
compaction_strategy: CompactionStrategy | None = None,
@@ -1893,7 +1887,7 @@ def __init__(
18931887
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
18941888
default_options: OptionsCoT | None = None,
18951889
context_providers: Sequence[ContextProvider] | None = None,
1896-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1890+
middleware: Sequence[MiddlewareTypes] | None = None,
18971891
require_per_service_call_history_persistence: bool = False,
18981892
compaction_strategy: CompactionStrategy | None = None,
18991893
tokenizer: TokenizerProtocol | None = None,

‎python/packages/core/agent_framework/_clients.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ def as_agent(
578578
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
579579
default_options: OptionsCoT | Mapping[str, Any] | None = None,
580580
context_providers: Sequence[Any] | None = None,
581-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
581+
middleware: Sequence[MiddlewareTypes] | None = None,
582582
require_per_service_call_history_persistence: bool = False,
583583
compaction_strategy: CompactionStrategy | None = None,
584584
tokenizer: TokenizerProtocol | None = None,

‎python/packages/core/agent_framework/_harness/_agent.py‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .._clients import SupportsShellTool, SupportsWebSearchTool
2121
from .._compaction import CompactionProvider, ContextWindowCompactionStrategy
2222
from .._feature_stage import ExperimentalFeature, warn_experimental_feature
23+
from .._middleware import _copy_middleware_sequence # pyright: ignore[reportPrivateUsage]
2324
from .._sessions import ContextProvider, HistoryProvider, InMemoryHistoryProvider, MessageInjectionMiddleware
2425
from .._skills import SkillsProvider
2526
from .._telemetry import FeatureIndex, mark_feature_used
@@ -347,7 +348,7 @@ def create_harness_agent(
347348
loop_max_iterations: int | None = DEFAULT_MAX_ITERATIONS,
348349
otel_provider_name: str | None = None,
349350
context_providers: Sequence[ContextProvider] | None = None,
350-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
351+
middleware: Sequence[MiddlewareTypes] | None = None,
351352
default_options: Mapping[str, Any] | None = None,
352353
) -> Agent[OptionsCoT]:
353354
"""Create a pre-configured agent with batteries included.
@@ -669,11 +670,8 @@ def create_harness_agent(
669670
# Message injection is always on. It is a no-op when no messages are queued for the session,
670671
# so there is no opt-out.
671672
assembled_middleware.append(MessageInjectionMiddleware())
672-
# Bare-source normalization (a single middleware object or a MiddlewareBundle is
673-
# one element) is owned by _as_middleware_list.
674-
from .._middleware import _as_middleware_list # pyright: ignore[reportPrivateUsage]
675-
676-
assembled_middleware.extend(_as_middleware_list(middleware))
673+
if middleware is not None:
674+
assembled_middleware.extend(_copy_middleware_sequence(middleware))
677675

678676
agent = Agent(
679677
client,

‎python/packages/core/agent_framework/_harness/_agent.pyi‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,6 @@ def create_harness_agent(
8989
loop_max_iterations: int | None = DEFAULT_MAX_ITERATIONS,
9090
otel_provider_name: str | None = None,
9191
context_providers: Sequence[ContextProvider] | None = None,
92-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
92+
middleware: Sequence[MiddlewareTypes] | None = None,
9393
default_options: Mapping[str, Any] | None = None,
9494
) -> Agent[OptionsCoT]: ...

‎python/packages/core/agent_framework/_middleware.py‎

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,15 @@ def __repr__(self) -> str:
828828
)
829829

830830

831+
def _copy_middleware_sequence(source: object | None) -> list[MiddlewareTypes]:
832+
"""Validate and copy a middleware sequence."""
833+
if source is None:
834+
return []
835+
if isinstance(source, (str, bytes)) or not isinstance(source, Sequence):
836+
raise TypeError("middleware must be a non-string sequence of middleware.")
837+
return list(cast("Sequence[MiddlewareTypes]", source))
838+
839+
831840
def agent_middleware(func: AgentMiddlewareCallable) -> AgentMiddlewareCallable:
832841
"""Decorator to mark a function as agent middleware.
833842
@@ -1425,14 +1434,15 @@ class AgentMiddlewareLayer:
14251434
def __init__(
14261435
self,
14271436
*args: Any,
1428-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1437+
middleware: Sequence[MiddlewareTypes] | None = None,
14291438
**kwargs: Any,
14301439
) -> None:
1431-
middleware_list = categorize_middleware(middleware)
1440+
middleware_sequence = _copy_middleware_sequence(middleware) if middleware is not None else None
1441+
middleware_list = categorize_middleware(middleware_sequence)
14321442
self.agent_middleware = middleware_list["agent"]
14331443
self._cached_agent_middleware_pipeline: AgentMiddlewarePipeline | None = None
14341444
# Pass middleware to super so BaseAgent can store it for dynamic rebuild
1435-
super().__init__(*args, middleware=middleware, **kwargs) # type: ignore[call-arg]
1445+
super().__init__(*args, middleware=middleware_sequence, **kwargs) # type: ignore[call-arg]
14361446
# Note: We intentionally don't extend client's middleware lists here.
14371447
# Chat and function middleware is passed to the chat client at runtime via kwargs
14381448
# in AgentMiddlewareLayer.run(), where it's properly combined with run-level middleware.
@@ -1456,7 +1466,7 @@ def run(
14561466
*,
14571467
stream: Literal[False] = ...,
14581468
session: AgentSession | None = None,
1459-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1469+
middleware: Sequence[MiddlewareTypes] | None = None,
14601470
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
14611471
options: ChatOptions[ResponseModelBoundT],
14621472
compaction_strategy: CompactionStrategy | None = None,
@@ -1472,7 +1482,7 @@ def run(
14721482
*,
14731483
stream: Literal[False] = ...,
14741484
session: AgentSession | None = None,
1475-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1485+
middleware: Sequence[MiddlewareTypes] | None = None,
14761486
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
14771487
options: ChatOptions[None] | None = None,
14781488
compaction_strategy: CompactionStrategy | None = None,
@@ -1488,7 +1498,7 @@ def run(
14881498
*,
14891499
stream: Literal[True],
14901500
session: AgentSession | None = None,
1491-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1501+
middleware: Sequence[MiddlewareTypes] | None = None,
14921502
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
14931503
options: ChatOptions[Any] | None = None,
14941504
compaction_strategy: CompactionStrategy | None = None,
@@ -1503,7 +1513,7 @@ def run(
15031513
*,
15041514
stream: bool = False,
15051515
session: AgentSession | None = None,
1506-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1516+
middleware: Sequence[MiddlewareTypes] | None = None,
15071517
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
15081518
options: ChatOptions[Any] | None = None,
15091519
compaction_strategy: CompactionStrategy | None = None,
@@ -1512,14 +1522,13 @@ def run(
15121522
client_kwargs: Mapping[str, Any] | None = None,
15131523
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
15141524
"""MiddlewareTypes-enabled unified run method."""
1515-
# Re-categorize self.middleware at runtime to support dynamic changes. The raw
1516-
# attribute is passed straight through: categorize_middleware owns the rule
1517-
# that a bare single source (one middleware object or a MiddlewareBundle
1518-
# assigned directly to the attribute) is one element — never silently dropped.
1525+
base_middleware = getattr(self, "middleware", None)
15191526
base_middleware_list = categorize_middleware(
1520-
cast("MiddlewareTypes | Sequence[MiddlewareTypes] | None", getattr(self, "middleware", None))
1527+
_copy_middleware_sequence(base_middleware) if base_middleware is not None else None
1528+
)
1529+
run_middleware_list = categorize_middleware(
1530+
_copy_middleware_sequence(middleware) if middleware is not None else None
15211531
)
1522-
run_middleware_list = categorize_middleware(middleware)
15231532
pipeline = self._get_agent_middleware_pipeline([*base_middleware_list["agent"], *run_middleware_list["agent"]])
15241533

15251534
# Combine base and run-level function/chat middleware for forwarding to chat client

‎python/packages/core/agent_framework/observability.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2542,7 +2542,7 @@ def run(
25422542
*,
25432543
stream: Literal[False] = ...,
25442544
session: AgentSession | None = None,
2545-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
2545+
middleware: Sequence[MiddlewareTypes] | None = None,
25462546
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
25472547
options: ChatOptions[ResponseModelBoundT],
25482548
compaction_strategy: CompactionStrategy | None = None,
@@ -2558,7 +2558,7 @@ def run(
25582558
*,
25592559
stream: Literal[False] = ...,
25602560
session: AgentSession | None = None,
2561-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
2561+
middleware: Sequence[MiddlewareTypes] | None = None,
25622562
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
25632563
options: ChatOptions[None] | None = None,
25642564
compaction_strategy: CompactionStrategy | None = None,
@@ -2574,7 +2574,7 @@ def run(
25742574
*,
25752575
stream: Literal[True],
25762576
session: AgentSession | None = None,
2577-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
2577+
middleware: Sequence[MiddlewareTypes] | None = None,
25782578
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
25792579
options: ChatOptions[Any] | None = None,
25802580
compaction_strategy: CompactionStrategy | None = None,
@@ -2589,7 +2589,7 @@ def run(
25892589
*,
25902590
stream: bool = False,
25912591
session: AgentSession | None = None,
2592-
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
2592+
middleware: Sequence[MiddlewareTypes] | None = None,
25932593
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
25942594
options: ChatOptions[Any] | None = None,
25952595
compaction_strategy: CompactionStrategy | None = None,

‎python/packages/core/pyproject.toml‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ dependencies = [
3131
]
3232

3333
[project.optional-dependencies]
34-
# Deliberately NOT part of `all`: the agent-hooks enforcement middleware is an
35-
# explicitly opt-in experimental feature.
36-
agent-hooks = [
37-
"agent-hooks-sdk>=0.1.0a4,<0.2",
38-
]
3934
all = [
4035
"mcp>=1.24.0,<2",
4136
"agent-framework-a2a",

0 commit comments

Comments
 (0)