惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs
博客园 - 叶小钗
P
Privacy International News Feed
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
IT之家
IT之家
博客园 - 聂微东
Know Your Adversary
Know Your Adversary
Help Net Security
Help Net Security
罗磊的独立博客
I
Intezer
S
Schneier on Security
博客园_首页
C
CERT Recently Published Vulnerability Notes
雷峰网
雷峰网
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
P
Privacy & Cybersecurity Law Blog
T
The Exploit Database - CXSecurity.com
PCI Perspectives
PCI Perspectives
Security Latest
Security Latest
H
Heimdal Security Blog
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
Y
Y Combinator Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Microsoft Security Blog
Microsoft Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
The GitHub Blog
The GitHub Blog
A
Arctic Wolf
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
Engineering at Meta
Engineering at Meta
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
[MAF预定义ChatClient中间件-09]MessageInjectingChatClient-赋予工具消息注入的能力
Artech · 2026-06-15 · via 博客园_首页

MAF中的ReAct循环是由FunctionInvokingChatClient中间件完成的,这意味着AIFunction的调用也是由它驱动。在默认的情况下,AIFunction绑定的委托执行完成后,结果会转换成AIContent并被封装成一个角色为Tool的ChatMessage。这个ChatMessage最终被添加到对话历史中,并作为后续调用LLM的输入。换言之,AIFunction不像LangChain的工具函数一样,可以通过返回Command对象添加具有合法结构的消息列表到对话历史中。当工具在执行过程借助注入对话历史的消息来描述当前的情况,以辅助LLM后续能够更加精准的推理,这是非常有价值的。比如工具在执行过程中发现验证的风控风险,可以注入一条Assistant消息模拟LLM的回复来提示用户风险的存在。

1. 从工具函数中注入消息

下面的演示程序展示了如何通过MessageInjectingChatClient来实现工具函数中注入消息的能力。这个程序模拟了一个银行转账的业务场景,转账的工具函数是TransferMoney。如代码片段所示,我们从当前Agent执行上下文(AIAgent.CurrentRunContext)中获取当前的Session对象,并检查Session的StateBag中是否存在一个键为UserConfirmed的值来判断用户是否已经确认过转账。如果用户没有确认,工具函数会通过MessageInjectingChatClientEnqueueMessages方法来注入一条Assistant消息到对话历史中,提示用户存在欺诈风险并要求提供手机验证码。与此同时,工具函数会返回一条消息告知用户转账指令已提交至系统缓冲区,等待合规审查。

using Azure;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using OpenAI;

dotenv.net.DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var agent = new OpenAIClient(
        credential: new AzureKeyCredential(apiKey),
        options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model: model)
    .AsIChatClient()
    .AsBuilder()
    .UseMessageInjection()
    .Build()
    .AsAIAgent(tools: [AIFunctionFactory.Create(TransferMoney, name:nameof(TransferMoney))]);

var prompt = "从账号`4242 4242 4242 4242` 转账100块到账号 `5555 5555 5555 4444`";
var response = await agent.RunAsync(prompt);
Console.WriteLine(response);

Console.WriteLine($"\n{new string('-', 100)}\n");
var session = await agent.CreateSessionAsync();
session.StateBag.SetValue("UserConfirmed", (object)true);
response = await agent.RunAsync(prompt, session);
Console.WriteLine(response);

static string TransferMoney(string from, string to, decimal ammount)
{ 
    var session = AIAgent.CurrentRunContext?.Session
        ?? throw new InvalidOperationException("No active session found.");
    if (session.StateBag.TryGetValue<object>(key: "UserConfirmed", out var confirmed) && (confirmed is bool b && b))
    {
        return $"成功从`{from}`转账`{ammount}`元到`{to}`账户。";
    }
    else
    {
        var chatClient = (AIAgent.CurrentRunContext.Agent as ChatClientAgent)?.ChatClient 
            ?? throw new InvalidOperationException("Not a ChatClientAgent.");
        var injectingChatClient = chatClient.GetService<MessageInjectingChatClient>() 
            ?? throw new InvalidOperationException("Underlying chat client is not an InjectingChatClient.");
        var content = "风控系统检测到该笔交易存在欺诈风险, 你必须向用户说明情况,要求其提供手机验证码,并在未验证前拒绝完成转账。";
        injectingChatClient.EnqueueMessages(session, [new ChatMessage(ChatRole.Assistant, content)]);
        return "转账指令已提交至系统缓冲区,等待合规审查。";
    }    
}

在调用AsIChatClient扩展方法将OpenAIClient转换成IChatClient对象之后,我们调用了AsBuilder扩展方法生成了构建ChatClient管道的ChatClientBuilder对象,并在此基础上通过调用UseMessageInjection扩展方法注册了MessageInjectingChatClient中间件来启用消息注入的能力。TransferMoney方法调用IChatClientGetService方法获取MessageInjectingChatClient对象就是此对象。最后基于构建的ChatClient管道创建了Agent对象。

我们两次调用AgentRunAsync方法来测试工具函数中注入消息的功能。第一次调用时,我们没有在SessionStateBag中设置UserConfirmed键值对,所以工具函数会注入一条Assistant消息来提示用户存在欺诈风险,并且返回一条消息告知用户转账指令已提交至系统缓冲区,等待合规审查。第二次调用时,我们先创建了一个Session对象,并在StateBag中设置了UserConfirmed键值对为true,表示用户已经确认过转账了。这一次工具函数就不会注入提示风险的消息,而是直接返回一条成功转账的消息。如下是两次调用的输出结果:

为保障账户安全,我无法在此渠道收集或处理手机验证码等一次性敏感信息。

由于系统检测到该笔交易存在风险,请您通过官方银行 App 或拨打银行客服热线,在安全的验证流程中完成身份确认和转账操作。

在未通过官方安全验证前,本次转账将不会继续执行。

----------------------------------------------------------------------------------------------------

✅ 转账成功!

已从账户 **4242 4242 4242 4242** 转出 **100 元** 至账户 **5555 5555 5555 4444**。

如需继续操作,请告诉我 😊

2. 查看注入的消息

为了查看工具函数注入的消息,我们定义了如下这个MessageTrackingChatClient中间件。在它重写的GetResponseAsync方法中,我们遍历当前请求的消息列表,并根据消息内容的不同类型(FunctionCallContentFunctionResultContentTextContent等)来格式化输出(对于我们的例子,每个消息有且只有一个内容)。

class MessageTrackingChatClient(IChatClient innerClient) : DelegatingChatClient(innerClient)
{
    public override Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages, 
        ChatOptions? options = null, 
        CancellationToken cancellationToken = default)
    {
        foreach (var message in messages)
        {
            var role = message.Role;
            var content = message.Contents.Single();
            var line = content switch
            {
                FunctionCallContent functionCallContent 
                    => $"[{role}]function-call: {functionCallContent.Name}",
                FunctionResultContent functionResultContent 
                    => $"[{role}]function-result: {functionResultContent.Result}",
                TextContent textContent 
                    => $"[{role}]{textContent.Text}",
                _ => $"[{role}]{content.ToString() ?? string.Empty}"
            };
            Console.WriteLine(line);
        }
        Console.WriteLine();
        return base.GetResponseAsync(messages, options, cancellationToken);
    }
}

我们调用ChatClientBuilderUse方法将这个MessageTrackingChatClient中间件注册到管道上,并且置于注册的MessageInjectingChatClient中间件的后面,这样就能在MessageInjectingChatClient注入消息之后看到这些消息被添加到请求消息列表中了。

using Azure;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using OpenAI;

dotenv.net.DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var agent = new OpenAIClient(
        credential: new AzureKeyCredential(apiKey),
        options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model: model)
    .AsIChatClient()
    .AsBuilder()
    .UseMessageInjection()
    .Use(inner => new MessageTrackingChatClient(inner))
    .Build()
    .AsAIAgent(tools: [AIFunctionFactory.Create(TransferMoney, name:nameof(TransferMoney))]);

var prompt = "从账号`4242 4242 4242 4242` 转账100块到账号 `5555 5555 5555 4444`";
await agent.RunAsync(prompt);

输出结果:

[user]从账号`4242 4242 4242 4242` 转账100块到账号 `5555 5555 5555 4444`

[user]从账号`4242 4242 4242 4242` 转账100块到账号 `5555 5555 5555 4444`
[assistant]function-call: TransferMoney
[tool]function-result: 转账指令已提交至系统缓冲区,等待合规审查。
[assistant]风控系统检测到该笔交易存在欺诈风险, 你必须向用户说明情况,要求其提供手机验证码,并在未验证前拒绝完成转账。

输出的最后一条消息正是工具函数中注入的消息,说明MessageInjectingChatClient确实成功地将这条消息添加到了请求消息列表中,并且在后续调用LLM时被包含在输入上下文里了。为了保证对话历史具有合法的结构,注入的消息会放在承载工具函数调用结果的Tool消息之后。

3. MessageInjectingChatClient

在了解了MessageInjectingChatClient的作用和基本编程模式之后,我们继续介绍MAF针对这个ChatClient中间件的设计和实现。

3.1 在ChatClient管道中的位置

MessageInjectingChatClient一般位于FunctionInvokingChatClientPerServiceCallChatHistoryPersistingChatClient之间(如下所示),这一点非常重要,它决定了MessageInjectingChatClient中间件在每个ReAct循环中都会被执行。如果开启了针对每个ReAct循环的及时存档,注入的消息会被PerServiceCallChatHistoryPersistingChatClient捕获并存储到ChatHistoryMemoryProvider中,这样就能让Agent在后续的ReAct循环中基于这些注入的消息进行推理了。

[外部请求] 
    => FunctionInvokingChatClient 
       => MessageInjectingChatClient 
           => PerServiceCallChatHistoryPersistingChatClient 
              => LLM

这个特定的拓扑结构,说明了以下关键底座逻辑:

  • 工具函数具备改写认知的能力:由于FunctionInvokingChatClient处于MessageInjectingChatClient的上游,决定了:
    • 工具执行的副作用可被捕获:当FunctionInvokingChatClient触发并执行某个工具函数时,如果该工具内部触发了前文提到的风控逻辑、反思逻辑或上下文切换,它排队的临时消息正处于 FunctionInvokingChatClient的处理边界之内。
    • 下游管道立即可见:工具函数产生的注入消息,能立刻在向下传递给PerServiceCallChatHistoryPersistingChatClient之前被消费并合并。
  • 注入的消息是临时干预,还是永久记忆MessageInjectingChatClient位于PerServiceCallChatHistoryPersistingChatClient的上游,这个顺序界定了注入消息的生命周期:
    • 被持久化组件捕获:当 MessageInjectingChatClient将排队的消息动态拼接到当前的对话历史后,这些新组合的消息会原封不动地流向底部的PerServiceCallChatHistoryPersistingChatClient
    • 实现“单次服务调用”的存档:这意味着,在工具函数中注入的消息,会被当做本次生命周期的一部分,一并写入对话历史。在后续的用户多轮对话中,注入的消息会变成不可分割的永久记忆,而不是一次性的临时缓存;

MessageInjectingChatClient所处的位置说明了MAF将消息注入视为一种连接动态运行时(Tools/Agent 决策)静态持久层(Database/Session)的管道桥梁。如果把MessageInjectingChatClient挪到FunctionInvokingChatClient之前,工具函数内部就失去了操作注入客户端的上下文权限;如果把它挪到PerServiceCallChatHistoryPersistingChatClient之后,注入的消息就只能直达大模型,而无法在数据库中留下任何历史存档。目前这个位置,是支持工具内隐式风控、自我反思闭环的黄金分水岭。

3.2 基于Session的消息存储

MessageInjectingChatClient将注入的消息存储在当前SessionStateBag中,这样就能保证这些消息在当前Session的生命周期内都是可见的了。具体存储的是一个List<ChatMessage>对象,对应的键为MessageInjectingChatClient.PendingInjectedMessages。如下所示的是用来注入消息列表的EnqueueMessages方法的定义。

public sealed class MessageInjectingChatClient : DelegatingChatClient
{
    public void EnqueueMessages(AgentSession session, IEnumerable<ChatMessage> messages)
    {
        var queue = GetOrCreateQueue(session);
        lock (queue)
        {
            foreach (var message in messages)
            {
                queue.Add(message);
            }
        }
    }

    private static List<ChatMessage> GetOrCreateQueue(AgentSession session)
    {
        if (session.StateBag.TryGetValue<List<ChatMessage>>("MessageInjectingChatClient.PendingInjectedMessages", out var queue))
        {
            return queue!;
        }

        var newQueue = new List<ChatMessage>();
        session.StateBag.SetValue("MessageInjectingChatClient.PendingInjectedMessages", newQueue);
        return newQueue;
    }
}

3.3 针对注入消息的处理

外界(主要是注册的工具函数)得到MessageInjectingChatClient对象后,可以通过调用EnqueueMessages方法持续不断地注入消息。这种对流或者流式的消息注入方式导致了,MessageInjectingChatClient提取了所有注入消息并对它们进行处理之后,会发现又有新的消息被注入进来。所以MessageInjectingChatClient会在一个循环中提取和处理注入的消息,这一逻辑实现在重写的GetResponseAsyncGetStreamingResponseAsync方法中。

public sealed class MessageInjectingChatClient : DelegatingChatClient
{
    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default);

    public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default);
}

GetResponseAsync方法为例,它的执行流程如下:

  • 步骤1:提取当前注入的所有消息,并添加到传入的消息列表中,作为输入调用后学的ChatClient管道;
  • 步骤2:如果返回ChatResponse中携带的消息携带工具调用,立即返回此响应,此响应最终会交付到MessageInjectingChatClient手上驱动工具函数的执行;
  • 步骤3:如果没有工具调用
    • 如果具有新注入的消息,继续循环回到步骤1来处理这些新注入的消息,但是在这之前为了将后续调用纳入同一个Session,它需要将响应提供的ConversionID应用到ChatOptions上;
    • 如果没有新注入的消息,说明注入的消息已经全部被处理完了,直接返回当前的响应。

3.4 如何提取MessageInjectingChatClient

通过我们上面的介绍,我们知道了注入的消息被存储在SessionStateBag中,而且还知道键的名称和数据结构,我们可以直接操作Session来完成消息注入。但是我们最好不要这么做,因为还需要考虑针对消息列表的创建(对于第一次注入)以及多线程并发问题。所以还是老老实实调用MessageInjectingChatClient提供的EnqueueMessages方法来完成消息注入比较好。我们不能直接将此对象注入工具函数,最常用的方式就是像上面演示的那样,在工具函数内部通过AIAgent.CurrentRunContext.Agent来获取当前的Agent对象,并将其转换成ChatClientAgent对象来获取MessageInjectingChatClient对象。

var chatClient = (AIAgent.CurrentRunContext.Agent as ChatClientAgent)?.ChatClient 
    ?? throw new InvalidOperationException("Not a ChatClientAgent.");
var injectingChatClient = chatClient.GetService<MessageInjectingChatClient>() 
    ?? throw new InvalidOperationException("Underlying chat client is not an InjectingChatClient.");

3.5 UseMessageInjection扩展方法

针对MessageInjectingChatClient的注册可以通过调用ChatClientBuilderUseMessageInjection扩展方法来完成。这个扩展方法的实现非常简单,它调用了ChatClientBuilderUse方法来将MessageInjectingChatClient中间件注册到管道上。

public static class ChatClientBuilderExtensions
{
    public static ChatClientBuilder UseMessageInjection(this ChatClientBuilder builder)
        => builder.Use(innerClient => new MessageInjectingChatClient(innerClient));
}