两个事件域
运行时事件分为两个域,选对域是第一步决策:
- 持久化 session 事件(
turn/*、step/*、user/message、assistant/*、tool/*)活在只追加的 session 日志上,并通过session/event广播。当事实必须跨重载存活时用它。 - 活跃
agent/*事件携带活跃的Agent并按作用域过滤到它。用来观察或拦截进行中的工作。
本页是"活跃"那一半。持久化序列见仓库的 docs/agent-lifecycle.md。
turn 与 step 术语
来自 docs/architecture.md:
- step 是一次模型请求加上它调用的工具。
- turn 是零个或多个 step:在它的第一个输入被认领之前打开,在不再亏欠任何响应时关闭。
持久化边界是 turn/start → step/start → … → step/end → turn/end,由驱动器通过 session.append 记录。被拒绝或为空的首次认领仍会关闭一个没有消耗任何 step 的持久化 turn,因此日志记录了这次尝试。
Agent 接口
packages/core/agent/src/runtime-types.ts 定义公开的 Agent:
| 成员 | 含义 |
|---|---|
id | 与 session 共享的单一身份 |
options | provider 路由与模型(provider、model、maxTokens) |
session | 活跃 session;其日志是持久化的事实来源 |
inbox | agent 拥有的持久化待处理工作的投影 |
status | 'idle' | 'running' |
ctx | agent 作用域上下文;贡献在销毁时展开 |
send(message, target, wakeup) | 把输入路由到某个 inbox 边界,可选唤醒驱动器 |
followup(message) | 排队一次普通跟进 turn 并唤醒 |
steer(message) | 为最近的一个 step 提交转向(steering) |
inject(message) | 为下一次 pre-step 排队模型面对的上下文,不唤醒 |
cancel(cause, options) | 中止活跃 turn 或 turn 间任务 |
whenIdle() | 在整个 agent 活动达到静默后 resolve |
runMaintenance(task) | 从真正的 idle 阶段运行一个非 turn 维护任务 |
生命周期状态
AgentStatus 只有两个值:'idle'(无驱动器活跃)与 'running'(唤醒输入已开始可取消的 pre-step 处理,驱动器正在排空、关闭或 checkpoint turn)。销毁从注册表移除 agent——它不是第三个可观察状态。
活跃事件(来自 packages/core/agent/src/runtime-types.ts):
| 事件 | 模式 | 含义 |
|---|---|---|
agent/created | emit | 一个完全配置好的 agent + 活跃 session 已发布 |
agent/disposed | emit | 一个 agent 离开了注册表 |
agent/status | emit | 状态在 idle ⇄ running 间翻转 |
agent/inbox/inserted | emit | 一条消息进入了活跃 inbox |
agent/inbox/claimed | emit | 一条消息在其打开的 turn 内离开了 inbox |
agent/inbox/discarded | emit | 一条消息从活跃 inbox 被丢弃 |
agent/session-start | emit | session 生命周期开始,在首个 turn 之前发生一次(source:startup/resume/clear/compact) |
agent/pre-step | waterfall | 拒绝一个拟议 step 或替换进入它的消息 |
agent/request | waterfall | 替换冻结的调用配置 |
agent/request-error | waterfall | 在重试/关闭前处理一次失败的模型请求尝试 |
agent/turn-stopping | serial | turn 即将关闭;反对的监听者可以 steer |
agent/error | emit | 某个 step 或 turn 出错 |
Waterfall 事件(pre-step、request、request-error)与 llm/stream,以及三个 tools/* 事件,都要求监听者调用 next() 来委托;agent/turn-stopping 是 serial 且没有 next()。
Scope key = 活跃 agent
packages/core/scope/src/index.ts 是让按 agent 事件路由安全的库。key 就是 agent 对象本身:
createScope(ctx, key)铸造一个带不透明ScopeKey标签的Context,外加精确/共享 disposer。scopeTarget(base, key)构建一个仅路由的 carrier,其过滤器放行匹配该 key 或任一祖先(bindScopeParent)的监听者。事件沿链向上流动、从不向下——因此一个常驻组合可以观察在其下组合出的每个 agent。scopeOf(ctx)读取最近的 scope 标签。
在 packages/core/agent-loop/src/agent.ts 中,每个 ReactLoopAgent 铸造自己的 scope 并派生 agent.ctx:
this.scope = createScope(loopCtx, this)
this.ctx = this.scope.ctx.extend({ agent: this })
this.dispatch = agentEvents(loopCtx, this)scope 正是"setup 窗口"组合作用域化世界的地方:通过 agent.ctx 注册的一切(作用域工具、prompt 段、restrict()、监听者、await 的子插件)都是 agent 局部的,在首次 prompt 组装之前就已存在,并在销毁时展开。
事件分发模型
packages/core/agent/src/dispatch.ts 把 agent 主体与其 scope carrier 耦合,使二者无法分叉:
export function agentEvents(ctx, agent, carrier = agentCarrier(agent)): AgentEventDispatch {
// 通过 emit / serial / waterfall 融合分发,把 `agent` 注入 payload
}carrier 作为 Cordis 监听者过滤器的 this 传入;emit 路径自己解析过滤后的回调集,并逐个监听者地包含同步抛出与返回 promise 的拒绝,因此通知无法否决生命周期进展。循环驱动器在 agent 构造函数中构建一次分发器,并在热路径上复用。
注册表主体事件——agent/created、agent/disposed,以及配对的 session/created/session/disposed/session/event——通过注册表(packages/core/agent/src/index.ts)分发,注册表按 carrier 过滤,因此 agent 作用域监听者只收到 this agent 的事件,而无作用域监听者全局观察。
Setup 窗口:CreateAgentOptions.setup
编程式创建经由 AgentRegistry.create → 注册的 AgentFactory(@deepseek-ai/dsh-agent-loop)。工厂铸造 agentCtx,在插入或宣布 session 或 agent 之前 await setup,因此观察者永远不会看到未完全配置的世界:
setup?: (agentCtx: Context) => AgentSetupCommit | Promise<AgentSetupCommit | void> | void通过 agentCtx 注册的一切(作用域工具、prompt 段/变量、监听者、await 的子插件)在 session/created、agent/created、agent/session-start 以及首次 prompt 组装之前就已存在。Setup 只做组合,从不驱动——只在创建 resolve 之后驱动 agent。setup 抛出、commit 抛出或属主销毁都会回滚 scope,且不发布任何一个 id。
resume 对持久化 session 镜像这个窗口:先加载持久化,然后铸造 agentCtx 并 await setup,同时重建的 session 与 agent 保持未发布。
发布顺序
工厂的 publish(source) 序列(packages/core/agent-loop/src/index.ts):
- 把 agent 插入注册表(
enter)——作用域已生效,但尚未宣布。 - 宣布(
agent/created)——同步监听者失败会否决发布。 - 发出
agent/session-start { source }——第一个启动驱动扩展点。 - 启动机器(驱动器)。
同步的 agent/created 监听者失败会回滚,并把任何已开始的创建宣布配对为 agent/disposed 或 session/disposed。启动器通过 CONFIGURED_AGENT_IDENTITIES_KEY 拥有配置 agent 的确切 session 身份,该 key 在任何 Loader 条目挂载之前用 ctx.provide() 设置。
驱动一个 turn
ReactLoopAgent.kick() → turn() 在 turn() 返回 true 期间循环。一个 turn:
turn/start { turn }
preStep(target, {turn, step})
认领 inbox 批次 每条消息 agent/inbox/claimed
system-prompt/assemble waterfall
agent/pre-step waterfall → enter(messages) | reject
若 enter:逐条消息追加 user/message
step/start
buildRequest -> agent/request waterfall -> llm/stream -> assistant/chunk*
-> assistant/message
工具调用 -> tools/pre-execute|execute|post-execute -> tool/result*
step/end
若 turnEnds 且下一个 step 的 inbox 为空:
agent/turn-stopping (serial) — 监听者可以 steer()
turn/end { reason }每个 step 读取插件注册的 prompt 段与工具 schema,并从日志派生模型历史(deriveMessages())。模型可见即已记录:任何到达模型请求的内容都必须能从日志重建。
拆除
AgentHandle.dispose()(只返回给创建该 agent 的消费者属主)停止循环、await 其退出、展开作用域世界——通过 agent.ctx 做出的每个注册项按相反顺序销毁——然后才注销该 agent(发出 agent/disposed)并从存储中移除其 session。cancel(cause) 是突然路径:不保留 inbox 时清除排队与转向工作,并中止活跃 turn 或 turn 间任务。whenIdle() 同时跟随活跃活动及其背后释放的任何替换工作。
延伸阅读
- 架构一览——核心包对照表与事件分类。
- 扩展(Cordis)系统——驱动循环的类型化事件与可逆副作用。
- 仓库文档:
docs/agent-lifecycle.md(持久化序列)、docs/event-producer-consumer.md(每个事件由谁分发/监听)、docs/subsystems/core.md。 - 源码:
packages/core/agent/src/runtime-types.ts、packages/core/agent/src/dispatch.ts、packages/core/agent-loop/src/agent.ts。 - 源码:
packages/core/scope/src/index.ts、packages/core/session/src/types.ts(SessionEventMap)。