Files
microsoft--agent-framework/docs/decisions
MohammadHaroonAbuomar 10bf8d7d9e .NET: agent-hooks interception contract as a first-class experimental feature (#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 (#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>
2026-08-19 10:48:13 +00:00
..

Architectural Decision Records (ADRs)

An Architectural Decision (AD) is a justified software design choice that addresses a functional or non-functional requirement that is architecturally significant. An Architectural Decision Record (ADR) captures a single AD and its rationale.

For more information see

How are we using ADRs to track technical decisions?

  1. Copy docs/decisions/adr-template.md to docs/decisions/NNNN-title-with-dashes.md, where NNNN indicates the next number in sequence.
    1. Check for existing PR's to make sure you use the correct sequence number.
    2. There is also a short form template docs/decisions/adr-short-template.md
  2. Edit NNNN-title-with-dashes.md.
    1. Status must initially be proposed
    2. List of deciders must include the github ids of the people who will sign off on the decision.
    3. The relevant EM and architect must be listed as deciders or informed of all decisions.
    4. You should list the names or github ids of all partners who were consulted as part of the decision.
    5. Keep the list of deciders short. You can also list people who were consulted or informed about the decision.
  3. For each option list the good, neutral and bad aspects of each considered alternative.
    1. Detailed investigations can be included in the More Information section inline or as links to external documents.
  4. Share your PR with the deciders and other interested parties.
    1. Deciders must be listed as required reviewers.
    2. The status must be updated to accepted once a decision is agreed and the date must also be updated.
    3. Approval of the decision is captured using PR approval.
  5. Decisions can be changed later and superseded by a new ADR. In this case it is useful to record any negative outcomes in the original ADR.