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)
Problem
A decorator's position inside
ChatClientAgent's default chat pipeline is a construction-time secret:WithDefaultAgentMiddlewarecomposesApprovalResponseBinding → 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
FunctionInvokingChatClientso every model service call of the tool loop is bracketed individually), the only viable strategy today is:IChatClientand decorate it before agent construction (AgentHooksChatClientExtensions.Compose);FunctionInvokingChatClient(it would put the tool loop below the seam — tools executing before anypost_model_callverdict);ChatClientAgentRunOptions.ChatClientFactorycallbacks (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;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.:
InsertBefore<TStage>/InsertAfter<TStage>onChatClientAgentOptions(or a pipeline-builder callback that receives the ordered default stages and may splice);With that, "add hooks by decorating an existing ChatClientAgent" becomes possible and all four rejections above reduce to ordinary insertion.
Context
AgentHooksChatClientExtensions.Compose,AgentHooksAgent.WrapRunOptions/IsFrameworkFunctionMiddlewareFactory)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)