Uh oh!
There was an error while loading. Please reload this page.
Python: agent-hooks interception contract as a first-class experimental core feature - #7515
Conversation
Implement the AGENT-HOOKS-0.1 interception contract as a first-class experimental feature in agent_framework core. - Single public factory agent_hooks_middleware() returning a private agent/chat/function middleware trio (one object per middleware category); partial or stacked installs fail closed with loud errors. - All eight interception points: input/output at the agent seam, pre/post_model_call at the chat seam, pre/post_tool_call at the function seam, agent_startup/agent_shutdown bracketing each run. - Fail-closed enforcement throughout: transforms write back into the native contexts (messages, arguments, results) or raise; content is preserved as Content objects; MiddlewareTermination short-circuits are guarded at every seam; enforcement-layer failures halt the run; interceptor crashes surface as host_error denies. - Streaming is fully buffered per spec buffered_output semantics: no update egresses before the post_model_call/output verdicts; a deny at pull time releases zero updates; run state stays active across lazy pulls with cleanup on every exit path. - Session scoping: per-run by default (startup/shutdown bracket each run) or host-owned via emitter/builder parameters for one session spanning multiple runs. - agent-hooks-sdk is an opt-in agent-hooks extra (not in all), lazy-imported per the _mcp.py pattern; core imports cleanly without it and the factory raises a clear ModuleNotFoundError. - ExperimentalFeature.AGENT_HOOKS + @experimental decorator, lazy root export, typing surface, PACKAGE_STATUS.md entry. - 55 tests built on real Agent/mock-client flows covering deny-before- execution, transform write-back, rich-content preservation, complete streaming ordering, error cleanup, concurrency isolation, nested agents, and importability without the optional SDK. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an experimental, first-class implementation of the AGENT-HOOKS-0.1 interception/enforcement contract to the Python core package (agent-framework-core), including an opt-in extra for the SDK dependency and a comprehensive test suite validating fail-closed behavior across agent/chat/tool seams (including buffered streaming).
Changes:
- Introduces
agent_framework/_agent_hooks.pywith the publicagent_hooks_middleware(...)factory that returns an agent/chat/function middleware trio implementing all eight interception points and fail-closed semantics. - Adds the opt-in
agent-hooksextra (agent-hooks-sdk>=0.1.0a4,<0.2) and updates exports + experimental feature registration/documentation. - Adds extensive unit tests covering deny/transform semantics, streaming buffering, short-circuit guarding, partial install detection, and optional-dependency importability.
Show a summary per file
| File | Description |
|---|---|
| python/uv.lock | Adds the agent-hooks extra lock entries and locks agent-hooks-sdk 0.1.0a4. |
| python/packages/core/tests/core/test_agent_hooks.py | New test suite for agent-hooks enforcement and semantics across seams (incl. streaming). |
| python/packages/core/pyrightconfig.dependency.json | Excludes the new module from dependency-bound pyright checking. |
| python/packages/core/pyproject.toml | Adds agent-hooks optional dependency extra (explicitly not part of all). |
| python/packages/core/agent_framework/_feature_stage.py | Registers ExperimentalFeature.AGENT_HOOKS. |
| python/packages/core/agent_framework/_agent_hooks.py | Implements the enforcement middleware trio + projections/write-back + buffering semantics. |
| python/packages/core/agent_framework/init.pyi | Adds typing export for agent_hooks_middleware. |
| python/packages/core/agent_framework/init.py | Adds lazy runtime export for agent_hooks_middleware. |
| python/PACKAGE_STATUS.md | Documents the new experimental feature and its opt-in extra. |
Review details
- Files reviewed: 8/9 changed files
- Comments generated: 2
- Review effort level: Lite
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The pre-commit pyupgrade hook rewrites the quoted forward reference; ResponseStream is imported at runtime in this module, so the quotes were unnecessary. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Reworks the agent-hooks feature per PR review: - Verdicts now precede durability: a run-scoped persistence gate (_sessions.py) defers per-service-call history persistence and after-run provider work until the covering post_model_call/output verdict permits; denied content never persists, transforms persist post-write-back. Unhooked runs are unchanged (verified against an instrumented baseline). - ResponseStream.buffered_and_gated: a buffered-gate combinator that applies the run's pending stream hooks before the gate, then seals the stream, so no middleware can rewrite egress after the output verdict. Replaces the hand-rolled replay iterator. - MiddlewareBundle (public, _middleware.py): the factory returns an indivisible bundle categorize_middleware splits, making partial installs impossible by construction; members are validated at construction. Bare (non-sequence) middleware at agent construction is now normalized instead of silently dropped, and unrecognized middleware logs a warning instead of vanishing. - Factory split and rename: create_agent_hooks_middleware (per-run sessions) and create_agent_hooks_middleware_from_emitter (host-owned); the sentinel parameter-diffing is gone. - Wire conversions live in per-point codec classes owning to_wire and write_back. Fixes in that code: tool-call name transforms apply or raise; non-object args transforms raise; argument write-back merges only changed keys (original values, including bytes, preserved by identity); message-list write-back matches by identity, not index. - function_approval_request objects on the normal return path pass through un-emitted, preserving the human approval pause. - Hosted (service-executed) tool calls surface in the post_model_call content projection; the tool-seam limitation is documented. - Import probe covers the full SDK surface and re-raises as missing-extra only for the agent_hooks module; module logger added; _json_safe replaced by make_json_safe (which gained bytes support); tools_registered uses normalize_tools; dependency-pyright analyzes the module again via the test dependency-group. - Tests: 75 in the feature suite (persistence gating, stream-hook sealing, approval passthrough, codec units, bundle validation, bare-bundle installs), full core suite green. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
Evan Mattson (moonbox3)
left a comment
There was a problem hiding this comment.
Have a look at the failing CI/CD (code quality checks) too, please. Thanks.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Addresses the second review round on the agent-hooks feature: - Nested-run persistence ownership: RawAgent.run stamps a run identity over the run's dynamic extent (including streaming pulls and result hooks); the persistence gate binds to its owning run via an offer/adopt handshake keyed to the agent instance and accepts only its owner's persists — nested runs persist inline regardless of how they were started (tool calls, middleware, custom run loops). The tool-seam suspension remains for custom-loop sub-agents invoked as tools; the one residual case (custom loop nested in a custom loop off the tool path) is fail-closed and documented. Fixes a latent pre-existing re-deferral: flush() now drains with the gate context suspended, so a nested hooked run's permitted after-run persistence no longer re-defers into an enclosing gate. - as_tool stream_callback consumes the released (verdicted) stream; observers cannot see denied or pre-transform content. Both directions are regression-tested. - categorize_middleware gained supported_categories: a bundle member landing in a category a call site cannot install raises; bare middleware warns like _add_middleware. Wired at the chat-client sites and the provider seam. - ResponseStream.buffered_and_gated owns the re-derivation rule via a rederive callable (gates cannot choose released updates) and is marked experimental. - Wire codecs compare with bool-aware equality (Python == equates 1 == True, which made bool/number transforms look untouched and get dropped) and _ToolResultCodec.write_back owns the untouched-wire rule via the before value. - middleware parameters accept a bare middleware or bundle everywhere the runtime does (constructors, run overloads, as_agent, telemetry and harness layers, foundry); the bare-source rule has a single owner in categorize_middleware; bare middleware assigned to the attribute now executes (documented behavior change). - MiddlewareBundle is experimental and validates members; approval passthrough, typing-check fixes (ty ignores mypy-coded ignore comments), logging, and documentation updates per review. Test count: 85 feature tests plus 12 new this round across sessions, middleware, agents; full core suite green; typing checked under mypy, pyrefly, ty, zuban, and pyright. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Per review: docstrings describe current behavior only. The bare-middleware behavior change stays recorded in the PR description and commit history. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The streaming agent seam ran call_next() outside the persistence gate (only _consume entered it later), so a retry middleware that drained a successful attempt with get_final_response() and discarded it persisted that attempt's exchange before any verdict existed; a later deny dropped only the retry attempt's deferred work. The descent is now wrapped in the gate exactly like the non-streaming seam: attempt identities adopted during descent are accepted owners, so in-pipeline draining defers, deny drops every attempt, and a middleware that raises after draining strands the pending persists unexecuted. The bind_owner docstring now states the actual soundness invariant covering both bind sites: every bind comes from a run inside the covered pipeline. New tests cover drained-and-discarded attempts (deny and allow, both stream modes) and a sub-agent tool inside a drained attempt; the streaming deny variant fails with the gate wrap reverted. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
…ation With the pipeline descent now running inside the persistence gate, a middleware that drains a successful attempt and then terminates without a result left that attempt's deferred persistence stranded: the streaming no-result termination path raised before any flush, so history of exchanges that really happened and passed their own verdicts quietly vanished (streaming only; non-streaming already flushes before its re-raise). The path now flushes before re-raising the termination, with a state.halted guard first so an enforcement failure during the drained attempt still strands pending fail-closed and surfaces the halt, mirroring the non-streaming ordering exactly. The regression test covers both seams; the streaming variant fails without the fix. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…report (33/47) Add a registry entry and claim artifacts for the agent-hooks middleware merged upstream as microsoft/agent-framework#7515 (agent-framework-core 1.13.0, main @ 4b1afd9052), run against the 47-vector CTK from agent-hooks-sdk 0.1.0a4 with a harness that drives the production middleware bundle over a real Agent (only model/tool I/O mocked). Result: 33 passed, 14 failed, 0 skipped. All 14 failures trace to two documented host-semantics divergences, analysed in the report: - F1 (13 vectors): MAF deliberately halts the run on a host_error:* deny at the tool seam; every vector fails solely on run_outcome blocked-vs-completed. Spec 6.2's "unless the host's own semantics terminate the turn" clause permits the posture, but the CTK's run_outcome grammar cannot express it. - F2 (AH-CTK-100): the vector pins the reference host's blocked-tool transcript convention (tool message at index 1, "blocked: <reason>" string), which protocol-valid transcripts cannot match. - F3 (observation, no vector failure): constructor-registered tools do not surface in agent_startup.tools_registered (MAF-side fix material). The entry is explicitly recorded as partial cross-validation, not a 13.1 conformance claim; harness, runner entry point, and per-part report live under conformance/claims/maf/ so the run is reproducible from this repository alone. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
… feature (microsoft#7564) * feat(dotnet): agent-hooks interception contract as an experimental package Add Microsoft.Agents.AI.AgentHooks, implementing the AGENT-HOOKS-0.1 control contract on the framework's native decorator seams, mirroring the merged Python feature (microsoft#7515) in .NET idiom: - One public factory (CreateAIAgentWithAgentHooks, per-run and host-owned-session overloads) composes agent, chat and function seams as one indivisible unit; the seam decorators are internal, so partial installs are impossible by construction. - All eight interception points: input/output at the agent seam, pre/post_model_call below the function-invocation loop (every model service call bracketed individually), pre/post_tool_call via the function-invocation middleware seam, agent_startup/agent_shutdown bracketing each run. - Fail-closed enforcement throughout: transforms write back into the native messages/arguments/results or throw; rich content is preserved as AIContent objects; interceptor crashes surface as host_error denies; enforcement-layer failures halt the run through FunctionInvocationContext.Terminate (the loop's only loud escape). - Streaming is fully buffered per spec buffered_output semantics: a deny releases zero updates; transformed responses re-derive the released updates so egress never diverges from verdicted content. - Verdict-before-durability: end-of-run history and context-provider writes defer behind the output verdict via gating provider wrappers (flushed post-transform with verdicted-message substitution for streams, dropped on deny); per-service-call persistence sits above the chat seam and is covered by its own post_model_call verdict; per-run history-provider overrides in run options are wrapped too; nested guarded sub-agents persist inline at their own boundaries. - Opt-in dependency: ResponsibleAI.AgentHooks 0.1.0-alpha.4 (bundles native runtimes) referenced only by the new package; no existing framework source is modified. - 58 tests: deny-before-execution and transform write-back per seam, rich-content preservation, streaming ordering with zero egress on deny, error bracketing, concurrency isolation, host-owned sessions, evaluate_only, approval-seam lift, persistence gating, misuse fail-closed paths, and codec units. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * fix(dotnet): close structural bypasses at the ChatClientAgent boundary Address both reviewers' probe-confirmed findings; the runtime enforcement held everywhere, every fix is at the structural boundary: - Gate the implicit default ChatHistoryProvider: with no provider configured, ChatClientAgent creates an InMemoryChatHistoryProvider the factory never saw, so denied output became durable session history and replayed to the model on the zero-config path (both stream modes). The factory now materializes and gates the default, setting the history-conflict flags to mimic implicit-default semantics for service-managed-history agents. - Wrap per-run provider overrides on BOTH dictionaries: base AgentRunOptions.AdditionalProperties is merged into the chat options with precedence, so a base-level override bypassed (and displaced) the wrapped ChatOptions-level entry. Plain AgentRunOptions is covered too, and the wrap is copy-on-write — the caller's options and dictionaries are never mutated. - Reject per-run ChatClientFactory on guarded agents (fail closed): it would replace the guarded chat pipeline and the tool-wrapping stage riding it, silently removing the chat and tool seams. - Reject a supplied client already containing a FunctionInvokingChatClient: it would execute tools below the chat seam, before any post_model_call verdict and outside the tool seam. - Run wire projections inside the guarded blocks at the chat and function seams: a poisoned value whose serialization throws now fails the run closed (function seam: host_error halt; chat seam: gated persistence refused before the failure propagates). - Suppress provider failure notifications once a run-level deny or halt stands, so the denied turn's request messages never reach provider code. - Document the deferred-OpenTelemetry observer channel (request-side spans capture pre-transform content under sensitive-data telemetry). - Rename the factory to AsAIAgentWithAgentHooks per repo convention. 10 new boundary regression tests mined from the review probes (default-provider durability in both stream modes with session-replay assertions, both override dictionaries incl. the displacement shape, plain-run-options override, copy-on-write, factory and supplied-FICC rejections, poisoned-projection fail-closed); 68 total, all green. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * fix(dotnet): redact denied-run failure notifications for both provider kinds The deny/halt handling of provider failure notifications only covered the chat-history wrapper; a context provider still received the denied turn's request messages on its failure notification. Both gating wrappers now REDACT instead of suppress: the notification is forwarded with empty request messages and the original exception, preserving the documented failure-cleanup contract (providers releasing per-run resources on the failure signal keep working) while the denied turn's request messages never reach provider code. Regression tests assert both provider kinds receive the redacted notification (zero request messages) on a denied run and full notifications on ordinary, verdict-free failures. 70 tests total. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * fix(dotnet): address Copilot review on the agent-hooks PR - Run options: always clone chat-typed run options (the framework's function-invocation middleware chains its per-run factory onto the instance it receives, so forwarding the caller's instance leaked that factory into it — reuse tripped the rejection, concurrent reuse raced), and recognize the framework middleware's own factory as legitimate: it wraps the guarded pipeline (tool rewriting), so outer function-middleware composition now works, while its chained factories are walked so a caller-supplied factory cannot ride in unnoticed. - Streaming: re-derived (transformed) updates preserve the response's ContinuationToken (ToAgentResponseUpdates does not project it), so transformed background streaming responses remain resumable; a message-less response releases a metadata-only update carrying it. - Codecs: transformed tool calls are validated for complete shape and uniqueness before reconciliation (non-empty string id and name, object-valued args, distinct ids) — malformed shapes fail closed instead of becoming invalid native calls. Deliberately stricter than the merged Python codec, which coerces added-call shapes. - Role defaulting in message write-backs is confirmed exact Python parity (user/assistant defaults per the merged codecs) and is now locked by tests rather than changed. - ADR 0035 records the seam order, persistence gating, fail-closed behavior, alternatives and known limitations. 14 new tests (options reuse, outer function-middleware composition, smuggled-factory rejection, continuation-token preservation, 8 malformed tool-call shapes, 2 role-default parity); 84 total, green. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * feat(dotnet): project the per-call tool set on pre_model_call emissions Context providers can register additional tools during run preparation, after agent_startup has been emitted, so tools_registered is inherently a run-start snapshot and can be a partial view of the tools eventually offered to the model. - Emit the spec's optional pre_model_call tools field ({name, description?}) from the per-call effective ChatOptions.Tools — the completed set for each call, including provider-added tools. - Document tools_registered as the run-start snapshot on the agent seam (dynamic registrations surface per call and are bracketed by the tool seam when invoked). - Probe-confirm enforcement completeness for provider-added tools: they flow through the guarded pipeline's tool-wrapping stage, emit pre/post_tool_call, and a pre_tool_call deny blocks their invocation exactly like constructor-registered tools. Two new tests (bracketing + audit projections, deny-blocks); 86 total, green. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * refactor(dotnet): one artifact per file; rewrap foreign gating wrappers Per review: - Split the three multi-type files (AgentHooksGatingProviders.cs, AgentHooksRunState.cs, AgentHooksWireCodecs.cs) into one type per file, file name matching the type name, per repo convention. No behavior changes; namespaces and access levels unchanged. - Close a validation asymmetry at the provider gate: the per-run override wrap skipped any gating wrapper, including one owned by a DIFFERENT agent-hooks installation — which runs inline under this run's state (its own gate is not covering here), so a denied run's history could persist straight through it. Overrides are now re-wrapped unless the wrapper belongs to this installation (reference-equal configuration). The provider seam's inline behavior for foreign/absent state is otherwise deliberate: inline is the safe direction there (content of unguarded or differently guarded runs is covered by its own verdicts or none), and throwing would break the legitimate double-wrap flush flow. One new regression test (foreign wrapper as per-run override on a denied run persists nothing); 87 total, green. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * refactor(dotnet): accept params IEnumerable for agent-hooks interceptors Per review: the constructor only iterates the interceptors, so widen the parameter from params IInterceptor[] to the C# 13 params IEnumerable<IInterceptor>. The sequence is enumerated exactly once into the internal registration list (sequences may be single-enumeration); per-item null validation and the factory's at-least-one-interceptor check are unchanged, and an explicit null sequence now throws ArgumentNullException. Params-form call sites are source-compatible. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * build(dotnet): ship Microsoft.Agents.AI.AgentHooks as a preview package Per maintainer review on the PR: - Add the project to agent-framework-release.slnf and import the shared packaging props so the package ships. Version follows the repo default for unmarked packages (preview suffix), matching the package's [Experimental] surface and alpha upstream dependency: 1.17.0-preview.<date>.1. - Package metadata: sibling-style title, fuller description, tags; shared icon and NUGET.md readme via the packaging props. Verified dotnet pack locally: ResponsibleAI.AgentHooks 0.1.0-alpha.4 flows as a normal dependency and the project references become 1.17.0 package dependencies. - Update ADR 0035: shipping as preview per maintainer decision replaces the build-only-pending-maturity stance. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * build(dotnet): version the agent-hooks package as alpha Per maintainer review: the package's maturity marker follows the ResponsibleAI.AgentHooks dependency it is built on (alpha), rather than the repo's default preview suffix. Packs as 1.17.0-alpha.260804.1; ADR 0035 updated. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * refactor(dotnet): group agent-hooks internals into Core and Codecs folders Per review: only the public surface (the factory extensions and options) stays at the project root; the internal seam decorators, run state and gating providers move to Core/, and the wire projection codecs to Codecs/. Pure file moves — namespaces stay flat per the core package's folder convention (ChatClient/, Memory/); no content changes. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * docs(dotnet): clarify session scoping and name the sessionId argument Per review: - Name the AgentContextBuilder arguments at the run-state factory so the GUID reads as what it is (the per-run agent-hooks session id). - Document both branches of CreateRunState: session-scoped means the host owns the emitter/builder and the session boundaries (one session spanning runs, no agent_startup/agent_shutdown emitted by the agent); the default is one session per run with a fresh emitter, fresh sequence and isolated record trail, which is what keeps concurrent runs' emissions from interleaving. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * fix(dotnet): harden agent-hooks factory and input projection per review - Input projection returns (payload, content, role) as one typed result so the emission site never re-reads payload properties by name: the both-fields-exist invariant holds by construction. (The previous reads were fail-closed even hypothetically — JsonObject's indexer yields null, and a null content is rejected by the SDK's envelope validation — but reading back what we just produced was needlessly fragile-looking.) - Reject UseProvidedChatClientAsIs on the factory: it signals a fully custom, do-not-touch client stack, which is incompatible with a factory whose job is to decorate the supplied client and rely on the agent's default pipeline above the chat seam. Honoring it would silently change where (and whether) the seams sit. - Log swallowed agent_shutdown emission failures (logger resolved the same way the agent resolves its own: services, then the chat client, then null) so incomplete session trails are trackable; OutOfMemoryException stays unswallowed. The swallow remains correct: the run's own outcome is already propagating and the trail closure is best-effort by contract. 89th test: UseProvidedChatClientAsIs rejection. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * build(dotnet): attribute the Agent-Hooks protocol in the package identity Per review: - Title per suggestion: 'Microsoft Agent Framework - Responsible AI Agent-Hooks Protocol Support'; description names the protocol precisely (AGENT-HOOKS-0.1, maintained by the Responsible AI project at github.com/responsibleai/agent-hooks) so the package reads as protocol support, not a MAF-owned feature; tags aligned. - Drop the [Experimental] attributes: per repo convention the attribute gates unstable surface inside released packages (Harness, core), while pre-release packages (Valkey and Mcp at alpha, Mem0 and LocalCodeAct at preview) carry none — the version suffix is the maturity signal. - Drop the describing comment on the central package version entry. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> * refactor(dotnet): split agent-hooks test fixtures into Support files Per review: one type per file under Support/ (mock client, guards, recording providers, helpers), matching the src-side convention; pure mechanical split, flat namespace. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> --------- Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
Motivation & Context
Runtime controls for agents (policy engines, approval flows, information-flow checks, budget guards, audit pipelines) currently require one adapter per framework, and no framework defines what happens when a guardrail callback fails or lets a control author verify "supported" claims. AGENT-HOOKS-0.1 is a framework-neutral interception contract addressing this: eight interception points, a three-verdict model (allow / deny with liftable approval / transform), fail-closed host obligations, payload-free audit records, and a conformance test kit.
PR #7444 proposed this as an external adapter package. Maintainer feedback asked for a first-class experimental feature in core instead, with a single public factory, private middleware, an opt-in extra, and corrections to transform write-back, content preservation, and streaming semantics. This PR supersedes #7444 and implements exactly that design.
Description & Review Guide
agent_framework/_agent_hooks.py: one public factory,agent_hooks_middleware(...), returning a private agent/chat/function middleware trio (one object per middleware category, percategorize_middleware()). Partial installs and stacked trios fail closed with explicit errors, so a caller cannot accidentally install part of the control contract.input/outputat the agent seam,pre/post_model_callat the chat seam,pre/post_tool_callat the function seam,agent_startup/agent_shutdownbracketing each run. Transforms write back into the native contexts (messages,arguments,results) asContentobjects; an unappliable transform raises rather than proceeding untransformed.MiddlewareTerminationshort-circuits are guarded at every seam (a substituted result passes the relevant interception point before egress); enforcement-layer failures halt the run; interceptor crashes surface ashost_errordenies.buffered_outputsemantics): no update egresses before thepost_model_call/outputverdicts; a deny at pull time releases zero updates; run state stays active across lazy pulls (ResponseStream.from_awaitable+ result/cleanup hooks) with cleanup on every exit path.emitter/builderparameters for one audit session spanning multiple runs.agent-hooks-sdkis an opt-inagent-hooksextra (not inall), lazy-imported per the_mcp.pypattern; core imports cleanly without it.ExperimentalFeature.AGENT_HOOKS+@experimental, lazy root export, typing surface,PACKAGE_STATUS.mdentry.uv lock --checkpass locally.MiddlewareTerminationguarding at the four seams; the buffered-streaming trade-off (callers get the stream API but updates arrive only after theoutputverdict — the only fully fail-closed option); tool-seam deny semantics (policy deny returns a reason-only error payload and the loop continues;host_error:*halts the run); and the sibling/stacking verification approach.Known limitation to resolve before merge:
agent-hooks-sdkon PyPI currently ships a linux-x86_64 wheel only, souv sync --all-extrasbuilds it from sdist elsewhere (macOS/Windows wheels are being published; will update this PR when live).Related Issue
Supersedes #7444 (external-adapter draft, closed in favor of this first-class design per maintainer feedback).
Contribution Checklist
Behavior change note
A bare middleware object passed to an agent constructor or assigned to the
middlewareattribute was previously ignored byrun()(any non-sequence collapsed to no middleware); it now executes, matching the per-runmiddleware=parameter semantics.