ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

DeepSeek Harness Agent 事件系统改造:单一 Payload 对象与融合式 Scope Dispatch 架构解析

DeepSeek Harness Agent 事件系统改造:单一 Payload 对象与融合式 Scope Dispatch 架构解析 DeepSeek Harness Agent 事件系统改造单一 Payload 对象与融合式 Scope Dispatch 架构解析【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harnessAgent 作用域agent-scoped事件是 DeepSeek Harness 中插件监听 Agent 生命周期、干预请求与步骤的关键通道。本文以 Agent Note: Agent 作用域事件 dispatch 单个 payload 对象 为骨架结合deepseek-ai/dsh-agent、deepseek-ai/dsh-agent-loop与deepseek-ai/dsh-goal的源码实现完整讲解此次从「位置参数」到「单一 payload 对象」的事件签名迁移你将掌握受影响的事件全集、payload 的字段约定、next参数在 waterfall/serial 事件中的位置以及agentEvents(ctx, agent)融合式 dispatcher 如何同时保证「作用域键与 payload 主体不脱钩」与「热路径零分配」。背景为什么位置参数签名是长期负担在改造之前Agent 作用域事件的历史签名采用位置参数positional arguments开头是agent主体subject中间是事件专属字段event-specific fields末尾是用于 waterfall瀑布式/ serial串行事件的next回调。这套约定带来两个结构性痛点改动成本爆炸新增一个字段或退役一个上下文类型如PreStepContext与RequestFailureContext都必须跨包重写每一个监听器listener和发射器emitter。事件契约散落在参数列表的位置中编译器只能依赖参数顺序而非具名结构来约束调用。约定不可见契约「分散在参数列表中而不是集中在一个具名 payload 中」阅读监听器签名无法一眼看出事件携带哪些字段、next处于什么位置。这份已实现Status: implemented的架构决策目标就是把约定收敛为一个具名对象。决策每个事件恰好一个 payload 对象作为第一个参数新的统一签名规则为每个 agent 作用域事件都将恰好一个 payload 对象作为其第一个参数。payload 始终携带主体agent、事件的字段以及事件拥有取消信号时的取消signalnext仍然是 waterfall/serial 事件的最后一个参数不再与 payload 混在参数列表中间PreStepContext与RequestFailureContext两个上下文类型被退役其字段直接内联进agent/pre-step与agent/request-error的 payload。受影响的事件全集被改造的事件共三类合计 14 个十二个agent/*事件、agent-loop/config-start-failed唯一没有主体的事件以及goal/changed。十二个agent/*事件定义在 runtime-types.ts 的declare module deepseek-ai/cordis事件扩展中可归纳为四组事件名分发模式payload 核心字段不含agent语义agent/createdemit{ agent }配置完成的 Agent 与会话被发布agent/disposedemit{ agent }Agent 离开注册表agent/statusemit{ agent, status }生命周期状态翻转idle⇄runningagent/inbox/insertedemit{ agent, message }消息进入活动 inboxagent/inbox/claimedemit{ agent, message, turn }消息被 open turn 认领agent/inbox/discardedemit{ agent, message }消息从 inbox 被丢弃agent/session-startemit{ agent, source }会话生命周期开始startup/resume/clear/compactagent/pre-stepwaterfall{ agent, messages, turn, step, signal }拒绝或替换进入步骤的消息agent/requestwaterfall{ agent, turn, step, signal }替换冻结的模型请求配置agent/request-errorwaterfall{ agent, turn, step, provider, failure, retryPolicy, signal }处理一次失败的模型请求agent/turn-stoppingserial{ agent, turn, signal }turn 即将关闭前的收尾钩子agent/erroremit{ agent, turn, step, error }步骤或 turn 出错通知agent-loop/config-start-failed在 agent-loop/src/index.ts 中定义为payload: { sessionId: SessionId; error: unknown }它是唯一不带agent主体的事件因此不参与agentEvents的融合注入由 AgentLoop 在其自己的调度路径中直接以单一 payload 分发。goal/changed定义于 goal/src/domain.tspayload 为{ agent, change: GoalChanged }并在 goal/src/index.ts 中通过agentEvents(this.ctx, agent).emit(goal/changed, { change: notification })发射——注意调用方不需要、也不能传入agent它由 dispatcher 注入。退役的上下文类型字段直接内联改造前agent/pre-step携带一个PreStepContext对象agent/request-error携带一个RequestFailureContext。改造后这两个类型被删除其字段被拍平进各自 payloadagent/pre-stepmessages: UserMessage[]、turn、step、signal见 runtime-types.tsagent/request-errorprovider、failure: LlmFailure、retryPolicy: ResolvedRetryPolicy | undefined、signal见 runtime-types.ts。好处是监听器只需要认识 payload 的形状而不必再导入一个额外的上下文包装类型。融合式 dispatcheragentEvents(ctx, agent)如何保证主体不脱钩新架构的核心机制是融合fuseddispatcher实现在 dispatch.tsexport function agentEvents( ctx: Context, agent: Agent, carrier: ScopedAgent agentCarrier(agent), ): AgentEventDispatch它做两件事把agent主体注入进每次 dispatch 的 payload调用方只需传入「去掉agent之后的其余字段」类型层面定义为PayloadRestK OmitPayloadOfK object, agentdispatcher 内部通过({ ...payload, agent })合成完整 payload。展开在前、主体在后因此即使某个结构上可接受的 payload 恰好携带了agent字段注入的主体依然优先the injected subject wins——杜绝了「作用域载体键与 payload 的agent分叉」的可能。把主体绑定到作用域载体scope carriercarrier默认由agentCarrier(agent)生成即scopeTarget(agent, agent)——主体 Agent 本身就是作用域键。dispatcher 以carrier作为thisArg调用 Cordis 的emit/serial/waterfall实现 agent 作用域过滤只有注册在该 agent 作用域下的监听器才会收到事件。AgentEventDispatch暴露三个与 Cordis 三种分发模式一一对应的方法见 dispatch.ts方法底层 Cordis 模式语义emit(name, payload)emit即发即忘的通知不可否决同步抛错与 Promise reject 逐监听器隔离并记日志serial(name, payload)serial按注册顺序 await 的串行分发返回首个 bail 值waterfall(name, payload, ...rest)waterfall环绕式中间件分发rest即事件参数中 payload 之后的部分末尾元素是最内层nextemit的特殊之处在于它没有直接调用 Cordis 的ctx.emit而是自行取回调集合并逐监听器 try/catch 包裹见 dispatch.ts。原因在源码注释中写得很清楚Cordis 的 emit 通过Array.map调用回调一个同步抛错会饿死后续监听器而返回的 Promise 会被丢弃Agent 的通知是非否决non-vetoing的因此 dispatcher 需要同时隔离这两种失败模式——同步异常与异步 rejection 都会被ctx.logger.warn记录后继续。此外还提供一次性入口emitAgentEvent(ctx, agent, name, payload)用于「只发一次、不值得保留 dispatcher」的场景内部临时构造 dispatcher 后立即 emit。零分配热路径ReactLoopAgent在构造函数中构建一次 dispatcherReactLoopAgent是默认的 Agent 驱动器driver实现在 agent-loop/src/agent.ts。它把「dispatch 一次构建、处处复用」落实到了极致private readonly dispatch: AgentEventDispatch constructor(...) { this.dispatch agentEvents(loopCtx, this) ... }dispatch在构造函数中构建一次并保存为实例字段此后每个 emit、serial 和 waterfall 都经由它路由见 agent.ts。由于载体对象是持久的热路径上的 dispatch 不再产生任何对象分配hot-path dispatches allocate nothing。从调用点可以看到三种模式在真实 loop 中的分工waterfallagent/pre-stepagent.ts——next返回默认PreStepDecision{ kind: enter, messages }监听器可改为{ kind: reject }或替换消息agent/requestagent.ts——next产生冻结的seedConfig监听器可返回替代配置agent/request-erroragent.ts——next产生默认undefined失败终结监听器可返回{ kind: retry }接管重试。serialagent/turn-stoppingagent.ts——turn 关闭前按序 await监听器可用agent.steer(...)阻止关闭。emitagent/statusagent.ts、agent/erroragent.ts、以及构造函数中为Inbox钩子注册的agent/inbox/inserted、agent/inbox/discarded、agent/inbox/claimedagent.ts。这一设计意味着无论事件发生在 loop 的哪个位置、以哪种模式分发主体注入都只发生在唯一的 dispatcher 里不存在第二次手工构造{ agent: this, ... }的机会。为什么不做另外两种方案文档明确记录了被否决的替代方案理解它们有助于把握新设计的取舍边界方案一保留位置签名。这是「不做任何事」的基线。新增字段或退役上下文类型依旧会迫使重写每个监听器和 emitter契约继续分散在参数列表中。改造的动机正是消除这种持续的成本。方案二在每个 dispatch 位置手工构造主体。loop 的中间设计曾调用ctx.waterfall(this.carrier, …)传入手工构造的{ agent: this, … }payload。它虽然避免了每次 dispatch 的分配但重复了主体注入逻辑并让「作用域键」与「payload 主体」存在分叉的可能——某处手写、某处忘记写就会出现载体键与 payload 不一致的隐性 bug。最终落地的融合式 dispatcher 之所以被选中是因为它是每种 dispatch 模式的唯一注入点既保住了零分配又把主体/作用域耦合收拢到一处。后果与收益一次形状变更one-shape change监听器签名一次性命名完整 payload扩展 payload 或退役上下文类型时对所有监听器和 emitter 都是一次统一的形状变更而非逐个参数调整。主体/作用域耦合由 dispatcher 强制执行作用域载体键与 payload 的agent不可能分叉注入主体优先于任何结构上可接受的冗余字段。热路径保持零分配ReactLoopAgent只构建一次 dispatcher所有 emit/serial/waterfall 复用同一载体。如何编写符合新约定的监听器以一个在agent/pre-step上做消息过滤、并在agent/request-error上接管重试的插件为例import { agentEvents, type PreStepDecision, type RequestErrorAction } from deepseek-ai/dsh-agent // 监听器注册到 agent 作用域只有该 agent 的事件会命中 ctx.scope(agent, () { ctx.on(agent/pre-step, async ({ agent, messages, turn, step, signal }, next) { // payload 具名携带全部字段next 是最后一个参数 const decision: PreStepDecision await next() // 或直接返回 { kind: reject } return decision }) ctx.on(agent/request-error, async ({ agent, turn, step, provider, failure, retryPolicy, signal }, next) { if (failure.code RATE_LIMITED) return { kind: retry } // 接管恢复 return next() // 委托默认策略 }) }) // 发射端只需要 payload 中除 agent 之外的字段 agentEvents(ctx, agent).emit(agent/status, { status: running }) agentEvents(ctx, agent).waterfall(agent/request, { turn, step, signal }, () Promise.resolve(seedConfig), )关键点回顾监听器第一个参数是完整 payload含agentnext始终在最后发射端永远不传agent由融合 dispatcher 注入无取消信号的事件如agent/created、agent/inbox/inserted的 payload 不含signal需要取消协作的agent/pre-step、agent/request、agent/request-error、agent/turn-stopping都携带signal。更完整的类型与调用示例可继续阅读 dispatch.ts、runtime-types.ts以及事件在 loop 中的全部调用点 agent.tsgoal/changed的消费方示例见 goal-round-driver/src/index.ts 与其 README 中对持久性检查点的说明。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表