Files
deepseek-harness/packages/core/agent-loop/README.zh.md
T
Yichen Jiang 21aeff3029 Merge remote-tracking branch 'origin/master' into worktree/llm-reasoning-effort
# Conflicts:
#	.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml
#	docs/config-catalog.md
#	docs/core-data-structures/session.i18n.yaml
#	scripts/type-equiv.manifest.json
2026-07-27 13:35:10 +08:00

132 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# dsh-agent-loop
[English](README.md) | 中文
唯一的实体 agent(智能体)插件与循环驱动器。其包(package)内部实现满足 `Agent` 接口,并驱动会话/轮次/步骤生命周期。
这是 harness 中唯一包含实体循环逻辑的包。其他所有内容要么是抽象服务,要么是针对扩展 seam 的插件:新行为应放入插件,而不是这里。
## 服务:`AgentLoop`ctx 键:`agentLoop`
### 公开 API
创建与恢复属于同一个受回滚保护的事务:构造私有会话、实体 agent 和带作用域的上下文;等待可选 setup;进入两个注册表;依次宣告 `session/created``agent/created`;发出 `agent/session-start`;此后才启动驱动器。Setup 接收完整的带作用域 `Context`,作为受信任的同进程组合代码,并且不得驱动尚未发布的 agent。普通的类型化身份与选项输入遵循只读契约以借用方式传入;seed 事件与会话元数据会跨越持久会话边界,因此系统会验证并快照它们。可选的 `AbortSignal` 只取消加载/setup/发布,并在返回的 handle 可见前分离。
调用方 fiber 与 AgentLoop 提供方共同拥有 agent。`AgentFactory.createAgent(ownerCtx, options)``resume(ownerCtx, options)` 显式接收调用方所有权,而工厂为 `sessions`/`llm`/`tools`/`systemPrompt` 保留自身的依赖上下文;这样,调用方可以只注入 `agents`,而不会缩减新 agent 的服务接口。调用方卸载、handle 释放或提供方卸载都会汇合到同一个记忆化的完全停稳边界。提供方关闭会同时等待资源 teardown,以及已经观测到停用的公开 create/resume 包装层,因此依赖消失后,任何 continuation 都无法继续发布。
每个 agent 与其会话共享一个由调用方选择的 `SessionId`,并假设它在全局唯一;意外的 UUID 冲突不属于受支持模型。两个使用同一 id 的并发操作都可以进行准备,但最终的 `enter()` 调用会裁决发布,所有失败方都会回滚各自的私有资源。每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。在同步创建通知期间请求的 detach 会等待该次分发退栈,从而保留 created/disposed 配对。Teardown 顺序为停止并 drain(包括尚未完成的空闲注入 flush)→ detach agent → detach 会话 → 撤销作用域;detach 完成后,即使私有作用域仍在完成清理,该 id 也可以复用。普通、不可 veto 的 `agent/*` 通知通过 `agentEvents(ctx, agent)` 发出;逐步骤组装通过 `assembleContextFor(agent)` 完成;轮次结束时的持久性检查点通过 `ctx.sessions.flush(session)` 完成。
- `ctx.agentLoop.create(id: SessionId, options?: AgentOptions, meta?: { cwd?: string }): Agent`:在确切共享的 agent/会话 id 下同步创建,不运行 setup,并随调用 fiber 释放。声明式配置把 `agents[].id` 视为稳定 label,通常会先生成 `${label}-session-<uuid>`,再调用此边界。应用也可以提供稳定且确切的 `sessionId`:首次使用时创建;重新挂载且持久化内容已存在时,则恢复已经实体化的历史。`resumeSessionId` 要求并加载现有的持久化 id,且与 `sessionId` 互斥。这样,默认的全新重启不会冲突,也无需保留第二个实时路由身份。
`AgentLoop` 还实现 `AgentFactory` seam,并通过 `ctx.agents.setFactory(this)` 注册自身,因此插件会通过接口 `ctx.agents` 创建/恢复 agent
- `ctx.agents.create({ sessionId, meta?, seed?, agentOptions?, setup?, signal? }): Promise<AgentHandle>`:使用调用方提供的共享 id 以编程方式创建。它会等待尚未发布的 setup 事务,然后才返回;`meta` 携带 cwd/谱系/seed 边界元数据,`seed` 则在会话边界验证并快照持久值后,重建 fork 子级的前缀。`signal` 只在此 Promise 结算前生效。解析得到的 [`AgentHandle`](../agent/README.md) 拥有确切的 teardown。
- `ctx.agents.resume({ resumeSessionId, agentOptions?, setup?, signal? }): Promise<AgentHandle>`:通过 `ctx.sessionPersistence` 加载持久化会话(参见[会话持久化](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)),使用同一 id 注册 agent,重建历史,然后针对全新且尚未发布的 agent 作用域等待 setup,再执行受回滚保护的发布。轮次编号和派生历史从已加载日志继续。此操作要求存在会话持久化后端(不会硬注入,因此非持久化 demo 仍能工作;缺少持久化时,`resume` 会以明确错误拒绝)。`signal` 仅用于创建。返回 `AgentHandle`
配置驱动的 `ctx.agentLoop.create()` 路径让循环 fiber 拥有其 agent(该路径会丢弃 handle)。对于以编程方式创建的 agent,handle 持有者是唯一面向消费方的 teardown 能力;AgentLoop 提供方卸载是一条独立的结构化 teardown 边,而不是向应用代码公开的另一个 handle。
### 注入的服务
`agents``sessions``llm``tools``systemPrompt`:全部 5 个接口服务。
### 不变量配套入口
可选的 `@deepseek-ai/dsh-agent-loop/invariant` 配套入口会向 `ctx.invariants` 注册请求重建。循环会把每个确切的冻结请求记录在 `dsh-llm` 拥有的进程本地身份集合中;随后,配套入口要求存在实时会话,并根据日志独立重建消息边界和折叠后的请求 header。即使调用方冻结直接的一次性调用,或为其附加会话 id,这类调用仍不属于该契约。
### 配置(Schemastery
```ts
interface Config {
maxParallelToolCalls?: number // default 10; 1 is serial
agents: Array<{
id: string // required
provider?: string
model?: string
resumeSessionId?: string // load this persisted session instead of creating one
cwd?: string // optional workspace cwd for the fresh session
}>
}
```
通过配置创建的 agent 会自动启动。模型调用同时需要 `provider``model``agent/request` 可以在分发前补齐缺失的这一对值。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10``cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona;编程式 setup 可以按 agent 遮蔽它。该插件提供逐 agent 的 `provider``model``cwd` 提示词变量;harness 身份与部署 persona 属于 `dsh-system-prompt`
### 包内部实体驱动器
实体 `ReactLoopAgent` 适配器、其 `Inbox``runLoop`,以及绑定实例的发布/启动控制均为包内部实现。包根只导出插件/服务/配置契约,包导出映射不提供 `./src/*` 逃逸路径;生命周期拥有方通过 `ctx.agents` 创建 agent,而不是点名、构造或启动驱动器内部组件。一个准备完成的会话只能由一个实体驱动器认领;所有可观测行为都通过会话事件和 `agent/*` 事件分类体系发生。
`ReactLoopAgent.send()` 实现公开且完全解析的接纳路径。`followup()`/`queue()`/`steer()`/`inject()` 辅助方法会先解析每个可选字段,再委托给它;直接调用方通过 `ResolvedAgentInput` 提供必填的内容、来源、上下文、元数据、目标与唤醒事实。`followup()``queue()` 加入普通 FIFO,前者会唤醒空闲驱动器,后者则让其保持停驻。认领后的普通项是所属轮次的唯一消息;其上下文是提示词 waterfall(瀑布式事件)的默认附加上下文,只在通过接纳后实体化。缺少 placement 或 placement 为 `separate` 时,会追加一条独立注入的 `user/message`placement 为 `prompt-prefix` 时,则把上下文、稳定的 `## My request:` 分隔符和有效请求写入同一条 `user/message`,其对模型隐藏的 envelope 保留显示内容和上下文描述符。waterfall 返回的允许决定具有权威性,因此,使用 `next()` 包装下游的监听器会保留下游 `content``additionalContexts`,除非它有意替换相应字段。后续普通项会等待前一普通轮次的检查点结算;取消、释放、提示词阻止或启动前失败则可能让上下文随消息一同丢弃。运行期间调用 `steer()`,或使用等效的 `send()` 路由,会在不分发 `agent/prompt-submit` 的情况下,把相同记录形态加入 steering FIFO;下一个检查点会对 `steering/message` 应用相同的独立或前缀 placement,但策略仍可以在另一步骤前停止。轮次及其检查点关闭后遗留的 steering 会连同上下文转为之后的排队输入,除非终止轮次策略、取消或释放将其丢弃。`inject()` 和不唤醒的下一步骤接纳要求上下文元组为空,绕过两个 FIFO 并直接追加持久上下文:轮次打开时,注入会在当前步骤执行 assistant 工具调用期间延后到一个 FIFO 中(成功批次把它放在所有结果之后,中断批次则在轮次关闭前 drain);空闲时,注入会包在一次性 `injection` 轮次中。每次 FIFO 入队都会发布 `agent/inbox/enqueue`;驱动器的认领会发布 `agent/inbox/dequeue``cancel()` 在不带 `keepInbox` 时会发布 `agent/inbox/discard`。格式错误的数据会在入队或追加前抛出。
### 循环生命周期(`loop.ts`
驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。包私有的编排入口点会恢复确切的 Agent,一次性派生 `agent.session`,并让操作局部的辅助函数捕获它,而不是通过浅层接口继续传递实体驱动器或每次操作的 `Session`。如果显式 `Session` 正是辅助函数的实际接口,该辅助函数会保留它;创建、持久化加载、未发布 setup、服务、worker、进程、持久化和 wire 协议则继续保留各自的显式身份。[agent 服务](../agent/README.md#initiating-agent-scope)规定传播、teardown 和分离工作规则。
每次提供方调用成功结束时,都会恰好追加一个 `assistant/message` 完成锚点,包括无内容调用和以 `max-tokens` 结束的调用。成功的 `agent/step-result` 存储其转换后内容;被拒绝的结果会先记录空内容,再继续抛出原始失败。该锚点保留确切的 chunk 溯源(流没有 chunk 时为 `[]`),并在用量可用时保留用量;空内容不会进入派生消息历史。
`agent/request` 返回提供方/模型调用配置后,循环会调用 `ctx.llm.prepareCall()`,在活跃轮次信号的控制下校验由适配器持有的推理(reasoning)强度,并填入其配置默认值。准备完成的调用会在这次异步解析、`request/header` 日志记录和最终分派期间保留同一项确切的适配器注册,因此 HMR(热模块替换)不会把某个适配器的能力解析结果与另一适配器的请求混用。生效配置会在分派前写入日志,因此监听器可以在步骤之间更改推理强度,而不会产生未记录的请求变化。没有已注册适配器的路由会保留原定配置,使 `llm/stream` 监听器可以接管并短路该请求;最终分派仍会以 `NO_ADAPTER` 拒绝未得到处理的路由。新循环实例仅在初始提供方/模型路由与日志路由完全一致时恢复上次的推理强度;路由变化会丢弃由前一模型持有的不透明 ID,并单独解析新模型。
插件失败会结束当前轮次,而不是结束循环。只有最终适配器分发/迭代失败,以及带内的终止错误或中止结束原因,才进入 `agent/request-error`;中间件、结果处理、工具和 `agent/post-step` 仍属于普通轮次失败。失败步骤关闭后,恢复逻辑会接收确切的实时错误、不可变的提供方事实和不可变的先前失败。重试会在新的编号步骤中根据持久日志重建;成功会清除连续失败历史;耗尽后只在 `turn/end` 上记录一次结构化失败。AgentLoop 私下拥有一个取消持有者,其显式信号覆盖提示词策略、组装、每个步骤、模型与工具工作、恢复、continuation 和终止停止;它会在发布 `turn/end` 前立即退役该持有者,而驱动器可以在持久性 flush 期间继续保持 `running`。有效的 `cancel()` 会先发出仅存在于运行时的类型化 `user | parent` 原因,再清除待处理工作,并以协作方式中止该持有者;通知失败无法 veto 取消,通知观察方排队的工作会被清除,之后由中止观察方排队的工作属于下一轮次,空闲取消则不发出任何内容。持久 `turn/end` 仍使用粗粒度的 `aborted`;未分发的模型工具调用会收到合成的 `tool/call``ABORTED_BEFORE_DISPATCH` 结果对。释放会在终止分类中胜出;忽略信号的工作必须先结算,系统才能完全停稳。[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)规定生命周期与竞态契约。终止 continuation 的停止决定在轮次关闭和持久性 flush 期间始终具有权威性。
在步骤内,独占调用形成屏障;并行安全调用使用有界滚动池,并在启动前重新分类。只有分发/主体会重叠。策略、持久结果和结果上下文仍保持模型顺序。中止会停止新调用、drain 已启动的结果,然后在轮次通过普通中止路径关闭前,drain 已接纳的批次上下文。
### 插件负责的内容
超出「调用模型、运行工具、重复」的所有内容,都属于监听事件分类体系的插件:
- 钩子与策略:相关的 `agent/*` 检查点,加上受守卫保护的 `tools/pre-execute``tools/execute``tools/post-execute` → 定义拥有的 `finalizeContent``tools/result` 流水线;确切事件签名与 mode 位于生成的[事件目录](../../../docs/cordis-catalog/events.md)
- 压缩(compaction):在 `agent/post-step` 上观测压力;在 `agent/request-error` 上处理规范上下文溢出
- 瞬时模型恢复:`dsh-llm-retry` 监听 `agent/request-error`,使用有限且针对错误码的预算,并发出不进入表层的 `llm/retry` 状态事件
- 沙箱、权限、计划模式:使用 `tools/pre-execute` 提供可扩展的拒绝/询问,使用 `tools.guard()` 提供单调拥有方策略,使用 `tools/post-execute` 处理结果决定,并使用 `tools/result` 进行最终观测
- subagent:在循环外部实现为 `ctx.subagents` 提供方;进程内提供方使用 `ctx.agents.create()` 和拥有的 `AgentHandle` 进行 teardown,而通用的 [`ctx.tasks`](../../tasks/tasks/) 与 [`dsh-tool-subagent`](../../subagent/tool-subagent/) 负责后台收集。
- 持久化:`session/event` + `session/flush`
- UI`session/event`assistant token 流、边界、工具活动)+ `agent/*` 控制事件(`agent/status``agent/created`/`agent/disposed`
## 模型体验
### 完整对话请求
#### 模型所见
每个步骤中,循环会发送针对该 agent 呈现的系统提示词、可见工具 schema、冻结的会话前缀和会话派生消息。它提供 `model``cwd` 变量值,但不添加固定文案。
#### Token 影响
每个步骤都会再次计入系统文本、schema 与前缀。逐 agent 作用域决定初始贡献,而权威组装 waterfall 可以改变最终请求,并使其监听器负责保持协议连贯。
#### KV Cache 影响
只有在同一提供方和模型路由下,系统文本、schema、会话前缀与先前历史保持逐字节相同时,才保持仅追加。携带 token 的组装改写或组合变更可能从第一个改变的请求 token 起使复用失效。
### 保留的消息历史
#### 模型所见
已接纳的 user 消息、assistant 消息、工具调用与结果、注入上下文和 steering 都会记录,并在后续步骤中发送。原始流分片、生命周期边界和其他仅写入日志的事件会被排除。
#### Token 影响
输入会随每条表层消息增长,直到压缩替换遮蔽较旧节点;包含多个步骤的工具轮次会在每个步骤重新发送累积的前缀与历史。
#### KV Cache 影响
普通历史增长仅追加,并保留可复用条目。接口替换或压缩会从第一个被遮蔽的历史 token 起使复用失效。
### 取消后未分发的调用
#### 模型所见
如果后续请求回放一个中止的步骤,取消所阻止分发的每个工具调用都有错误码 `ABORTED_BEFORE_DISPATCH`,结果文本为 `Error: tool call aborted before dispatch`
#### Token 影响
每个跳过的调用都会在历史中保留一个固定错误结果,直到压缩将其遮蔽。
#### KV Cache 影响
仅追加;每个合成结果都位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
## 已知限制与暂缓工作
- **分类是一元的**:安全性取决于比较同级调用或资源的调用必须保持独占(参见[设计原理](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md))。
- **配置 label 默认每次新建**:省略 `sessionId` 会在每次启动时创建全新的 `${id}-session-<uuid>`;确切的恢复或创建行为要求显式提供稳定的 `sessionId`,而 `resumeSessionId` 要求已有持久化历史。
- **配置 agent 没有逐 agent persona 字段或 setup 钩子**:它们使用部署 persona;只有编程式 `ctx.agents.create()` / `resume()` 工厂选项支持带作用域的 persona/工具组合。
- **没有内置轮次预算**:只要步骤包含工具调用或 steering,默认 continuation 就是 `continue`;限制失控轮次需要使用 `agent/turn-continuation` 强制停止插件。