Skip to content

.NET: addressable decorator positions in ChatClientAgent's default chat pipeline #7726

Description

@MohammadHaroonAbuomar

Problem

A decorator's position inside ChatClientAgent's default chat pipeline is a construction-time secret: WithDefaultAgentMiddleware composes ApprovalResponseBinding → ApprovalNotRequiredFunctionBypassing → FunctionInvokingChatClient → [MessageInjecting] → [PerServiceCallChatHistoryPersisting] → DeferredOpenTelemetry → <supplied client>, and once the agent exists there is no supported way to observe or change where a decorator sits.

For enforcement-style extensions that need a specific position (the agent-hooks integration from #7564 needs its chat seam below FunctionInvokingChatClient so every model service call of the tool loop is bracketed individually), the only viable strategy today is:

  • start from the raw IChatClient and decorate it before agent construction (AgentHooksChatClientExtensions.Compose);
  • reject a supplied client that already contains a FunctionInvokingChatClient (it would put the tool loop below the seam — tools executing before any post_model_call verdict);
  • reject caller-supplied per-run ChatClientAgentRunOptions.ChatClientFactory callbacks (they replace the pipeline wholesale), while recognizing the framework's own function-invocation middleware factory by walking its closure fields via reflection to make outer function-middleware composition work (AgentHooksAgent.IsFrameworkFunctionMiddlewareFactory) — the single most fragile piece of the integration;
  • reject UseProvidedChatClientAsIs = true (incompatible with a factory that must customize the stack).

Proposed shape (for discussion)

Some form of addressable pipeline for the default stack, e.g.:

  • named stages (or well-known stage types) with InsertBefore<TStage> / InsertAfter<TStage> on ChatClientAgentOptions (or a pipeline-builder callback that receives the ordered default stages and may splice);
  • per-run factories declaring intent ("wraps" vs "replaces") so wrappers compose instead of being indistinguishable from pipeline replacement.

With that, "add hooks by decorating an existing ChatClientAgent" becomes possible and all four rejections above reduce to ordinary insertion.

Context

Not urgent (per the review thread) — the shipped factory covers the common cases; this is about making the next such extension not need the rejection dance. cc westey (@westey-m)

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .Net

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions