跳到主要内容

Agent 编排与流式:AgentClient 深入

30 秒导读: AgentClient 是 LibreChat 现代 agent 主路径的核心类(约 1900 行)。它做四件事:组装 一次请求的上下文 → 调外部包 @librechat/agents 把 agent 图跑起来 → 把图流式吐出的事件转成 SSE 帧推给前端 → 把 token 用量累计并落账

一句话记住边界: 图怎么走、LLM 调用循环怎么转,是外部包 @librechat/agents(一个 LangGraph 运行器)的事;本仓只是集成层。分不清这条边界,读这一章会一直找错地方。

本章只讲 agent 运行时与流式。工具怎么装载见 04;上下文/文件/记忆/token 预算的细节见 05;这条消息在到达 AgentClient 之前的完整生命周期见 01


1. 这是什么(先建立直觉)

AgentClient 继承自老的 BaseClient(api/app/clients/BaseClient.js)。你可以把它想成一个「聊天客户端」的实现:上层路由拿到一条用户消息后,构造一个 AgentClient 实例,调它的 sendCompletion,它负责把这条消息变成一次完整的 agent 应答(可能带思考、带工具调用、带子 agent),边生成边推流,最后把用量记账。

它自己不发 LLM 请求、不跑 agent 循环。真正「给模型发消息、收 tool_call、再发、再收」这个循环,发生在 Run 对象里——Run 来自外部包 @librechat/agentsAgentClient 的活是「把料备好、把 Run 起起来、把 Run 吐的东西接住」。

一句类比:AgentClient 是剧务,@librechat/agentsRun 是导演。 剧务准备道具(上下文)、开机(createRun)、把导演喊的每句话转播出去(SSE)、结束后结账(usage);戏怎么演是导演的事。


2. 顶层全景(它大概怎么转)

2.1 一次应答的主流程

sendCompletion 是入口(api/server/controllers/agents/client.js:895),它几乎立刻转交给 chatCompletion(client.js:1141)。下面这张图是从「一条已经组装好的 payload」到「流完+落账」的骨架——从上往下是时间顺序,右侧标出跨过边界进入外部包的那一步:

sendCompletion(payload) ┌ 本仓(集成层)
└─ chatCompletion({ payload }) │
1. 组装 config(thread_id/user/signal…) │ client.js:1159
2. formatAgentMessages(payload) ←────────┼─ 外部包:把 DB 消息转成 BaseMessage[]
3. runAgents(messages): │
run = await createRun({ … }) ←───────┼─ 本仓 packages/api:搭 graphConfig
└─ Run.create(runConfig) ←──────┼════ 跨边界 → @librechat/agents(LangGraph)
this.run = run │ client.js:1411
this._resolveRun(run) │ ← 握手:唤醒等 run 的 titleConvo
await run.processStream(…) ←──────────┼════ 跨边界 → 图执行 + LLM 循环在这里跑
每个图事件 → customHandlers ────┼─ callbacks.js 把事件转 SSE 帧回流
4. finally:
finalizeSubagentContent() │ client.js:1520
flush 子 agent usage 的 emit │
recordCollectedUsage() ← 落账 │ client.js:1540
└─ 回到 sendCompletion: │
completion = filterMalformedContentParts(this.contentParts)
metadata = buildResponseMetadata() │ client.js:919
return { completion, metadata } └

关键:createRunrun.processStream 就是那条边界。 createRun 还在本仓(packages/api/src/agents/run.ts),它把 agent 列表、子 agent 配置、图类型、自定义处理器打包成 runConfig,最后一行 Run.create(runConfig)(run.ts:1173)才真正跨进 @librechat/agentsprocessStream 里发生的一切(给模型发消息、收 tool_call、递归子图)全在外部包。

2.2 部件一句话职责

部件干什么在哪
AgentClient集成层主类:组装/起 run/接流/记账client.js:91
chatCompletion运行时主方法,协调上面整套流程client.js:1141
createRun把 agent 配置搭成图配置,再交给外部包packages/api/src/agents/run.ts:878
Run(外部)真正的图执行 + LLM 循环 + streamEvents@librechat/agents(不在本仓)
getDefaultHandlers造一组事件处理器,把图事件转成 SSEcallbacks.js:293
createContentAggregator(外部)把流式增量聚合成 contentParts[]@librechat/agents,在 initialize.js:134 实例化
三个 sink收集上下文快照/用量/子 agent 用量,供落账initialize.js:244-262

2.3 三个 sink:谁在收集什么

AgentClient 的很多字段其实是**「共享收集桶」**——不是它自己 new 的,而是上游 initialize.js new 好、同时交给「事件处理器」和「客户端」两边共享引用。处理器往桶里写,客户端从桶里读来落账。构造函数把它们解构进来(client.js:112-160):

桶(字段)谁往里写谁读它用途
collectedUsageModelEndHandler(每次 chat_model_end)recordCollectedUsage逐次模型调用的 token 用量 → 记账/扣费
usageEmitSinkemitTokenUsage(每个 on_token_usage)buildResponseMetadata复刻前端看到的用量汇总,持久化到 metadata.usage
contextUsageSinkON_CONTEXT_USAGE 处理器buildResponseMetadata上下文仪表盘的「最新可见快照」
contentParts内容聚合器getContentParts/sendCompletion最终要保存/展示的应答内容块
subagentAggregatorsByToolCallIdON_SUBAGENT_UPDATE 处理器finalizeSubagentContent每个子 agent 的完整活动,挂回父级 tool_call

为什么要「共享桶」而不是回调返回值? 因为图执行在外部包里、事件是异步流式来的,AgentClient 没法用「函数返回值」拿到它们。于是本仓的模式统一是:initialize.js 分配可变容器,靠闭包共享给处理器,处理器边流边填,AgentClientfinally 里统一收口。 记住这个模式,后面几节都是它的变体。


3. buildMessages:把上下文组装成 payload

这节讲 AgentClient 怎么把一串 DB 消息 + 附件 + 记忆 + RAG 组装成能喂给图的 payload只讲编排骨架;具体的 token 计数、文件上下文、记忆抽取规则属于 05

buildMessages(client.js:292)是 sendCompletion 之前上层调的组装步。它做的事按顺序:

  1. 排序消息链 —— 用基类静态方法 getMessagesForConversation(BaseClient.js:1059)沿 parentMessageId 回溯出有序对话;多 agent 场景用 createMultiAgentMapper 做映射。
  2. 收集所有 agent —— 主 agent + agentConfigs(来自 handoff / addedConvo 并行执行的其它 agent),先 normalizeInstructionsinstructions/additional_instructions 去空(client.js:307-322)。
  3. 绑附件与文件上下文 —— 当前轮附件 inline 绑到最后一条用户消息(addFileContextToMessage / processAttachments)。
  4. 逐条格式化 + 数 token —— formattedMessages = orderedMessages.map(...)(client.js:362),同时维护 indexTokenCountMap(按位置)和 tokenCountMap(按 messageId)。这个 map 后面要交给图做格式化和剪枝,所以存到 this.indexTokenCountMap(client.js:537)。
  5. 拼共享运行上下文 —— RAG 增广 prompt、记忆(useMemory)、按 agent 作用域的上下文,最后 applyContextToAgent 把稳定指令留在 instructions、把动态运行上下文追加到 additional_instructions(client.js:571-592)。

一个易忽略的设计:稳定 vs 动态指令分离。稳定的(agent 自身指令、MCP 指令)留在 instructions,每轮都变的(记忆、RAG、本轮文件)进 additional_instructions 这条「动态系统尾巴」。这样上游 prompt-cache 的前缀能命中,不会因为记忆变化把整个系统块的缓存打穿。

getSaveOptions / getBuildMessagesOptions

两个小方法,别被名字唬住:

  • getSaveOptions(client.js:233):返回要存进消息记录的选项快照——spec/iconURL/endpoint/agent_id/maxContextTokens 等,加上 payloadParser(this.options) 解析出的 run 选项。removeNullishValues 去空后返回。注意源码里那条 TODO:runOptions 可能含敏感数据,尚未按 provider 过滤(client.js:257)。
  • getBuildMessagesOptions(client.js:268):直接返回 {}。因为 agent 的指令是在 buildMessages 里直接从 agent 对象取的,不走这条老的选项通道。这是从 BaseClient 继承来的钩子,agent 路径故意留空。

4. chatCompletion:运行时主方法

这是本章的心脏。chatCompletion(client.js:1141)把 payload 变成一次跑起来的 run,并接住它的流。

4.1 组装 config

进来先建 config(client.js:1159)——这是要传给图运行时的 GraphRunnableConfig:

config = {
runName: 'AgentRun',
configurable: {
thread_id: this.conversationId, // 会话线程
user_id: ..., // 用户
hide_sequential_outputs: ..., // 是否隐藏中间 agent 输出
requestBody: { messageId, conversationId, parentMessageId },
user: createSafeUser(...), // 脱敏后的用户对象
},
recursionLimit: resolveRecursionLimit(...), // 图递归上限
signal: abortController.signal, // abort 信号
streamMode: 'values',
version: 'v2',
};

signal 是这里的关键伏笔:它一路透传到图里,是 abort 能层层生效的根。

4.2 备料:格式化消息、注入 skill、初始化会话

在真正起 run 前,chatCompletion 做几件预处理(全在 try 块里,client.js:1179-1320):

  • formatAgentMessages(payload, …)(外部包)—— 把本仓格式的消息转成图要的 BaseMessage[],同时返回 indexTokenCountMap、跨轮 summary、边界 token 调整。
  • skill 预备 —— primeInvokedSkills 解析被调用的 skill 正文;injectSkillPrimes 把 SKILL.md 正文按位置拼进消息数组(细节归 04/05,这里只需知道它在起 run 前发生)。
  • buildInitialToolSessions —— 把各 agent 的 code-env 文件 + skill 预备产物合并成图的初始会话种子(client.js:1194)。

这些属于「把料备齐」,大量委托给 @librechat/api 的辅助函数。本章不展开,记住它们都是为起 run 铺垫即可。

4.3 起 run:createRun 与那条边界

核心在内部函数 runAgents(client.js:1325)。它收齐 agent 列表(主 + agentConfigs),然后:

run = await createRun({
agents,
messages,
indexTokenCountMap,
initialSummary,
initialSessions,
runId: this.responseMessageId,
signal: abortController.signal,
customHandlers: this.options.eventHandlers, // ← getDefaultHandlers 造的那组
tokenCounter,
summarizationConfig: appConfig?.summarization,
subagentUsageSink: createSubagentUsageSink( // ← 子 agent 用量回收
this.collectedUsage,
this.buildSubagentUsageEmitter(appConfig),
),

});

createRun(run.ts:878)本身还在本仓:它给每个 agent 调 buildAgentInput 拼出 AgentInputs(provider、llmConfig、指令、工具定义、子 agent 配置、上下文剪枝配置……),判断是 standard 还是 multi-agent 图,组成 runConfig,最后一行 Run.create(runConfig)(run.ts:1173)才跨进 @librechat/agents

这就是要点出的边界:本仓负责「把 agent 语义翻译成图配置」,外部包负责「拿图配置真正执行」。 customHandlers(见第 5 节)是本仓塞进外部包的「回调钩子」,让外部包在图执行时反过来调本仓的处理器。

起完 run,做一次 promise 握手(client.js:1411):

this.run = run;
if (this._resolveRun) {
this._resolveRun(run); // 唤醒任何在等 run 的地方(见第 7 节 titleConvo immediate)
this._resolveRun = null;
}

然后开跑:await run.processStream({ messages }, config, { callbacks: { [Callback.TOOL_ERROR]: logToolError } })(client.js:1428)。这一个 await 期间,整场戏都在演——图在外部包里一步步走,每产出一个事件就回调 customHandlers,处理器把它转成 SSE 推给前端。processStream 返回时,这一轮 agent 应答就生成完了。

4.4 收尾与 finally

try 块尾部还会处理 skill 卡片、hide_sequential_outputs 过滤等(client.js:1470-1490)。真正的收口在 finally(client.js:1507),无论正常结束还是 abort 都会跑:

  1. 抓校准状态 —— this.run.getCalibrationRatio(),存到 contextMeta 供下一轮 token 估算复用。
  2. finalizeSubagentContent() —— 把子 agent 的内容挂回父 tool_call(第 6 节)。
  3. flush 子 agent 用量 emit —— await Promise.allSettled(this.pendingSubagentEmits),确保落账前这些异步 emit 都持久化了(第 8 节)。
  4. 记账 —— 未被 abort 时调 recordCollectedUsage(第 8 节);被 abort 时跳过,交给 abort 中间件处理,避免双重扣费。

5. 流式回流:事件如何变成 SSE 帧

这节讲图吐出的事件,怎么一步步变成前端收到的 SSE 帧。这是「流式产出」的机制核心。

5.1 谁造处理器、谁挂上去

处理器不是 AgentClient 造的,是上游 initialize.jsgetDefaultHandlers(callbacks.js:293)造好,存进 endpointOption.eventHandlers,再由 chatCompletion 通过 customHandlers: this.options.eventHandlers(client.js:1387)透传给 createRun,最终挂进外部包的 Run

getDefaultHandlers 返回一个 事件名 → 处理器 的字典(callbacks.js:344)。图在 processStream 里每 dispatch 一个事件,就按名字找到对应处理器调它。主要几类:

图事件处理器干什么位置
CHAT_MODEL_ENDModelEndHandler:收 usage 进 collectedUsage、emit on_token_usage、抓 Vertex 思考签名callbacks.js:345
TOOL_ENDToolEndHandler(外部)+ toolEndCallback 处理产物附件callbacks.js:350
ON_RUN_STEP / _DELTA / _COMPLETED聚合内容 + 按可见性转发callbacks.js:351/380/398
ON_MESSAGE_DELTA文本增量:聚合 + 转发callbacks.js:416
ON_REASONING_DELTA思考增量:聚合 + 转发callbacks.js:432
ON_SUBAGENT_UPDATE转发 + 折进每子 agent 的聚合器callbacks.js:454
ON_CONTEXT_USAGE抓上下文快照进 contextUsageSink + 转发callbacks.js:535

每个内容类处理器都是两步走:先 aggregateContent({ event, data }) 把增量喂给内容聚合器(累积成最终 contentParts),再 emitEvent(...) 把同一份增量推给前端。前者管持久化/最终形态,后者管实时展示

5.2 一帧 SSE 长什么样

底层出口只有两个函数。标准模式用 sendEvent(packages/api/src/utils/events.ts:8):

export function sendEvent(res, event) {
if ('data' in event && event.data === '') return; // 空串丢弃
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
}

注意:SSE 帧的 event: 行几乎永远是 message,真正的图事件名(on_message_delta 等)藏在 JSON body 里。也就是说前端收到的是:

event: message
data: {"event":"on_message_delta","data":{...增量...}}

前端按 body 里的 event 字段分流。少数几类走独立帧名——附件走 event: attachment(callbacks.js:585)、错误走 event: error(events.ts:21)。

5.3 两种传输模式:res vs 可恢复流

emitEvent(callbacks.js:219)是所有内容处理器的统一出口,它按有没有 streamId 分两条路:

emitEvent(res, streamId, eventData)
├─ streamId 存在 → await GenerationJobManager.emitChunk(streamId, eventData) // 可恢复流(Redis)
└─ streamId 为 null → sendEvent(res, eventData) // 标准直连

可恢复模式(streamId)把帧写进作业发射器(可持久化、断线重连能补),并且await 以保证增量顺序;标准模式直接写 res。这条抽象让 callbacks 里的每个处理器不用关心自己跑在哪种模式下。

5.4 Open Responses:另一套帧格式

本仓还有一条 OpenAI 兼容的 Responses API 入口(api/server/controllers/agents/responses.js),它复用同一个 createRun + processStream(responses.js:766/809),但用自己的 SSE 帧格式:附件走 librechat:attachment 扩展事件、带 tracker.nextSequence() 序号(callbacks.js:891writeResponsesAttachment)。也就是说图执行是同一套,回流帧格式按入口不同各自封装——这正好印证第 2 节的边界:图是共享的,SSE 表达是集成层的事。


6. 内容聚合:contentParts 怎么攒出来

这节讲流式增量最终怎么变成能保存、能刷新后还在的应答内容。

6.1 主内容聚合器

内容聚合器由外部包的 createContentAggregator()initialize.js:134 实例化,解构出 { contentParts, aggregateContent }contentParts 这个数组的引用同时给了处理器(往里聚合)和 AgentClient(读出来)。AgentClient.getContentParts()(client.js:182)就是直接返回它。

流程:每个 ON_MESSAGE_DELTA / ON_REASONING_DELTA / ON_RUN_STEP* 处理器调 aggregateContent({ event, data }),聚合器按事件类型把增量并进对应的 content part(文本块累加、tool_call 拼装……)。等 processStream 结束,contentParts 就是这一轮应答的完整内容块数组。

sendCompletion 最后对它做一次 filterMalformedContentParts 清洗(client.js:903),得到 completion 返回给上层保存。

6.2 子 agent 内容:finalizeSubagentContent

子 agent(一个 agent 作为工具被父 agent 调用)的活动是单独一路。ON_SUBAGENT_UPDATE 处理器(callbacks.js:454)为每个 parentToolCallId 懒创建一个独立的 createContentAggregator,存进 subagentAggregatorsByToolCallId map,把子 agent 的增量折进去。

到消息保存时,finalizeSubagentContent()(client.js:195)遍历主 contentParts,找到类型为 subagent 的 tool_call,用它的 id 去 map 里取对应聚合器,把子 agent 的 contentParts 挂成 toolCall.subagent_content:

contentParts(主) subagentAggregatorsByToolCallId
├─ text … { "toolcall_abc": aggregatorA,
├─ tool_call(subagent, id=abc) ────┐ "toolcall_def": aggregatorB }
│ ← 挂上 aggregatorA.contentParts
└─ text … └─ 取 id=abc 的桶,过滤空槽,挂成 subagent_content

为什么要这一步? 因为前端存子 agent 活动的 Recoil atom 是「仅本会话内存」的,页面一刷新就没了。把子 agent 的完整推理/工具调用/最终文本持久化到父 tool_call 上,刷新后才能重建出来(client.js:187-223 的注释详述)。这一步在 finally 里跑,保证 abort 时也执行。

可见性一致性(易踩的坑): ON_SUBAGENT_UPDATE 处理器把聚合(持久化)和转发(SSE)用同一条可见性规则门控(callbacks.js:471-483)。若对一个被隐藏的中间 agent 仍做聚合,finalizeSubagentContent 会把它挂进保存的消息——刷新后就泄露了本该隐藏的活动。所以 hide_sequential_outputs 是「连记都不记」的一致规则。


7. run 的 promise 握手:_waitForRun / _resolveRun

这节讲一个时序难题:标题生成想用 this.run,但它可能在 run 还没建好时就被触发。

this.run 直到 chatCompletion 跑到 createRun 之后才存在(client.js:1411)。但「立即模式」的标题生成(immediate: true)会在请求刚发出就想调 this.run.generateTitle(...)——这时 run 往往还没 ready。旧行为是直接抛 Run not initialized

解法是一对 promise 握手字段(client.js:104-110):

  • _runReady:一个 promise,run 建好时 resolve 出 run(或失败时 null)。
  • _resolveRun:上面 promise 的 resolve 函数。

_waitForRun(signal)(client.js:1574)的逻辑:

_waitForRun(signal)
├─ this.run 已存在 → 直接 Promise.resolve(this.run)
├─ 否则建 this._runReady(存下 _resolveRun 待唤醒)
├─ 无 signal → 返回 _runReady
├─ signal 已 abort → 立刻 reject
└─ 否则:竞速(_runReady vs signal 的 abort 事件),谁先到听谁的

谁来 resolve?两处:run 建好时(client.js:1412)、以及 finally 兜底(client.js:1556,即使 run 建失败也 resolve 成 this.run ?? null,免得等待方永久挂起)。

titleConvo({ immediate: true })(client.js:1607)据此把「run 未初始化」从「抛错」改成「等一下」:run 没好就 await this._waitForRun(...);等到了还是 null 就静默返回。立即模式下标题只用用户输入,所以 contentParts 传空数组(client.js:1770)。


8. token 用量:累计、汇总、落账

这节讲一次应答里 token 从「逐次收集」到「记账扣费」再到「持久化元数据」的三条去向。

8.1 逐次收集:collectedUsage

每次模型调用结束(CHAT_MODEL_END),ModelEndHandler.handle(callbacks.js:77)从 data.output.usage_metadata 取用量,打上 model/provider/agentId 标签,还会按情况标 usage_type:

  • 摘要节点产生的 → summarization(callbacks.js:118,markSummarizationUsage)。
  • 被隐藏的中间 sequential agent → sequential(callbacks.js:123-129)。

然后 this.collectedUsage.push(taggedUsage)(callbacks.js:131)。这个数组就是记账的原料。

8.2 记账扣费:recordCollectedUsage

AgentClient.recordCollectedUsage(client.js:1008)在 finally 里被调,把 collectedUsage 交给 @librechat/api 的同名辅助,后者调 db.spendTokens / db.spendStructuredTokens 真正扣费、写交易。

一个要点是多 endpoint 定价:resolveAgentEndpointTokenConfig(client.js:992)按每条用量的 agentId 找它自己 endpoint 的定价配置。因为一个图里可能混着不同 endpoint 的 agent(连接 agent、子 agent),各自费率不同——已知 agent 的配置(哪怕是 undefined,表示用内置费率)才是权威,只有完全未知的 agent 才回退到主配置。

记账结果存回 this.usage;getStreamUsage()(client.js:1045)只是把它返回给上层。

8.3 持久化元数据:buildResponseMetadata

buildResponseMetadata(client.js:919)在 sendCompletion 尾部调,产出要存到 responseMessage.metadata 的三块:

来源 sink作用
thoughtSignaturescollectedThoughtSignaturesVertex Gemini 3 思考签名,跨 DB 往返恢复 tool 轮次,避免下轮 400
contextUsagecontextUsageSink.latest上下文仪表盘断点重建
usageusageEmitSink(经 aggregateEmittedUsage)分支/总成本汇总,复刻前端实时看到的数字

contextUsage 有个精细的落账门控(client.js:941-955):只有当「最新可见快照所属那次 run 的 PRIMARY 用量事件在快照之后到达」才持久化。用 run id 匹配,是为了在并行/直连 run 交错时,completedOutputTokens 仍是真正的「快照后增量」,而不会把 B 的快照错配上 A 的输出。这段逻辑很绕,但目的单一:保证仪表盘断点重建的数字是准的。

8.4 子 agent 用量:buildSubagentUsageEmitter

子 agent 的模型调用在子图里跑,不经过 processStream 的 streamEvents 循环,所以 ModelEndHandler 根本看不到它们。这就需要一条旁路。

createRun 收了个 subagentUsageSink(client.js:1401),它由 createSubagentUsageSink(this.collectedUsage, this.buildSubagentUsageEmitter(appConfig)) 组成:外部包每 billed 一次子 agent 调用,就把用量塞进 collectedUsage(标 usage_type: 'subagent',一样进记账),并调 emitter。

buildSubagentUsageEmitter(client.js:1058)返回的 emitter 做两件事:

  1. 同步把这条用量 push 进 usageEmitSink(client.js:1090),这样持久化汇总能包含子 agent 成本。
  2. 异步发一个 on_token_usage SSE 帧(标 subagent,只进会话成本/总计,不进实时上下文表)。

第 2 步的异步 emit 不被 await,所以留一个 promise 进 pendingSubagentEmits;finally 里 Promise.allSettled 等它们全部持久化完(client.js:1525)——否则作业清理会和 emit 的持久化竞态,重连的客户端就丢了这些 billed 用量。

子 agent 模型调用(子图内,streamEvents 看不到)

▼ subagentUsageSink
┌───┴────────────────────────────────┐
│ push → collectedUsage(标 subagent) │ → recordCollectedUsage 扣费
│ emitter: │
│ ├─ 同步 push → usageEmitSink │ → buildResponseMetadata 汇总
│ └─ 异步 emit → on_token_usage 帧 │ → pendingSubagentEmits → finally flush
└────────────────────────────────────┘

9. abort 如何清理

abort(用户点停 / 断线)必须干净:不丢已生成内容、不双重扣费、不留悬挂 promise。

一根信号贯穿始终:config.signal = abortController.signal(client.js:1174),透传进 createRun 和图。abort 触发时,processStream 的 await 抛出,进 catch(client.js:1491):

  • 是 abort 造成的 → 只 debug 日志,contentParts 塞错误块(已生成的部分内容要保留)。
  • 是真错误 → error 日志 + push 一个 ContentTypes.ERROR 块给前端看(client.js:1502)。

finally(无条件跑)负责真正的清理:

  1. contextMeta 校准状态(client.js:1510)——即使 abort 也要,供下轮复用。
  2. finalizeSubagentContent()——已流入的子 agent 内容照样挂回、保存。
  3. flush pendingSubagentEmits
  4. abort 时跳过 recordCollectedUsage(client.js:1538):wasAborted 为真就不在这扣费,交给 abort 中间件(abortMiddleware.js)统一处理,避免与 /api/agents/chat/abort 双重扣费
  5. 兜底 resolve _runReady(client.js:1556),免得 _waitForRun 的等待方永久挂起。
  6. 清引用(run = null; config = null; memoryPromise = null;)助 GC。

10. 边界与局限(诚实)

  • 图执行不在本仓。 agent 循环、LLM 调用、子图递归、streamEvents 全在 @librechat/agents(Run 类)。本章多处「外部包」标注即此。想读那部分源码得去那个包(CLAUDE.md 提到团队本地路径是 /home/danny/agentus,但不在本 clone 内)。
  • 大量委托给 @librechat/api(packages/api)。 formatAgentMessagesbuildInitialToolSessionsinjectSkillPrimesrecordCollectedUsageaggregateEmittedUsage 等都是从那里 import 的(client.js:1-55)。AgentClient编排者,不是这些逻辑的实现者。
  • AgentClient 继承老 BaseClient 它自带 /** @deprecated */ isChatCompletion = true(client.js:98)等历史包袱;getBuildMessagesOptions 返回空、checkVisionRequest 空实现,都是为兼容基类接口而留的钩子。
  • hide_sequential_outputslast_agent_id@deprecated Agent Chain 的遗留(callbacks.js:198client.js:1426)。可见性门控现在仍靠 checkIfLastAgent 判断,但注释明确标了废弃。
  • usage 落账门控很脆。 buildResponseMetadata 里靠 run id / usage index 匹配来决定持久化哪块快照(client.js:941-970),注释承认这是为修各种并行/摘要交错的边界 bug 一层层打上的补丁;老版本 SDK 或 resume 场景(事件无 run id)走宽松回退。

11. 代码地图(导航索引)

主题文件路径符号名
客户端主类 / 构造(注入 sink)api/server/controllers/agents/client.js:91AgentClient (constructor)
上下文组装api/server/controllers/agents/client.js:292buildMessages
保存选项快照api/server/controllers/agents/client.js:233getSaveOptions
空钩子(agent 直取指令)api/server/controllers/agents/client.js:268getBuildMessagesOptions
运行时主方法api/server/controllers/agents/client.js:1141chatCompletion
起 run / processStreamapi/server/controllers/agents/client.js:1325runAgents(内部函数)
内容读出api/server/controllers/agents/client.js:182getContentParts
子 agent 内容挂回api/server/controllers/agents/client.js:195finalizeSubagentContent
run promise 握手api/server/controllers/agents/client.js:1574_waitForRun / _resolveRun
立即标题生成api/server/controllers/agents/client.js:1607titleConvo
逐次用量记账api/server/controllers/agents/client.js:1008recordCollectedUsage
流用量返回api/server/controllers/agents/client.js:1045getStreamUsage
响应元数据(usage/context/签名)api/server/controllers/agents/client.js:919buildResponseMetadata
子 agent 用量发射器api/server/controllers/agents/client.js:1058buildSubagentUsageEmitter
tokenizer 选择api/server/controllers/agents/client.js:1894getEncoding
事件处理器工厂api/server/controllers/agents/callbacks.js:293getDefaultHandlers
模型结束(收 usage/签名)api/server/controllers/agents/callbacks.js:37ModelEndHandler
SSE 出口(双模式)api/server/controllers/agents/callbacks.js:219emitEvent
子 agent 事件聚合/转发api/server/controllers/agents/callbacks.js:454ON_SUBAGENT_UPDATE handler
上下文快照捕获api/server/controllers/agents/callbacks.js:535ON_CONTEXT_USAGE handler
工具产物附件回调api/server/controllers/agents/callbacks.js:647createToolEndCallback
集成层建 run(边界前)packages/api/src/agents/run.ts:878createRun
跨进外部包那一行packages/api/src/agents/run.ts:1173Run.create
SSE 底层写帧packages/api/src/utils/events.ts:8sendEvent
处理器/聚合器/sink 装配api/server/services/Endpoints/agents/initialize.js:134createContentAggregator / getDefaultHandlers
Responses API 复用同一 runapi/server/controllers/agents/responses.js:766createRun / processStream