Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions python/PACKAGE_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ listed below.
`create_agent_hooks_middleware_from_emitter` from `agent_framework/_agent_hooks.py`,
the AGENT-HOOKS-0.1 enforcement middleware bundle, and the `MiddlewareBundle`
container from `agent_framework/_middleware.py` that both factories produce
(`MiddlewareBundle` itself needs no extra). Requires the opt-in
`agent-framework-core[agent-hooks]` extra (`agent-hooks-sdk`), which is deliberately
not part of `agent-framework-core[all]`. Known limitation: service-side (hosted) tool
(`MiddlewareBundle` itself needs no optional dependency). Requires installing the
`agent-hooks-sdk` package separately. Known limitation: service-side (hosted) tool
execution never passes through the framework's function-invocation seam, so the
`pre_tool_call`/`post_tool_call` points cannot intercept it; hosted tool calls and
outputs are surfaced in the `post_model_call` content projection instead.
Expand Down
6 changes: 3 additions & 3 deletions python/packages/core/agent_framework/_agent_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@

The ``agent-hooks-sdk`` dependency is optional: importing this module (and the lazy root
exports) works without it, and the factories raise a descriptive ``ModuleNotFoundError``
when the SDK is missing. Install it via ``pip install agent-framework-core[agent-hooks]``.
when the SDK is missing. Install it via ``pip install agent-hooks-sdk``.
"""

from __future__ import annotations
Expand Down Expand Up @@ -162,7 +162,7 @@

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

_TRIO_REQUIRED_MESSAGE = (
Expand Down Expand Up @@ -191,7 +191,7 @@ def _require_sdk() -> None:
"""Import the SDK surface this module uses at runtime, with a helpful install hint.

Only a genuinely missing ``agent_hooks`` package is translated into the
install-the-extra message; anything else (a broken installation, an incompatible
SDK installation message; anything else (a broken installation, an incompatible
SDK version missing symbols, a failing transitive import) propagates unchanged so
real breakage is not masked as a missing extra.
"""
Expand Down
28 changes: 11 additions & 17 deletions python/packages/core/agent_framework/_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
FunctionInvocationContext,
MiddlewareTypes,
_as_middleware_list, # pyright: ignore[reportPrivateUsage]
_copy_middleware_sequence, # pyright: ignore[reportPrivateUsage]
categorize_middleware,
)
from ._serialization import SerializationMixin
Expand Down Expand Up @@ -439,7 +440,7 @@ def __init__(
name: str | None = None,
description: str | None = None,
context_providers: Sequence[ContextProvider] | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
) -> None:
"""Initialize a BaseAgent instance.
Expand All @@ -450,10 +451,8 @@ def __init__(
name: The name of the agent, can be None.
description: The description of the agent.
context_providers: Context providers to include during agent invocation.
middleware: List of middleware, or a single middleware object (including a
``MiddlewareBundle``) which is treated as a one-element list. The
constructor copies the sequence; assign to or mutate the
``middleware`` attribute for post-construction changes.
middleware: List of middleware. The constructor copies the sequence; assign
to or mutate the ``middleware`` attribute for post-construction changes.
additional_properties: Additional properties set on the agent.
"""
if id is None:
Expand All @@ -462,11 +461,8 @@ def __init__(
self.name = name
self.description = description
self.context_providers: list[ContextProvider] = list(context_providers or [])
# Canonicalize storage: the bare-source rule (a single middleware object or a
# MiddlewareBundle is one element) is owned by _as_middleware_list; storing a
# normalized list keeps the declared attribute type honest.
self.middleware: list[MiddlewareTypes] | None = (
_as_middleware_list(middleware) if middleware is not None else None
_copy_middleware_sequence(middleware) if middleware is not None else None
)
self.additional_properties: dict[str, Any] = cast(dict[str, Any], additional_properties or {})

Expand Down Expand Up @@ -819,7 +815,7 @@ def __init__(
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
context_providers: Sequence[ContextProvider] | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
require_per_service_call_history_persistence: bool = False,
compaction_strategy: CompactionStrategy | None = None,
tokenizer: TokenizerProtocol | None = None,
Expand All @@ -838,8 +834,6 @@ def __init__(
description: A brief description of the agent's purpose.
context_providers: Context providers to include during agent invocation.
middleware: List of middleware to intercept agent and function invocations.
A single middleware object (including a ``MiddlewareBundle``) is
treated as a one-element list.
require_per_service_call_history_persistence: When True (and a HistoryProvider is
present), the provider always persists history via per-service-call middleware,
regardless of whether the client stores history server-side. If the client does
Expand Down Expand Up @@ -1808,7 +1802,7 @@ def run(
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[ResponseModelBoundT],
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -1824,7 +1818,7 @@ def run(
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: OptionsCoT | ChatOptions[None] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -1840,7 +1834,7 @@ def run(
*,
stream: Literal[True],
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: OptionsCoT | ChatOptions[Any] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -1855,7 +1849,7 @@ def run(
*,
stream: bool = False,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: OptionsCoT | ChatOptions[Any] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand Down Expand Up @@ -1893,7 +1887,7 @@ def __init__(
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
context_providers: Sequence[ContextProvider] | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
Comment thread
eavanvalkenburg marked this conversation as resolved.
require_per_service_call_history_persistence: bool = False,
compaction_strategy: CompactionStrategy | None = None,
tokenizer: TokenizerProtocol | None = None,
Expand Down
2 changes: 1 addition & 1 deletion python/packages/core/agent_framework/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ def as_agent(
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | Mapping[str, Any] | None = None,
context_providers: Sequence[Any] | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
require_per_service_call_history_persistence: bool = False,
compaction_strategy: CompactionStrategy | None = None,
tokenizer: TokenizerProtocol | None = None,
Expand Down
10 changes: 4 additions & 6 deletions python/packages/core/agent_framework/_harness/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .._clients import SupportsShellTool, SupportsWebSearchTool
from .._compaction import CompactionProvider, ContextWindowCompactionStrategy
from .._feature_stage import ExperimentalFeature, warn_experimental_feature
from .._middleware import _copy_middleware_sequence # pyright: ignore[reportPrivateUsage]
from .._sessions import ContextProvider, HistoryProvider, InMemoryHistoryProvider, MessageInjectionMiddleware
from .._skills import SkillsProvider
from .._telemetry import FeatureIndex, mark_feature_used
Expand Down Expand Up @@ -347,7 +348,7 @@ def create_harness_agent(
loop_max_iterations: int | None = DEFAULT_MAX_ITERATIONS,
otel_provider_name: str | None = None,
context_providers: Sequence[ContextProvider] | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
default_options: Mapping[str, Any] | None = None,
) -> Agent[OptionsCoT]:
"""Create a pre-configured agent with batteries included.
Expand Down Expand Up @@ -669,11 +670,8 @@ def create_harness_agent(
# Message injection is always on. It is a no-op when no messages are queued for the session,
# so there is no opt-out.
assembled_middleware.append(MessageInjectionMiddleware())
# Bare-source normalization (a single middleware object or a MiddlewareBundle is
# one element) is owned by _as_middleware_list.
from .._middleware import _as_middleware_list # pyright: ignore[reportPrivateUsage]

assembled_middleware.extend(_as_middleware_list(middleware))
if middleware is not None:
assembled_middleware.extend(_copy_middleware_sequence(middleware))

agent = Agent(
client,
Expand Down
2 changes: 1 addition & 1 deletion python/packages/core/agent_framework/_harness/_agent.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,6 @@ def create_harness_agent(
loop_max_iterations: int | None = DEFAULT_MAX_ITERATIONS,
otel_provider_name: str | None = None,
context_providers: Sequence[ContextProvider] | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
default_options: Mapping[str, Any] | None = None,
) -> Agent[OptionsCoT]: ...
35 changes: 22 additions & 13 deletions python/packages/core/agent_framework/_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,15 @@ def __repr__(self) -> str:
)


def _copy_middleware_sequence(source: object | None) -> list[MiddlewareTypes]:
"""Validate and copy a middleware sequence."""
if source is None:
return []
if isinstance(source, (str, bytes)) or not isinstance(source, Sequence):
raise TypeError("middleware must be a non-string sequence of middleware.")
return list(cast("Sequence[MiddlewareTypes]", source))


def agent_middleware(func: AgentMiddlewareCallable) -> AgentMiddlewareCallable:
"""Decorator to mark a function as agent middleware.

Expand Down Expand Up @@ -1425,14 +1434,15 @@ class AgentMiddlewareLayer:
def __init__(
self,
*args: Any,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
**kwargs: Any,
) -> None:
middleware_list = categorize_middleware(middleware)
middleware_sequence = _copy_middleware_sequence(middleware) if middleware is not None else None
middleware_list = categorize_middleware(middleware_sequence)
self.agent_middleware = middleware_list["agent"]
self._cached_agent_middleware_pipeline: AgentMiddlewarePipeline | None = None
# Pass middleware to super so BaseAgent can store it for dynamic rebuild
super().__init__(*args, middleware=middleware, **kwargs) # type: ignore[call-arg]
super().__init__(*args, middleware=middleware_sequence, **kwargs) # type: ignore[call-arg]
# Note: We intentionally don't extend client's middleware lists here.
# Chat and function middleware is passed to the chat client at runtime via kwargs
# in AgentMiddlewareLayer.run(), where it's properly combined with run-level middleware.
Expand All @@ -1456,7 +1466,7 @@ def run(
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[ResponseModelBoundT],
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -1472,7 +1482,7 @@ def run(
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[None] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -1488,7 +1498,7 @@ def run(
*,
stream: Literal[True],
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[Any] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -1503,7 +1513,7 @@ def run(
*,
stream: bool = False,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[Any] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -1512,14 +1522,13 @@ def run(
client_kwargs: Mapping[str, Any] | None = None,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
"""MiddlewareTypes-enabled unified run method."""
# Re-categorize self.middleware at runtime to support dynamic changes. The raw
# attribute is passed straight through: categorize_middleware owns the rule
# that a bare single source (one middleware object or a MiddlewareBundle
# assigned directly to the attribute) is one element — never silently dropped.
base_middleware = getattr(self, "middleware", None)
base_middleware_list = categorize_middleware(
cast("MiddlewareTypes | Sequence[MiddlewareTypes] | None", getattr(self, "middleware", None))
_copy_middleware_sequence(base_middleware) if base_middleware is not None else None
)
run_middleware_list = categorize_middleware(
_copy_middleware_sequence(middleware) if middleware is not None else None
)
run_middleware_list = categorize_middleware(middleware)
pipeline = self._get_agent_middleware_pipeline([*base_middleware_list["agent"], *run_middleware_list["agent"]])

# Combine base and run-level function/chat middleware for forwarding to chat client
Expand Down
8 changes: 4 additions & 4 deletions python/packages/core/agent_framework/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -2542,7 +2542,7 @@ def run(
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[ResponseModelBoundT],
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -2558,7 +2558,7 @@ def run(
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[None] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -2574,7 +2574,7 @@ def run(
*,
stream: Literal[True],
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[Any] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand All @@ -2589,7 +2589,7 @@ def run(
*,
stream: bool = False,
session: AgentSession | None = None,
middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[Any] | None = None,
compaction_strategy: CompactionStrategy | None = None,
Expand Down
5 changes: 0 additions & 5 deletions python/packages/core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ dependencies = [
]

[project.optional-dependencies]
# Deliberately NOT part of `all`: the agent-hooks enforcement middleware is an
# explicitly opt-in experimental feature.
agent-hooks = [
"agent-hooks-sdk>=0.1.0a4,<0.2",
]
all = [
"mcp>=1.24.0,<2",
"agent-framework-a2a",
Expand Down
Loading
Loading