Skip to content

Python: agent-hooks interception contract as a first-class experimental core feature - #7515

Merged
Evan Mattson (moonbox3) merged 9 commits into
microsoft:mainfrom
MohammadHaroonAbuomar:mhabuomar/agent-hooks-first-class
Aug 7, 2026
Merged

Python: agent-hooks interception contract as a first-class experimental core feature#7515
Evan Mattson (moonbox3) merged 9 commits into
microsoft:mainfrom
MohammadHaroonAbuomar:mhabuomar/agent-hooks-first-class

Conversation

@MohammadHaroonAbuomar

@MohammadHaroonAbuomar MohammadHaroonAbuomar commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

  • What are the major changes?
    • New agent_framework/_agent_hooks.py: one public factory, agent_hooks_middleware(...), returning a private agent/chat/function middleware trio (one object per middleware category, per categorize_middleware()). Partial installs and stacked trios fail closed with explicit errors, so a caller cannot accidentally install part of the control contract.
    • All eight interception points mapped to native seams: 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. Transforms write back into the native contexts (messages, arguments, results) as Content objects; an unappliable transform raises rather than proceeding untransformed.
    • Fail-closed enforcement: MiddlewareTermination short-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 as host_error denies.
    • Streaming is fully buffered (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 (ResponseStream.from_awaitable + result/cleanup hooks) with cleanup on every exit path.
    • Session scoping: per-run by default, or host-owned via emitter/builder parameters for one audit 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. ExperimentalFeature.AGENT_HOOKS + @experimental, lazy root export, typing surface, PACKAGE_STATUS.md entry.
  • What is the impact of these changes? No behavior change for anyone not calling the factory. New optional extra; no new required dependencies. 55 new tests built on the real Agent/mock-client flows (deny-before-execution, transform write-back, rich-content preservation, complete streaming ordering, error cleanup, concurrency isolation, nested agents, importability without the SDK). Full core suite, ruff, strict pyright, and uv lock --check pass locally.
  • What do you want reviewers to focus on? The MiddlewareTermination guarding at the four seams; the buffered-streaming trade-off (callers get the stream API but updates arrive only after the output verdict — 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-sdk on PyPI currently ships a linux-x86_64 wheel only, so uv sync --all-extras builds 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

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change.

Behavior change note

A bare middleware object passed to an agent constructor or assigned to the middleware attribute was previously ignored by run() (any non-sequence collapsed to no middleware); it now executes, matching the per-run middleware= parameter semantics.

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>
Copilot AI lite review requested due to automatic review settings August 4, 2026 18:43
@agent-framework-automation agent-framework-automation Bot added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _agent_hooks.py75115179%266, 303, 306–309, 311–322, 325–326, 332–333, 360, 364, 397, 399, 407–408, 414, 419, 462, 480–481, 555, 564–565, 571–572, 579, 583, 594–595, 598–599, 603, 615–616, 619, 622, 632–634, 636–637, 643, 650–658, 661–662, 669–672, 674–675, 700, 704, 732–738, 741–743, 767–774, 776–777, 796, 798, 807, 809–814, 824, 835–838, 854, 865–874, 994–995, 1049–1050, 1098, 1108, 1122–1123, 1151–1152, 1167, 1169, 1181–1186, 1254–1255, 1300, 1361, 1384, 1395, 1400, 1410, 1413–1417, 1438
   _agents.py4534590%565, 577, 632, 1170, 1221, 1303–1307, 1406, 1436, 1473, 1565, 1593, 1606, 1658, 1660, 1669–1674, 1679, 1681, 1687–1688, 1695, 1697–1698, 1706–1707, 1710–1712, 1722–1727, 1731, 1736, 1738
   _clients.py1431291%325, 376, 539–542, 992, 994–995, 997–999
   _feature_stage.py1901094%110, 149, 165, 220, 231, 252, 290, 329, 379, 407
   _middleware.py4401696%377, 743, 750–751, 933, 1004, 1022, 1024, 1157, 1160, 1187, 1189, 1325, 1329, 1516, 1592
   _serialization.py1761293%356–357, 562, 572, 637, 640, 683–684, 688–689, 691, 693
   _sessions.py9445793%163, 175–176, 213, 224, 238, 262, 307, 312, 314, 324, 356, 368, 378, 487, 556–557, 1270–1274, 1289, 1319, 1356–1357, 1371, 1373, 1392, 1394, 1477, 1517, 1594, 1598, 1608, 1825, 1858–1859, 1864, 1879, 1957–1958, 1960, 2044, 2061, 2140, 2213, 2242, 2261, 2264, 2272–2273, 2285–2286, 2298, 2308, 2338
   _tools.py13768294%231–232, 409, 411, 424, 449–451, 459, 477, 491, 498, 505, 528, 530, 537, 545, 680, 714–716, 719–721, 723, 729, 780–782, 807, 833, 837, 875–877, 881, 1054, 1066, 1073–1076, 1097, 1105, 1119–1121, 1491, 1574, 1624, 1684–1685, 1742, 1789, 1796–1797, 1887, 1949–1950, 1980, 2076, 2090, 2093, 2106, 2109, 2132, 2139, 2148, 2152, 2177, 2211, 2279, 2308–2309, 2406, 2434, 2474, 2477, 2534, 2687, 2776, 3270
   _types.py12919992%63, 73, 127, 136, 155, 157, 161, 165, 167, 169, 171, 189, 193, 219, 241, 246, 251, 255, 285, 711–712, 893–894, 1352, 1427, 1462, 1482, 1492, 1686–1688, 1973–1978, 2003, 2058, 2063, 2073, 2081, 2088–2092, 2110, 2183, 2196, 2201, 2314, 2337, 2598, 2622, 2721, 2902–2903, 3005, 3288, 3341, 3360, 3399, 3410, 3412–3416, 3418, 3421–3429, 3439, 3604, 3611, 3615–3617, 3762, 3767, 3772, 3777, 3781, 3867–3869, 3898, 3986–3990
   observability.py10297992%448, 450–451, 454, 457, 460–461, 466–467, 473–474, 480–481, 488, 490–491, 494, 497, 500–501, 506–507, 513–514, 520–521, 528, 705–706, 916, 920–922, 924, 932–933, 937, 981, 983, 994–996, 998–1000, 1004, 1012, 1136–1137, 1372, 1659–1660, 1923, 1966–1967, 2156, 2384–2385, 2389, 2429–2430, 2434, 2475–2478, 2519–2520, 2671, 2674, 2686, 2703, 2707–2708, 2711, 2717, 2837, 3058, 3060
packages/core/agent_framework/_harness
   _agent.py132496%202, 625–626, 628
packages/foundry/agent_framework_foundry
   _agent.py2683487%124, 127, 251, 256, 265, 369, 477–478, 502, 505, 516–528, 532–533, 731–732, 735, 761, 771, 793, 881, 886, 890
TOTAL45648425290% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
9261 34 💤 0 ❌ 0 🔥 2m 28s ⏱️

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py with the public agent_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-hooks extra (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

Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/pyrightconfig.dependency.json
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>
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py
Comment thread python/packages/core/agent_framework/_agent_hooks.py
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
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>

@moonbox3 Evan Mattson (moonbox3) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look at the failing CI/CD (code quality checks) too, please. Thanks.

Comment thread python/packages/core/agent_framework/_sessions.py Outdated
Comment thread python/packages/core/agent_framework/_types.py
Comment thread python/packages/core/agent_framework/_middleware.py
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_middleware.py Outdated
Comment thread python/packages/core/agent_framework/_middleware.py
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>
Comment thread python/packages/core/agent_framework/_agent_hooks.py
Comment thread python/packages/core/agent_framework/_agents.py Outdated
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>
Comment thread python/packages/core/agent_framework/_sessions.py
Comment thread python/packages/core/agent_framework/_harness/_agent.py Outdated
Comment thread python/packages/core/agent_framework/_agent_hooks.py Outdated
Comment thread python/packages/core/agent_framework/_sessions.py Outdated
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>
Comment thread python/packages/core/agent_framework/_agent_hooks.py
…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>
@westey-m
westey (westey-m) added this pull request to the merge queue Aug 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Aug 6, 2026
@westey-m
westey (westey-m) added this pull request to the merge queue Aug 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Aug 6, 2026
@moonbox3
Evan Mattson (moonbox3) added this pull request to the merge queue Aug 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Aug 6, 2026
@moonbox3
Evan Mattson (moonbox3) added this pull request to the merge queue Aug 6, 2026
@moonbox3
Evan Mattson (moonbox3) removed this pull request from the merge queue due to a manual request Aug 7, 2026
@moonbox3
Evan Mattson (moonbox3) added this pull request to the merge queue Aug 7, 2026
Merged via the queue into microsoft:main with commit 7302d0b Aug 7, 2026
36 checks passed
MohammadHaroonAbuomar added a commit to responsibleai/agent-hooks that referenced this pull request Aug 7, 2026
…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>
KirschQAQ (KirschBluteX) pushed a commit to KirschBluteX/agent-framework that referenced this pull request Aug 19, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants