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

@MohammadHaroonAbuomarMohammadHaroonAbuomar 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>
CopilotAI lite review requested due to automatic review settings August 4, 2026 18:43
@agent-framework-automationagent-framework-automationBot 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-actionsBot 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

TestsSkippedFailuresErrorsTime
926134 💤0 ❌0 🔥2m 28s ⏱️

CopilotAI 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
FileDescription
python/uv.lockAdds the agent-hooks extra lock entries and locks agent-hooks-sdk 0.1.0a4.
python/packages/core/tests/core/test_agent_hooks.pyNew test suite for agent-hooks enforcement and semantics across seams (incl. streaming).
python/packages/core/pyrightconfig.dependency.jsonExcludes the new module from dependency-bound pyright checking.
python/packages/core/pyproject.tomlAdds agent-hooks optional dependency extra (explicitly not part of all).
python/packages/core/agent_framework/_feature_stage.pyRegisters ExperimentalFeature.AGENT_HOOKS.
python/packages/core/agent_framework/_agent_hooks.pyImplements the enforcement middleware trio + projections/write-back + buffering semantics.
python/packages/core/agent_framework/init.pyiAdds typing export for agent_hooks_middleware.
python/packages/core/agent_framework/init.pyAdds lazy runtime export for agent_hooks_middleware.
python/PACKAGE_STATUS.mdDocuments 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 threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/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 threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py
Comment threadpython/packages/core/agent_framework/_agent_hooks.py
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/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>

@moonbox3Evan 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 threadpython/packages/core/agent_framework/_sessions.py Outdated
Comment threadpython/packages/core/agent_framework/_types.py
Comment threadpython/packages/core/agent_framework/_middleware.py
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/packages/core/agent_framework/_middleware.py Outdated
Comment threadpython/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 threadpython/packages/core/agent_framework/_agent_hooks.py
Comment threadpython/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 threadpython/packages/core/agent_framework/_sessions.py
Comment threadpython/packages/core/agent_framework/_harness/_agent.py Outdated
Comment threadpython/packages/core/agent_framework/_agent_hooks.py Outdated
Comment threadpython/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 threadpython/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 queueAug 6, 2026
@github-merge-queue
github-merge-queueBot 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 queueAug 6, 2026
@github-merge-queue
github-merge-queueBot 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 queueAug 6, 2026
@github-merge-queue
github-merge-queueBot 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 queueAug 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 queueAug 7, 2026
Merged via the queue into microsoft:main with commit 7302d0bAug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationUsage: [Issues, PRs], Target: documentation in the code base and learn docspythonUsage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@MohammadHaroonAbuomar@TaoChenOSU@moonbox3@westey-m