LobeHub Agent Signal 领域包深度解析:源事件、作用域键与信号节点的共享契约层
Agent Signal(智能体信号)是 LobeHub 中把"发生了什么事"(用户发言、执行完成、网关事件)送入策略与运行时的统一事件化语义层。而 @lobechat/agent-signal 正是这一语义层的纯领域契约包:它只定义信号节点的形状、源事件的类型与载荷、以及作用域键(scope key)的推导规则,不含任何策略运行、模型调用、数据库读取或工作流触发逻辑。本文以 packages/agent-signal/README.md 为主体,结合包内源码与测试,梳理它的设计边界、公共 API、源事件目录、作用域键算法与新增源事件的完整流程,帮助你在运行时代码、调度器、策略或可观测性逻辑中正确消费与扩展这套共享契约。
一、包定位:只定义"语言",不执行"动作"
@lobechat/agent-signal 在 package.json 中描述为 "Shared AgentSignal producer helpers and contracts",其核心职责边界非常清晰:
- 它定义 Agent Signal 节点(source / signal / action / executor)的领域语言,以及源事件(source event)的类型、常量、载荷映射与作用域键工具;
- 它不做:不运行策略(policies)、不调用模型运行时(model runtimes)、不读取数据库、不触发工作流,也不导入任何应用服务端模块。
因此它可以在浏览器端与服务端同时安全使用——这正是选择它承载共享类型与纯函数的原因。当代码需要"浏览器安全或服务器安全的源事件类型、源事件常量、作用域键工具,以及核心节点形状"时,就该引入此包。
设计上有一条铁律:不要在此包中做应用集成。副作用(side effects)应放在浏览器侧的服务门面或服务端的 agentSignal 服务实现中——在本文所基于的 monorepo 布局里,浏览器侧对应 src/services/agentSignal.ts 及其桥接层,服务端对应 apps/server 下的相关服务与路由。从包入口 src/index.ts 的导出可以看出,包同时再导出内置信号词汇(AGENT_SIGNAL_TYPES、SignalPlan、SignalActionApplied/Failed/Skipped 等,定义见 src/types/events.ts 与 src/types/builtin.ts),构成完整的"源 → 信号 → 动作"语义闭环。
二、包结构与双入口导出
包目录非常精简,源码全部集中在 src 下:
packages/agent-signal/
├── package.json # exports: "." 与 "./source" 双入口
├── src/
│ ├── index.ts # 包根入口:核心节点 API + 内置信号类型
│ ├── base/ # 核心节点:types / builders / guards / registries
│ ├── source/ # 源事件:sourceEvent / sourceTypes / scopeKey
│ └── types/ # 内置信号:events / builtin
package.json 的 exports 暴露了两个语义不同的入口:
| 入口 | 目标文件 | 用途 |
|---|---|---|
@lobechat/agent-signal |
src/index.ts |
构建或检查 source、signal、action、executor 节点 |
@lobechat/agent-signal/source |
src/source/index.ts |
生产、类型化、校验或路由源事件 |
包根入口同时再导出 ./source,因此从包根也可拿到源事件 API。生产源事件时优先从 ./source 子路径导入,语义更精确、依赖面更小。
1. 核心节点 API(包根入口)
import type { AgentSignalSource, BaseAction, BaseSignal } from '@lobechat/agent-signal';
import { createAction, createSignal, createSource } from '@lobechat/agent-signal';
这些 API 面向运行时、调度器、策略与可观测性代码,用于获得归一化后的语义节点。核心节点形状定义在 src/base/types.ts:
AgentSignalSource:源节点,必带sourceId、sourceType、scopeKey、timestamp、chain(因果链元数据)与payload;AgentSignalSignal(兼容别名BaseSignal):信号节点,通过source引用(SourceRef)回指产生它的源,并带signalId/signalType/chain;AgentSignalActionNode(兼容别名BaseAction):动作节点,通过source与signal(SignalRef)双重引用回指因果来源;AgentSignalScope:单条信号链的作用域元数据(topicId/taskId/botScopeKey/agentId/userId/workspaceId);AgentSignalChainRef(别名ChainRef):链元数据,记录chainId、rootSourceId与父节点引用(parentSourceId/parentSignalId/parentActionId)。
节点由 src/base/builders.ts 中的三个纯函数构造,构造过程会自动补全 ID 与链关系:
- 节点 ID 优先使用
globalThis.crypto.randomUUID()(返回source:<uuid>形式),不可用时退化为kind:Date.now 36 进制:递增种子; chainId统一形如chain:${rootSourceId};createSignal会把parentNodeId指向源节点,并把SourceRef固化进信号;createAction会把父节点指向信号、parentSignalId与parentNodeId均设为信号的 ID。
类型层面还提供了执行器与运行时结果契约:ExecutorResult(applied / skipped / failed 三态,见 src/base/types.ts)、RuntimeProcessorResult 判别联合(wait/dispatch/schedule/conclude)、SignalTriggerMetadata、以及去重结果 EmitSourceEventResult(deduped 时区分 duplicate 与 scope_locked 两种原因)。
2. 源事件 API(/source 入口)
import {
AGENT_SIGNAL_SOURCE_TYPES,
createSourceEvent,
type AgentSignalSourceEvent,
type AgentSignalSourceEventInput,
} from '@lobechat/agent-signal/source';
const event = createSourceEvent({
payload: {
message: 'Please remember that I prefer concise answers.',
messageId: 'msg_1',
topicId: 'topic_1',
},
sourceId: 'source_1',
sourceType: AGENT_SIGNAL_SOURCE_TYPES.agentUserMessage,
});
createSourceEvent 会补齐两个关键字段(见 src/source/sourceEvent.ts 的实现):
scopeKey:推导顺序为topicId优先 → bot 线程元数据 →fallback:global(详见下文作用域键一节);timestamp:调用方未传入时使用Date.now()(毫秒)。
两个类型参数有明确分工:
AgentSignalSourceEventInput<TSourceType>:面向生产者输入,scopeKey与timestamp可选;AgentSignalSourceEvent<TSourceType>:归一化后的不可变事件,scopeKey与timestamp为必填,可直接进入持久化、去重或工作流交接。
类型层面,sourceType 与 payload 通过 AgentSignalSourcePayloadMap(src/source/sourceTypes.ts)做了一一对应的强类型约束——选择不同的源类型常量,payload 的必填/可选字段随之变化,从类型上杜绝生产出"字段与事件不匹配"的事件。
3. 作用域键 API
import { AgentSignalScopeKey, getSourceEventScopeKey } from '@lobechat/agent-signal/source';
const topicScopeKey = getSourceEventScopeKey({ topicId: 'topic_1' });
const botScopeKey = AgentSignalScopeKey.forBotThread({
applicationId: 'discord-app',
platform: 'discord',
platformThreadId: 'thread_1',
});
一组实际推导结果如下:
| 输入 | 推导结果 |
|---|---|
{ topicId: 'topic-1' } |
topic:topic-1 |
{ taskId: 'task-1' } |
task:task-1 |
{ platform: 'discord', applicationId: 'discord-app', platformThreadId: 'thread-1' } |
bot:discord:discord-app:thread-1 |
{ agentId: 'agent-1', userId: 'user-1' } |
agent:agent-1:user:user-1 |
{ userId: 'user-1' } |
user:user-1 |
| 均无 | fallback:global |
三、作用域键:信号链的"分桶"算法
作用域键是 Agent Signal 架构里去重、工作流与运行时泳道协调的地基:同一条 topic、同一个 bot 线程、同一个任务或同一对 agent/user 下的事件必须能稳定地归入同一个桶。该逻辑集中实现在 src/source/scopeKey.ts 的 AgentSignalScopeKey 命名空间对象中。
AgentSignalScopeKey 提供两类能力:
① 具名构建器,直接给定结构化元数据:
forTopic({ topicId })→topic:{topicId}forTask({ taskId })→task:{taskId}forBotThread({ platform, applicationId, platformThreadId })→bot:{platform}:{applicationId}:{platformThreadId}forAgentUser({ agentId, userId })→agent:{agentId}:user:{userId}forUser({ userId })→user:{userId}
② 从输入元数据自动推导:fromProducerInput 的判定优先级是
topicId→topic作用域;taskId→task作用域;- 同时具备
platform + applicationId + platformThreadId→bot线程作用域; - 同时具备
agentId + userId→agent/user作用域; - 仅有
userId→user作用域; - 全部缺失 → 兜底
fallback:global。
另有 fromRuntimeScope 用于已持有结构化 AgentSignalScope 的场景:按 topicId → botScopeKey → taskId → agentId(+userId) → userId 的顺序映射。对于从任意 payload 取字符串字段的场景,getSourceEventScopeKey(src/source/scopeKey.ts)会从 payload 中 pickString 出 agentId/applicationId/platform/platformThreadId/taskId/topicId/userId 再交给 fromProducerInput——这正是 createSourceEvent 补齐 scopeKey 时使用的默认逻辑。
以上推导规则都有测试用例背书:在 src/source/sourceEvent.test.ts 中,bot.message.merged 事件被断言为得到 bot:discord:discord-app:thread-1;同时携带 topicId 与 bot 元数据时断言优先使用 topic 作用域(topic:topic-1);nightly review 事件断言为 agent:agent-1:user:user-1;self-reflection 事件在带 taskId 时断言进入 task 作用域。
四、支持的源事件目录
源事件是 Agent Signal 的最外层输入,由各生产者(服务端运行时、浏览器网关、Bot 路由等)发出。AGENT_SIGNAL_SOURCE_TYPES 在 src/source/sourceTypes.ts 中定义,README 的目录表给出主要事件如下(文件名与主要生产者的仓库路径已按当前 monorepo 布局标注):
| 源事件 | 常量 | 主要生产者 | Payload 意图 |
|---|---|---|---|
agent.execution.completed |
agentExecutionCompleted |
Agent Runtime | 服务端 agent 执行完成,携带 operation、step、topic 与上下文元数据 |
agent.execution.failed |
agentExecutionFailed |
Agent Runtime | 服务端 agent 执行失败,携带 operation、reason、error、topic 与上下文 |
agent.user.message |
agentUserMessage |
服务端工作流桥接与 Bot 路由 | 用户反馈/消息内容,供策略分析记忆、prompt、文档或技能变更 |
bot.message.merged |
botMessageMerged |
Bot 路由 | Bot 平台消息内容并入某个会话作用域 |
client.gateway.error |
clientGatewayError |
浏览器网关事件处理器 | 客户端运行时操作的浏览器网关错误 |
client.gateway.runtime_end |
clientGatewayRuntimeEnd |
浏览器网关事件处理器 | 客户端操作的浏览器网关运行时结束 |
client.gateway.step_complete |
clientGatewayStepComplete |
浏览器网关事件处理器 | 浏览器网关步骤完成,携带 operation 与 step 序号 |
client.gateway.stream_start |
clientGatewayStreamStart |
浏览器网关事件处理器 | 浏览器网关流开始,携带 operation 与首个 step 元数据 |
client.runtime.complete |
clientRuntimeComplete |
聊天流式执行器 | 浏览器聊天运行时完成,携带 operation、topic、thread 与状态 |
client.runtime.start |
clientRuntimeStart |
聊天流式执行器 | 浏览器聊天运行时启动;工作流可将其桥接为 agent.user.message 并携带序列化上下文 |
runtime.after_step |
runtimeAfterStep |
Agent Runtime | 服务端运行时步骤结束 |
runtime.before_step |
runtimeBeforeStep |
Agent Runtime | 服务端运行时步骤即将执行 |
浏览器侧两个主要生产者的入口源码在 src/store/chat/slices/agentRun/actions/lifecycle/agentSignalBridge.ts(特性开关门控的非阻塞发射桥)与其配套测试 agentSignalBridge.test.ts;服务端侧 Agent Runtime 的发射钩子集成测试见 apps/server/src/services/agentRuntime/tests/agentSignalHooks.integration.test.ts,Bot 路由测试见 apps/server/src/services/bot/tests/BotMessageRouter.test.ts。
除目录表外,实际的类型目录还内置了更多源类型常量(同样定义于 src/source/sourceTypes.ts),包括 agent.nightly_review.requested、agent.self_reflection.requested、agent.self_feedback_intent.declared、tool.outcome.completed、tool.outcome.failed,它们服务于夜间回顾、自我反思、自我反馈意图与工具结果等闭环场景——服务端实现可参见 apps/server/src/services/toolExecution/serverRuntimes/ 下的 agentSignalReview.ts、agentSignalReflection.ts、agentSignalFeedbackIntent.ts、agentSignalSkillManagement.ts。从源码结构看,这是一个会持续扩充的目录,新增事件需要遵循第五节的可扩展性约定。
客户端白名单:AGENT_SIGNAL_CLIENT_SOURCE_TYPES 是浏览器来源事件的允许清单,目前只包含所有 client.* 事件(见 src/source/sourceTypes.ts,其类型上用 Extract<AgentSignalSourceType, \client.${string}`>做了约束)。该清单由认证后的服务端入口消费:在apps/server/src/routers/lambda/agentSignal.ts(含配套测试 agentSignal.test.ts`)中校验浏览器来源事件的类型是否合法。
事件守卫(类型收窄):包内还提供了一组类型守卫(同样在 src/source/sourceTypes.ts),用于在运行时中间件或工作流入口处把"泛化节点"收窄为强类型的特定源变体:isAgentSignalKnownSource(是否属于内置目录)、isClientRuntimeStartSource(是否为 client.runtime.start,工作流入口用它识别待桥接的浏览器运行时启动)、isAgentUserMessageSource(反馈分类器只处理用户消息源)、isNightlyReviewSource、isSelfReflectionSource、isSelfFeedbackIntentSource、isToolOutcomeSource。配合 AgentSignalSourceVariant<TSourceType> 与成组的具名变体类型(SourceAgentUserMessage、SourceAgentExecutionCompleted、SourceEventClientRuntimeStart 等),可让调用点在收窄后直接获得按源类型精确化的 payload 类型。
五、分层使用指南:浏览器、包、服务端各司其职
README 对"该在哪一层使用什么"给出了明确分界,这直接影响你写业务代码时 import 哪一侧:
在此包中做(纯契约):
- 浏览器/服务端共享的源事件类型
- 源类型常量与载荷映射
- 作用域键推导
- 纯 source / signal / action / result / trigger 类型
- 纯节点构建器
在浏览器服务门面做(副作用):
- 从浏览器运行时代码发射源事件(见 src/services/agentSignal.ts)
- 经 tRPC 发送事件
- 保持 UI 路径非阻塞
浏览器侧的"门控 + 非阻塞"发射由桥接层承担:src/store/chat/slices/agentRun/actions/lifecycle/agentSignalBridge.ts,其配合测试 agentSignalBridge.test.ts 位于同目录。它会先检查特性开关,再决定真正把事件送出还是静默跳过,避免 UI 主链路被旁路信号阻塞。
在服务端服务做(副作用与执行):
- 特性门控(feature gating)
- 数据库访问
- 工作流交接(workflow handoff)
- 策略/运行时执行
- Redis 或内存去重
- 可观测性持久化
服务端对外入口即上面提到的 Lambda 路由 apps/server/src/routers/lambda/agentSignal.ts:它负责认证浏览器来源的 ingress,并基于 AGENT_SIGNAL_CLIENT_SOURCE_TYPES 做客户端源类型校验;服务端产源侧的代表性实现则是 AgentRuntimeService 与 BotMessageRouter。可观测性消费侧可参考 packages/agent-tracing/src/viewer/agentSignal.ts,它把同一套语义节点落到追踪视图中。
从仓库布局看,README 中 src/server/... 前缀所指的服务端集成点(发射器、编排器、源去重存储、异步工作流触发等)对应当前 monorepo 下 apps/server 一侧的服务实现;浏览器侧 src/services 与 src/store/... 路径则与仓库布局一致。整体链路可以概括为:浏览器/运行时/机器人生产者 → createSourceEvent 归一化(本包)→ 经桥接与 tRPC 或服务内直连进入服务端 → 服务端校验白名单 → 按 scopeKey 去重与分桶 → 编排器执行策略与运行时 → 可观测性与持久化。
六、如何新增一个源事件:五步扩展流程
当出现一种新的、需要被策略感知的"发生了什么事"时,README 给出了标准五步流程:
- 在 src/source/sourceTypes.ts 添加常量与 payload 形状:同时把新类型写进
AGENT_SIGNAL_SOURCE_TYPES与AgentSignalSourcePayloadMap,从而获得端到端强类型; - 在服务端源渲染层添加渲染器:如果泛化 payload 在成为
AgentSignalSource之前需要归一化(例如把浏览器侧的扁平字段映射为服务端规范结构),则为其编写 renderer; - 把渲染器注册到源构建路径(buildSource)中,保证新事件在入口处被正确归一化;
- 仅当浏览器代码应被允许经 Lambda 路由发射该事件时,才把它加入
AGENT_SIGNAL_CLIENT_SOURCE_TYPES——所有非client.*服务端事件默认不应出现在白名单中; - 依据新的生产路径补充聚焦测试:源事件创建(本包内,参照 src/source/sourceEvent.test.ts)、路由白名单校验(参照
apps/server/src/routers/lambda/agentSignal.test.ts)、渲染器归一化或工作流桥接(参照apps/server/src/workflows/agentSignal.test.ts)。
这条流程把"加常量"到"加测试"串成了一条可回溯的扩展链路:核心契约层只做类型与归一化,是否允许浏览器直发、如何渲染进服务端、如何被工作流桥接,都留给了各自的层去决定与验证。
稳定性约定:新增事件时必须保持既有源事件稳定。因为源类型字符串会被持久化到**追踪记录(traces)、去重键(dedupe keys)与工作流载荷(workflow payloads)**中——一旦线上已有历史数据携带旧字符串,改名或删除将造成去重失效与历史追踪断链。扩展时应只增不改,并通过第 5 步的测试锁住字符串常量与 scopeKey 派生结果。
七、小结
@lobechat/agent-signal 的价值在于把 Agent Signal 的全链路词汇沉淀为一份不依赖任何运行时的共享契约:核心节点类型与纯构建器让"源 → 信号 → 动作"的因果链在任何端侧都能被一致地构造与检查;createSourceEvent 让所有生产者产出同构事件;作用域键算法让去重、工作流与运行时按稳定语义分桶;客户端白名单与守卫函数让浏览器侧在受控范围内安全发声。当你在 LobeHub 中扩展一个新的策略入口或新的运行时事件时,最合理的做法就是先回到这里定义你的源事件契约,再交给上层去执行——这也正是 README 反复强调的分层纪律:契约留在包内,副作用回归宿主。
延伸阅读:包完整文档见 packages/agent-signal/README.md;想深入类型系统,建议按 src/base/types.ts → src/source/sourceTypes.ts → src/source/scopeKey.ts 的顺序阅读,并结合 src/source/sourceEvent.test.ts 验证行为预期。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00