Files
deepseek-harness/packages/core/agent-loop/README.zh.md
T
Tianyi Cui 4c9f5efc07 fix(agent-loop): reserve the initial empty system head
Cause: SystemPromptProjection skipped the first empty rendered prompt. The initial admitted user then occupied surface node zero, so a later nonempty prompt appended behind user history. Routes without in-history system support lost the leading system role; pi-ai demotes a non-leading system message to user content.

Fix: append the initial system node even when its content is empty. The existing loop commit order reserves node zero before admitted user messages; later prompt text replaces that node. Empty content still derives to no wire message. Keep retained-node replacement, clearing, multi-system handling, and pi-ai conversion unchanged; this addresses only the reviewed PR3476 initial-empty finding, not PR3483.

Tests: added initial-empty projection and two-turn loop regressions for empty wire output, reserved surface head, later leading system role, replacement intent, and series header. Negative control failed before the source fix. Focused projection/runtime-context/loop/request-reconstruction/session-surface/pi-ai-context suites passed 176 tests; exact runtime-context.ts coverage is 100% statements, branches, functions, and lines. test:docs passed all 15 gates. Updated README EN/ZH, architecture map and owning architecture note; recorded all three translation pairs. Broad doc-sync/lint stopped at parent request for combined-layer validation. No normalize.ts conflict-comment edit.
2026-09-06 20:42:43 +08:00

15 KiB
Raw Blame History

description, kind
description kind
面向用户与维护者的默认 agent 驱动器说明,用于选择、配置或调试 agent 的创建方式以及轮次与步骤的运行方式。 package-reference

@deepseek-ai/dsh-agent-loop

English | 中文

概述

dsh-agent-loop 创建 agent——全新创建或从持久化历史恢复——并运行轮次与步骤生命周期:领取提示词、组装请求、流式接收模型响应、分发工具调用,并把每个结果追加回会话日志。作为默认驱动器,它实现 dsh-agentAgent 接口并在此注册工厂,因此插件通过 ctx.agents 创建与驱动 agent,而不必依赖本包。声明式配置项会在启动时自动启动 agent,maxParallelToolCalls 限制同时运行的并行安全工具调用数量。它是 harness 唯一的具象循环——超出「调用模型、运行工具、重复」的所有内容都属于监听事件分类体系的插件。标准组合请选择它作为驱动器;如需替换,请实现 Agent 并通过 ctx.agents 注册。

目录


使用本包

在任何应运行 agent 的组合中挂载 dsh-agent-loop。它提供 ctx.agents 背后的驱动器,并启动你在配置中声明的 agent;dsh-basedsh-sdk-minimal 都将它作为显式配置行挂载。

配置声明式 agent

配置中声明的 agent 会在插件加载时自动启动。每个条目需要一个 id 标签;模型调用还同时需要 providermodelagent/request 可以在分发前补齐缺失的这一对值)。

- name: '@deepseek-ai/dsh-agent-loop'
  config:
    maxParallelToolCalls: 10
    agents:
      - id: 'main'
        provider: deepseek
        model: deepseek-chat
        reasoningEffort: high
        cwd: /workspace
字段 默认值 含义
maxParallelToolCalls 10 每个步骤同时在途的并行安全工具调用数;1 为串行
agents[].id 必填 稳定标签;未设置 sessionId 时,全新会话会生成 ${id}-session-<uuid>
agents[].provider / agents[].model 模型路由;分发前两者都必须存在
agents[].reasoningEffort 非空的初始推理等级;agent/request 可以覆盖它
agents[].maxTokens 正数的逐请求输出 token 上限
agents[].cwd 全新会话的工作目录
agents[].sessionId 确切身份:首次使用创建,重新挂载时恢复已实体化的历史
agents[].resumeSessionId 加载这个持久化会话而不是创建新会话;与 sessionId 互斥

生成的配置目录是每个受支持字段的穷尽式真源。适配器会校验有效推理等级,循环则把它记录在请求头中。maxParallelToolCalls 也是整个 agent-loop 设置分节,因此叠加在该条目之上的用户层无需重启即可限制下一组工具调用。

以编程方式创建或恢复 agent

插件与宿主通过 ctx.agents.create() 创建 agent,通过 ctx.agents.resume() 恢复持久化会话;两者都返回 AgentHandle,其 dispose() 拥有确切的 teardown 能力。循环会把每个创建的 agent 运行到完成——只有调用方需要自行拆除 agent 时才需要句柄。

const handle = await ctx.agents.create({
  sessionId,
  agentOptions: { provider: 'deepseek', model: 'deepseek-chat' },
  setup: (agentCtx) => { /* scoped tools, prompt sections, listeners */ },
})

一个步骤做什么

每个步骤都会发送会话的派生历史——以该 agent 渲染后的系统提示词作为 surface 第 0 号节点(一个 system/message 事件)开头——及其可见工具 schema;模型的工具调用经过受守卫的工具流水线,每个被接纳的事实都会在下一步据此派生之前追加到会话日志。并行安全调用最多可重叠 maxParallelToolCalls 个;独占调用单独运行并构成排序屏障。取消是协作式的:agent.cancel() 中止当前活动,并在未设置 keepInbox 时清除待处理工作;被取消的流会终结已送达用户的文本。


理解实现

实现细节——点击展开

本节解释该包如何实现上述行为;可观察约定已在使用本包中完整说明。

设计理念

该包是公开 Agent 约定的唯一具象实现。它在 ctx.agents 上把自身注册为 AgentFactory,因此消费方从不导入本包;每个创建 agent 的所有权归属于调用方 fiber 与循环提供方,并汇合到同一个记忆化的完全停稳边界。每个可观察效果都通过会话事件与 agent/* 分类体系发生——包内部从不属于公开表面。

请求 header 与适配器默认值

agent/request 返回后,ctx.llm.prepareCall() 会在活跃轮次信号下校验适配器持有的字段,并解析推理强度和输出 token 默认值。循环会在解析、request/header 记录与分派期间保留同一个适配器。循环会为首次请求、变化的 envelopeconfig 或 tools——提示词不属于 header)、显式消息序列起点、surface 替换(提示词变更或压缩(compaction))后的请求及恢复写入完整 header;同一序列内内容未变的步骤、重试与普通后续轮次继承最新 header。在 header 之外,循环还会记录 request/context——提供方、模型与 contextWindow——且仅在其中任何一项与最新快照不同时记录。下一次 waterfall 前,循环移除适配器默认字段,使当前路由重新解析它们;显式设置则保留。未处理的路由仍以 NO_ADAPTER 失败。

源码地图

文件 职责
src/index.ts 插件入口:AgentLoop 服务、配置 schema、声明式 agent 启动、工厂注册
src/agent.ts 具体 ReactLoopAgent 驱动器:收件箱、轮次/步骤状态机、取消
src/tool-calls.ts 工具调度:独占屏障与有界并行池
src/runtime-context.ts 每步骤 runtime-context 快照处理
src/constants.ts DEFAULT_MAX_PARALLEL_TOOL_CALLS
src/invariant.ts 不变式配套:从会话日志重建请求

创建与拆除

创建是同一个受回滚保护的事务:构造私有会话、具象 agent 与带作用域上下文;等待可选 setup;进入两个注册表;依次宣告 session/createdagent/created;发出 agent/session-start;此后才启动驱动器。Setup 抛出、commit 失败或所有者 dispose 都会回滚事务而不发布任一 id。Teardown 顺序是停止并排空、关闭会话的写路径、撤销作用域、detach agent、再 detach 会话,且每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。

持久化集成

循环是会话写句柄在生产环境中的获取点。挂载 ctx.sessionPersistence 后,create/createAgent 调用 persistence.create(header)——在发布之前存储持久身份并取得写所有权——并通过句柄追加构造 seed;resume 先调用 persistence.open(id, 'write')(排除同 id 的并发恢复),通过句柄读取物理上有效的日志,并为在轮次中途崩溃的日志把 interruptedTurnClosers 作为普通批次追加——语义崩溃修复是 agent 层的职责,而非存储入口。发布前的最后一刻,appendUnstoredSuffix 存储 setup 窗口期间追加的事件(seed 标记、委派策略记录),它们绝不会经由 session/event 重新发出。发布之后,挂载的后端按会话 id 把该会话的 session/event 批次、session/flush 屏障与 session/disposed 退役路由进活跃写句柄;循环只通过它拥有的句柄触碰存储。记忆化的 teardown 在循环提交会话的收尾事件之后关闭句柄——close 会排空任何已路由的缓冲——可证明地释放写所有权。没有后端时,会话只存在于内存中,其余一切不变。

轮次与步骤流程

驱动器在其整个生命周期内拥有一个 agent,并在 ctx.agents.withInitiator(agent, ...) 内运行。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。在 agent/pre-step 之前,驱动器组装并渲染提示词,再把渲染文本与存活的 system/message 节点比对投影(runtime-context.ts 中的 SystemPromptProjection):没有存活节点时即使提示词为空也追加(预留第 0 号节点,但不产生协议消息),文本不同时恰好替换该节点,文本未变时不产生任何事件。agent/pre-step 决定什么进入该步骤。进入步骤的决定会紧接 step/start 之后追加待提交的 system/message,随后在驱动器再次领取消息前追加完整的 user/message 批次,因此日志顺序即协议顺序;被拒绝的决定则不追加任何消息。请求由 header.configderiveMessages()header.tools 构成;请求不携带 system 字段。每次模型尝试会发出一个进程本地 start,仅在匹配的持久 assistant-frame 结算之后发出各个 chunk,并恰好发出一个终态 end;最终组装或消息追加失败时以 aborted 结算,committed 则出现在持久 assistant/message 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 interrupted: true 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。

失败与取消

最终适配器选择、分发与迭代失败以终止结束的形式到达并进入 agent/request-error;拥有恢复权的监听器返回 { kind: 'retry' } 且不调用 next(),未被处理的失败则是终态。Middleware、结果处理、工具及其他扩展失败仍会抛出并直接关闭轮次——插件失败结束的是轮次,不是循环。取消后未分发的模型工具调用会收到合成的 tool/callABORTED_BEFORE_DISPATCH 结果对。显式取消决策 拥有信号生命周期。


进一步探索

包级约定对大多数消费方已经足够;需要周边领域与设计原理时再阅读以下页面。


模型体验

完整对话请求

模型看到什么

每个步骤中,循环会发送会话的派生消息(其首条消息是 surface 第 0 号节点承载的、针对该 agent 渲染的系统提示词)与可见工具 schema。它提供 providermodelcwd 变量值,但不添加固定文案。

Token 影响

系统文本与 schema 在每个步骤都会再次计入。逐 agent 作用域决定贡献,而权威组装 waterfall 可以改变最终请求,并使其监听器负责保持协议连贯。

KV Cache 影响

只有在同一提供方与模型路由下,且系统文本、schema 与此前历史都保持逐字节一致时,请求才保持仅追加。渲染后的提示词未变时,surface 第 0 号节点保持原位,缓存前缀得以保留。提示词变更会用新的 system/message 替换第 0 号节点,因此请求从其第一个 token 起就不同,提供方前缀缓存整体未命中;schema 或组合变更则从第一个改变的请求 token 起使复用失效。

保留的消息历史

模型看到什么

已接纳的 user 消息、assistant 消息、工具调用与结果、注入上下文与 steering 都会记录,并在后续步骤中发送。原始流分片、生命周期边界与其他仅写入日志的事件会被排除。

Token 影响

输入会随每条表层消息增长,直到压缩(compaction)替换遮蔽较旧节点;包含多个步骤的工具轮次会在每个步骤重新发送累积的历史。

KV Cache 影响

普通历史增长仅追加,并保留可复用条目。表层替换或压缩会从第一个被遮蔽的历史 token 起使复用失效。

取消后未分发的调用

模型看到什么

如果后续请求回放一个中止的步骤,取消所阻止分发的每个工具调用都有错误码 ABORTED_BEFORE_DISPATCH,结果文本为 Error: tool call aborted before dispatch

Token 影响

每个跳过的调用都会在历史中保留一个固定错误结果,直到压缩将其遮蔽。

KV Cache 影响

仅追加;每个合成结果都位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。

已知限制与延期工作

这些限制说明循环何时需要特别留意。它们是当前包约束,不是任务积压。

  • 分类是一元的:安全性取决于比较同级调用或资源的调用必须保持独占(原理)。
  • 配置标签默认对应新会话:省略 sessionId 时,每次启动都会创建新的 ${id}-session-<uuid>;如需确切的恢复或创建行为,必须显式提供稳定的 sessionId,而 resumeSessionId 要求已有持久化历史。
  • 配置 agent 没有逐 agent persona 字段或 setup 钩子:它们使用部署 persona;只有编程式 ctx.agents.create() / resume() 工厂选项支持带作用域的 persona 与工具组合。
  • 没有内置轮次预算:工具调用或 steering 会让当前轮次继续;限制失控轮次的策略必须从既有生命周期扩展点(如 agent/turn-stopping)执行取消。

开发备注

维护者的工作上下文——点击展开

无。