Docs: translate the nine numbered documents to Chinese and rename them
This commit is contained in:
+2
-2
@@ -98,8 +98,8 @@ worker 环境里永远没有 token 变量。
|
||||
cursor: family('gitea-cursor:'), stream: family('gitea-stream:') }`(tests/store.spec.ts 只在那里要求 key 字面量)。
|
||||
6. 构建接线:tsdown entry `'gitea-watch': 'src/sources/gitea.ts'`;package.json exports `"./gitea-watch"`;
|
||||
cordis.patch.yml bundle 插入 `dsh-superagent-gitea` 名 `dsh-superagent/gitea-watch` **disabled: true**
|
||||
(router profile 启用它;web profile 必须不启)。007-architecture.md 模块+测试表(tests/docs.spec.ts
|
||||
强制完整性)+ 001-domain "What is reusable" 行。测试:`tests/gitea.spec.ts`(fetch 用注入的
|
||||
(router profile 启用它;web profile 必须不启)。007-架构.md 模块+测试表(tests/docs.spec.ts
|
||||
强制完整性)+ 001-领域模型 "哪些是可复用的" 行。测试:`tests/gitea.spec.ts`(fetch 用注入的
|
||||
fetchImpl / vi.stubGlobal mock),模式同 tests/sources.spec.ts。
|
||||
|
||||
## MVP 裁剪线(M2 在后):Gitea PR 观察 + threadClosed/bindThread + 自动合并;行内评审评论;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# 安装(本 fork,中文速查)
|
||||
|
||||
把本仓库作为 DSH 插件栈装上手的完整步骤。英文版见 [docs/006-deployment.md](docs/006-deployment.md)(Install / Machine configuration / Start and stop)。
|
||||
把本仓库作为 DSH 插件栈装上手的完整步骤。参考 [docs/006-部署.md](docs/006-部署.md)(安装 / 机器配置 / 启动与停止)。
|
||||
|
||||
## 前置条件
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
dsh-superagent 是为一个 GitHub 仓库服务的主动式编码 agent,打包成 [DeepSeek Harness](https://www.deepseek.com/en/harness/) 的 bundle。它监听 GitHub,可选地也监听飞书。当允许名单里的人向 agent 要活儿时,它开一个工单,在自己独立的 agent 会话和目录里干活,并在原对话里答复——比如提一个 pull request。Web 面板显示哪些事需要人。
|
||||
|
||||
它也是一种与 agent 协作的方式:**尽量少的占用人的注意力,尽量多的交付价值。** 一个能替整个团队行动的 agent 也能浪费整个团队的注意力,所以它只在被要求、或被团队自己写下的规则允许时才行动,只在有人必须行动、或能用上结果时才说话。[docs/000-philosophy.md](docs/000-philosophy.md) 解释这个原则以及它对操作者的要求。
|
||||
它也是一种与 agent 协作的方式:**尽量少的占用人的注意力,尽量多的交付价值。** 一个能替整个团队行动的 agent 也能浪费整个团队的注意力,所以它只在被要求、或被团队自己写下的规则允许时才行动,只在有人必须行动、或能用上结果时才说话。[docs/000-理念.md](docs/000-理念.md) 解释这个原则以及它对操作者的要求。
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -16,21 +16,21 @@ dsh-superagent 是为一个 GitHub 仓库服务的主动式编码 agent,打包
|
||||
|
||||
它会一次只问一个短问题,展示一次完整计划,然后安装并配置好一切。[SETUP.md](SETUP.md) 就是它遵循的协议。
|
||||
|
||||
本 fork 另有一篇动手向的中文速查,覆盖插件安装、Gitea Source 与模型 provider:[INSTALL.md](INSTALL.md)。英文参考仍是 [docs/006-deployment.md](docs/006-deployment.md)。
|
||||
本 fork 另有一篇动手向的中文速查,覆盖插件安装、Gitea Source 与模型 provider:[INSTALL.md](INSTALL.md)。完整参考是 [docs/006-部署.md](docs/006-部署.md)。
|
||||
|
||||
## 文档
|
||||
|
||||
按编号顺序读;每篇只建立在前几篇之上。
|
||||
|
||||
- [000 理念](docs/000-philosophy.md)
|
||||
- [001 领域模型](docs/001-domain.md)
|
||||
- [002 授权](docs/002-authorization.md)
|
||||
- [003 执行](docs/003-execution.md)
|
||||
- [004 提示词](docs/004-prompts.md)
|
||||
- [005 面板](docs/005-panel.md)
|
||||
- [006 部署](docs/006-deployment.md):手工安装、配置、升级与排障
|
||||
- [007 架构](docs/007-architecture.md)
|
||||
- [008 手册](docs/008-cookbook.md):加一个 Source
|
||||
- [000 理念](docs/000-理念.md)
|
||||
- [001 领域模型](docs/001-领域模型.md)
|
||||
- [002 授权](docs/002-授权.md)
|
||||
- [003 执行](docs/003-执行.md)
|
||||
- [004 提示词](docs/004-提示词.md)
|
||||
- [005 面板](docs/005-面板.md)
|
||||
- [006 部署](docs/006-部署.md):手工安装、配置、升级与排障
|
||||
- [007 架构](docs/007-架构.md)
|
||||
- [008 手册](docs/008-手册.md):加一个 Source
|
||||
|
||||
## 开发
|
||||
|
||||
@@ -46,7 +46,7 @@ dsh-superagent 是为一个 GitHub 仓库服务的主动式编码 agent,打包
|
||||
|
||||
## 安装本 fork 插件栈(速览)
|
||||
|
||||
人话版安装步骤;英文版见 [docs/006-deployment.md](docs/006-deployment.md) 的 Install / Machine configuration。前提是已能使用 `dsh` CLI(没有就先 `npm install -g @deepseek-ai/dsh`,版本需满足 `package.json` 的 `engines.dsh`)。
|
||||
人话版安装步骤;参考 [docs/006-部署.md](docs/006-部署.md) 的安装 / 机器配置 节。前提是已能使用 `dsh` CLI(没有就先 `npm install -g @deepseek-ai/dsh`,版本需满足 `package.json` 的 `engines.dsh`)。
|
||||
|
||||
```sh
|
||||
git clone <本仓库> dsh-superagent && cd dsh-superagent
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
## 部署流程怎么走
|
||||
|
||||
部署遵循 [docs/000-philosophy.md](docs/000-philosophy.md):尽量少的占用操作者的注意力。
|
||||
部署遵循 [docs/000-理念.md](docs/000-理念.md):尽量少的占用操作者的注意力。
|
||||
|
||||
- **问第一个问题之前**,只读这个文件和 `docs/000-philosophy.md`,并把下面的探测跑一遍。在所有答案收齐之前,不要读其它任何东西。
|
||||
- **问第一个问题之前**,只读这个文件和 `docs/000-理念.md`,并把下面的探测跑一遍。在所有答案收齐之前,不要读其它任何东西。
|
||||
- **一条消息只问一个问题**,只问一件事。等回答到了再问下一个。探测已经能回答的问题就跳过。
|
||||
- **每条消息不超过 300 词**,除非操作者要求更多。每个问题给出一个操作者可以一句话同意的推荐答案,并一句话说明它为什么重要。
|
||||
- **问的时候不要动手**。收齐所有答案后,发一条包含完整计划的消息,等操作者确认了才执行。
|
||||
@@ -26,11 +26,11 @@ grep -l -E 'id: dsh-superagent\\b' "${DSH_HOME:-$HOME/.dsh}"/profiles/*/cordis.p
|
||||
|
||||
- **前置条件**:Node.js 22.19+ 或 24+、pnpm、git 2.32+、tmux、Python 3.8+、GitHub CLI 2.48+,以及 DSH 版本在 `package.json` 的 `engines.dsh` 范围内。缺哪个就把它变成计划里的一项,而不是一个问题。DSH 还需要一个模型 provider;如果操作者从没配过模型的 DSH,配置它属于操作者的步骤。
|
||||
- **操作者登录名**:`gh api user` 的结果是操作者自己的 GitHub 登录名,作为第 5 问的推荐值。
|
||||
- **已有 profile**:某个 profile 里已有 `dsh-superagent` 条目,说明是早前的部署:问是继续它、保留它的配置,并去读 `docs/006-deployment.md` 的升级一节。
|
||||
- **已有 profile**:某个 profile 里已有 `dsh-superagent` 条目,说明是早前的部署:问是继续它、保留它的配置,并去读 `docs/006-部署.md` 的升级一节。
|
||||
|
||||
## 2. 提问
|
||||
|
||||
按这个顺序问,一条消息一个,跟着 [docs/001-domain.md](docs/001-domain.md) 的工作流走:先问 Trigger 从哪来,再问谁可以在那里提工作,最后问 Executor 拿到工单后要做什么。推荐值放在方括号里。
|
||||
按这个顺序问,一条消息一个,跟着 [docs/001-领域模型.md](docs/001-领域模型.md) 的工作流走:先问 Trigger 从哪来,再问谁可以在那里提工作,最后问 Executor 拿到工单后要做什么。推荐值放在方括号里。
|
||||
|
||||
**来源:请求从哪里来。**
|
||||
|
||||
@@ -64,18 +64,18 @@ grep -l -E 'id: dsh-superagent\\b' "${DSH_HOME:-$HOME/.dsh}"/profiles/*/cordis.p
|
||||
|
||||
## 4. 执行
|
||||
|
||||
1. **操作者步骤先行。** 读 `docs/006-deployment.md` 的 Accounts 一节,自己写好 CLI wrapper,然后发一条消息列出只有操作者能做的事:建 agent 账号;开了自动合并的话,用有权开它的账号登录一个 CLI;以 agent 账号接受仓库邀请;每个 wrapper 跑 `auth login`;用飞书的话,创建并发布 bot 应用,带着它的密钥跑 `config init`。等操作者说做完了,再用 `<wrapper> api user --jq .login` 逐个核对登录。
|
||||
2. **安装并启动。** 按 `docs/006-deployment.md` 的 Install、Machine configuration、Start and stop 三节做。跳过本仓库的 `git clone`:你就在它的 checkout 里。第 7 问要新 clone 的,去 clone 第 2 问的那个仓库。已有 profile 时,跳过 `dsh superagent --from-default-profile` 命令——它会替换 profile 并删掉它的 `cordis.patch.yml`;改为编辑那个文件,不要替换。
|
||||
3. **保存设置。** 按 `docs/006-deployment.md` 的 Management API 保存,顺序:`github` Source、(如用)`feishu` Source、`policy` 块。然后按 Delivery requirements 存提示词(第 8、9 问的答案)。返回 `restarting: true` 的保存会重启 dsh-superagent:等 `scripts/dsh-superagent status` 不再以 1 退出再发下一个请求,并从 `GET settings` 重新读 revision。
|
||||
4. **失败。** 被拒的保存会在响应体里点名 setup 问题。去 `docs/006-deployment.md` 的 Troubleshooting 表里找它,修好再继续。只有你自己修不了的才问操作者。
|
||||
1. **操作者步骤先行。** 读 `docs/006-部署.md` 的 Accounts 一节,自己写好 CLI wrapper,然后发一条消息列出只有操作者能做的事:建 agent 账号;开了自动合并的话,用有权开它的账号登录一个 CLI;以 agent 账号接受仓库邀请;每个 wrapper 跑 `auth login`;用飞书的话,创建并发布 bot 应用,带着它的密钥跑 `config init`。等操作者说做完了,再用 `<wrapper> api user --jq .login` 逐个核对登录。
|
||||
2. **安装并启动。** 按 `docs/006-部署.md` 的 Install、Machine configuration、Start and stop 三节做。跳过本仓库的 `git clone`:你就在它的 checkout 里。第 7 问要新 clone 的,去 clone 第 2 问的那个仓库。已有 profile 时,跳过 `dsh superagent --from-default-profile` 命令——它会替换 profile 并删掉它的 `cordis.patch.yml`;改为编辑那个文件,不要替换。
|
||||
3. **保存设置。** 按 `docs/006-部署.md` 的 Management API 保存,顺序:`github` Source、(如用)`feishu` Source、`policy` 块。然后按 Delivery requirements 存提示词(第 8、9 问的答案)。返回 `restarting: true` 的保存会重启 dsh-superagent:等 `scripts/dsh-superagent status` 不再以 1 退出再发下一个请求,并从 `GET settings` 重新读 revision。
|
||||
4. **失败。** 被拒的保存会在响应体里点名 setup 问题。去 `docs/006-部署.md` 的 Troubleshooting 表里找它,修好再继续。只有你自己修不了的才问操作者。
|
||||
|
||||
## 5. 验证与交接
|
||||
|
||||
`scripts/dsh-superagent status` 以 0 退出且没有 setup 问题。如果你运行在操作者自己的 DSH profile 里,用你的日程工具建 `docs/006-deployment.md` Hourly check 一节里的那个每小时检查;否则把那条 prompt 交给操作者,让他在自己的地方排期。然后发一条消息,不超过 300 词:
|
||||
`scripts/dsh-superagent status` 以 0 退出且没有 setup 问题。如果你运行在操作者自己的 DSH profile 里,用你的日程工具建 `docs/006-部署.md` Hourly check 一节里的那个每小时检查;否则把那条 prompt 交给操作者,让他在自己的地方排期。然后发一条消息,不超过 300 词:
|
||||
|
||||
- 面板 URL,以及侧边栏里 **Superagent** 条目是看哪里。
|
||||
- 怎么试:用允许名单里的账号,在仓库的某个 issue 下评论 `@<agent 登录名> <请求>`。大约一分钟内,overview 上会出现工单。
|
||||
- 给操作者的三条规则,来自 `docs/000-philosophy.md` 的 Responsibility 一节:从小开始、往少噪音调、为 agent 说的话负责。
|
||||
- 给操作者的三条规则,来自 `docs/000-理念.md` 的 Responsibility 一节:从小开始、往少噪音调、为 agent 说的话负责。
|
||||
- 想读更多:`docs/` 里编号的文档,按顺序读。
|
||||
|
||||
## Gitea Source 配置要点(本 fork)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# dsh-superagent bundle layer: inserts the plugins without configuration, and the agent presets its Sessions run under. The
|
||||
# profile supplies the `dsh-superagent` entry's stateDir (see docs/006-deployment.md and examples/profile.cordis.patch.yml); the plugin
|
||||
# profile supplies the `dsh-superagent` entry's stateDir (see docs/006-部署.md and examples/profile.cordis.patch.yml); the plugin
|
||||
# refuses to start without stateDir.
|
||||
- insert:
|
||||
- id: dsh-superagent
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# 000 Philosophy
|
||||
|
||||
dsh-superagent is a framework, and also a way of working with an agent. Its core principle:
|
||||
|
||||
**Take as little human attention as possible, and give as much value as possible.**
|
||||
|
||||
Every part of the project is judged by the attention it costs. A message, a notification, a question, a setting, a panel element and a paragraph of documentation each cost someone's attention, and each has to give more value than that, or it is removed.
|
||||
|
||||
## What follows for the agent
|
||||
|
||||
- **It acts only when asked, or when a written rule allows it.** A request counts only when it addresses the agent directly and comes from a person the team allowed: one on an allowed list, or anyone in a conversation the team opened to requests. Work nobody asked for needs a rule the team wrote, and starts in a mode that only records what the agent would have done.
|
||||
- **It speaks only when a person must act or can use a result.** It confirms a request once and names what it will do. It stays quiet while it works. It writes again when a result is ready, when it needs a decision, or when it cannot finish. Messages are short and go to the conversation where the request was made.
|
||||
- **It asks one concrete question at a time,** in a structured form, so the question reaches the person instead of staying inside a long report.
|
||||
- **It stops by impact, not by effort.** A local change goes as far as a reviewable result. A change that needs a design decision stops with a clear question.
|
||||
- **It reports honestly.** A draft is marked unfinished, a failure is reported as a failure, and a limit is never bypassed to avoid asking.
|
||||
|
||||
## What follows for the design
|
||||
|
||||
- **Few concepts.** The design has only its domain concepts, such as a Source, and their concrete implementations, such as GitHub and Feishu. A business need becomes an attribute of one of them, or a prompt the agent follows. The design does not add a concept for it.
|
||||
- **Few settings.** A default replaces a setting when one value suits almost every team. Behaviour that a team describes in words is prompt text, not a setting.
|
||||
- **A panel that shows only what needs a person.** It lists first what waits for a person. A healthy state takes no space.
|
||||
- **Documents read in order.** Each numbered document builds only on the ones before it, so a reader never needs a concept that has not been introduced yet.
|
||||
|
||||
## Responsibility
|
||||
|
||||
The agent comments, opens pull requests and messages people under accounts your team sees every day. Every message it sends costs your colleagues' attention, and you are responsible for it. The more the agent can do, the more care its operator owes the team:
|
||||
|
||||
- **Start narrow.** Begin with a short allowed list and with proactive work only recorded. Widen both once what the agent would have done looks right.
|
||||
- **Tune for less noise.** When the agent is noisy, make its prompts shorter and stricter, or pause it. Do not add more messages to explain the noise.
|
||||
- **Do not use it to produce volume.** Mass pull requests, broadcast messages and busywork cost the team more than they give.
|
||||
- **Own what it says.** Read what the agent posts in your name, and correct it in the prompts when it is wrong.
|
||||
|
||||
The goal is the right help at the right time, and nothing at any other time.
|
||||
@@ -0,0 +1,33 @@
|
||||
# 000 理念
|
||||
|
||||
dsh-superagent 是一个框架,也是一种与 agent 协作的方式。它的核心原则:
|
||||
|
||||
**尽量少地占用人的注意力,尽量多地交付价值。**
|
||||
|
||||
项目的每一处都按它花费的注意力来评判。一条消息、一次通知、一个问题、一个设置、一个面板元素、一段文档,各自都要消耗某个人的注意力,各自都必须给出比这更多的价值,否则就删掉。
|
||||
|
||||
## agent 要遵守的
|
||||
|
||||
- **它只在被要求、或被写下的规则允许时才行动。** 只有直接面向 agent、且来自团队允许的人的请求才算数:允许名单上的人,或团队开放给请求的会话里的任何人。没人要求的工作需要团队写下的规则,并且以只记录 agent 本会做什么的模式开始。
|
||||
- **它只在有人必须行动、或用得上结果时才说话。** 它确认一次请求,并说明它将做什么。干活期间保持安静。有结果、需要决定、或无法完成时再写。消息简短,发到提出请求的那个对话。
|
||||
- **它一次只问一个具体问题,** 用结构化表单,让问题到达人那里,而不是埋在一长段报告里。
|
||||
- **它按影响停下,不按费力程度。** 本地改动推进到可评审的结果为止。需要设计决定的改动停在一个清楚的问题上。
|
||||
- **它如实汇报。** 草案标未完成,失败按失败报告,绝不为了少问一句而绕过限制。
|
||||
|
||||
## 设计要遵守的
|
||||
|
||||
- **概念少。** 设计里只有它的领域概念(例如 Source)和这些概念的具体实现(例如 GitHub 和 Feishu)。一个业务需求要么变成其中某个概念的属性,要么变成 agent 遵循的一段提示词。设计不为此新增概念。
|
||||
- **设置少。** 当一个取值几乎适合所有团队时,用默认值代替设置。团队用语言描述的行为是提示词文本,不是设置。
|
||||
- **面板只显示需要人的东西。** 它先列出等待人的事项。健康的状态不占空间。
|
||||
- **文档按顺序读。** 每篇编号文档只建立在前几篇之上,读者永远不会遇到一个还没引入的概念。
|
||||
|
||||
## 责任
|
||||
|
||||
agent 以你们团队天天见到的账号发评论、开 pull request、给人发消息。它发出的每条消息都消耗你同事的注意力,你要为此负责。agent 能做得越多,操作者对团队就越要多留心:
|
||||
|
||||
- **从窄处开始。** 先用一份短的允许名单,主动工作只做记录。等 agent 本会做的事看起来对了,再放宽这两样。
|
||||
- **往噪声更少的方向调。** agent 太吵时,把它的提示词改短、改严,或者暂停它。不要为了解释噪声而多发消息。
|
||||
- **不要拿它堆产量。** 大批量 pull request、群发消息、刷出来的活儿,团队亏的比赚的多。
|
||||
- **替它说的话负责。** 读 agent 以你的名义发的内容,发现不对就在提示词里纠正。
|
||||
|
||||
目标是恰好的帮助出现在恰好的时间,其余时间什么都不出现。
|
||||
@@ -1,112 +0,0 @@
|
||||
# 001 Domain model
|
||||
|
||||
This document names the concepts of dsh-superagent and the rules each one keeps. The code, the tools the agents call, the panel and the other documents use these terms and no others. Following [000 Philosophy](000-philosophy.md), the model holds only concepts the work needs, and a business need becomes an attribute of one of them.
|
||||
|
||||
## Flow
|
||||
|
||||
Work moves from Trigger to Eval to execution. A Source produces Triggers. A Trigger fans out as context to the Tickets subscribed to its Threads. A person's message that reached a Ticket this way stays with that Ticket; every other Trigger enters Eval, which decides whether it asks for new work. When Eval accepts, it creates a Ticket with a Mandate. An Assignment hands the Ticket to an Executor, and results go back to the Source through a Channel.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S[Source] -->|Trigger| T[Thread]
|
||||
T -->|batch| E[Eval]
|
||||
T -->|Subscription fan-out| K[Ticket]
|
||||
E -->|accept with Mandate / stop| K
|
||||
E -->|ignore / unavailable| X[recorded]
|
||||
P[Policy change] -->|notice| K
|
||||
K -->|Assignment| W[Executor]
|
||||
K -->|Channel| S
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
Source, Thread, Eval, Mandate and Subscription form the core domain; the rest supports them.
|
||||
|
||||
| Concept | Definition | Rules it keeps | Why it exists |
|
||||
| --- | --- | --- | --- |
|
||||
| Source | A stream of events. A Source kind is a reusable implementation; a Source instance is one configuration of a kind: whether it runs and what it listens to | Holds its read position and its own bot identity. Defines what counts as an event, including conditions that only hold over time. Drops the bot's own messages and Triggers outside what it listens to before they enter the domain. Translates each Trigger into the Threads it belongs to and the Channel addresses to reply on | A restart must not replay events |
|
||||
| Trigger | One immutable event with the Source's own metadata, such as its actor | Belongs to one or more Threads. Accepting a Trigger twice creates one Ticket | The unit of audit and of idempotence |
|
||||
| Thread | Triggers in time order; the Source defines which Triggers form one Thread | At most one Eval runs on it at a time. It may have several Tickets. It holds its Subscriptions, the running Eval and its Decision history | Gives Eval a consistent view and its context |
|
||||
| Correlation | The relation between Threads that one Trigger belongs to | Adds context; never routes | Lets Eval follow related evidence |
|
||||
| Channel | The write side of a Thread; the Source defines its addresses | A Thread may have several Channels; a Ticket holds a set of them | Acknowledgements, replies, pull requests and resolved reviews are all writes |
|
||||
| Policy | The versioned assessment rules and each Source's part: who may originate work through that Source, in that Source's own identities, how the list is synchronized, and which bots may make candidates | The Policy version covers the rules, the accept mode and the Source parts. A change that bears on a live Ticket, such as a change to a rule it was accepted under, is delivered to its Assignment; it does not revoke Mandates. Identities on different Sources are never mapped to each other | When the rules change, a running Eval must be discarded |
|
||||
| Eval | The judgement on a batch of Triggers, in two layers: gate and assessment | Input: the Trigger batch, a Thread snapshot, every Ticket on the Thread including ended ones, and the current Policy. Output: a Decision. Holds no state | Decides whether new work starts, goes on or stops |
|
||||
| Mandate | What an accept grants: the Policy version, the scope, the strength, the matched rules, the sender's identity on that Source and how the Source received the Trigger | Immutable; wider scope means a new Ticket. A Ticket's end does not end it: only a person's stop withdraws it, and a Policy that no longer holds it revokes it. Revalidated against the current Policy before an ended Ticket resumes | States why the work may be done, and how far it may go |
|
||||
| Ticket | A unit of authorized work | Only Eval creates one. Refers to its Mandate; holds its Channels and Inputs. Goes open → ended, and back to open when a person's message on one of its Threads resumes it, unless a person stopped it. Defines its own state instead of following the Executor's | Owns the execution and its reports |
|
||||
| Assignment | The binding of a Ticket to one Executor instance | A Ticket has several over time. Goes pending → running → ended independently of the Ticket. Only its final report may end the Ticket on the Executor's side | The Executor is outside the domain, but who executes must be inside it |
|
||||
| Subscription | A many-to-many relation between Tickets and Threads, held by the Thread | A matching Trigger fans out to every live subscribed Ticket, with no check of who sent it; a person's message that reaches a Ticket this way takes no Eval. A Thread an Executor follows for context reaches its Ticket the same way, but lends it no authority and does not resume it. An ended Ticket keeps its Subscriptions, including that of a pull request that closed or merged: a person's message resumes it under the same Mandate, with no Eval or Decision; messages from bots and CI do not reach it, since its Executor reads its Threads again when it resumes. A Ticket a person stopped receives nothing. The main Session attaches a Thread to an existing Ticket only when its message points at that Ticket explicitly. A person can drop every Subscription on a Thread; an Executor can drop only its own Ticket's Subscription to a Thread it finds unrelated, except the Thread its Ticket started on and a Thread a Source made it follow, such as its pull request's | Later discussion reaches the Executor without anyone mentioning the agent again |
|
||||
|
||||
A person is not a domain concept. A Trigger carries its actor's identifier as the Source gives it. An allowed list is a Source's part of the Policy, in that Source's identities. That one person has accounts on several Sources is prompt text that the agents read when they need it.
|
||||
|
||||
The Executor, with its Sessions and directories, is outside the domain and connects through Assignments.
|
||||
|
||||
Aggregates: a Source holds its read position. A Thread holds its Subscriptions, its running Eval, which runs serially and merges Triggers that arrive meanwhile into the next batch, and its Decision history. A Ticket holds its Assignments, Channels and Inputs, and refers to Decisions by id.
|
||||
|
||||
## Rejected concepts
|
||||
|
||||
| Rejected | Reason |
|
||||
| --- | --- |
|
||||
| Principal or Membership as an entity, or mapping identities across Sources | A person is Trigger metadata; an allowed list is a Source's part of the Policy; a cross-Source mapping is prompt text |
|
||||
| Eval scheduling its own delayed Triggers | A condition that needs waiting is an ordinary Source, so Eval never depends back on Sources |
|
||||
| Waiting or revoked Eval states | Waiting belongs to Sources; an Eval that runs out of time queues its Triggers again, or ends `unavailable` |
|
||||
| At most one live Ticket per Thread | One Thread can carry several independent pieces of work; the assessment avoids duplicates from the Ticket list |
|
||||
| An "append to Ticket" Decision | Appending is Subscription fan-out and needs no Eval |
|
||||
| Admission checks on Subscriptions | A Subscription covers the whole Thread; the Mandate separates authorization from context |
|
||||
| Correlation that routes | Subscriptions route; Correlation only adds context |
|
||||
| Ticket state that follows the DSH Session | A Ticket can be handed off, resumed, or run by several Executors, so it defines its own state |
|
||||
| An Executor declaring that a person asked it to stop | Only Eval decides the strength of an end |
|
||||
| Cancelling running Tickets on a Policy change | A cancelled change leaves half-finished work; the change is recorded and delivered instead |
|
||||
| Locking all of an Executor Session's tools when it fails or blocks | It would refuse a person working directly in the Session, and the report after recovery; pausing automatic input is enough |
|
||||
| Inferring "waiting on a person" from a report's free text | The classification needs structured facts; the Executor asks through a structured field |
|
||||
|
||||
## What is reusable
|
||||
|
||||
The concepts above and their rules are reusable. The Source implementations, the Policy contents, the Executor and the Channel implementations belong to a deployment. Each Source keeps its own words internally, such as message, chat or notification, and translates them into Triggers.
|
||||
|
||||
| Part | In this repository |
|
||||
| --- | --- |
|
||||
| Source | Feishu: live events, history recovery, polling of subscribed topics. GitHub: notifications, subscribed Issues and pull requests, CI changes, pull requests merged or closed, and, where switched on, every new Issue, pull request and comment in the repository and the default branch's CI |
|
||||
| Policy | Assessment rules, and each Source's allowed list, its sync, its bot list and its admission table |
|
||||
| Executor | A DSH Executor Session and a directory per Ticket, in an execution environment whose variables the Executor and the Sources register |
|
||||
| Channel | Feishu topic replies, GitHub comments and review thread replies |
|
||||
|
||||
## Glossary
|
||||
|
||||
A reference list of the terms this and the later documents use, with their Chinese names; the later documents explain the terms not introduced above. The panel shows one language at a time; its (?) button opens this glossary with both languages. The messages agents send to people on GitHub and Feishu use plain language, not these terms.
|
||||
|
||||
| Term | 中文 | Meaning |
|
||||
| --- | --- | --- |
|
||||
| Agent | 智能体 | The automated team member dsh-superagent runs, acting on Sources through identities such as a GitHub account and a Feishu bot |
|
||||
| Main Session | 主 Session | The DSH Session that evaluates Triggers, accepts and dispatches Tickets, and replies; `orchestrator` in code and prompts |
|
||||
| Executor Session | 执行器 Session | The DSH Session an Executor runs for one Ticket; `worker` in code and prompts |
|
||||
| Source | 来源 | A stream of events: a kind that a plugin provides, plus one instance configuration |
|
||||
| Trigger | 触发 | One immutable event from a Source |
|
||||
| Thread | 线程 | Triggers in time order; the unit within which Evals run one at a time |
|
||||
| Correlation | 关联 | The relation between Threads that share a Trigger; it adds context only |
|
||||
| Channel | 渠道 | The write side of a Thread |
|
||||
| Policy | 策略 | The versioned rules for accepting work, and each Source's part of it, such as who may ask for work there |
|
||||
| Policy version | 策略版本 | The identifier of one combination of rules, Source parts and accept mode; every Mandate records it |
|
||||
| Eval | 评估 | The process that turns a batch of Triggers into a Decision |
|
||||
| Gate | 准入 | The deterministic first layer of Eval: who may originate work, and with what strength |
|
||||
| Assessment | 研判 | The model's judgement during Eval |
|
||||
| Decision | 决定 | The result of Eval |
|
||||
| Mandate | 授权 | What an accept grants: strength, scope and the Policy version it relied on |
|
||||
| Ticket | 工单 | A unit of authorized work |
|
||||
| Assignment | 派工 | The binding of a Ticket to one Executor instance |
|
||||
| Subscription | 订阅 | The relation between a Ticket and a Thread |
|
||||
| Fan-out | 扇出 | Delivering Triggers of a subscribed Thread to a Ticket as context |
|
||||
| Executor | 执行器 | The port that runs Assignments: one Executor Session and one directory per Ticket |
|
||||
| Execution environment | 执行环境 | Where Executor Sessions and the prepare command run: variables that the Executor and the Sources register, each with a key, a value and a description |
|
||||
| Report | 报告 | The state an Assignment reports to its Ticket |
|
||||
| Attention | 待人工决定 | A question on a Ticket that waits for a person's answer |
|
||||
| Disposition | 去向 | The record of a Trigger being filtered, evaluated or fanned out |
|
||||
| Input | 输入 | Context delivered to a Ticket: fanned-out Triggers, Policy changes, resumes and text |
|
||||
| Scope | 范围 | The work a Mandate allows |
|
||||
| Strength | 强度 | How strong the basis of a Mandate or an Eval is |
|
||||
|
||||
| Group | Values |
|
||||
| --- | --- |
|
||||
| Decision | accept 接单 · stop 停止 · ignore 忽略 · unavailable 未完成 |
|
||||
| Strength | explicit 明确 · inferred 推断 |
|
||||
| Assignment | pending 待启动 · running 执行中 · ended 已结束 |
|
||||
| Report | progress 进展 · waiting 等待 · ready 就绪 · blocked 受阻 · failed 失败 · dropped 无需处理 |
|
||||
@@ -0,0 +1,112 @@
|
||||
# 001 领域模型
|
||||
|
||||
本文命名 dsh-superagent 的各个概念,以及每个概念守住的规则。代码、agent 调用的工具、面板和其他文档都使用这些术语,不用别的。依照 [000 理念](000-理念.md),模型只保留工作需要的概念,一个业务需求会变成其中某个概念的属性。
|
||||
|
||||
## 流程
|
||||
|
||||
工作从 Trigger 到 Eval 再到执行。Source 产出 Triggers。一个 Trigger 作为上下文扇出给订阅了其 Threads 的各个 Tickets。以这种方式到达某个 Ticket 的人的消息就留在那个 Ticket 上;其余每个 Trigger 都进入 Eval,由它决定是否发起新的工作。Eval 接单时,创建一个带 Mandate 的 Ticket。Assignment 把 Ticket 交给一个 Executor,结果通过 Channel 回到 Source。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S[Source] -->|Trigger| T[Thread]
|
||||
T -->|batch| E[Eval]
|
||||
T -->|Subscription fan-out| K[Ticket]
|
||||
E -->|accept with Mandate / stop| K
|
||||
E -->|ignore / unavailable| X[recorded]
|
||||
P[Policy change] -->|notice| K
|
||||
K -->|Assignment| W[Executor]
|
||||
K -->|Channel| S
|
||||
```
|
||||
|
||||
## 概念
|
||||
|
||||
Source、Thread、Eval、Mandate 与 Subscription 构成核心领域;其余概念支撑它们。
|
||||
|
||||
| 概念 | 定义 | 它守住的规则 | 为什么存在 |
|
||||
| --- | --- | --- | --- |
|
||||
| Source | 一条事件流。Source kind 是一个可复用的实现;Source instance 是某个 kind 的一份配置:是否运行、监听什么 | 保存自己的读取位置与自己的 bot 身份。定义什么算事件,包括只在时间上成立的条件。在进入领域之前,丢弃 bot 自己的消息和监听范围之外的 Triggers。把每个 Trigger 翻译成它所属的 Threads 与用于答复的 Channel 地址 | 重启不得重放事件 |
|
||||
| Trigger | 一条不可变事件,带有 Source 自己的元数据,例如它的 actor | 属于一个或多个 Threads。同一个 Trigger 被接单两次只创建一个 Ticket | 审计与幂等的单位 |
|
||||
| Thread | 按时间排列的 Triggers;由 Source 定义哪些 Triggers 构成一个 Thread | 同一时刻至多运行一个 Eval。它可以有多个 Tickets。它保存自己的 Subscriptions、正在运行的 Eval 和 Decision 历史 | 给 Eval 一个一致的视图和它的上下文 |
|
||||
| Correlation | 同一个 Trigger 所属 Threads 之间的关系 | 只增加上下文;从不路由 | 让 Eval 能追踪相关证据 |
|
||||
| Channel | Thread 的写入侧;地址由 Source 定义 | 一个 Thread 可以有多个 Channels;一个 Ticket 持有其中一个集合 | 认领回执、答复、pull requests 和 resolved reviews 都是写入 |
|
||||
| Policy | 带版本的评估规则,加上每个 Source 自己的部分:谁可以通过该 Source 发起工作、用的是该 Source 自己的身份、名单如何同步、以及哪些 bots 可以产生候选 | Policy 版本覆盖规则、accept mode 和各 Source 部分。一个与某个活着的 Ticket 相关的变化——比如它接单所依据的规则变了——会投递给它的 Assignment;但不吊销 Mandates。不同 Source 上的身份彼此绝不映射 | 规则变了,正在运行的 Eval 必须作废 |
|
||||
| Eval | 对一个 Trigger 批次的判断,分两层:gate 与 assessment | 输入:Trigger 批次、一个 Thread 快照、该 Thread 上的所有 Tickets(含 ended 的)、以及当前的 Policy。输出:一个 Decision。不持有状态 | 决定新的工作是开始、继续还是停止 |
|
||||
| Mandate | 一次接单所授予的东西:Policy 版本、scope、强度、命中的规则、发送者在该 Source 上的身份、以及 Source 收到该 Trigger 的方式 | 不可变;更宽的 scope 意味着新的 Ticket。Ticket 的结束不等于它的结束:只有人的 stop 才撤回它,一个不再成立的 Policy 会吊销它。ended Ticket 恢复之前会按当前 Policy 重新校验 | 说明工作为什么可以做、能做到什么程度 |
|
||||
| Ticket | 一份授权工作的单位 | 只能由 Eval 创建。引用它的 Mandate;持有它的 Channels 与 Inputs。走 open → ended,当有人在它某个 Thread 上发的消息让它复活时会回到 open,除非它是被人停掉的。它定义自己的状态,不跟随 Executor 的 | 拥有执行及其报告 |
|
||||
| Assignment | 一个 Ticket 与某一个 Executor 实例的绑定 | 一个 Ticket 随时间会有多个。独立于 Ticket 走 pending → running → ended。在 Executor 一侧,只有它的最终报告能结束 Ticket | Executor 在领域之外,但"由谁执行"必须在领域之内 |
|
||||
| Subscription | Ticket 与 Threads 之间的多对多关系,由 Thread 持有 | 命中的 Trigger 会扇出给每个订阅着的活 Ticket,不检查发送者是谁;以这种方式到达某个 Ticket 的人的消息不经过 Eval。Executor 为上下文关注的 Thread 同样会抵达它的 Ticket,但不给它带来任何授权、也不使它复活。ended Ticket 保留它的 Subscriptions,包括一个已关闭或已合并的 pull request 的订阅:人的消息会在同一个 Mandate 下让它复活,不经过 Eval 也不产生 Decision;来自 bots 和 CI 的消息到不了它,因为它的 Executor 恢复时会重新读取它的 Threads。被人停掉的 Ticket 什么都收不到。只有当一条消息明确指向某个既有 Ticket 时,main Session 才把 Thread attach 到它。人可以移除某个 Thread 上的所有 Subscriptions;Executor 只能移除自己的 Ticket 对一个它判定为无关的 Thread 的订阅,除了它的 Ticket 由此开始的那个 Thread、以及被 Source 要求关注的那个 Thread(例如它的 pull request) | 让后续讨论不需要任何人再次点名 agent,就能到达 Executor |
|
||||
|
||||
人不是领域概念。Trigger 带着 Source 给出的 actor 标识符。允许名单是 Source 在 Policy 里的那部分,用的是该 Source 的身份体系。同一个人拥有几个 Source 上的账号——这是 agent 需要时才会读的提示词文本。
|
||||
|
||||
Executor 连同它的 Sessions 和目录在领域之外,通过 Assignments 连接。
|
||||
|
||||
聚合:Source 持有它的读取位置。Thread 持有它的 Subscriptions、正在运行的 Eval——Eval 串行运行,期间到达的 Triggers 并入下一批——以及它的 Decision 历史。Ticket 持有它的 Assignments、Channels 与 Inputs,并按 id 引用 Decisions。
|
||||
|
||||
## 被拒绝的概念
|
||||
|
||||
| 被拒绝的 | 理由 |
|
||||
| --- | --- |
|
||||
| Principal 或 Membership 作为实体、或跨 Sources 映射身份 | 人是 Trigger 的元数据;允许名单是 Source 在 Policy 里的部分;跨 Source 的映射是提示词文本 |
|
||||
| Eval 自己调度延迟的 Triggers | 一个需要等待的条件就是一个普通的 Source,这样 Eval 永远不反过来依赖 Sources |
|
||||
| waiting 或 revoked 的 Eval 状态 | 等待属于 Sources;超时的 Eval 把它的 Triggers 重新排队,或以 `unavailable` 结束 |
|
||||
| 每个 Thread 至多一个活 Ticket | 一个 Thread 可以承载多件相互独立的工作;评估靠 Ticket 列表来避免重复 |
|
||||
| "append to Ticket" 这个 Decision | 追加是 Subscription 扇出,不需要 Eval |
|
||||
| 对 Subscriptions 做准入检查 | Subscription 覆盖整个 Thread;Mandate 负责区分授权与上下文 |
|
||||
| 会路由的 Correlation | 路由归 Subscriptions;Correlation 只增加上下文 |
|
||||
| Ticket 状态跟随 DSH Session | Ticket 可以被移交、复活、或由多个 Executors 执行,所以它定义自己的状态 |
|
||||
| 由 Executor 声称有人要求它停止 | 只有 Eval 决定结束的强度 |
|
||||
| Policy 变更时取消正在运行的 Tickets | 被取消的改动会留下做了一半的工作;正确的做法是记录并投递这个变更 |
|
||||
| Executor Session 失败或受阻时锁掉它的全部工具 | 那会连累直接在 Session 里工作的人、以及恢复之后的报告;暂停自动输入就够了 |
|
||||
| 从报告的自由文本推断"在等人" | 分类需要结构化事实;Executor 用结构化字段来问 |
|
||||
|
||||
## 哪些是可复用的
|
||||
|
||||
上面的概念及其规则是可复用的。Source 实现、Policy 内容、Executor 与 Channel 实现属于具体的部署。每个 Source 在内部保留自己的说法,例如 message、chat 或 notification,并把它们翻译成 Triggers。
|
||||
|
||||
| 部分 | 在本仓库中 |
|
||||
| --- | --- |
|
||||
| Source | Feishu:实时事件、历史恢复、对订阅话题的轮询。GitHub:notifications、被订阅的 Issues 与 pull requests、CI 变化、pull requests 的合并或关闭,以及(开启时)仓库中每一个新的 Issue、pull request、评论和默认分支的 CI |
|
||||
| Policy | 评估规则,以及每个 Source 的允许名单、它的同步、bot 名单与准入表 |
|
||||
| Executor | 每个 Ticket 一个 DSH Executor Session 和一个目录,运行在一个由 Executor 与各 Sources 注册变量的执行环境里 |
|
||||
| Channel | Feishu 话题回复、GitHub 评论与 review thread 回复 |
|
||||
|
||||
## 术语表
|
||||
|
||||
本及后续文档所用术语的对照表,附中文名;上面没有引入的术语由后续文档解释。面板一次只显示一种语言;它的 (?) 按钮打开这份双语术语表。agent 在 GitHub 与 Feishu 上发给人的消息使用平实的语言,不用这些术语。
|
||||
|
||||
| 术语 | 中文 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| Agent | 智能体 | dsh-superagent 运行的自动化团队成员,通过 GitHub 账号、Feishu bot 这样的身份在 Sources 上行动 |
|
||||
| Main Session | 主 Session | 评估 Triggers、接单并派发 Tickets、以及答复的那个 DSH Session;代码与提示词里叫 `orchestrator` |
|
||||
| Executor Session | 执行器 Session | Executor 为一个 Ticket 运行的 DSH Session;代码与提示词里叫 `worker` |
|
||||
| Source | 来源 | 一条事件流:由插件提供的一个 kind,加上一份实例配置 |
|
||||
| Trigger | 触发 | 来自 Source 的一条不可变事件 |
|
||||
| Thread | 线程 | 按时间排列的 Triggers;Eval 在其上逐个运行的单位 |
|
||||
| Correlation | 关联 | 共享一个 Trigger 的 Threads 之间的关系;只增加上下文 |
|
||||
| Channel | 渠道 | Thread 的写入侧 |
|
||||
| Policy | 策略 | 用于接受工作的带版本规则,以及每个 Source 在其中的部分,例如谁可以在那里请求工作 |
|
||||
| Policy version | 策略版本 | 规则、Source 部分与 accept mode 的一种组合的标识;每个 Mandate 都记录它 |
|
||||
| Eval | 评估 | 把一个 Trigger 批次变成一个 Decision 的过程 |
|
||||
| Gate | 准入 | Eval 确定性的第一层:谁可以发起工作、以什么强度 |
|
||||
| Assessment | 研判 | Eval 期间模型做出的判断 |
|
||||
| Decision | 决定 | Eval 的结果 |
|
||||
| Mandate | 授权 | 一次接单所授予的东西:强度、scope 和它所依据的 Policy 版本 |
|
||||
| Ticket | 工单 | 一份授权工作的单位 |
|
||||
| Assignment | 派工 | 一个 Ticket 与某一个 Executor 实例的绑定 |
|
||||
| Subscription | 订阅 | 一个 Ticket 与一个 Thread 之间的关系 |
|
||||
| Fan-out | 扇出 | 把被订阅 Thread 的 Triggers 作为上下文交付给一个 Ticket |
|
||||
| Executor | 执行器 | 运行 Assignments 的端口:每个 Ticket 一个 Executor Session 和一个目录 |
|
||||
| Execution environment | 执行环境 | Executor Sessions 与 prepare command 运行的地方:由 Executor 与各 Sources 注册的变量,每个都有 key、取值和说明 |
|
||||
| Report | 报告 | 一个 Assignment 向它的 Ticket 报告的状态 |
|
||||
| Attention | 待人工决定 | Ticket 上等待人答复的一个问题 |
|
||||
| Disposition | 去向 | 一个 Trigger 被过滤、被评估或被扇出的记录 |
|
||||
| Input | 输入 | 投递给一个 Ticket 的上下文:扇出的 Triggers、Policy 变更、resume 与文本 |
|
||||
| Scope | 范围 | 一个 Mandate 所允许的工作 |
|
||||
| Strength | 强度 | 一个 Mandate 或 Eval 的依据有多强 |
|
||||
|
||||
| 分组 | 取值 |
|
||||
| --- | --- |
|
||||
| Decision | accept 接单 · stop 停止 · ignore 忽略 · unavailable 未完成 |
|
||||
| Strength | explicit 明确 · inferred 推断 |
|
||||
| Assignment | pending 待启动 · running 执行中 · ended 已结束 |
|
||||
| Report | progress 进展 · waiting 等待 · ready 就绪 · blocked 受阻 · failed 失败 · dropped 无需处理 |
|
||||
@@ -1,155 +0,0 @@
|
||||
# 002 Authorization
|
||||
|
||||
Authorization determines who may start work and what that work may include. This document builds on [001 Domain model](001-domain.md).
|
||||
|
||||
The host enforces allowed lists, rules and Mandates. Messages, comments and quoted documents are inputs to a decision; they cannot change those permissions.
|
||||
|
||||
## Two paths for a Trigger
|
||||
|
||||
### Eval: deciding whether to start work
|
||||
|
||||
The Source records whether a message addresses the agent: `mention` for a mention of the bot or agent account, and `direct` for a direct message. The gate checks the sender and the Source's Policy:
|
||||
|
||||
- An allowed person who addresses the agent makes an explicit request.
|
||||
- A person's follow-up on a Thread with Tickets follows the rules below.
|
||||
- Other human messages use the admission table for their receiving scope.
|
||||
- A bot on the Source's bot list can produce an inferred candidate, never an explicit request. Other bots are not admitted.
|
||||
|
||||
### Subscriptions: continuing existing work
|
||||
|
||||
Every message on a subscribed Thread reaches its subscribed Tickets as context, including messages from bots and CI. This delivery bypasses the gate and grants no new authority. Each Ticket remains limited by its Mandate.
|
||||
|
||||
A person's message on a Ticket's own Thread belongs to that Ticket's Executor to decide. The same applies when the main Session attaches a message to a Ticket. While an Executor owns the message, it takes no Eval and gets no Decision. Each Executor either handles the request within its Ticket's scope or hands it back for routing elsewhere.
|
||||
|
||||
Ownership continues while the Ticket is live, even if a person removes its Subscription. It also covers an ended Ticket that was not stopped by a person and has the message waiting to resume it.
|
||||
|
||||
The main Session receives a follow-up notice. While an Executor owns the message, the main Session may only stop work on the person's request; accept, attach, ignore and requeue are refused. A stop still passes through the gate with the follow-up strength described below.
|
||||
|
||||
### Handing back and releasing follow-ups
|
||||
|
||||
An Executor hands a message back once when it asks for work beyond the Ticket's scope or belongs to another Ticket. The Ticket keeps its Subscription. If an attached Thread is unrelated, the Executor hands back the messages it owns there.
|
||||
|
||||
A message goes to a fresh Eval only after no Ticket still owns it. Its candidate includes the Executors' reasons. The gate judges it as an ordinary message on that Thread, and the main Session can accept it, attach it elsewhere or ignore it. Handing back a message the Ticket did not own, such as a bot message or one a person already sent to Eval, queues nothing.
|
||||
|
||||
When a Ticket ends, it releases follow-ups its Executor did not read or take up. These include messages never delivered to the Executor and messages delivered after its last Report or turn end, provided it neither acknowledged, answered nor handed them back. Each released message goes to Eval when no other Ticket owns it. A stop ends only the stopped Ticket's work.
|
||||
|
||||
A stopped Ticket does not send its stop message to Eval again unless another Ticket handed it back. The main Session that stopped it may accept the rest of the message as new work.
|
||||
|
||||
A person can send an owned follow-up to Eval from the panel. If the main Session then attaches it back to a live Ticket, the Executor receives it again. The panel also offers re-evaluation when no effective judgement covers the message.
|
||||
|
||||
### Context-only subscriptions
|
||||
|
||||
An Executor can follow a related Thread, such as another Issue or pull request, for context. Its messages reach the Ticket but grant no authority or follow-up strength. Human messages there do not resume an ended Ticket unless the main Session attaches a message from that Thread. Notes from the main Session about other work are also context only.
|
||||
|
||||
Human messages that reach no Ticket, including Threads whose Tickets were stopped by a person, take the Eval path. Bot messages take that path independently of their delivery to subscribed Tickets.
|
||||
|
||||
## Strength
|
||||
|
||||
| Strength | Cases |
|
||||
| --- | --- |
|
||||
| explicit | A person on the list addresses the agent, or follows up on a Thread that already has a Ticket; a cell of the admission table set to explicit request |
|
||||
| inferred | A person not on the list follows up on a Thread with a Ticket, unless their cell of the admission table is explicit; a cell of the admission table set to inferred candidate; a message of a bot on the Source's bot list that addresses the agent or arrives where the Source receives every message |
|
||||
| not admitted | Any other bot's message, everything the admission table ignores, and every inferred case while the accept mode is `off` |
|
||||
|
||||
For an unlisted person, an explicit admission cell takes precedence even on a Thread with Tickets and works in every accept mode. Otherwise their follow-up is inferred and is not admitted in `off` mode. Messages on Threads followed only for context use the admission table instead of follow-up strength.
|
||||
|
||||
## Admission
|
||||
|
||||
Each Source's Policy has an admission table. Its rows are receiving scopes:
|
||||
|
||||
- **Feishu:** each configured chat, other chats the bot receives mentions from, and direct messages. Other chats are received only when **其他群** is set to **仅 @** on the Feishu card.
|
||||
- **GitHub:** new Issues, new pull requests, comments and the default branch's CI.
|
||||
|
||||
A row has up to three cells. Each cell decides whether a message becomes an explicit request, an inferred candidate or nothing:
|
||||
|
||||
| Cell | Choices | Default |
|
||||
| --- | --- | --- |
|
||||
| On the list, not addressing the agent | inferred, ignore | inferred |
|
||||
| Not on the list, addressing the agent | explicit, inferred, ignore | ignore |
|
||||
| Not on the list, not addressing the agent | inferred, ignore | ignore |
|
||||
|
||||
Cells for unaddressed messages are available only where the Source receives every message, such as a Feishu chat set to **全部消息** or a GitHub scope with its receive-all switch enabled. A CI result has no sender, so its row offers only the last cell. Bot messages use the bot list instead of these cells.
|
||||
|
||||
Setting the unlisted-addressed cell to explicit lets anyone in that scope request work by mentioning the agent. This can be useful for a dedicated support chat. Inferred candidates still depend on the accept mode. Follow-ups normally bypass this table, except that an explicit cell still grants explicit strength.
|
||||
|
||||
The admission table is part of the Policy version. Weakening a cell does not immediately stop open Tickets accepted under it. It can revoke their Mandates when they are revalidated before resuming.
|
||||
|
||||
### Bots and automated reviewers
|
||||
|
||||
The host recognizes GitHub Apps, logins ending in `[bot]`, and Feishu bots as bots. A Source can list bots such as `dependabot[bot]` whose messages may become inferred candidates. They must either address the agent or post in a scope that receives every message. They cannot make explicit requests or stop Tickets.
|
||||
|
||||
An automated reviewer using an ordinary account counts as a person to the host. Its comments reach subscribed Tickets and generate follow-up notices. Team prompt text must identify such accounts as automated reviewers.
|
||||
|
||||
## Routing candidates
|
||||
|
||||
The main Session routes explicit candidates with `orchestrator_route`. One call can contain several accept, attach, stop or ignore decisions. Each item is processed independently: a refused item or a missing field does not prevent the others from running.
|
||||
|
||||
An inferred candidate normally needs a full assessment. There is one shortcut: if its card clearly touches no enabled rule, such as plain chat or unrelated discussion, the main Session may ignore it directly. The Decision records `ignoredFromCard` to distinguish this from a rule assessment. The main Session reads the enabled rules in its Policy prompt section.
|
||||
|
||||
Accepting an inferred candidate always requires an assessment. Direct accept refuses a Trigger that the gate did not admit as explicit.
|
||||
|
||||
## Accept mode and rules
|
||||
|
||||
The accept mode is part of the Policy:
|
||||
|
||||
| Mode | Effect |
|
||||
| --- | --- |
|
||||
| `off` | Nothing inferred becomes a candidate, such as an unaddressed message, a bot's message or the follow-up of a person not on the list |
|
||||
| `observe` (default) | Inferred candidates are assessed; a positive assessment is recorded as ignore marked "would accept", and nothing is accepted |
|
||||
| `auto` | A positive assessment of an inferred candidate accepts it |
|
||||
|
||||
Explicit requests work in every mode. Start in `observe` and switch to `auto` after reviewing the recorded Decisions. Each assessment judges every enabled rule exactly once, whether it matches or not. A proactive accept requires at least one matching rule.
|
||||
|
||||
Each rule describes a kind of proactive work. The Ticket's Mandate records the matched rules, and its Executor receives their names and instructions.
|
||||
|
||||
For that Ticket, a rule's process and communication requirements override the defaults. A silence rule can suppress an acknowledgement; a review rule can name the required reviewer and approval before merge. Requirements that apply to every Ticket belong in team prompt sections instead.
|
||||
|
||||
## Policy version
|
||||
|
||||
Rules, accept mode and each Source's Policy form the Policy version. Every Mandate records the version under which it was accepted.
|
||||
|
||||
A version change discards running Evals and queues their Triggers again. It also sends a policy-changed Input to each affected live Ticket. The Input explains whether:
|
||||
|
||||
- a matched rule changed, including its new text, or no longer applies;
|
||||
- the requester may no longer start work; or
|
||||
- proactive work is no longer accepted, for example because the mode changed from `auto`.
|
||||
|
||||
Unaffected Tickets receive no notice. The change does not immediately revoke live Tickets' Mandates.
|
||||
|
||||
Source configuration is separate from Policy. It controls what enters the system: which chats are read and whether a scope receives all messages or only mentions. A Mandate records its Source and receiving scope. Resuming needs the Source to still listen to the Thread, but does not require it to still receive every message there.
|
||||
|
||||
Replies wait while their Source is off. They are dropped if the Source no longer listens to their destination.
|
||||
|
||||
A condition on what the Executor may do with its own result, such as when it may merge, does not decide who may authorize work. It is part of the agent's instructions, not of the Policy.
|
||||
|
||||
## Resume and stop
|
||||
|
||||
An ended Ticket keeps its Mandate. A person's message on one of its own Threads can resume the same work without a new Eval or Decision, regardless of who sent it. Bots and CI do not wake ended Tickets. The Executor decides whether the request fits the scope and ends the round if it does not.
|
||||
|
||||
Only three operations change a Mandate: accept creates it, a person's stop withdraws it, and failed revalidation revokes it. A Ticket stopped by a person stays ended and receives nothing more. Work requested there needs a new accept.
|
||||
|
||||
### Revalidation before resume
|
||||
|
||||
The Source must still listen to the original Thread. The remaining checks depend on the Mandate's basis:
|
||||
|
||||
- An explicit follow-up Mandate passes.
|
||||
- An inferred Mandate needs `auto` mode and its matched rules still enabled.
|
||||
- A Mandate admitted by a table cell must still receive at least its original strength under today's table and sender membership. Removing a sender from the list can therefore revoke a Mandate accepted through the listed-sender cell.
|
||||
- A bot's Mandate requires that bot to remain on the Source's bot list. Inferred table and bot Mandates also need `auto` mode and enabled matched rules.
|
||||
- Other Mandates require the sender to remain on the Source's allowed list.
|
||||
|
||||
If revalidation fails, the Ticket stays ended and takes none of its waiting messages. Each message is treated as having reached no Ticket; if no other Ticket owns it, it goes to Eval. The main Session is not told it reached a worker. This also applies after a processing pause. Attaching to the Ticket is refused, and further work requires a new accept.
|
||||
|
||||
- **A person saying "stop" in a Thread is an ordinary Trigger,** and Eval decides stop. An Executor cannot declare that a person asked it to stop.
|
||||
- **Stop in the panel is a command.** It acts on the Ticket directly and is recorded as a manual stop.
|
||||
- **Waiting conditions belong to Sources.** A condition such as "no assignee for N hours" would be a Source that waits and checks again itself; Eval never waits.
|
||||
|
||||
## When Eval does not finish
|
||||
|
||||
The time limit starts when the candidate reaches the main Session. Time spent waiting for delivery does not count, and an undelivered candidate never expires.
|
||||
|
||||
An expired Eval queues its Triggers for another attempt. After two consecutive retries, the third expiry records `unavailable`. An assessment failure also records `unavailable`; a later Trigger can start a new Eval. A Policy change or a decision made against an outdated Policy records `unavailable` and immediately queues the batch under the current version.
|
||||
|
||||
The `evals` health entry reports unresolved unavailable Decisions from the last hour. `scripts/dsh-superagent status` includes that error.
|
||||
|
||||
The panel asks for human attention only when the unavailable Decision is less than 24 hours old, is still the latest Decision on its Thread, has not been dismissed, and the Thread is neither queued nor evaluating. A person can evaluate the Trigger again or dismiss the problem.
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
# 002 授权
|
||||
|
||||
授权决定谁可以开始工作、以及那项工作可以包含什么。本文建立在 [001 领域模型](001-领域模型.md) 之上。
|
||||
|
||||
host 强制执行允许名单、规则与 Mandates。消息、评论与被引用的文档只是决策的输入;它们不能改变那些权限。
|
||||
|
||||
## 一条 Trigger 的两条路径
|
||||
|
||||
### Eval:决定是否开始工作
|
||||
|
||||
Source 记录消息是否点名了 agent:提到 bot 或 agent 账号的记为 `mention`,私聊记为 `direct`。gate 检查发送者与该 Source 的 Policy:
|
||||
|
||||
- 名单内的人点名 agent,构成显式请求。
|
||||
- 人在一个已有 Tickets 的 Thread 上的跟进,遵循下面的规则。
|
||||
- 其他人类消息按它们的接收范围走准入表。
|
||||
- Source bot 名单上的 bot 可以产生 inferred 候选,永远不能构成显式请求。其他 bots 不予准入。
|
||||
|
||||
### Subscriptions:继续已有的工作
|
||||
|
||||
被订阅 Thread 上的每一条消息都会作为上下文到达订阅它的 Tickets,包括来自 bots 与 CI 的消息。这条投递路径绕过 gate,不授予任何新的授权。每个 Ticket 仍受它的 Mandate 约束。
|
||||
|
||||
Ticket 自己的 Thread 上的人的消息,归该 Ticket 的 Executor 决定。main Session 把一条消息 attach 给某个 Ticket 时同理。消息仍由 Executor 认领期间,不进入 Eval、不产生 Decision。每个 Executor 要么在自己 Ticket 的 scope 内处理这个请求,要么把它交还给别处路由。
|
||||
|
||||
认领在 Ticket 存活期间持续,即使有人移除了它的 Subscription。认领也覆盖这样一个 ended Ticket:它不是被人停掉的,并且有消息正在等待让它恢复。
|
||||
|
||||
main Session 会收到一条跟进通知。消息仍由 Executor 认领时,main Session 只能应人的要求停止工作;accept、attach、ignore 与 requeue 都会被拒绝。一次停止仍然要过 gate,带着下面所述的跟进强度。
|
||||
|
||||
### 交还与释放跟进
|
||||
|
||||
Executor 只在一条消息要求的工作超出 Ticket 的 scope、或属于另一个 Ticket 时,把它交还一次。Ticket 保留它的 Subscription。如果一个被 attach 上来的 Thread 无关,Executor 交还它在其中认领的那些消息。
|
||||
|
||||
一条消息只有在没有任何 Ticket 仍认领它之后,才进入一次全新的 Eval。它的候选里带着各个 Executors 的理由。gate 按该 Thread 上的一条普通消息来判它,main Session 可以接单、把它 attach 到别处、或忽略。交还一条 Ticket 并未认领的消息——例如一条 bot 消息、或一条人已经送进 Eval 的消息——什么都不会排队。
|
||||
|
||||
一个 Ticket 结束时,会释放它的 Executor 没有读过或没有认领的跟进。其中包括从未投递给 Executor 的消息,以及在其最后一次 Report 或回合结束之后才送达的消息,只要它既没有认领、答复过、也没有交还。每条被释放的消息在没有其他 Ticket 认领时进入 Eval。一次 stop 只结束被停那个 Ticket 的工作。
|
||||
|
||||
被停掉的 Ticket 不会把它的 stop 消息再送一次 Eval,除非有另一个 Ticket 交还过它。停掉它的 main Session 可以把这条消息的其余部分接成一项新工作。
|
||||
|
||||
人可以从面板把一个被认领的跟进送进 Eval。若 main Session 随后又把它 attach 回一个活着的 Ticket,Executor 会再次收到它。当没有任何有效的判断覆盖这条消息时,面板还提供重新评估。
|
||||
|
||||
### 仅上下文的订阅
|
||||
|
||||
Executor 可以为了上下文关注一个相关的 Thread,例如另一个 Issue 或 pull request。它的消息能到达 Ticket,但不授予授权或跟进强度。那里的人类消息不会恢复一个 ended Ticket,除非 main Session 把来自那个 Thread 的一条消息 attach 过来。main Session 关于其他工作的备注同样只是上下文。
|
||||
|
||||
没有到达任何 Ticket 的人类消息——包括 Tickets 都被人停掉的 Threads——走 Eval 路径。bot 消息走哪条路径,与它们向订阅 Tickets 的投递互不相干。
|
||||
|
||||
## 强度
|
||||
|
||||
| 强度 | 情形 |
|
||||
| --- | --- |
|
||||
| explicit | 名单上的人点名 agent,或在一个已经有 Ticket 的 Thread 上跟进;准入表中设为 explicit request 的单元格 |
|
||||
| inferred | 不在名单上的人在一个有 Ticket 的 Thread 上跟进,除非他的准入表单元格是 explicit;准入表中设为 inferred candidate 的单元格;Source bot 名单上某个 bot 的消息点名了 agent,或落在 Source 全量接收消息之处 |
|
||||
| not admitted | 其他任何 bot 的消息、准入表忽略的一切、以及 accept mode 为 `off` 时所有 inferred 的情形 |
|
||||
|
||||
对一个未列入名单的人,explicit 准入单元格有优先权——即使是在有 Tickets 的 Thread 上,并且在任何 accept mode 下都成立。否则他的跟进按 inferred 处理,在 `off` 模式下不予准入。只为上下文而关注的 Threads 上的消息走准入表,而不是跟进强度。
|
||||
|
||||
## 准入
|
||||
|
||||
每个 Source 的 Policy 都有一张准入表。它的行是接收范围:
|
||||
|
||||
- **Feishu:** 每个已配置的 chat、bot 收到过 @ 的其他 chats、以及私聊。其他 chats 只有当 Feishu 卡片上的 **其他群** 设为 **仅 @** 时才接收。
|
||||
- **GitHub:** 新的 Issues、新的 pull requests、评论、以及默认分支的 CI。
|
||||
|
||||
一行至多三个单元格。每个单元格决定一条消息变成显式请求、inferred 候选、还是什么都不是:
|
||||
|
||||
| 单元格 | 选项 | 默认 |
|
||||
| --- | --- | --- |
|
||||
| 名单内、未点名 agent | inferred、ignore | inferred |
|
||||
| 名单外、点名 agent | explicit、inferred、ignore | ignore |
|
||||
| 名单外、未点名 agent | inferred、ignore | ignore |
|
||||
|
||||
未点名消息的单元格,只在 Source 全量接收消息的地方可用,例如设为 **全部消息** 的 Feishu chat、或开了全量接收开关的 GitHub scope。CI 结果没有发送者,所以它的行只提供最后一个单元格。bot 消息用 bot 名单判断,不看这些单元格。
|
||||
|
||||
把"名单外、点名 agent"单元格设为 explicit,等于允许那个范围内的任何人通过 @ agent 请求工作。这对一个专门的支持群可能有用。inferred 候选仍然取决于 accept mode。跟进通常绕过这张表,唯一的例外是 explicit 单元格仍会给出 explicit 强度。
|
||||
|
||||
准入表是 Policy 版本的一部分。削弱一个单元格不会立刻停止按它接单的那些 open Tickets。在恢复前的重新校验中,它可以吊销它们的 Mandates。
|
||||
|
||||
### Bots 与自动化评审者
|
||||
|
||||
host 把 GitHub Apps、以 `[bot]` 结尾的 logins、以及 Feishu bots 认作 bots。Source 可以列出 `dependabot[bot]` 这样的 bots,它们的消息可以成为 inferred 候选。它们要么点名 agent,要么发在一个全量接收消息的范围里。它们不能构成显式请求,也不能停止 Tickets。
|
||||
|
||||
用普通账号的自动化评审者,在 host 看来是人。它的评论会到达订阅的 Tickets 并产生跟进通知。团队的提示词文本必须把这类账号指认为自动化评审者。
|
||||
|
||||
## 路由候选
|
||||
|
||||
main Session 用 `orchestrator_route` 路由显式候选。一次调用可以包含多个 accept、attach、stop 或 ignore 决定。每一项独立处理:某一项被拒或字段缺失,不妨碍其余各项执行。
|
||||
|
||||
inferred 候选通常需要一次完整的评估。有一个捷径:如果它的卡片明显不触及任何启用的规则——例如纯闲聊或无关讨论——main Session 可以直接忽略它。Decision 会记录 `ignoredFromCard`,以区别于一次规则评估。main Session 在它的 Policy 提示词节里读取启用的规则。
|
||||
|
||||
接一个 inferred 候选永远需要评估。直接 accept 会被拒绝——如果 gate 并没有把它准入为 explicit。
|
||||
|
||||
## Accept mode 与规则
|
||||
|
||||
accept mode 是 Policy 的一部分:
|
||||
|
||||
| 模式 | 效果 |
|
||||
| --- | --- |
|
||||
| `off` | 任何 inferred 的东西都不成为候选,例如未点名的消息、bot 的消息、名单外的人的跟进 |
|
||||
| `observe`(默认) | inferred 候选会被评估;一次正向的评估记为 ignore 并标上 "would accept",什么都不接单 |
|
||||
| `auto` | 对 inferred 候选的一次正向评估即接单 |
|
||||
|
||||
显式请求在所有模式下都有效。从 `observe` 起步,评审过记录的 Decisions 之后再切到 `auto`。每次评估对每条启用的规则恰好判断一次,无论命中与否。一次主动接单至少要求一条命中的规则。
|
||||
|
||||
每条规则描述一类主动工作。Ticket 的 Mandate 记录命中的规则,它的 Executor 会收到这些规则的名字与指令。
|
||||
|
||||
对那个 Ticket 而言,规则的流程与沟通要求会覆盖默认值。一条静默规则可以压掉认领回执;一条评审规则可以指定合并前所需的评审人与批准。适用于每个 Ticket 的要求应当放进团队提示词分节。
|
||||
|
||||
## Policy 版本
|
||||
|
||||
规则、accept mode 与各 Source 的 Policy 共同构成 Policy 版本。每个 Mandate 都记录它接单时所在的版本。
|
||||
|
||||
一次版本变更会丢弃正在运行的 Evals,并把它们的 Triggers 重新排队。它还会给每个受影响的活 Ticket 发一条 policy-changed Input。这条 Input 说明是哪种情况:
|
||||
|
||||
- 一条命中的规则变了(包括它的新文本),或不再适用;
|
||||
- 发起人不再可以开始工作;或
|
||||
- 主动工作不再被接受,例如模式从 `auto` 改走了。
|
||||
|
||||
未受影响的 Ticket 收不到通知。这次变更不会立刻吊销活着的 Tickets 的 Mandates。
|
||||
|
||||
Source 配置与 Policy 是分开的。它控制什么能进入系统:读哪些 chats、某个 scope 是接收全部消息还是只收 @。一个 Mandate 记录它的 Source 与接收范围。恢复要求 Source 仍然监听这个 Thread,但不要求它仍然全量接收那里的一切。
|
||||
|
||||
答复在它们的 Source 关闭时等待。若 Source 不再监听它们的目的地,答复被丢弃。
|
||||
|
||||
关于 Executor 可以拿自己的结果做什么的条件——例如它何时可以合并——不决定谁可以授权工作。它是 agent 指令的一部分,不是 Policy 的一部分。
|
||||
|
||||
## 恢复与停止
|
||||
|
||||
一个 ended Ticket 保留它的 Mandate。有人在它自己的某个 Thread 上发一条消息,无需新的 Eval 或 Decision 就能恢复同一份工作,无论发送者是谁。Bots 与 CI 不会唤醒 ended Tickets。由 Executor 决定这个请求是否合 scope,不合就结束这一轮。
|
||||
|
||||
只有三个操作会改变一个 Mandate:accept 创建它,人的 stop 撤回它,重新校验失败吊销它。被人停掉的 Ticket 保持 ended,之后什么都不收。那里要请求的工作需要一次新的接单。
|
||||
|
||||
### 恢复前的重新校验
|
||||
|
||||
Source 必须仍然监听原始的 Thread。其余的检查取决于 Mandate 的依据:
|
||||
|
||||
- 一条 explicit 跟进类的 Mandate 直接通过。
|
||||
- 一条 inferred Mandate 需要 `auto` 模式、并且它命中的规则仍然启用。
|
||||
- 由准入表单元格准入的 Mandate,按今天的准入表和发送者的成员身份,必须至少仍能收到它原来的强度。因此把发送者移出名单可以吊销一个经"名单内发送者"单元格接下的 Mandate。
|
||||
- bot 的 Mandate 要求该 bot 仍在 Source 的 bot 名单上。inferred 的表类与 bot 类 Mandates 还需要 `auto` 模式与仍然启用的命中规则。
|
||||
- 其余 Mandates 要求发送者仍在该 Source 的允许名单上。
|
||||
|
||||
若重新校验失败,Ticket 保持 ended,一条等待中的消息都不取。每条消息按"没有到达任何 Ticket"处理;若没有其他 Ticket 认领它,它进入 Eval。main Session 不会被告知它到达过 worker。处理被暂停之后也是如此。attach 到该 Ticket 会被拒绝,继续工作需要一次新的接单。
|
||||
|
||||
- **一个人在 Thread 里说"停"是一条普通 Trigger,** 由 Eval 决定 stop。Executor 不能声称有人要求它停止。
|
||||
- **面板里的 Stop 是一条命令。** 它直接作用于 Ticket,并被记录为一次手动停止。
|
||||
- **等待类条件属于 Sources。** 一个像"N 小时无人 assign"这样的条件,应当是一个会自己等待并再次检查的 Source;Eval 从不等待。
|
||||
|
||||
## Eval 没有完成时
|
||||
|
||||
时限从候选到达 main Session 时开始计。等待投递的时间不计入,一个从未送达的候选永不过期。
|
||||
|
||||
一次超时的 Eval 把它的 Triggers 重新排队再试。连续两次重试之后,第三次超时记为 `unavailable`。一次评估失败也记 `unavailable`;后续的一个 Trigger 可以发起新的 Eval。一次 Policy 变更、或依据过期 Policy 做出的判断,记为 `unavailable` 并立刻按当前版本把该批次重新排队。
|
||||
|
||||
`evals` 健康条目报告最近一小时内未解决的 unavailable Decisions。`scripts/dsh-superagent status` 会包含那个错误。
|
||||
|
||||
面板只在以下条件全部满足时才请求人工注意:这条 unavailable Decision 距今不足 24 小时、仍是它所在 Thread 上最新的 Decision、没有被 dismiss、且该 Thread 既不处于 queued 也不处于 evaluating。人可以再次评估这个 Trigger,或 dismiss 这个问题。
|
||||
@@ -1,203 +0,0 @@
|
||||
# 003 Execution
|
||||
|
||||
This document follows an accepted Ticket through execution, delivery and recovery. It builds on [001 Domain model](001-domain.md) and [002 Authorization](002-authorization.md).
|
||||
|
||||
## Ticket stages
|
||||
|
||||
Each open Ticket has one stage, determined by what it is waiting for. Read the table from top to bottom; the first matching row wins.
|
||||
|
||||
| Stage | Condition | Panel label |
|
||||
| --- | --- | --- |
|
||||
| person / delivery | Delivering an Input to the Executor Session failed; it is retried after 2 seconds, then after a delay that doubles up to a minute, and a success, a new Assignment or the Ticket's end clears it | Input undelivered |
|
||||
| person / answer | It has an Attention | Awaiting answer |
|
||||
| person / review | The latest Report is `ready` | Awaiting review |
|
||||
| person / blocked, failed | The latest Report is `blocked` or `failed`, and no person's message reached the Ticket since | Blocked · Failed |
|
||||
| working / queued | No Assignment, or a pending one | Queued |
|
||||
| idle | The latest Report is `waiting`, or the Assignment ended | Waiting for a Trigger |
|
||||
| working | Everything else | Working |
|
||||
|
||||
A `person` stage means the Ticket cannot continue without human input. A new human message releases a `blocked` or `failed` Ticket to its Executor, so it moves to the next matching stage, usually `working`.
|
||||
|
||||
An `idle` Ticket has finished its turn without an outstanding question. A Trigger on a subscribed Thread can continue it.
|
||||
|
||||
## The Executor
|
||||
|
||||
Each Ticket gets an Executor Session named `worker-<ticket id>` and a directory named after the Ticket's short name. Directories are created under the configured execution root, which defaults to `<stateDir>/work`.
|
||||
|
||||
Executor Sessions run commands without approval as the operator's OS user. Their `dsh-superagent-worker` preset contains the tools needed for work and moves foreground shell commands to background jobs after 5 seconds. This lets a worker receive new messages at the next step instead of waiting for a long command to finish.
|
||||
|
||||
The main Session uses `dsh-superagent-router`, with the same 5-second shell and job-wait limits and a smaller tool set for routing. Sessions retain the preset selected at creation. Worker subagents use DeepSeek Harness's `standard` preset.
|
||||
|
||||
### Background work and worker slots
|
||||
|
||||
When a turn ends with shell jobs or subagents still running, the Ticket keeps its worker slot and stays working. It is not reported as waiting, and the main Session receives no waiting report. A completed job or subagent wakes the Executor with its result in the same slot. Once a turn ends with no background work, the Executor reports waiting normally.
|
||||
|
||||
After each turn, background work has at most 30 minutes to finish. At that limit, shell jobs are killed and the Ticket is reported waiting. The report includes the Executor's last words and which jobs were killed or subagents remain active. Subagents are allowed to continue. The Executor reads killed-job results on its next turn.
|
||||
|
||||
A handoff to a person frees the slot immediately and leaves the Report in effect. This applies after a `ready` Report or a question, unless another Input arrived since, and after any `blocked` or `failed` Report.
|
||||
|
||||
At handoff, subagents stop. Shell jobs, such as a server offered for review, may continue until the Assignment ends. They hold no slot and are exempt from the 30-minute limit, including after later turns. Their completion does not wake the Executor; it reads the results on its next turn.
|
||||
|
||||
A person can also start a turn by writing directly in the Executor Session. Its background work follows the same rules.
|
||||
|
||||
When an Assignment ends, its shell jobs are killed without waking the Session. Its subagents are released, and any completion turns they wake are dropped. The replacement Assignment and its waiting Inputs start after that shutdown finishes.
|
||||
|
||||
Stopping or restarting the plugin inside the DeepSeek Harness process uses the same shutdown path. Inputs not yet taken into Session history are retained. Shutdown waits up to 10 seconds for each worker's background work; a process restart ends that work outright. An Assignment interrupted while busy restarts with a recovery note.
|
||||
|
||||
Worker subagents, including nested ones, use DeepSeek Harness's `standard` preset with the worker's permissions. They retain only the worker's allowed work tools, and lose those tools when the Ticket ends. Under `standard`, a shell command moves to the background after 60 seconds. A subagent's jobs are killed when it ends.
|
||||
|
||||
### Preparing a Ticket directory
|
||||
|
||||
Before the first Assignment, if the Ticket directory does not exist, the prepare command runs once with `sh` in the execution root. It has ten minutes to finish. Without a prepare command, the worker starts in an empty directory.
|
||||
|
||||
A failed command fails the Ticket and records its output. A retry or a person's message on one of the Ticket's Threads tries preparation again. A typical command uses `git worktree` to create a checkout. A pull request belongs to the Ticket when its branch's last path segment matches the Ticket's short name.
|
||||
|
||||
### Concurrency
|
||||
|
||||
A Ticket holds a worker slot from the start of a launch, including preparation, or from the delivery that wakes its idle Executor. It also holds the slot throughout an active turn.
|
||||
|
||||
The configured concurrency limit applies to these slots, with one extra slot for a person's message or a status request. Such a wake can use the extra slot when normal slots are full, and takes the next free slot before a new launch. New work therefore cannot prevent a person from continuing an existing Ticket.
|
||||
|
||||
Other Inputs, such as CI results, wait for a normal slot. A launch waits in `working / queued` until a slot is free. Launches run concurrently, so a slow prepare command delays only its own Ticket.
|
||||
|
||||
### Execution variables
|
||||
|
||||
The prepare command and Executor instructions receive the same execution variables. The command reads environment variables; the Executor reads their values and descriptions in its prompt. None contains credentials.
|
||||
|
||||
| Variable | Registered by | Value |
|
||||
| --- | --- | --- |
|
||||
| `DSH_SUPERAGENT_DIR`, `DSH_SUPERAGENT_NAME`, `DSH_SUPERAGENT_TICKET` | Executor | The Ticket's directory, short name and id |
|
||||
| `DSH_SUPERAGENT_BLOCKED_COMMANDS` | Executor | Commands the Executor may not run |
|
||||
| `DSH_SUPERAGENT_GITHUB_REPOSITORY` | GitHub | The repository the Source watches, `owner/name` |
|
||||
| `DSH_SUPERAGENT_GITHUB_ACCOUNT`, `DSH_SUPERAGENT_GITHUB_COMMIT_EMAIL` | GitHub | The agent account and its commit email |
|
||||
| `DSH_SUPERAGENT_GITHUB_NUMBER`, `DSH_SUPERAGENT_GITHUB_PR_HEAD` | GitHub | The Issue or pull request that started the Ticket, and its head ref when it is a pull request |
|
||||
| `DSH_SUPERAGENT_FEISHU_BOT`, `DSH_SUPERAGENT_FEISHU_CHAT` | Feishu | The bot, and the chat that started the Ticket |
|
||||
|
||||
Ticket-specific values come from its directory and originating Trigger. Other values come from the current deployment settings. Only the panel can change execution environment settings; text sent to the main Session cannot change the configured shell command.
|
||||
|
||||
Workers reach GitHub through the agent account's `gh` and Feishu through the bot's `lark-cli`. Blocked commands fail when invoked by name.
|
||||
|
||||
These wrappers and directories establish defaults, not a security boundary. A worker can still execute a program by absolute path.
|
||||
|
||||
## Merging
|
||||
|
||||
Auto-merge is governed by the **Delivery requirements** prompt. The host has no merge-specific decision logic.
|
||||
|
||||
By default, the Executor never enables auto-merge as the agent account: GitHub attributes the merge to whoever enabled it. Delivery requirements must name another account's CLI for merging. Without one, people merge the pull request.
|
||||
|
||||
The default instructions allow auto-merge only for a pull request opened for the Ticket, when it is ready, the required approvals cover the head commit, and additions plus deletions total fewer than 200 lines. Before pushing another revision, the Executor withdraws auto-merge.
|
||||
|
||||
Reviews without a body, such as approvals, also arrive as Triggers and can wake an Executor waiting for review. These instructions govern the agent, not GitHub's merge rules. To enforce approvals, require them in branch protection and dismiss stale approvals. Omit the merge CLI or remove the auto-merge paragraph to require manual merging.
|
||||
|
||||
## How a Ticket ends
|
||||
|
||||
- **Ready without a pull request:** a `ready` Report ends the Ticket as completed.
|
||||
- **Nothing to do:** an Executor that finds no work for a person to act on, such as a request that does not reproduce or is superseded, reports `dropped`. The Ticket ends without a message, unless a matched rule asks for one, and a person's later message on its Threads resumes it.
|
||||
### Pull requests
|
||||
|
||||
A Ticket follows the pull request it started from once its Executor reports it. It also follows each pull request opened from its branch once reported by the Executor or found by the GitHub Source. Reopened pull requests are followed again.
|
||||
|
||||
The Ticket is done when none of the pull requests it opened remain open. If it opened none, all pull requests it follows must be closed. It then ends with the outcome of the last relevant close and keeps its Subscriptions, allowing a later human comment to resume it.
|
||||
|
||||
A close does not end the Ticket while resumed, retried or recovered work is unfinished. It also waits if the Executor acknowledged or answered a follow-up it owns but has not since made a non-progress Report or ended a turn after reading that message. A later `ready` Report ends the Ticket once the same pull-request completion rule is satisfied.
|
||||
|
||||
Chat Channels receive one final notice about the pull-request outcome. GitHub already displays the outcome on the pull request itself. Removing a Ticket's Subscription to a pull request also stops it following that pull request.
|
||||
|
||||
"Awaiting review" therefore means a `ready` Report with a pull request.
|
||||
|
||||
### Follow-ups and context Threads
|
||||
|
||||
The Executor decides human follow-ups on its Ticket's own Threads. It handles in-scope requests and hands back requests beyond its scope or for another Ticket. Once no Executor owns the message, it becomes a candidate for the main Session. The main Session may otherwise act on the follow-up only to stop work at the person's request.
|
||||
|
||||
An Input counts as read after the Session takes it into a step and subsequently reports or ends a turn. When a Ticket ends, unread messages that were not acknowledged, answered or handed back return to Eval, as described in [002 Authorization](002-authorization.md). Read messages left without action, such as thanks, do not.
|
||||
|
||||
Threads followed only for context still deliver messages, but do not give the Ticket ownership of human follow-ups or resume ended Tickets. Those messages can be assessed independently.
|
||||
|
||||
Sources poll subscribed Threads while a live Ticket subscribes, and for two weeks after the later of the last Trigger and the subscribing Tickets' end. Afterwards, messages arrive only through ordinary intake: GitHub notifications or Feishu live events.
|
||||
|
||||
## Failure and recovery
|
||||
|
||||
### Executor blocked or failed
|
||||
|
||||
When an Executor Session reports `blocked` or `failed`, or fails to start or errors, which records `failed`, the Ticket takes no automatic Input.
|
||||
|
||||
A passing failure of the model provider or the network, an HTTP 408, 429 or 5xx or a refused, reset or timed-out connection, is tried again first, three times with a growing backoff: a launch runs again, and a worker whose turn failed is told to go on; a failed prepare command is not tried again.
|
||||
|
||||
Once failed, CI and bot messages are recorded on it, but they are not delivered and do not wake the Session.
|
||||
|
||||
Its tools stay available, so a person can continue in the Session directly.
|
||||
|
||||
A launch that failed is not tried again on its own.
|
||||
|
||||
A person's message, a later Report that is neither `blocked` nor `failed`, or a retry ends the hold.
|
||||
|
||||
It is worked out from the stored Reports and Inputs, so a restart keeps it.
|
||||
### Retry
|
||||
|
||||
Available while an open Ticket is in `person / delivery`, `person / blocked` or `person / failed`: a new Assignment, which starts its worker again and takes the Inputs that wait, is the remedy for each.
|
||||
|
||||
Once a person's message reached a blocked or failed Ticket, the Executor has the message, so the Ticket shows as working, or as queued when a failed launch starts again, and offers no retry, unless delivering that message failed.
|
||||
|
||||
A retry ends the current Assignment and starts a new one in the same Executor Session, directory, branch and pull request.
|
||||
|
||||
The Executor model applies only when a Session is created; an existing Session keeps its model.
|
||||
### Restart
|
||||
|
||||
Running Assignments end with "host restarted" and new ones start with a recovery note that tells the Executor to check its directory, git history and GitHub before repeating any outside action, oldest Ticket first.
|
||||
|
||||
Assignments that are waiting, ready, blocked or failed stay as they are, unless an Input reached them after that Report, in which case they start again too.
|
||||
|
||||
Sessions, directories, Subscriptions, pending messages and the stored work below survive a restart, and so does the record of which messages each Executor Session took into its history, which a step does once it claimed them and before its model call: a message taken by a turn the restart cut off counts as read once a Report or the end of a turn follows, while one a step claimed but a cancel or error cut off before the history counts as unread.
|
||||
|
||||
A retry or a restart gives the new Assignment again the messages delivered to the Assignment it replaces that its Executor Session did not take, which a cancel or a cut-off step may have dropped; a copy still waiting in the Session, kept by a restart or persisted across a crash, is taken out before the new Assignment's first message, so the new delivery is the only copy.
|
||||
### Stored work
|
||||
|
||||
What dsh-superagent does after a decision, such as relaying a candidate, a follow-up or a report to the main Session, launching an Assignment or delivering an Input, is stored first and done afterwards.
|
||||
|
||||
A relay or a delivery that fails, such as when its Session cannot be reached, is retried after 2 seconds, then after a delay that doubles up to a minute; a launch that fails fails its Ticket, unless the failure was a passing one, as above.
|
||||
|
||||
Each kind of work reports an error among the overview's system problems only after one item failed three times in a row; a failed Input delivery shows on its Ticket instead.
|
||||
|
||||
Relays to the main Session keep their order, and so do notices; a failed one holds back the later ones of its kind.
|
||||
### Source failure
|
||||
|
||||
The GitHub Source reads each Issue, pull request and Thread on its own: a failed read keeps its error on its Thread, is read again on the next poll, and counts towards an error of the Source, which it reports after three failed polls in a row by default; any successful poll resets the count.
|
||||
|
||||
GitHub answers 404 also when the agent lost access to a private repository, so a 404 is retried.
|
||||
|
||||
A notified Issue or pull request whose reads keep failing is dropped a day after its first failed read, and at once when GitHub reports it as deleted; it then no longer fails the poll, its Thread keeps the error, and the Source's state names it.
|
||||
|
||||
Its read position stays where it was, so a later notification or a poll of its Thread reads what the failed reads missed.
|
||||
|
||||
The Feishu event consumer reports every failure, and polling of subscribed topics keeps each failure on its Thread.
|
||||
|
||||
A malformed item in a GitHub list or a Feishu page is skipped, and the Source's state counts it.
|
||||
### Channel failure
|
||||
|
||||
Outgoing messages are stored and sent as stored work, retried with backoff from 5 seconds, doubling up to 300.
|
||||
|
||||
A Ticket's messages to one conversation go out in the order they were queued, so its acknowledgement comes before its later answer or result: a message waits while an earlier one of the same Ticket to the same conversation has not gone out, until that one has failed three times in a row and shows as a problem.
|
||||
|
||||
Messages of other Tickets, and messages of no Ticket, do not wait for it.
|
||||
|
||||
A message with failed attempts is a system problem until it goes out or is withdrawn.
|
||||
|
||||
A message is dropped only when its Source no longer listens to the conversation, or when the Source answers that the conversation can never receive it; while its Source is off, it waits.
|
||||
|
||||
The Sources answer that for a deleted Issue or a locked conversation on GitHub, and for a recalled message or one Feishu no longer finds on Feishu.
|
||||
|
||||
A reply to a review comment that no longer exists goes to the pull request's conversation instead.
|
||||
|
||||
A dropped message keeps its record, with when and why it was dropped and the Ticket it was for, so it is never queued again; a dropped acknowledgement reads as dropped, not sent.
|
||||
### Pause processing
|
||||
|
||||
Stops dispatch, new Evals, Eval timeouts, Input delivery and replies; a launch already under way waits until processing resumes.
|
||||
|
||||
Sources keep recording Triggers, and a running Executor turn is not interrupted.
|
||||
|
||||
A person's message to an ended Ticket waits: the first pass after the pause resumes the Ticket, if its Mandate holds under the Policy then.
|
||||
|
||||
An Eval in progress does not expire during a pause; its deadline moves later by the pause's length.
|
||||
### Invalid configuration
|
||||
|
||||
A stored Source configuration that its kind rejects disables that Source instance.
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
# 003 执行
|
||||
|
||||
本文带着一份已接单的 Ticket 走完执行、交付与恢复。它建立在 [001 领域模型](001-领域模型.md) 与 [002 授权](002-授权.md) 之上。
|
||||
|
||||
## Ticket 阶段
|
||||
|
||||
每个 open Ticket 恰好处于一个阶段,由它在等什么决定。表从上往下读,第一条匹配的行生效。
|
||||
|
||||
| 阶段 | 条件 | 面板标签 |
|
||||
| --- | --- | --- |
|
||||
| person / delivery | 向 Executor Session 投递一个 Input 失败;2 秒后重试,之后延迟逐次翻倍、封顶一分钟;一次成功、一个新的 Assignment、或 Ticket 的结束都会清掉它 | Input undelivered |
|
||||
| person / answer | 它有一个 Attention | Awaiting answer |
|
||||
| person / review | 最近的 Report 是 `ready` | Awaiting review |
|
||||
| person / blocked, failed | 最近的 Report 是 `blocked` 或 `failed`,且此后没有人的一条消息到达过该 Ticket | Blocked · Failed |
|
||||
| working / queued | 没有 Assignment,或只有一个 pending 的 | Queued |
|
||||
| idle | 最近的 Report 是 `waiting`,或 Assignment 已结束 | Waiting for a Trigger |
|
||||
| working | 其余一切 | Working |
|
||||
|
||||
`person` 阶段意味着这个 Ticket 没有人的输入就走不下去。一条新的人类消息会把 `blocked` 或 `failed` 的 Ticket 释放给它的 Executor,于是它进入下一条匹配的阶段,通常是 `working`。
|
||||
|
||||
一个 `idle` Ticket 结束了它的回合,没有悬而未决的问题。被订阅 Thread 上的一个 Trigger 可以让它继续。
|
||||
|
||||
## Executor
|
||||
|
||||
每个 Ticket 得到一个名为 `worker-<ticket id>` 的 Executor Session,和一个以 Ticket 短名命名的目录。目录创建在配置的执行根之下,默认为 `<stateDir>/work`。
|
||||
|
||||
Executor Sessions 以操作者的 OS 用户身份免审批地运行命令。它们的 `dsh-superagent-worker` 预设包含干活所需的工具,并把前台 shell 命令在 5 秒后移入后台任务。这让一个 worker 能在下一个步骤接收新消息,而不是干等一条长命令跑完。
|
||||
|
||||
main Session 使用 `dsh-superagent-router`,带同样的 5 秒 shell 与 job 等待上限,以及一个更适合路由的更小工具集。Sessions 保留创建时选定的预设。Worker subagents 使用 DeepSeek Harness 的 `standard` 预设。
|
||||
|
||||
### 后台工作与 worker 槽位
|
||||
|
||||
当一个回合结束、还有 shell 任务或 subagents 在运行时,Ticket 保留它的 worker 槽位、维持 working。它不会被上报为等待,main Session 也不会收到 waiting 报告。一个完成的 job 或 subagent 带着它的结果、在同一个槽位里唤醒 Executor。一旦某个回合在没有后台工作的情况下结束,Executor 才照常上报 waiting。
|
||||
|
||||
每个回合之后,后台工作至多还有 30 分钟完成。到点时,shell 任务被杀掉,Ticket 被上报为 waiting。报告会带上 Executor 最后说的话、以及哪些 job 被杀、哪些 subagents 仍在活动。Subagents 允许继续运行。Executor 在它的下一个回合读取被杀 job 的结果。
|
||||
|
||||
向人的移交会立刻释放槽位,并让 Report 保持有效。这适用于一次 `ready` Report 或一个问题之后——除非此后又有一个 Input 到达——以及任何 `blocked` 或 `failed` Report 之后。
|
||||
|
||||
移交时,subagents 停止。Shell 任务——比如一个为评审而开着的服务器——可以继续到 Assignment 结束为止。它们不占槽位、也不受 30 分钟上限约束,包括在之后的回合里。它们的完成不唤醒 Executor;Executor 在下一个回合读它们的结果。
|
||||
|
||||
人也可以直接在 Executor Session 里写一条消息来发起一个回合。它的后台工作遵循同样的规则。
|
||||
|
||||
一个 Assignment 结束时,它的 shell 任务被杀掉且不唤醒 Session。它的 subagents 被释放,它们唤醒的任何完成回合都被丢弃。接替的 Assignment 及其等待中的 Inputs 在那次关闭完成之后才开始。
|
||||
|
||||
在 DeepSeek Harness 进程内停止或重启插件走同一条关闭路径。尚未进入 Session 历史的 Inputs 会被保留。关闭会为每个 worker 的后台工作至多等待 10 秒;一次进程重启会直接终结这些工作。一个在忙碌中被中断的 Assignment 会带着恢复备注重启。
|
||||
|
||||
Worker subagents(包括嵌套的)使用 DeepSeek Harness 的 `standard` 预设,并带着 worker 的权限。它们只保留 worker 允许的工作工具,并在 Ticket 结束时失去这些工具。`standard` 之下,一条 shell 命令在 60 秒后转入后台。一个 subagent 的 jobs 在它结束时被杀掉。
|
||||
|
||||
### 准备 Ticket 目录
|
||||
|
||||
在第一个 Assignment 之前,若 Ticket 目录不存在,prepare command 会在执行根用 `sh` 运行一次。它有十分钟完成。没有 prepare command 时,worker 在一个空目录里启动。
|
||||
|
||||
命令失败会让 Ticket 失败并记录它的输出。一次重试、或 Ticket 某个 Threads 上的一条人类消息,会再次尝试准备。一条典型的命令用 `git worktree` 创建一个 checkout。当一个分支的最后一段路径与 Ticket 的短名一致时,那个 pull request 就属于这个 Ticket。
|
||||
|
||||
### 并发
|
||||
|
||||
一个 Ticket 从一次启动(包括准备)开始、或从唤醒其空闲 Executor 的那次投递开始,持有 worker 槽位。在一个活跃回合的整个过程里,它也持有该槽位。
|
||||
|
||||
配置的并发上限作用于这些槽位,并为人类消息或状态请求多留一个额外槽位。这样的唤醒在普通槽位占满时可以使用额外槽位,并在新启动之前拿走下一个空槽。因此,新的工作永远不会妨碍一个人继续一个已有的 Ticket。
|
||||
|
||||
其他 Inputs——例如 CI 结果——等待普通槽位。一次启动在 `working / queued` 里等到有槽位空出。启动们并发运行,所以一条慢的 prepare command 只拖慢它自己的 Ticket。
|
||||
|
||||
### 执行变量
|
||||
|
||||
prepare command 与 Executor 指令收到同样的执行变量。命令读环境变量;Executor 在它的提示词里读这些变量的值与说明。其中没有凭据。
|
||||
|
||||
| 变量 | 注册方 | 值 |
|
||||
| --- | --- | --- |
|
||||
| `DSH_SUPERAGENT_DIR`、`DSH_SUPERAGENT_NAME`、`DSH_SUPERAGENT_TICKET` | Executor | Ticket 的目录、短名与 id |
|
||||
| `DSH_SUPERAGENT_BLOCKED_COMMANDS` | Executor | Executor 不得运行的命令 |
|
||||
| `DSH_SUPERAGENT_GITHUB_REPOSITORY` | GitHub | 该 Source 盯守的仓库,`owner/name` |
|
||||
| `DSH_SUPERAGENT_GITHUB_ACCOUNT`、`DSH_SUPERAGENT_GITHUB_COMMIT_EMAIL` | GitHub | agent 账号及其 commit email |
|
||||
| `DSH_SUPERAGENT_GITHUB_NUMBER`、`DSH_SUPERAGENT_GITHUB_PR_HEAD` | GitHub | 发起该 Ticket 的 Issue 或 pull request,以及它是 pull request 时的 head ref |
|
||||
| `DSH_SUPERAGENT_FEISHU_BOT`、`DSH_SUPERAGENT_FEISHU_CHAT` | Feishu | 发起该 Ticket 的 bot 与 chat |
|
||||
|
||||
Ticket 专属的值来自它的目录与发起它的 Trigger。其他值来自当前的部署设置。执行环境设置只有面板能改;发给 main Session 的文本改不了配置好的 shell 命令。
|
||||
|
||||
Workers 通过 agent 账号的 `gh` 访问 GitHub,通过 bot 的 `lark-cli` 访问 Feishu。被屏蔽的命令按名字调用时会失败。
|
||||
|
||||
这些包装器与目录立的是默认值,不是安全边界。worker 仍可以用绝对路径执行一个程序。
|
||||
|
||||
## 合并
|
||||
|
||||
Auto-merge 由 **Delivery requirements** 提示词管辖。host 没有合并专属的决策逻辑。
|
||||
|
||||
默认情况下,Executor 从不以 agent 账号开启 auto-merge:GitHub 会把合并记在开启它的人头上。Delivery requirements 必须点名另一个账号的 CLI 来合并。没有指定时,由人来合并 pull request。
|
||||
|
||||
默认指令只在这个 pull request 为该 Ticket 所开、已经就绪、所需批准覆盖了 head commit、且新增加删除合计少于 200 行时,允许 auto-merge。在推送下一个 revision 之前,Executor 先撤回 auto-merge。
|
||||
|
||||
没有正文的评审——例如批准——也作为 Triggers 到达,并能唤醒一个在等评审的 Executor。这些指令管的是 agent,不是 GitHub 的合并规则。要强制评审,就在分支保护里要求评审并让过期批准失效。想强制人工合并,就不配合并 CLI、或删掉 auto-merge 那一段。
|
||||
|
||||
## Ticket 如何结束
|
||||
|
||||
- **没有 pull request 的就绪:** 一次 `ready` Report 把 Ticket 以 completed 结束。
|
||||
- **无事可做:** 一个发现没有可供人行动的工作的 Executor——例如无法复现或已被取代的请求——上报 `dropped`。Ticket 静默结束,除非命中的规则要求说一声;此后其 Threads 上的人类消息会让它恢复。
|
||||
### Pull requests
|
||||
|
||||
一个 Ticket 一旦它的 Executor 上报了它由此开始的那个 pull request,就开始跟踪它。它同样跟踪每个从其分支开出、并被 Executor 上报或被 GitHub Source 发现的 pull request。重新打开的 pull request 会被重新跟踪。
|
||||
|
||||
当它开出的 pull requests 没有一个还开着时,这个 Ticket 就完成了。若它一个都没开,则它跟踪的所有 pull requests 都必须已关闭。于是它带着最后一次相关关闭的结果结束,并保留它的 Subscriptions,让后来的评论能让它恢复。
|
||||
|
||||
在有被恢复、被重试或被找回的工作尚未完成时,一次关闭不会结束 Ticket。若 Executor 认领或答复过一条它认领的跟进、但读完那条消息之后既没有再发过一次非 progress 的 Report、也没有结束过一次回合,同样要等。之后的 `ready` Report 一旦满足同样的 pull-request 完成规则,就结束 Ticket。
|
||||
|
||||
Chat Channels 会收到一条关于 pull-request 结果的最终通知。GitHub 本来就会在 pull request 上显示结果。移除 Ticket 对某个 pull request 的 Subscription,也同时让它不再跟踪那个 pull request。
|
||||
|
||||
因此"Awaiting review"意味着一份带着 pull request 的 `ready` Report。
|
||||
|
||||
### 跟进与上下文 Threads
|
||||
|
||||
Executor 决定其 Ticket 自己 Threads 上的人类跟进。它处理 scope 内的请求,把超出 scope 或属于另一个 Ticket 的请求交还。一旦没有 Executor 认领这条消息,它成为 main Session 的候选。否则 main Session 对跟进只能应人的要求去停止工作。
|
||||
|
||||
一个 Input 在 Session 把它纳入一个步骤、并且该步骤随后上报或结束回合之后,算作已读。Ticket 结束时,那些未读、未被认领、未答复、未交还的消息回到 Eval,如 [002 授权](002-授权.md) 所述。已读而未处理的,例如感谢,不会。
|
||||
|
||||
仅为上下文关注的 Threads 仍会投递消息,但不让 Ticket 拥有人类跟进的所有权,也不恢复 ended Tickets。那些消息可以被独立评估。
|
||||
|
||||
Sources 在一个活着的 Ticket 订阅期间轮询被订阅的 Thread,并在最后一个 Trigger 与订阅的 Tickets 结束两者中较晚者之后继续轮询两周。此后消息只经普通准入到达:GitHub notifications 或 Feishu 实时事件。
|
||||
|
||||
## 失败与恢复
|
||||
|
||||
### Executor 受阻或失败
|
||||
|
||||
当一个 Executor Session 上报 `blocked` 或 `failed`,或启动失败、出错(后者记录为 `failed`)时,Ticket 停止接受自动的 Input。
|
||||
|
||||
一次会过去的模型 provider 或网络失败——HTTP 408、429、5xx,或被拒、被重置、超时的连接——先重试:共三次、退避递增;一次启动会重新运行,一个回合失败的 worker 会被告知继续;一条失败的 prepare command 不再重试。
|
||||
|
||||
一旦失败,CI 与 bot 消息会记录在它身上,但不投递、也不唤醒 Session。
|
||||
|
||||
它的工具保持可用,所以人可以直接在 Session 里继续。
|
||||
|
||||
一次失败的启动不会自行重试。
|
||||
|
||||
一条人类消息、一次此后既非 `blocked` 也非 `failed` 的 Report、或一次重试,会解除这个扣留。
|
||||
|
||||
它是从存储的 Reports 与 Inputs 推算出来的,所以重启后仍在。
|
||||
### 重试
|
||||
|
||||
当 open Ticket 处于 `person / delivery`、`person / blocked` 或 `person / failed` 时可用:一个新的 Assignment——重启它的 worker、拿走那些等待中的 Inputs——分别是它们的补救。
|
||||
|
||||
一旦一条人类消息到达过 blocked 或 failed 的 Ticket,消息就已在 Executor 手里,于是 Ticket 显示为 working——failed 启动重新进行时显示为 queued——并不再提供重试,除非那条消息的投递失败了。
|
||||
|
||||
一次重试结束当前的 Assignment,并在同一个 Executor Session、目录、分支与 pull request 里开始一个新的。
|
||||
|
||||
Executor 模型只在创建 Session 时生效;已有的 Session 保持它的模型。
|
||||
### 重启
|
||||
|
||||
运行中的 Assignments 以 "host restarted" 结束,新的带着一条恢复备注启动,要求 Executor 在重复任何对外动作之前先检查它的目录、git 历史与 GitHub,最老的 Ticket 优先。
|
||||
|
||||
处于 waiting、ready、blocked 或 failed 的 Assignments 保持原样——除非那份 Report 之后有 Input 到达过它们,那种情况下它们也会重新启动。
|
||||
|
||||
Sessions、目录、Subscriptions、待处理的消息、以及下文的 stored work 都挺过重启;"每条消息被哪个 Executor Session 纳入了历史"的记录同样挺过重启——一个步骤在它认领消息之后、发起模型调用之前写这条记录:重启打断的那个回合纳入的消息,在随后出现一次 Report 或回合结束时算作已读;被一个步骤认领、但在写入历史之前被取消或错误打断的,算作未读。
|
||||
|
||||
一次重试或重启,会把曾投递给被替换的那个 Assignment、而其 Executor Session 未纳入的消息——取消或被中断的步骤可能丢掉的那些——重新交给新的 Assignment;一份仍在 Session 队列里等待、被重启保留、或跨崩溃持久化的副本,会在新 Assignment 的第一条消息之前被取走,让新的投递是唯一的一份。
|
||||
### Stored work
|
||||
|
||||
dsh-superagent 在一个决定之后做的事——把一个候选、一条跟进或一份报告转交给 main Session,启动一个 Assignment,投递一个 Input——都先存储、后执行。
|
||||
|
||||
一次失败的转交或投递——例如它的 Session 联系不上——在 2 秒后重试,之后延迟逐次翻倍、封顶一分钟;一次失败的启动会失败它的 Ticket,除非那是一次会过去的失败,如上文。
|
||||
|
||||
每一种 work 只有在同一条目连续失败三次之后,才在 overview 的系统问题里报一个错;失败的 Input 投递改为显示在它的 Ticket 上。
|
||||
|
||||
向 main Session 的转交保持顺序,通知同样;一条失败会压住它后面同类的条目。
|
||||
### Source 失败
|
||||
|
||||
GitHub Source 逐个读取每个 Issue、pull request 与 Thread:一次失败的读取把它的错误留在它的 Thread 上,下一次轮询重读,并计入 Source 的错误——默认连续三次轮询失败后才上报;任何一次成功的轮询会重置计数。
|
||||
|
||||
GitHub 在 agent 失去对一个私有仓库的访问权时也回答 404,所以 404 会被重试。
|
||||
|
||||
一个被通知的 Issue 或 pull request,若读取持续失败,会在首次失败读取的一天后被放弃;GitHub 报告它已删除时则立刻放弃。此后它不再让轮询失败,它的 Thread 保留着错误,Source 的状态会点名它。
|
||||
|
||||
它的读取位置停在原地,所以后来的通知或对它 Thread 的轮询会补上失败读取错过的内容。
|
||||
|
||||
Feishu 事件消费者报告每一次失败;对订阅话题的轮询把每次失败留在它的 Thread 上。
|
||||
|
||||
GitHub 列表或 Feishu 分页里的一个畸形条目会被跳过,Source 的状态为它计数。
|
||||
### Channel 失败
|
||||
|
||||
对外消息作为 stored work 存储并发送,从 5 秒开始退避重试、逐次翻倍、封顶 300 秒。
|
||||
|
||||
一个 Ticket 发往同一个对话的消息按排队的先后发出,所以它的认领回执先于它后来的答复或结果:只要同一 Ticket 发往同一对话的更前一条还没发出,当前这条就等着——直到那一条连续失败三次、显示为一个问题。
|
||||
|
||||
其他 Tickets 的消息、以及不属于任何 Ticket 的消息,不为它等待。
|
||||
|
||||
一条有失败尝试的消息,在发出或被撤回之前,是一个系统问题。
|
||||
|
||||
一条消息只有在它的 Source 不再监听那个对话时、或 Source 回答那个对话永远收不到它时才被丢弃;它的 Source 关闭期间,它等着。
|
||||
|
||||
GitHub 对已删除的 Issue 或被锁定的对话给出这样的回答;Feishu 对被撤回或已不存在的消息给出这样的回答。
|
||||
|
||||
对一条已不存在的 review comment 的答复,改发到那个 pull request 的对话里。
|
||||
|
||||
一条被丢弃的消息保留它的记录——丢弃的时间、原因、以及它本属于哪个 Ticket——因此它绝不会被重新排队;一次被丢弃的认领回执读作 dropped,而非 sent。
|
||||
### 暂停处理
|
||||
|
||||
停止派发、新的 Evals、Eval 超时、Input 投递与答复;已在进行中的启动会等到处理恢复。
|
||||
|
||||
Sources 继续记录 Triggers,正在运行的 Executor 回合不被打断。
|
||||
|
||||
发往 ended Ticket 的人类消息会等待:暂停解除后的第一个回合若该 Ticket 的 Mandate 按当时的 Policy 仍然成立,就恢复它。
|
||||
|
||||
一次进行中的 Eval 在暂停期间不过期;它的截止时间按暂停的时长顺延。
|
||||
### 无效配置
|
||||
|
||||
一份被其 kind 拒绝的、已存储的 Source 配置,会禁用那个 Source 实例。
|
||||
@@ -1,96 +0,0 @@
|
||||
# 004 Prompts
|
||||
|
||||
This document explains what the agents read, what a team can edit, and when changes take effect. It builds on [000 Philosophy](000-philosophy.md) through [003 Execution](003-execution.md). Prompt text is written in English.
|
||||
|
||||
## What a team writes
|
||||
|
||||
A team can edit three team sections, add skills, and override every built-in system-prompt part. Most defaults live under `prompts/` and `skills/`. Tool descriptions, notices and cards remain in code and cannot be edited through prompt settings.
|
||||
|
||||
| Section | Holds |
|
||||
| --- | --- |
|
||||
| Communication | When to message people, how messages read, the line that begins every GitHub message (`This is by an automated agent, not a human` by default), how to handle reviews, and which ordinary accounts are review bots |
|
||||
| Ticket scope | When a Ticket goes on and when it stops with a draft |
|
||||
| Delivery requirements | Evidence in pull requests, the auto-merge condition with its line threshold, and the commit author; without one, the Executor commits as the agent account's no-reply address |
|
||||
| Team skills | Knowledge the agents load when they need it, such as an `identities` skill that says which Feishu user is which GitHub account |
|
||||
|
||||
The built-in text covers the roles, tool usage, host mechanics, authorization rules, and the GitHub and Feishu skills. It refers to the team sections by their headings instead of repeating them.
|
||||
|
||||
### Built-in parts and templates
|
||||
|
||||
Overrides use these keys:
|
||||
|
||||
| Parts | Keys |
|
||||
| --- | --- |
|
||||
| Shared instructions and roles | `runtime/shared`, `runtime/orchestrator`, `runtime/worker` |
|
||||
| Working instructions | `runtime/subscriptions`, `runtime/replies`, `runtime/dispatch`, `runtime/work`, `runtime/admission` |
|
||||
| Handoff instructions | `runtime/handoff/orchestrator`, `runtime/handoff/worker` |
|
||||
| Source skills | `skill/feishu`, `skill/github` |
|
||||
| Generated sections | `runtime/environment`, `runtime/policy`, `runtime/policy/none` |
|
||||
|
||||
The generated sections are editable templates. They must retain these placeholders:
|
||||
|
||||
- `runtime/environment`: `{variables}`, filled with execution environment lines.
|
||||
- `runtime/policy`: `{version}`, `{mode}` and `{rules}`, with one line per enabled rule.
|
||||
- `runtime/policy/none`: `{version}` and `{mode}`, used when no rule is enabled.
|
||||
|
||||
Editing a part changes the instructions the model receives. It does not change the gate, Mandates, tool checks or account wrappers, which remain enforced by code.
|
||||
|
||||
On an explicit human request, the main Session can also save overrides with `orchestrator_prompts_save`. If a save omits an override's `base`, it keeps the saved base. A removed part already present in the saved configuration is dropped on save; any other unknown part is refused.
|
||||
|
||||
An override records which built-in text it was based on. Upgrades preserve the team's text. If the default changes, the panel marks the part **Default updated** and shows the difference beside the team's text. The team can keep its text, use the new default or edit it. Parts without overrides automatically follow new defaults.
|
||||
|
||||
The main Session can read affected part names in `orchestrator_read prompts`, under `default_changed`.
|
||||
|
||||
## Assembly and precedence
|
||||
|
||||
The system prompt is assembled in this order:
|
||||
|
||||
1. Shared instructions and the role.
|
||||
2. The three team sections.
|
||||
3. Subscriptions and replies.
|
||||
4. Dispatch for the main Session, or work instructions for an Executor.
|
||||
5. `runtime/admission`, for the main Session only.
|
||||
6. Handoff instructions.
|
||||
7. `runtime/environment`.
|
||||
8. `runtime/policy`, for the main Session only.
|
||||
|
||||
Admission instructions come from `prompts/admission.md`. They describe proactive assessment and when an irrelevant candidate can be ignored from its card. `orchestrator_eval` returns context rather than repeating these instructions.
|
||||
|
||||
The environment section gives the main Session deployment-wide values and each Executor its Ticket-specific values. The Policy section is regenerated from settings on every model request. It contains the Policy version, accept mode, the explicit-request exemption, and enabled rules as `- <name> (<id>): <instruction>`, or a statement that none is enabled. It is last so a Policy change leaves the preceding prompt text unchanged.
|
||||
|
||||
Executors also read DeepSeek Harness's coding prompt and the repository's `AGENTS.md`; the main Session does not. An inferred Ticket adds a final note limiting its Executor to the matched rules. Assignments and incoming messages arrive inline as Trigger cards, with resource pointers only for truncated content.
|
||||
|
||||
Executors work unattended. Their tools exclude those that ask a person, present UI to them or change the profile, such as `ask_user_question`, `present` and `plugin_manager`.
|
||||
|
||||
From strongest to weakest:
|
||||
|
||||
1. Facts enforced by tools and the host: identities, permissions, tool argument checks, reply purposes, length limits. Prompt text cannot change them.
|
||||
2. Built-in and generated sections, as the team's overrides of them read.
|
||||
3. The process and communication in a proactive Ticket's matched rules, for that Ticket's Executor Session only.
|
||||
4. Team sections and team skills.
|
||||
5. Outside text: messages, comments, documents and quotes. It is data and never changes rules or authorization.
|
||||
|
||||
## Principles of the built-in text
|
||||
|
||||
These apply [000 Philosophy](000-philosophy.md) to the agents:
|
||||
|
||||
- **Separate roles.** The main Session only routes: it forwards the original Trigger and the routing facts, and makes no technical decisions, through one routing tool that takes the decisions on several waiting candidates in one call. A routing note has at most 30 words. The Executor reads the original sources, judges the scope, implements and verifies.
|
||||
- **The main Session does not wait.** Reports, follow-up notices, candidates and completed background jobs arrive automatically as messages. It ends the turn when nothing remains to do. A guard refuses `sleep`, shell timeouts over 30 seconds and job waits over 5 seconds, because a blocked step delays new candidates. These instructions replace DeepSeek Harness's own job-notification note in the main Session's complete prompt.
|
||||
- **Message only when a person must act or can use a result:** a result can be reviewed or used, a decision or resource is needed, or the work cannot be finished. Chat messages are one sentence by default, with a hard limit of 100 words.
|
||||
- **Executors decide their follow-ups.** Each Executor handles and acknowledges in-scope requests, and hands back requests beyond its Ticket or for another Ticket. A message becomes new work only after all its owners hand it back or end without taking it up. The main Session receives follow-up notices but may act on them only to stop work. Handed-back messages then arrive as candidates with the Executors' reasons.
|
||||
- **Coordinate through subscriptions and the main Session.** An Executor follows related Threads for context. The main Session can send a fact from another Ticket in a note of at most 100 words. Neither the Subscription nor the note grants authority.
|
||||
- **Attach only on an explicit reference.** A message must point to a Ticket's pull request, Issue, source message or acknowledgement, or to a recent change with exactly one matching Ticket. Topic similarity alone is not enough. Attachment subscribes the Ticket to the entire Thread; an Executor finding it unrelated uses `orchestrator_thread_unrelated` and does not answer there.
|
||||
- **Confirm concretely, once.** Before substantial work, the Executor confirms each new request and names the object and the expected result, as a reply under the original message.
|
||||
- **Ask through `human_action`.** The Executor puts one concrete question in its Report, and the main Session forwards it unchanged. If a person's message prompted the question, it names that message and the host asks there. Otherwise it asks in the Ticket's original conversation. Already authorized work can continue meanwhile.
|
||||
- **Honest delivery.** Opening a pull request is not readiness; a draft is marked unfinished; a UI change without a real before screenshot is not ready.
|
||||
- **A person's pause holds** across follow-ups, new Executor Sessions and restarts until a person lifts it. Automatic feedback never resumes paused work.
|
||||
- **Examples only show format.** An example sentence is not a fact the agent may claim.
|
||||
- **Handle reviews by outcome.** A fixed and verified comment is resolved silently; a declined one gets one sentence of reason. An automated reviewer's thread is resolved once it has an answer, and each automated reviewer gets at most three rounds per pull request.
|
||||
- **Yield to duplicate work.** When a person's branch or pull request already contains the Ticket's change, the Executor closes its own pull request with one sentence pointing to theirs.
|
||||
|
||||
## Versions
|
||||
|
||||
- Each save creates an immutable version containing the full configuration, revision number, predecessor, timestamp and origin (panel or tool). Restoring a version or defaults creates another version.
|
||||
- Saves name their base revision. A conflicting save is refused without discarding the draft.
|
||||
- Sessions read the latest version on every model request, including later steps in the same turn. Saving does not restart, cancel or wake a Session. The host records the revisions each Session used; an Executor can read any revision it worked under.
|
||||
- The deployment sets a total size limit, including overrides. Validation refuses unknown keys, reserved built-in skill names, unknown override parts and missing template placeholders. It never silently truncates text. An override identical to the default is removed so the part follows future defaults.
|
||||
@@ -0,0 +1,96 @@
|
||||
# 004 提示词
|
||||
|
||||
本文解释 agent 读到什么、团队能改什么、改动何时生效。它建立在 [000 理念](000-理念.md) 到 [003 执行](003-执行.md) 之上。提示词文本用英文书写。
|
||||
|
||||
## 团队写什么
|
||||
|
||||
团队可以编辑三个团队分节、添加技能(skills),并覆盖每一个内置系统提示词部件。多数默认文本在 `prompts/` 与 `skills/` 下。工具描述、通知与卡片留在代码里,不能通过提示词设置编辑。
|
||||
|
||||
| 分节 | 内容 |
|
||||
| --- | --- |
|
||||
| Communication | 何时给人发消息、消息读起来什么样、每条 GitHub 消息的开头固定句(默认是 `This is by an automated agent, not a human`)、如何处理 reviews、以及哪些普通账号是 review bots |
|
||||
| Ticket scope | Ticket 何时继续、何时停在一份草案上 |
|
||||
| Delivery requirements | pull request 里的证据、auto-merge 条件及其行数阈值、以及 commit 作者;不指定时,Executor 以 agent 账号的 no-reply 地址提交 |
|
||||
| Team skills | agent 在需要时装载的知识,例如一个 `identities` 技能,写明哪个 Feishu 用户对应哪个 GitHub 账号 |
|
||||
|
||||
内置文本覆盖角色、工具使用、host 机制、授权规则,以及 GitHub 和 Feishu 技能。它按标题引用团队分节,而不是重复它们。
|
||||
|
||||
### 内置部件与模板
|
||||
|
||||
覆盖使用这些键:
|
||||
|
||||
| 部件 | 键 |
|
||||
| --- | --- |
|
||||
| 共享指令与角色 | `runtime/shared`、`runtime/orchestrator`、`runtime/worker` |
|
||||
| 工作指令 | `runtime/subscriptions`、`runtime/replies`、`runtime/dispatch`、`runtime/work`、`runtime/admission` |
|
||||
| 交接指令 | `runtime/handoff/orchestrator`、`runtime/handoff/worker` |
|
||||
| Source 技能 | `skill/feishu`、`skill/github` |
|
||||
| 生成节 | `runtime/environment`、`runtime/policy`、`runtime/policy/none` |
|
||||
|
||||
生成节是可编辑的模板。它们必须保留这些占位符:
|
||||
|
||||
- `runtime/environment`:`{variables}`,填充执行环境行。
|
||||
- `runtime/policy`:`{version}`、`{mode}` 与 `{rules}`,每条启用的规则占一行。
|
||||
- `runtime/policy/none`:`{version}` 与 `{mode}`,没有任何启用规则时使用。
|
||||
|
||||
编辑一个部件改变的是模型收到的指令,不改变 gate、Mandates、工具校验与账号包装器——它们仍由代码强制执行。
|
||||
|
||||
在显式的人类请求下,main Session 也可以用 `orchestrator_prompts_save` 保存覆盖。若某次保存省略了覆盖的 `base`,则保留已存的 base。已从配置中移除、但仍在已存设置里的部件在保存时被丢弃;其他未知部件会被拒绝。
|
||||
|
||||
一个覆盖会记录它基于哪份内置文本。升级保留团队的文本。若默认值变了,面板把该部件标为 **Default updated**,并在团队文本旁显示差异。团队可以保留自己的文本、采用新默认值、或继续编辑。没有覆盖的部件自动跟随新默认值。
|
||||
|
||||
main Session 可以在 `orchestrator_read prompts` 的 `default_changed` 下读到受影响的部件名。
|
||||
|
||||
## 拼装与优先级
|
||||
|
||||
系统提示词按这个顺序拼装:
|
||||
|
||||
1. 共享指令与角色。
|
||||
2. 三个团队分节。
|
||||
3. Subscriptions 与 replies。
|
||||
4. main Session 用 dispatch,Executor 用 work instructions。
|
||||
5. `runtime/admission`,仅 main Session。
|
||||
6. 交接指令。
|
||||
7. `runtime/environment`。
|
||||
8. `runtime/policy`,仅 main Session。
|
||||
|
||||
准入指令来自 `prompts/admission.md`。它们描述主动评估,以及一个不相关的候选在什么情况下可以直接从它的卡片上忽略。`orchestrator_eval` 返回上下文,而不是重复这些指令。
|
||||
|
||||
环境节给 main Session 部署级的取值、给每个 Executor 它那份 Ticket 专属的取值。Policy 节在每次模型请求前从设置重新生成,包含 Policy 版本、accept mode、显式请求的豁免条款,以及启用的规则(形如 `- <name> (<id>): <instruction>`),或者一句没有任何启用的说明。它放在最后,这样一次 Policy 改动不会改变前面的提示词文本。
|
||||
|
||||
Executors 还会读 DeepSeek Harness 的编码提示词与仓库的 `AGENTS.md`;main Session 不读。一个推断出的 Ticket 会追加一条尾注,把它的 Executor 限定在命中的规则内。Assignments 与进来的消息以 Trigger 卡片的形式内联到达,只有被截断的内容才给资源指针。
|
||||
|
||||
Executors 无人值守地工作。它们的工具排除了那些会询问人、向人展示界面或修改 profile 的工具,例如 `ask_user_question`、`present` 与 `plugin_manager`。
|
||||
|
||||
从最强到最弱:
|
||||
|
||||
1. 由工具与 host 强制的事实:身份、权限、工具参数校验、reply purposes、长度限制。提示词文本改不了它们。
|
||||
2. 内置与生成节,按团队对它们的覆盖所读到的样子。
|
||||
3. 一个主动 Ticket 命中规则里的流程与沟通方式——仅对该 Ticket 的 Executor Session。
|
||||
4. 团队分节与团队技能。
|
||||
5. 外部文本:消息、评论、文档与引用。它们是数据,永远不改变规则或授权。
|
||||
|
||||
## 内置文本的原则
|
||||
|
||||
这些是把 [000 理念](000-理念.md) 应用到 agent 上:
|
||||
|
||||
- **角色分离。** main Session 只做路由:它转发原始 Trigger 与路由事实,不做任何技术决定,通过一个一次调用就能对多个等待中的候选做出决定的路由工具。一条路由备注至多 30 个词。Executor 读原始材料、判断 scope、实现并验证。
|
||||
- **main Session 不等待。** 报告、跟进通知、候选与已完成的后台任务会自动作为消息到达。无事可做时它就结束本轮。一个 guard 会拒绝 `sleep`、超过 30 秒的 shell 超时、以及超过 5 秒的 job 等待,因为一个被卡住的步骤会拖延新的候选。这些指令在 main Session 的完整提示词里取代 DeepSeek Harness 自己的 job 通知说明。
|
||||
- **只在有人必须行动或用得上结果时才发消息:** 结果可以被评审或使用、需要决定或资源、或工作无法完成。聊天消息默认一句话,硬上限 100 个词。
|
||||
- **Executors 自己决定后续。** 每个 Executor 处理并认领自己 scope 内的请求,交还超出其 Ticket 或属于另一个 Ticket 的请求。一条消息只有在它所有的认领者都交还、或结束而未认领之后,才成为新的工作。main Session 收到跟进通知,但只能拿它们来停止工作。被交还的消息随后作为候选到达,带着 Executors 的理由。
|
||||
- **通过 subscriptions 与 main Session 协调。** 一个 Executor 订阅相关的 Threads 以获得上下文。main Session 可以用一条至多 100 个词的备注,把另一个 Ticket 的事实带过来。无论 Subscription 还是备注都不授予权限。
|
||||
- **只在显式引用时才 attach。** 一条消息必须指向某个 Ticket 的 pull request、Issue、源消息或认领回执,或指向一次近期改动且恰好只有一个匹配的 Ticket。仅话题相似不够。attach 会让该 Ticket 订阅整个 Thread;Executor 若发现无关,使用 `orchestrator_thread_unrelated` 并不在那里回复。
|
||||
- **具体地确认,只确认一次。** 在实质性工作前,Executor 对每个新请求确认一次,说明对象与预期结果,以原消息下方回复的形式。
|
||||
- **通过 `human_action` 提问。** Executor 把一个具体问题放进它的 Report,main Session 原样转发。若这个问题由某条人类消息引出,就点名那条消息,host 到那里去问;否则在 Ticket 的原始对话里问。已获授权的工作可在此期间继续。
|
||||
- **诚实交付。** 开 pull request 不等于就绪;草案标未完成;没有真实 before 截图的 UI 改动不算完成。
|
||||
- **人的 pause 会顶住。** 跨跟进、新的 Executor Sessions 与重启持续生效,直到人来解除。自动反馈永远不会恢复被暂停的工作。
|
||||
- **示例只展示格式。** 一个例句不是 agent 可以援引的事实。
|
||||
- **按结果处理 reviews。** 被修复并验证通过的评论静默 resolve;被拒绝的给一句话理由。自动化评审者的 thread 一旦有其答复即可 resolve,每个自动化评审者在一个 pull request 里至多三轮。
|
||||
- **为重复工作让路。** 当某人的分支或 pull request 已包含该 Ticket 的改动时,Executor 关闭自己的 pull request,用一句话指向对方的那份。
|
||||
|
||||
## 版本
|
||||
|
||||
- 每次保存创建一个不可变版本,包含完整配置、revision 号、前驱版本、时间戳与来源(面板或工具)。恢复某个版本或默认值会再创建一个版本。
|
||||
- 保存会点名其 base revision。冲突的保存被拒绝,且不丢弃草稿。
|
||||
- Sessions 在每次模型请求时读取最新版本,包括同一轮里的后续步骤。保存不重启、不取消、也不唤醒任何 Session。host 记录每个 Session 用到的 revisions;Executor 可以读取它工作期间的任何版本。
|
||||
- 部署设置一个含覆盖在内的总大小上限。校验拒绝未知键、被保留的内置技能名、未知覆盖部件与缺失的模板占位符,且从不静默截断文本。与默认值完全相同的覆盖会被移除,让该部件跟随将来的默认值。
|
||||
@@ -1,58 +0,0 @@
|
||||
# 005 Panel
|
||||
|
||||
Open **Superagent** in the DeepSeek Harness Web sidebar to supervise the agent. The panel prioritizes work that needs a person, then system health and history. This document builds on [000 Philosophy](000-philosophy.md) through [004 Prompts](004-prompts.md).
|
||||
|
||||
## Three questions
|
||||
|
||||
Opening the panel answers three questions, in this order:
|
||||
|
||||
1. Does anything need me now?
|
||||
2. Is the system healthy and doing the right things?
|
||||
3. What is the history of one piece of work, from Trigger through Eval to Ticket?
|
||||
|
||||
## Tabs
|
||||
|
||||
The tabs follow the workflow: **Overview**, **Source**, **Trigger**, **Eval**, **Execution**, and **Settings**. Until repository and account setup succeeds, Overview shows what is missing and links to the relevant setting.
|
||||
|
||||
| Tab | Shows |
|
||||
| --- | --- |
|
||||
| Overview | Tickets needing action, system problems, activity counts for the last 24 hours, and Executor load. Each block links to its detailed page |
|
||||
| Source | Source state, setup problems and non-zero counts. Filtered Triggers and dropped messages cover the same 24-hour window as Overview |
|
||||
| Trigger | Threads filtered by Subscribed, Evaluating, New Triggers in 24 h, or All Threads. Each Thread shows its Triggers newest first, Decisions and Subscriptions |
|
||||
| Eval | Decisions with their reason and matched rules |
|
||||
| Execution | Executor state, Pause processing, and Tickets filtered by Open, Pending, Working, Waiting for a Trigger or Ended. Ticket details show actions, authorization, waiting Inputs, Subscriptions and Assignments |
|
||||
| Settings | Sources, Eval, Execution, Prompts and Deployment. Each group includes its save history |
|
||||
|
||||
### Reading individual records
|
||||
|
||||
A dropped message links to its Ticket, or to its Thread when it has no Ticket. The Source page explains whether its conversation disappeared or the Source stopped listening to it.
|
||||
|
||||
A human follow-up still owned by an Executor is marked **Delivered to Ticket**, without an Eval. The Thread also shows hand-back reasons and context-only Subscriptions. **Evaluate again** can send that follow-up to Eval, or retry a Trigger with no effective judgement: one filtered by its Source, an unavailable Eval, or a delivered message with neither a non-stop Decision nor an Executor acknowledgement.
|
||||
|
||||
Ticket details show the originating Trigger, Decision and reason, scope and Policy version. Assignments and secondary details are folded below the immediate actions. **Retry** is available for failed Input delivery or a Ticket waiting for a person after a blocked or failed Report. Ended Tickets show outcomes such as **Pull request merged** or **Stopped by a person**.
|
||||
|
||||
## Settings groups
|
||||
|
||||
- **Sources:** one card per Source with its switch, fields and verified account facts. GitHub also offers receive-all switches for new Issues, pull requests, comments and default-branch CI. Polling and read limits are under Advanced.
|
||||
- **Eval:** the Policy version and rules, followed by **On each Source** and the Eval settings. Each Source tab contains its allowed list, membership sync, admission table and bot list. Sync runs at startup and on **Sync now**. The admission table distinguishes receiving scope, sender membership and whether the message addresses the agent. Eval settings select the accept mode, timeout and assessment model.
|
||||
- **Execution:** worker model, concurrency, directory root, prepare command and blocked commands. Clicking an available variable inserts it into the prepare command.
|
||||
- **Prompts:** **Edit** changes team sections, built-in parts and skills. **Full prompt** shows the assembled text and compares it with saved text or defaults. **History** lists versions. Edited built-in parts are marked changed and can be reset; templates list their required placeholders.
|
||||
|
||||
An upgrade that changes an overridden default marks it **Default updated** (默认内容已更新). The panel compares the old and new defaults above the team's text. If the old default was not recorded, it compares the team's text with the new default instead. Choose **Keep my text** or **Use the new default**. Overrides for removed parts are listed below the tab bar and dropped on the next save.
|
||||
- **Deployment:** startup problems with **Check again**, the state directory and machine limits. Saved limits apply without restart. The DeepSeek Harness Plugin Manager exposes the same page.
|
||||
|
||||
## Search
|
||||
|
||||
The header search finds Triggers by text, id, Feishu message id or a stored link, such as a GitHub comment URL. A Feishu link missing from the original message becomes searchable only after the host reads it during the current process lifetime.
|
||||
|
||||
Search also finds Threads by references such as `#12` or a GitHub URL, and Tickets by title or a matching Thread.
|
||||
|
||||
## Principles
|
||||
|
||||
1. **Focus on what matters.** Every element answers one of the three questions, or it is removed. A number appears only when it changes what the supervisor does.
|
||||
2. **Precise definitions.** Categories have checkable definitions, are mutually exclusive and cover every case, as the Ticket stages do. A label says whom a row waits for and what it needs, not an internal status code.
|
||||
3. **One place per concept.** Source state belongs in Source; Threads and Triggers in Trigger; Decisions in Eval; Tickets in Execution. Settings are grouped by the concept they configure. Overview summarizes these pages and links to them instead of becoming another detailed view.
|
||||
4. **Domain structure.** The panel uses the domain terms. Internal identifiers appear only in folded details, and names replace identifiers where they can be resolved.
|
||||
5. **Honest state.** A count equals the length of the list it links to, and no state contradicts another fact.
|
||||
6. **Quiet by default.** Healthy states take no extra space. Problems must be actionable; a one-off network error is not enough. Rows needing a person offer their action directly. Keep page explanations to one sentence and put further detail in the (?) tooltip. Destructive actions require confirmation.
|
||||
7. **Consistent controls.** Use DeepSeek Harness design tokens and control behavior. The same information should look the same throughout the panel. Each settings block has one save bar, visible only while changes exist, a save runs or a save fails.
|
||||
@@ -0,0 +1,58 @@
|
||||
# 005 面板
|
||||
|
||||
在 DeepSeek Harness Web 侧边栏打开 **Superagent** 来监督 agent。面板优先显示需要人的工作,其次是系统健康与历史。本文建立在 [000 理念](000-理念.md) 到 [004 提示词](004-提示词.md) 之上。
|
||||
|
||||
## 三个问题
|
||||
|
||||
打开面板回答三个问题,按这个顺序:
|
||||
|
||||
1. 现在有事需要我吗?
|
||||
2. 系统健康吗,在做对的事吗?
|
||||
3. 一件工作从 Trigger 经 Eval 到 Ticket 的历史是什么?
|
||||
|
||||
## 标签页
|
||||
|
||||
标签页跟着工作流走:**Overview**、**Source**、**Trigger**、**Eval**、**Execution**、**Settings**。在仓库与账号设置成功之前,Overview 显示缺了什么,并链接到相关设置。
|
||||
|
||||
| 标签页 | 显示 |
|
||||
| --- | --- |
|
||||
| Overview | 需要处理的 Ticket、系统问题、最近 24 小时的活动计数、以及 Executor 负载。每个块链接到它的明细页 |
|
||||
| Source | Source 状态、设置问题、以及非零计数。被过滤的 Triggers 与被丢弃的消息覆盖与 Overview 相同的 24 小时窗口 |
|
||||
| Trigger | 按 Subscribed、Evaluating、New Triggers in 24 h 或 All Threads 过滤的 Threads。每个 Thread 按新到旧显示它的 Triggers、Decisions 与 Subscriptions |
|
||||
| Eval | Decisions 及其理由与命中的规则 |
|
||||
| Execution | Executor 状态、Pause 处理,以及按 Open、Pending、Working、Waiting for a Trigger、Ended 过滤的 Tickets。Ticket 详情显示 actions、授权、等待中的 Inputs、Subscriptions 与 Assignments |
|
||||
| Settings | Sources、Eval、Execution、Prompts 与 Deployment。每个分组都带有自己的保存历史 |
|
||||
|
||||
### 读单条记录
|
||||
|
||||
一条被丢弃的消息链接到它的 Ticket,没有 Ticket 时链接到它的 Thread。Source 页面说明是那个对话消失了,还是 Source 不再监听它。
|
||||
|
||||
仍由某个 Executor 认领的人类跟进会标为 **Delivered to Ticket**,不经过 Eval。Thread 上还会显示交还原因与仅收上下文的 Subscriptions。**Evaluate again** 可以把这条跟进送进 Eval,或重试一个从未得到有效判断的 Trigger:被其 Source 过滤掉的、Eval 不可用的、或已送达但既无非 stop 的 Decision 也无 Executor 认领的消息。
|
||||
|
||||
Ticket 详情显示发起的 Trigger、Decision 与理由、scope 和 Policy 版本。Assignments 与次要细节折叠在直接操作的下方。Input 投递失败、或一次被阻/失败的 Report 之后 Ticket 在等人时,可以 **Retry**。Ended Tickets 显示结果,例如 **Pull request merged** 或 **Stopped by a person**。
|
||||
|
||||
## Settings 分组
|
||||
|
||||
- **Sources:** 每个 Source 一张卡片,带它的开关、字段与已验证的账号事实。GitHub 还提供对 Issue、pull request、评论与默认分支 CI 全量接收的开关。轮询与读取上限在 Advanced 里。
|
||||
- **Eval:** Policy 版本与规则,然后是 **On each Source** 与 Eval 设置。每个 Source 标签里含它的允许名单、成员同步、准入表与 bot 名单。同步在启动时与点 **Sync now** 时运行。准入表区分接收范围、发送者成员身份、以及消息是否点名 agent。Eval 设置选择 accept mode、超时与评估模型。
|
||||
- **Execution:** worker 模型、并发、目录根、prepare command 与被屏蔽的命令。点一个可用变量会把它插入 prepare command。
|
||||
- **Prompts:** **Edit** 修改团队分节、内置部件与技能。**Full prompt** 显示拼装后的全文,并与已存文本或默认文本对比。**History** 列出各版本。编辑过的内置部件标为 changed 并可重置;模板列出其必需占位符。
|
||||
|
||||
升级改动了某个被覆盖的默认值时,会标 **Default updated**(默认内容已更新)。面板在团队文本上方比较新旧默认值;若旧的默认值没被记录,则改为比较团队文本与新默认值。选 **Keep my text** 或 **Use the new default**。已移除部件的覆盖列在标签栏下方,下次保存时被丢弃。
|
||||
- **Deployment:** 启动问题与 **Check again**、状态目录与机器限制。保存的限制无需重启即生效。DeepSeek Harness 的 Plugin Manager 暴露同一页面。
|
||||
|
||||
## 搜索
|
||||
|
||||
头部搜索按文本、id、Feishu 消息 id 或已存的链接(例如一条 GitHub 评论的 URL)查找 Triggers。原始消息里缺失的 Feishu 链接,只有在当前进程存活期间被 host 读到之后才可搜。
|
||||
|
||||
搜索还能按 `#12` 或 GitHub URL 这类引用找 Threads,按标题或匹配 Thread 找 Tickets。
|
||||
|
||||
## 原则
|
||||
|
||||
1. **只关心要紧的。** 每个元素要么回答三个问题之一,要么删掉。一个数字只有当它改变监督者的动作时才出现。
|
||||
2. **定义精确。** 各类别有可核对的定义、互斥、且覆盖所有情形,像 Ticket 阶段那样。标签说这一行在等谁、需要什么,而不是内部状态码。
|
||||
3. **一个概念一个地方。** Source 状态归 Source;Threads 和 Triggers 归 Trigger;Decisions 归 Eval;Tickets 归 Execution。设置按它们配置的概念分组。Overview 汇总这些页面并链接过去,而不是变成又一个明细视图。
|
||||
4. **领域结构。** 面板使用领域术语。内部标识符只出现在折叠的细节里,能解析出名字的地方用名字代替标识符。
|
||||
5. **状态诚实。** 一个计数等于它链接去的列表的长度,且没有任何状态与其他事实矛盾。
|
||||
6. **默认安静。** 健康的状态不多占空间。问题必须可操作;一次偶发的网络错误不算。需要人的行直接就地提供它的动作。页面解释保持一句话,更多细节放(?)提示里。破坏性动作要确认。
|
||||
7. **控件一致。** 使用 DeepSeek Harness 的设计令牌与控件行为。同样的信息在整个面板里应当长得一样。每个设置分组只有一条保存栏,仅在存在改动、保存进行中或保存失败时可见。
|
||||
@@ -1,274 +0,0 @@
|
||||
# 006 Deployment
|
||||
|
||||
Use this guide to install, configure and operate dsh-superagent. It covers accounts, startup checks, upgrades and troubleshooting, building on [001 Domain model](001-domain.md) through [005 Panel](005-panel.md).
|
||||
|
||||
## What runs
|
||||
|
||||
- **A dedicated DeepSeek Harness profile**, called `superagent` in this guide. Create it from the Web profile template. It loads the bundle and serves its panel on `127.0.0.1`. Keep it separate from the profile you work in: the plugin changes process-wide `PATH` and git settings, and its process needs independent restarts and upgrades.
|
||||
- **An agent GitHub account.** The agent comments, pushes branches and opens pull requests as this account, through a GitHub CLI signed in only as that account.
|
||||
- **Optional:** a Feishu bot for chat requests.
|
||||
|
||||
Plugin configuration holds the state directory and machine limits. Use the panel for everything else: Sources for the repository and accounts, Policy for allowed lists, and Execution for worker directories and commands.
|
||||
|
||||
### Permissions and isolation
|
||||
|
||||
The bundle defines `dsh-superagent-worker` for Executors and `dsh-superagent-router` for the main Session. They appear in the DeepSeek Harness preset list but are intended only for dsh-superagent. Do not select them for personal Sessions or disable their configuration rows. The application needs them to create and resume its Sessions.
|
||||
|
||||
Opening a blank managed Session in the Web UI can select the profile's default preset. dsh-superagent restores its role-specific preset before the first turn.
|
||||
|
||||
Both presets run with `danger-full-access` as the operator's OS user and read messages from subscribed Threads. Account wrappers choose the agent's credentials by default; they do not isolate the process. A worker can execute programs by path and read the operator's home directory. For isolation, use a dedicated OS account containing only the agent's credentials.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 22.19+ or 24+, pnpm, git 2.32+, Python 3.8+ and tmux
|
||||
- GitHub CLI (`gh`) 2.48 or later. Source CLI fields must point to `gh` or `lark-cli` wrappers; the host relies on their subcommands and output formats.
|
||||
- A repository on github.com; GitHub Enterprise Server is not supported. To create a checkout per Ticket, keep a local clone from which the prepare command can add worktrees. Install `git-lfs` if that repository uses Git LFS.
|
||||
- A model provider configured for DeepSeek Harness. dsh-superagent uses the profile's default model unless the Eval or Execution settings name another (optionally with a reasoning effort); a reused Session is re-pointed to its role's configured model on the next dispatch.
|
||||
- For Feishu: `lark-cli` (tested with 1.0.90) and a Feishu or Lark app with a bot
|
||||
|
||||
## Accounts
|
||||
|
||||
### Agent GitHub account
|
||||
|
||||
1. Create a GitHub user account for the agent and give it write access to the repository. A GitHub App cannot be the agent, because dsh-superagent reads the account's notifications.
|
||||
2. Give it a GitHub CLI that uses only its own credentials:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.local/bin
|
||||
cat > ~/.local/bin/gh-agent <<'EOF'
|
||||
#!/bin/sh
|
||||
GH_CONFIG_DIR="$HOME/.config/gh-agent" exec gh "$@"
|
||||
EOF
|
||||
chmod +x ~/.local/bin/gh-agent
|
||||
gh-agent auth login
|
||||
gh-agent api user --jq .login
|
||||
```
|
||||
|
||||
Sign in as the agent account; the last command prints its login. dsh-superagent reads the login the same way.
|
||||
|
||||
### Feishu bot (optional)
|
||||
|
||||
1. Create a self-built app with the bot capability. Grant it the permissions to receive message events, read and send chat messages, and read chat members, then publish the app version.
|
||||
2. Give the bot its own `lark-cli` configuration, restricted to the bot identity, and a wrapper:
|
||||
|
||||
```sh
|
||||
LARKSUITE_CLI_CONFIG_DIR="$HOME/.lark-cli-dsh-superagent" lark-cli config init --app-id <cli_…> --app-secret-stdin
|
||||
LARKSUITE_CLI_CONFIG_DIR="$HOME/.lark-cli-dsh-superagent" lark-cli config strict-mode bot --global
|
||||
cat > ~/.local/bin/lark-bot <<'EOF'
|
||||
#!/bin/sh
|
||||
export LARKSUITE_CLI_CONFIG_DIR="$HOME/.lark-cli-dsh-superagent"
|
||||
exec lark-cli "$@"
|
||||
EOF
|
||||
chmod +x ~/.local/bin/lark-bot
|
||||
```
|
||||
|
||||
The first command reads the app secret from standard input. Use a separate configuration directory: `lark-cli` allows one profile per app id, and your personal configuration may already contain a user login for that app.
|
||||
|
||||
The host refuses a CLI that can act as a user. It reads the app id, Feishu/Lark brand and bot `open_id` from the bot configuration.
|
||||
|
||||
Both configurations share the keychain entry for the app secret. To remove only the bot configuration, delete its directory; do not run `lark-cli config remove`, which would affect the shared secret.
|
||||
|
||||
## Install
|
||||
|
||||
Run this block once, on a machine that has no `superagent` profile yet:
|
||||
|
||||
```sh
|
||||
npm install -g @deepseek-ai/dsh@0.2.1-rc.3
|
||||
git clone <this repository> dsh-superagent && cd dsh-superagent
|
||||
pnpm install && pnpm build
|
||||
dsh superagent --from-default-profile web --dump-config > /dev/null
|
||||
dsh plugin --profile superagent add "link:$PWD"
|
||||
```
|
||||
|
||||
The pinned version above is public; other versions must satisfy `engines.dsh` in `package.json`.
|
||||
|
||||
The fourth command creates `~/.dsh/profiles/superagent` from the DeepSeek Harness Web template. The fifth installs this bundle. Set `DSH_HOME` to relocate the default `~/.dsh` directory.
|
||||
|
||||
Run the profile-creation command only for a new installation. If the profile already exists, that command replaces it and deletes its `cordis.patch.yml`, including machine configuration. Do not use it for upgrades.
|
||||
|
||||
## Machine configuration
|
||||
|
||||
Copy [examples/profile.cordis.patch.yml](../examples/profile.cordis.patch.yml) to `~/.dsh/profiles/superagent/cordis.patch.yml`, replacing that file, and replace every `<placeholder>`. For Feishu or the Gitea Source of this fork, also enable the `dsh-superagent-feishu` / `dsh-superagent-gitea` entry there. The `webserver` entry sets the panel's port.
|
||||
|
||||
| Key | Required | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `stateDir` | yes | Absolute path: the database, the lock, the command shims in `worker-bin`, the agent's git configuration and, by default, the Ticket directories in `work`. Back it up before upgrades |
|
||||
| `promptMaxChars`, `admissionContextMaxPages`, `admissionContextTimeoutMs` | no | The largest saved prompt configuration, and how many Thread pages one assessment reads within what time |
|
||||
|
||||
After the first start, edit machine limits in **Settings → Deployment** or the DeepSeek Harness Plugin Manager. Changes apply without restart. `stateDir` cannot be changed there. Invalid values or unknown configuration keys prevent startup; the log identifies the key.
|
||||
|
||||
## Start and stop
|
||||
|
||||
```sh
|
||||
scripts/dsh-superagent start
|
||||
scripts/dsh-superagent status
|
||||
```
|
||||
|
||||
`start` runs `dsh --profile superagent` in tmux session `dsh-superagent`, or `dsh-<profile>` for another profile. It waits up to one minute for the runtime to become ready or report a setup requirement. On a first run it prints the missing setup information. It refuses duplicate instances, including ones started outside this script.
|
||||
|
||||
`stop` and `restart` request graceful shutdown and wait up to five minutes for Executors. They never force-kill the process or touch other tmux sessions.
|
||||
|
||||
`status` prints process ids, health and setup problems. For a different profile or port, use `--profile` and `--url`, or set `DSH_SUPERAGENT_PROFILE` and `DSH_SUPERAGENT_URL`.
|
||||
|
||||
`status`, `start` and `restart` exit with one of four codes:
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| 0 | The service runs normally |
|
||||
| 1 | It does not run, runs twice, does not answer, or its runtime reports an error; a restart may fix it |
|
||||
| 2 | It runs, but a part other than the runtime, such as a Source or a Policy sync (`sync:<source id>`), reports an error; a restart does not fix it |
|
||||
| 3 | It runs and waits for setup; a restart does not fix it |
|
||||
|
||||
A Source that is still starting counts as normal operation. So does a Feishu bot that setup could not check.
|
||||
|
||||
`status` returns failed parts and their details under `errors`, and setup requirements under `setup`. `start` and `restart` print those problems as lines; they exit 1 if the service never comes up. Only an intentional `stop` pauses automatic recovery.
|
||||
|
||||
Each profile stores logs and control files under `.artifacts/`:
|
||||
|
||||
- Startup logs: `dsh-superagent-<profile>-<date>-<time>-<pid>.log`, where the pid belongs to the script. The newest 20 are kept.
|
||||
- Control files: `control/profiles/<profile>/`, containing the stop marker, lock and process id.
|
||||
|
||||
Open the URL printed in the log, then choose **Superagent** in the sidebar.
|
||||
|
||||
### Hourly check
|
||||
|
||||
Create the hourly check in the DeepSeek Harness profile you normally use, not in `superagent`. It must keep running when the agent service is down. Ask an agent in that profile to create an hourly schedule using this prompt, with the checkout path filled in:
|
||||
|
||||
```text
|
||||
Check dsh-superagent: run `scripts/dsh-superagent --profile superagent status` in <checkout>. Exit 0: do nothing and say nothing. Exit 2: do not restart; tell me each part under `errors` with its detail, unless you already told me about the same ones. Exit 3: do not restart; tell me what `setup` says setup needs, unless you already told me. If it reports manually_stopped: true, leave it stopped. Otherwise (exit 1) run `scripts/dsh-superagent --profile superagent --respect-stop restart`, which exits the same way: 0 means it recovered, so say nothing; 2 and 3 are reported as above; if it exits 1 or fails otherwise, read the newest .artifacts/dsh-superagent-superagent-*.log and the Troubleshooting section of docs/006-deployment.md, fix what you can, and tell me what was wrong and what you did.
|
||||
```
|
||||
|
||||
`--respect-stop` preserves an intentional stop. Only `stop` writes the marker; `start` clears it. `restart` writes no marker, so the check can recover a restart whose shutdown timed out.
|
||||
|
||||
## Settings
|
||||
|
||||
Setup must succeed before polling or work starts. Overview lists missing requirements and links to their settings. A settings change runs setup again. If you fix something outside the application, such as CLI authentication, choose **Check again**.
|
||||
|
||||
1. **Sources.** Set the GitHub repository (`owner/name`) and agent CLI, for example `/home/me/.local/bin/gh-agent`. Requests normally arrive through the agent account's notifications. A new Issue or comment mentioning the account arrives on the next poll; adding a mention by editing an existing Issue body does not. Post a new comment instead. For Feishu, set the bot CLI, chats, each chat's receive-all or mentions-only mode, and whether to receive direct messages.
|
||||
2. **Policy.** The two example rules can be edited or removed. Under **On each Source**, add the GitHub accounts and Feishu users who may ask for work; until a list has someone, no request can become a Ticket. With Feishu, a chat can be chosen to sync the list from, at startup and on **Sync now**.
|
||||
3. **Execution.** The directory root and a prepare command. To give each Ticket a checkout of the repository:
|
||||
|
||||
```sh
|
||||
git -C /home/me/src/widgets fetch --no-tags "https://github.com/$DSH_SUPERAGENT_GITHUB_REPOSITORY.git" "${DSH_SUPERAGENT_GITHUB_PR_HEAD:-HEAD}:refs/dsh-superagent/$DSH_SUPERAGENT_TICKET" &&
|
||||
git -C /home/me/src/widgets worktree add -b "agent/$DSH_SUPERAGENT_NAME" "$DSH_SUPERAGENT_DIR" "refs/dsh-superagent/$DSH_SUPERAGENT_TICKET"
|
||||
```
|
||||
|
||||
List personal CLIs, such as one signed in to your own account, as blocked commands.
|
||||
4. **Prompts.** Set the team sections: the GitHub disclosure line, review bots, the commit author and the auto-merge condition.
|
||||
|
||||
### Management API
|
||||
|
||||
The panel uses a local HTTP API that agents can also call. `SETUP.md` uses it to save settings, and the service script reads `GET state`.
|
||||
|
||||
Send requests to `http://127.0.0.1:<port>/orchestrator/api/` with `x-orchestrator-request: 1`. The host must be `127.0.0.1:<port>` or `localhost:<port>`. Missing headers or an `Origin` from another site receive 403.
|
||||
|
||||
API saves have the panel's permissions. They can change execution environment and setup account fields, unlike saves made through the main Session's tools.
|
||||
|
||||
Agents may rely on these routes and fields. The other routes in `src/app/api.ts` serve the panel and change with it.
|
||||
|
||||
| Request | Answer |
|
||||
| --- | --- |
|
||||
| `GET state` | What every page reads: `setup`, with `problem` and `feishuProblem`, each `null` or a setup problem with `place`, `code`, `values` and `detail`, and `accounts` once setup resolved them; `health`, an entry per part, below, with `state` (`starting`, `ready`, `paused` or `error`), `detail` and `at`; `load`, the Executor Sessions `running`, the Tickets `queued` and `maxWorkers`; `paused`; and `masterId` |
|
||||
| `GET settings` | `blocks`: `policy`, `eval` and `executor`, each with its `value` and its `revision`; `sources`: the Source instances, each with `id`, `kind`, `installed`, its block's `value`, `{"enabled": …, "config": {…}}`, and `revision`, its kind's form, and its setup `problem` when it has one; `policyVersion`; and `environment`, the execution environment's `root` and `variables` |
|
||||
| `GET sources` | Each Source's status: `state` (`missing`, `off`, `error`, `setup`, `starting`, `paused` or `ready`), the `health` of its parts, its setup `problem`, the last day's `stats`, the Triggers it `filtered` in that day, and the messages it `dropped` in that day, newest drop first, each with its `channel`, `preview`, `ticket` (`id` and `title`) when it was for one, its `thread` when the store has it, and `dropped.at` and `dropped.reason`, `channelGone` or `channelIgnored` |
|
||||
| `PUT settings/<block>` with `{"value": …, "revision": n}` | Saves one block: `saved`, the block's new `revision`, `policy_changed` and `restarting`. `value` is the whole block, as `GET settings` holds it; a missing key takes its default |
|
||||
| `POST executor/pause` with `{"paused": true}` or `false` | Pauses or resumes processing: `paused` |
|
||||
| `POST setup/check` | Runs setup again by restarting dsh-superagent: `restarting: true`. Refused when setup has nothing to fix |
|
||||
| `POST policy-sync/<source id>` | Syncs that Source's allowed list now: `policy`, the Source's part as saved |
|
||||
| `GET prompts`, `PUT prompts` with `{"config", "baseRevision"}` | Reads the prompt configuration: `current`, with its `revision` and `config`, `defaults`, `sections`, the team sections in the order Sessions read them, `skillName`, the pattern of a team skill's name, `builtin`, the names a team skill cannot take, and `skillPrefix`, which Sessions load a skill's name under. Or saves it: the new `revision` |
|
||||
| `POST tickets/<id>/retry`, `POST tickets/<id>/stop` | Retries or stops a Ticket, as the panel does |
|
||||
|
||||
Health entries are grouped by responsibility:
|
||||
|
||||
| Entry | Reports |
|
||||
| --- | --- |
|
||||
| `runtime` | The application itself |
|
||||
| `models` | Sessions waiting on a model-budget retry |
|
||||
| `evals` | Unavailable Evals from the last hour with no later judgement or dismissal |
|
||||
| `assessment`, `launch`, `delivery`, `relay`, `notice`, `reply` | Stored work described in [003 Execution](003-execution.md); delivery counts waiting work, while reply covers Channel messages |
|
||||
| `worker` | Executor Session event handling; appears after its first failure |
|
||||
| `github`, `feishu`, `history` | Source polling, events and history recovery |
|
||||
| `sync:<source id>` | Allowed-list synchronization |
|
||||
|
||||
A refusal has a status and `error`, its English text:
|
||||
|
||||
- 400: an invalid value (`issues`, each with its `path`), a failed account check (`setup`), a fixed refusal (`code` and `values`), or a body that is not JSON or is too large.
|
||||
- 404: something that does not exist.
|
||||
- 409: a stale revision.
|
||||
- 500: anything else.
|
||||
|
||||
The blocks:
|
||||
|
||||
- `source:github`, `source:feishu`: `{"enabled": true, "config": {…}}`. GitHub's config has `repository` and `cli`, and the switches `observeIssues`, `observePulls`, `observeComments` and `observeCi`, off by default, which receive every new Issue, new pull request and comment in the repository and the default branch's CI; Feishu's has `cli`, `chats` as `[{"chatId": "oc_…", "name": "…", "listen": "mentions"}]` (or `"all"`), `directMessages`, and `otherChats` as `"none"` or `"mentions"`.
|
||||
- `policy`: `{"rules": […], "sources": {"github": {"allowed": [{"id": "<login>"}], "bots": [{"id": "dependabot[bot]"}]}, "feishu": {"allowed": [{"id": "ou_…", "name": "…"}], "syncChat": "oc_…", "admission": {"oc_…": {"unlistedAddressed": "explicit"}}}}}`. `bots` lists the bots whose messages may become inferred candidates. An `admission` row is keyed by chat id, `other` or `direct` for Feishu, and by `issues`, `pulls`, `comments` or `ci` for GitHub; an unset cell takes its default (see [002 Authorization](002-authorization.md)).
|
||||
- `eval`: `mode`, `timeoutSeconds` and `model`.
|
||||
- `executor`: `model`, `maxWorkers`, `directory`, `prepare` and `blockedCommands`. The processing pause is a setting of its own, `paused`, which `POST executor/pause` sets.
|
||||
|
||||
Changing a repository or CLI runs the corresponding setup check first. A failed check returns 400 with details in `setup`. A successful account change, or any save while setup is pending, returns `restarting: true`. The API is temporarily unavailable while the application restarts.
|
||||
|
||||
```sh
|
||||
api=http://127.0.0.1:3082/orchestrator/api
|
||||
rev=$(curl -s -H 'x-orchestrator-request: 1' $api/settings | jq '.sources[] | select(.id == "github") | .revision')
|
||||
curl -s -X PUT -H 'x-orchestrator-request: 1' -H 'content-type: application/json' $api/settings/source:github \
|
||||
-d "{\"value\": {\"enabled\": true, \"config\": {\"repository\": \"acme/widgets\", \"cli\": \"$HOME/.local/bin/gh-agent\"}}, \"revision\": $rev}"
|
||||
```
|
||||
|
||||
## Setup checks
|
||||
|
||||
Setup uses the DeepSeek Harness subprocess service, as worker shells do. It removes environment variables whose names contain KEY, PASSWORD, SECRET or TOKEN.
|
||||
|
||||
Setup verifies the GitHub repository and the agent's push access, writes the worker environment, and checks that git uses only the agent credentials for github.com. A Feishu setup failure disables only the Feishu Source.
|
||||
|
||||
The process environment that setup writes:
|
||||
|
||||
- **Command shims** in `<stateDir>/worker-bin`, first on `PATH`: `gh` runs the agent's CLI; `lark-cli` runs the bot's CLI; blocked commands fail. A shim removes its own directory from `PATH` before it runs its target.
|
||||
- **git configuration** in `<stateDir>/worker-gitconfig`: replaces the operator's global and system git configuration. It selects the agent CLI as the github.com credential helper, rewrites GitHub SSH addresses to HTTPS and defines Git LFS filters. Personal git settings such as an HTTP proxy do not apply. No commit identity is set here, git never prompts for credentials, and `GH_TOKEN` and `GITHUB_TOKEN` are removed.
|
||||
|
||||
A file in `worker-bin` or `worker-gitconfig` that dsh-superagent did not write stops setup. A Ticket directory inside a git repository whose own configuration adds credentials fails the Ticket.
|
||||
|
||||
## Try it
|
||||
|
||||
From an allowed GitHub account, open an Issue or post a comment containing `@<agent login> <request>`. Within a polling interval (one minute by default), Overview should show a Ticket. The agent acknowledges the request, works in its own Session and directory, and delivers in the original conversation. For Feishu, mention the bot in a configured chat.
|
||||
|
||||
## Upgrade
|
||||
|
||||
1. Wait until no Executor is in the middle of a turn.
|
||||
2. Back up `<stateDir>/orchestrator.sqlite`.
|
||||
3. Run `git pull && pnpm install && pnpm build`.
|
||||
4. Run `scripts/dsh-superagent restart` and inspect its status.
|
||||
|
||||
A restart ends running Assignments and starts replacements with recovery notes. Team text and overridden prompt parts are retained; parts without overrides follow the new defaults. Review parts marked **Default updated** after upgrading.
|
||||
|
||||
A database migrated by a newer version cannot be opened by an older one. Rollback therefore requires the pre-upgrade backup. After upgrading DeepSeek Harness itself, restart the running instance so the panel loads matching artifacts.
|
||||
|
||||
### Agent presets
|
||||
|
||||
The worker and router presets move shell commands to the background after 5 seconds. A Session cannot load without its preset. Keep the `dsh-superagent-worker` and `dsh-superagent-router` rows in `cordis.patch.yml` while their Sessions are in use; otherwise resume fails with `agent-preset/not-found`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Setup problems appear on the overview with a link to the fix, in the panel's language; the log and the API show the English text below.
|
||||
|
||||
| Symptom | Cause and fix |
|
||||
| --- | --- |
|
||||
| `The agent GitHub CLI is not set.` | Set the agent's CLI on the GitHub card |
|
||||
| `The agent GitHub CLI was not found.` | Set the wrapper's absolute path, or a command name on `PATH` |
|
||||
| `The agent GitHub CLI cannot read its account` | Sign the wrapper in with `auth login` |
|
||||
| `@… cannot read …` or `@… cannot push to …` | Give the agent account write access to the repository |
|
||||
| `The GitHub Source names no repository.` | Set the repository, `owner/name`, on the GitHub card |
|
||||
| A Ticket fails with `The prepare command failed: …` | The output names the cause, such as a wrong clone path; fix the command, then retry the Ticket |
|
||||
| `git does not read the github.com credential configuration dsh-superagent writes` | Install git 2.32 or later |
|
||||
| A Ticket fails with `The git configuration of the repository in … sets …` | Remove the named credential or `http.*.extraheader` settings from the clone's `.git/config` |
|
||||
| `… was not written by dsh-superagent` | Move the named file out of `<stateDir>/worker-bin` or `<stateDir>/worker-gitconfig`, or choose another `stateDir` |
|
||||
| `The bot lark-cli allows user identities` | Point the Feishu Source at a bot-only configuration, as in Feishu bot above |
|
||||
| `The bot lark-cli does not work.` or `The bot identity of app … is …` | Check the configuration's app id and secret |
|
||||
| `The Feishu bot is not a member of the chat.` | Add the bot to the chat in the chat's settings; the next attempt succeeds |
|
||||
| `status` exits 2 | A part reports an error, which a restart does not fix: `errors` names each part, as listed under Management API, with its detail; each part clears its own entry once it works again |
|
||||
| `status` exits 3 | Setup waits: `setup` says what is missing, and the overview links to the setting that fixes it |
|
||||
| `status` exits 1 | The service does not run, runs twice, does not answer, or its runtime reports an error; the output and the newest log say which |
|
||||
| `… timed out after 90 s`, `… was stopped by …`, `… exited with code …` or `… could not run: …` | A CLI failed without printing why; run the named program by hand as the operator's user to see its own message |
|
||||
| The panel says dsh-superagent is not answering | It is restarting, or did not start because of its configuration; the DeepSeek Harness log names the cause |
|
||||
| `dsh-superagent configuration has keys it does not use` (log) | Remove the named keys; the message says where each value lives now |
|
||||
| The log says the plugin is incompatible with the running dsh | Install a DeepSeek Harness version within the `engines.dsh` range |
|
||||
| The panel shows "Failed to load plugins" after a DeepSeek Harness upgrade | Restart the running instance |
|
||||
| Nothing happens after a mention | Check that the author is on the Source's allowed list, the Source is on, and processing is not paused. On GitHub, a mention added by editing an Issue's body is not received; mention the agent in a comment |
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
# 006 部署
|
||||
|
||||
用这份指南安装、配置并运行 dsh-superagent。它覆盖账号、启动检查、升级与排障,建立在 [001 领域模型](001-领域模型.md) 至 [005 面板](005-面板.md) 之上。
|
||||
|
||||
## 运行的是什么
|
||||
|
||||
- **一个专用的 DeepSeek Harness profile**,本指南称 `superagent`。从 Web profile 模板创建。它加载 bundle,并在 `127.0.0.1` 上提供它的面板。把它与你日常干活的 profile 分开:插件会改动进程级的 `PATH` 与 git 设置,它的进程也需要独立重启与升级。
|
||||
- **一个 agent GitHub 账号。** agent 以这个账号发评论、推分支、开 pull request,通过一个只登录了该账号的 GitHub CLI。
|
||||
- **可选:** 一个用于聊天请求的 Feishu bot。
|
||||
|
||||
插件配置持有状态目录与机器限额。其余一切用面板:Sources 管仓库与账号,Policy 管允许名单,Execution 管 worker 目录与命令。
|
||||
|
||||
### 权限与隔离
|
||||
|
||||
bundle 为 Executors 定义了 `dsh-superagent-worker`,为 main Session 定义了 `dsh-superagent-router`。它们会出现在 DeepSeek Harness 的预设列表里,但只应给 dsh-superagent 用。不要给个人 Sessions 选它们,也不要禁用它们的配置行。应用需要它们来创建和恢复自己的 Sessions。
|
||||
|
||||
在 Web UI 里打开一个空白的受管 Session 时,可能选中 profile 的默认预设。dsh-superagent 会在第一个回合之前恢复它的角色专属预设。
|
||||
|
||||
两个预设都以操作者的 OS 用户身份、用 `danger-full-access` 运行,并从被订阅的 Threads 读取消息。账号包装器默认挑 agent 的凭据;它们不隔离进程。worker 可以按路径执行程序、读取操作者的 home 目录。要隔离,就用一个只装 agent 凭据的专用 OS 账号。
|
||||
|
||||
## 前提
|
||||
|
||||
- Node.js 22.19+ 或 24+、pnpm、git 2.32+、Python 3.8+ 与 tmux
|
||||
- GitHub CLI(`gh`)2.48 或更新。Source 的 CLI 字段必须指向 `gh` 或 `lark-cli` 包装器;host 依赖它们的子命令与输出格式。
|
||||
- github.com 上的一个仓库;不支持 GitHub Enterprise Server。要为每个 Ticket 建一个 checkout,保留一份本地 clone,让 prepare command 能在其中添加 worktrees。那个仓库若用 Git LFS,安装 `git-lfs`。
|
||||
- 为 DeepSeek Harness 配置好的 model provider。dsh-superagent 使用 profile 的默认模型,除非 Eval 或 Execution 设置点名了另一个(可以带 reasoning effort);被复用的 Session 会在下一次派发时被重新指到其角色配置的模型。
|
||||
- 用 Feishu 则需要:`lark-cli`(测试于 1.0.90),以及一个带 bot 的 Feishu 或 Lark 应用
|
||||
|
||||
## 账号
|
||||
|
||||
### agent GitHub 账号
|
||||
|
||||
1. 为 agent 建一个 GitHub 用户账号,授予它对仓库的写权限。GitHub App 不能当 agent,因为 dsh-superagent 读取该账号的 notifications。
|
||||
2. 给它一个只用自己凭据的 GitHub CLI:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.local/bin
|
||||
cat > ~/.local/bin/gh-agent <<'EOF'
|
||||
#!/bin/sh
|
||||
GH_CONFIG_DIR="$HOME/.config/gh-agent" exec gh "$@"
|
||||
EOF
|
||||
chmod +x ~/.local/bin/gh-agent
|
||||
gh-agent auth login
|
||||
gh-agent api user --jq .login
|
||||
```
|
||||
|
||||
以 agent 账号登录;最后一条命令会打印它的 login。dsh-superagent 用同样的方式读 login。
|
||||
|
||||
### Feishu bot(可选)
|
||||
|
||||
1. 创建一个带 bot 能力的自建应用。授予它接收消息事件、读取与发送 chat 消息、读取 chat 成员的权限,然后发布应用版本。
|
||||
2. 给 bot 一份它自己的 `lark-cli` 配置,限定 bot 身份,外加一个包装器:
|
||||
|
||||
```sh
|
||||
LARKSUITE_CLI_CONFIG_DIR="$HOME/.lark-cli-dsh-superagent" lark-cli config init --app-id <cli_…> --app-secret-stdin
|
||||
LARKSUITE_CLI_CONFIG_DIR="$HOME/.lark-cli-dsh-superagent" lark-cli config strict-mode bot --global
|
||||
cat > ~/.local/bin/lark-bot <<'EOF'
|
||||
#!/bin/sh
|
||||
export LARKSUITE_CLI_CONFIG_DIR="$HOME/.lark-cli-dsh-superagent"
|
||||
exec lark-cli "$@"
|
||||
EOF
|
||||
chmod +x ~/.local/bin/lark-bot
|
||||
```
|
||||
|
||||
第一条命令从标准输入读 app secret。用一个独立的配置目录:`lark-cli` 每个 app id 只允许一个 profile,而你的个人配置里可能已经有该应用的用户登录。
|
||||
|
||||
host 会拒绝一个能扮演用户的 CLI。它从 bot 配置读取 app id、Feishu/Lark 品牌与 bot 的 `open_id`。
|
||||
|
||||
两份配置共享 keychain 里 app secret 的那个条目。要只移除 bot 配置,删它的目录即可;不要运行 `lark-cli config remove`,那会影响共享的 secret。
|
||||
|
||||
## 安装
|
||||
|
||||
在还没有 `superagent` profile 的机器上,把这个块跑一遍:
|
||||
|
||||
```sh
|
||||
npm install -g @deepseek-ai/dsh@0.2.1-rc.3
|
||||
git clone <this repository> dsh-superagent && cd dsh-superagent
|
||||
pnpm install && pnpm build
|
||||
dsh superagent --from-default-profile web --dump-config > /dev/null
|
||||
dsh plugin --profile superagent add "link:$PWD"
|
||||
```
|
||||
|
||||
上面钉住的是公开版本;其他版本必须满足 `package.json` 里的 `engines.dsh`。
|
||||
|
||||
第四条命令从 DeepSeek Harness Web 模板创建 `~/.dsh/profiles/superagent`。第五条安装本 bundle。用 `DSH_HOME` 可以改变默认的 `~/.dsh` 目录位置。
|
||||
|
||||
profile 创建命令只在新安装时运行。若该 profile 已存在,那条命令会替换它并删掉它的 `cordis.patch.yml`,机器配置也一并没了。不要拿它做升级。
|
||||
|
||||
## 机器配置
|
||||
|
||||
把 [examples/profile.cordis.patch.yml](../examples/profile.cordis.patch.yml) 复制为 `~/.dsh/profiles/superagent/cordis.patch.yml`(替换那个文件),并替换每一个 `<placeholder>`。要用 Feishu 或本 fork 的 Gitea Source,就同时启用那里的 `dsh-superagent-feishu` / `dsh-superagent-gitea` 条目。`webserver` 条目设定面板的端口。
|
||||
|
||||
| 键 | 必填 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `stateDir` | 是 | 绝对路径:数据库、锁、`worker-bin` 里的命令垫片、agent 的 git 配置,默认还包括 `work` 下的 Ticket 目录。升级前先备份 |
|
||||
| `promptMaxChars`、`admissionContextMaxPages`、`admissionContextTimeoutMs` | 否 | 可保存的提示词配置的最大体积;一次研判在多长的时间窗内读多少个 Thread 分页 |
|
||||
|
||||
首次启动之后,机器限额到 **Settings → Deployment** 或 DeepSeek Harness Plugin Manager 里改。改动无需重启即生效。`stateDir` 不能在那里改。非法值或未知配置键会阻止启动;日志会指出是哪个键。
|
||||
|
||||
## 启动与停止
|
||||
|
||||
```sh
|
||||
scripts/dsh-superagent start
|
||||
scripts/dsh-superagent status
|
||||
```
|
||||
|
||||
`start` 在 tmux 会话 `dsh-superagent` 里运行 `dsh --profile superagent`,其他 profile 则是 `dsh-<profile>`。它至多等一分钟,等 runtime 就绪或报告一个设置需求。首次运行会打印缺少的设置信息。它拒绝重复实例,包括在本脚本之外启动的实例。
|
||||
|
||||
`stop` 与 `restart` 请求优雅关闭,并至多给 Executors 五分钟。它们从不强杀进程,也不碰其他 tmux 会话。
|
||||
|
||||
`status` 打印进程号、健康状况与设置问题。要换 profile 或端口,用 `--profile` 与 `--url`,或设置 `DSH_SUPERAGENT_PROFILE` 与 `DSH_SUPERAGENT_URL`。
|
||||
|
||||
`status`、`start` 与 `restart` 以四种退出码之一结束:
|
||||
|
||||
| 退出码 | 含义 |
|
||||
| --- | --- |
|
||||
| 0 | 服务正常运行 |
|
||||
| 1 | 它没在跑、跑了双份、不回应、或其 runtime 报错;重启可能修好 |
|
||||
| 2 | 它在跑,但 runtime 之外的某个部件——如某个 Source 或某次 Policy 同步(`sync:<source id>`)——在报错;重启修不好 |
|
||||
| 3 | 它在跑,但在等待设置;重启修不好 |
|
||||
|
||||
还在启动中的 Source 算正常运行。设置检查没能检查到的 Feishu bot 也算。
|
||||
|
||||
`status` 在 `errors` 下返回失败的部件及其细节,在 `setup` 下返回设置需求。`start` 与 `restart` 把这些问题逐行打印;服务始终起不来时它们以 1 退出。只有刻意的 `stop` 会暂停自动恢复。
|
||||
|
||||
每个 profile 把日志与控制文件存放在 `.artifacts/` 下:
|
||||
|
||||
- 启动日志:`dsh-superagent-<profile>-<date>-<time>-<pid>.log`,其中 pid 属于脚本。保留最新 20 个。
|
||||
- 控制文件:`control/profiles/<profile>/`,内含停止标记、锁与进程号。
|
||||
|
||||
打开日志里打印的 URL,然后在侧边栏选 **Superagent**。
|
||||
|
||||
### 每小时检查
|
||||
|
||||
每小时检查建在你平时使用的 DeepSeek Harness profile 里,不要建在 `superagent` 里。agent 服务宕机时它必须还能运行。在那个 profile 里让一个 agent 用下面这段提示词创建一个每小时计划,并把 checkout 路径填进去:
|
||||
|
||||
```text
|
||||
Check dsh-superagent: run `scripts/dsh-superagent --profile superagent status` in <checkout>. Exit 0: do nothing and say nothing. Exit 2: do not restart; tell me each part under `errors` with its detail, unless you already told me about the same ones. Exit 3: do not restart; tell me what `setup` says setup needs, unless you already told me. If it reports manually_stopped: true, leave it stopped. Otherwise (exit 1) run `scripts/dsh-superagent --profile superagent --respect-stop restart`, which exits the same way: 0 means it recovered, so say nothing; 2 and 3 are reported as above; if it exits 1 or fails otherwise, read the newest .artifacts/dsh-superagent-superagent-*.log and the Troubleshooting section of docs/006-部署.md, fix what you can, and tell me what was wrong and what you did.
|
||||
```
|
||||
|
||||
`--respect-stop` 保留一次刻意的停止。只有 `stop` 写那个标记;`start` 清除它。`restart` 不写标记,所以这个检查能恢复一次因关闭超时而中断的重启。
|
||||
|
||||
## 设置
|
||||
|
||||
设置必须成功,轮询与工作才会开始。Overview 列出缺失的需求并链接到对应设置。一次设置改动会重新运行设置检查。若你在应用之外修好了什么——例如 CLI 认证——点 **Check again**。
|
||||
|
||||
1. **Sources。** 设置 GitHub 仓库(`owner/name`)与 agent CLI,例如 `/home/me/.local/bin/gh-agent`。请求通常经 agent 账号的 notifications 到达。新 Issue 或提到该账号的评论会在下一次轮询到达;靠编辑已有 Issue 正文补一个 @ 是不会到的——请改发一条新评论。用 Feishu 时,设置 bot CLI、chats、每个 chat 的全量接收或仅 @ 模式、以及是否接收私聊。
|
||||
2. **Policy。** 两条示例规则可以编辑或删除。在 **On each Source** 下,添加可以请求工作的 GitHub 账号与 Feishu 用户;在名单里有了人之前,任何请求都不可能成为 Ticket。用 Feishu 时,可以选一个 chat 作为名单同步的来源,在启动时与点 **Sync now** 时同步。
|
||||
3. **Execution。** 目录根与一条 prepare command。要让每个 Ticket 得到一份仓库的 checkout:
|
||||
|
||||
```sh
|
||||
git -C /home/me/src/widgets fetch --no-tags "https://github.com/$DSH_SUPERAGENT_GITHUB_REPOSITORY.git" "${DSH_SUPERAGENT_GITHUB_PR_HEAD:-HEAD}:refs/dsh-superagent/$DSH_SUPERAGENT_TICKET" &&
|
||||
git -C /home/me/src/widgets worktree add -b "agent/$DSH_SUPERAGENT_NAME" "$DSH_SUPERAGENT_DIR" "refs/dsh-superagent/$DSH_SUPERAGENT_TICKET"
|
||||
```
|
||||
|
||||
把个人 CLI——例如登录了你自己账号的那些——列为被屏蔽的命令。
|
||||
4. **Prompts。** 设置团队分节:GitHub 披露句、review bots、commit 作者与 auto-merge 条件。
|
||||
|
||||
### Management API
|
||||
|
||||
面板使用一个本地 HTTP API,agent 也可以调用它。`SETUP.md` 用它保存设置,服务脚本读取 `GET state`。
|
||||
|
||||
向 `http://127.0.0.1:<port>/orchestrator/api/` 发送请求,带上 `x-orchestrator-request: 1`。host 必须是 `127.0.0.1:<port>` 或 `localhost:<port>`。缺少请求头、或 `Origin` 来自别的站点,都会得到 403。
|
||||
|
||||
API 保存拥有面板的全部权限。与通过 main Session 工具做的保存不同,它可以修改执行环境与设置里的账号字段。
|
||||
|
||||
agent 可以依赖这些路由与字段。`src/app/api.ts` 里的其他路由服务于面板,并随面板一起变化。
|
||||
|
||||
| 请求 | 应答 |
|
||||
| --- | --- |
|
||||
| `GET state` | 每个页面都会读的内容:`setup`,其中 `problem` 与 `feishuProblem` 各为 `null`、或一个带 `place`、`code`、`values` 与 `detail` 的设置问题,设置解析出账号后还有 `accounts`;`health`,每个部件一个条目(见下),带 `state`(`starting`、`ready`、`paused` 或 `error`)、`detail` 与 `at`;`load`,即 Executor Sessions 的 `running`、Tickets 的 `queued` 与 `maxWorkers`;`paused`;以及 `masterId` |
|
||||
| `GET settings` | `blocks`:`policy`、`eval` 与 `executor`,各含其 `value` 与其 `revision`;`sources`:各 Source 实例,各含 `id`、`kind`、`installed`、其 block 的 `value`(`{"enabled": …, "config": {…}}`)与 `revision`、其 kind 的表单,以及有设置问题时的 `problem`;`policyVersion`;以及 `environment`,即执行环境的 `root` 与 `variables` |
|
||||
| `GET sources` | 每个 Source 的状态:`state`(`missing`、`off`、`error`、`setup`、`starting`、`paused` 或 `ready`)、其各部件的 `health`、其设置 `problem`、最近一天的 `stats`、当天它 `filtered` 的 Triggers、当天它 `dropped` 的消息(最新丢弃在前,每条含其 `channel`、`preview`、属于某 Ticket 时的 `ticket`(`id` 与 `title`)、存储里有它时的 `thread`、以及 `dropped.at` 与 `dropped.reason`——`channelGone` 或 `channelIgnored`) |
|
||||
| `PUT settings/<block>`,体为 `{"value": …, "revision": n}` | 保存一个 block:`saved`、该 block 的新 `revision`、`policy_changed` 与 `restarting`。`value` 是完整的 block,如同 `GET settings` 返回的那样;缺失的键取默认值 |
|
||||
| `POST executor/pause`,体为 `{"paused": true}` 或 `false` | 暂停或恢复处理:`paused` |
|
||||
| `POST setup/check` | 通过重启 dsh-superagent 重新运行设置:`restarting: true`。设置无可修复时会被拒绝 |
|
||||
| `POST policy-sync/<source id>` | 立刻同步该 Source 的允许名单:`policy`,即已保存的该 Source 的部分 |
|
||||
| `GET prompts`、`PUT prompts`,体为 `{"config", "baseRevision"}` | 读取提示词配置:`current`(含其 `revision` 与 `config`)、`defaults`、`sections`(团队分节按 Sessions 读取的顺序)、`skillName`(团队技能名的模式)、`builtin`(团队技能不能占用的名字),以及 `skillPrefix`(Sessions 在该前缀下加载技能名)。或保存它:返回新 `revision` |
|
||||
| `POST tickets/<id>/retry`、`POST tickets/<id>/stop` | 重试或停止一个 Ticket,与面板一致 |
|
||||
|
||||
健康条目按职责分组:
|
||||
|
||||
| 条目 | 报告 |
|
||||
| --- | --- |
|
||||
| `runtime` | 应用自身 |
|
||||
| `models` | 正在等模型预算重试的 Sessions |
|
||||
| `evals` | 最近一小时内、没有更晚判断也没有被 dismiss 的 unavailable Evals |
|
||||
| `assessment`、`launch`、`delivery`、`relay`、`notice`、`reply` | [003 执行](003-执行.md) 所述的 stored work;delivery 统计排队中的工作,reply 覆盖 Channel 消息 |
|
||||
| `worker` | Executor Session 的事件处理;在其首次失败之后出现 |
|
||||
| `github`、`feishu`、`history` | Source 轮询、事件与历史恢复 |
|
||||
| `sync:<source id>` | 允许名单同步 |
|
||||
|
||||
一次拒绝有一个状态码与 `error`,即它的英文文本:
|
||||
|
||||
- 400:非法值(`issues`,每条含其 `path`)、失败的账号检查(`setup`)、固定的拒绝理由(`code` 与 `values`)、或不是 JSON / 过大的请求体。
|
||||
- 404:不存在的东西。
|
||||
- 409:过期的 revision。
|
||||
- 500:其他一切。
|
||||
|
||||
各 block:
|
||||
|
||||
- `source:github`、`source:feishu`:`{"enabled": true, "config": {…}}`。GitHub 的 config 有 `repository` 与 `cli`,以及默认关闭的开关 `observeIssues`、`observePulls`、`observeComments` 与 `observeCi`——它们分别接收仓库中每个新 Issue、新 pull request、评论与默认分支的 CI;Feishu 的有 `cli`、`chats`(形如 `[{"chatId": "oc_…", "name": "…", "listen": "mentions"}]`,或 `"all"`)、`directMessages`,以及取 `"none"` 或 `"mentions"` 的 `otherChats`。
|
||||
- `policy`:`{"rules": […], "sources": {"github": {"allowed": [{"id": "<login>"}], "bots": [{"id": "dependabot[bot]"}]}, "feishu": {"allowed": [{"id": "ou_…", "name": "…"}], "syncChat": "oc_…", "admission": {"oc_…": {"unlistedAddressed": "explicit"}}}}}`。`bots` 列出消息可以成为 inferred 候选的 bots。`admission` 的行以 chat id 为键——Feishu 还有 `other` 或 `direct`——GitHub 按 `issues`、`pulls`、`comments` 或 `ci`;未设置的单元格取其默认(见 [002 授权](002-授权.md))。
|
||||
- `eval`:`mode`、`timeoutSeconds` 与 `model`。
|
||||
- `executor`:`model`、`maxWorkers`、`directory`、`prepare` 与 `blockedCommands`。处理暂停是它自己的一项设置 `paused`,由 `POST executor/pause` 设定。
|
||||
|
||||
修改仓库或 CLI 会先运行对应的设置检查。检查失败返回 400,细节在 `setup` 里。一次成功的账号变更、或设置未就绪期间的任何保存,返回 `restarting: true`。应用重启期间 API 暂时不可用。
|
||||
|
||||
```sh
|
||||
api=http://127.0.0.1:3082/orchestrator/api
|
||||
rev=$(curl -s -H 'x-orchestrator-request: 1' $api/settings | jq '.sources[] | select(.id == "github") | .revision')
|
||||
curl -s -X PUT -H 'x-orchestrator-request: 1' -H 'content-type: application/json' $api/settings/source:github \
|
||||
-d "{\"value\": {\"enabled\": true, \"config\": {\"repository\": \"acme/widgets\", \"cli\": \"$HOME/.local/bin/gh-agent\"}}, \"revision\": $rev}"
|
||||
```
|
||||
|
||||
## 设置检查
|
||||
|
||||
设置检查使用 DeepSeek Harness 的 subprocess service,与 worker shell 相同。它会移除名字里含 KEY、PASSWORD、SECRET 或 TOKEN 的环境变量。
|
||||
|
||||
设置检查会验证 GitHub 仓库与 agent 的推送权限、写入 worker 环境,并确认 git 对 github.com 只使用 agent 的凭据。Feishu 的设置检查失败只禁用 Feishu Source。
|
||||
|
||||
设置检查写出的进程环境:
|
||||
|
||||
- **命令垫片**在 `<stateDir>/worker-bin`,位于 `PATH` 最前:`gh` 运行 agent 的 CLI;`lark-cli` 运行 bot 的 CLI;被屏蔽的命令失败。一个垫片在运行它的目标之前,先从 `PATH` 里移除自己所在的目录。
|
||||
- **git 配置**在 `<stateDir>/worker-gitconfig`:替换操作者的全局与系统 git 配置。它选择 agent CLI 作为 github.com 的凭据 helper,把 GitHub 的 SSH 地址重写为 HTTPS,并定义 Git LFS 过滤器。个人 git 设置——例如 HTTP 代理——不生效。这里不设提交身份,git 从不弹凭据提示,`GH_TOKEN` 与 `GITHUB_TOKEN` 会被移除。
|
||||
|
||||
`worker-bin` 或 `worker-gitconfig` 里有一个并非 dsh-superagent 写入的文件,设置检查就会停住。一个处在 git 仓库之内的 Ticket 目录,若该仓库自己的配置添加了凭据,这个 Ticket 就失败。
|
||||
|
||||
## 试一把
|
||||
|
||||
用一个被允许的 GitHub 账号,开一个 Issue,或发一条包含 `@<agent login> <request>` 的评论。在一个轮询间隔内(默认一分钟),Overview 应该出现一个 Ticket。agent 会确认请求、在它自己的 Session 与目录里干活,并在原始对话里交付。用 Feishu 时,在配置好的 chat 里 @ bot。
|
||||
|
||||
## 升级
|
||||
|
||||
1. 等到没有 Executor 正处于回合中间。
|
||||
2. 备份 `<stateDir>/orchestrator.sqlite`。
|
||||
3. 运行 `git pull && pnpm install && pnpm build`。
|
||||
4. 运行 `scripts/dsh-superagent restart` 并检查它的状态。
|
||||
|
||||
一次重启会结束运行中的 Assignments,并带着恢复备注启动接替者。团队文本与被覆盖的提示词部件会保留;没有覆盖的部件跟随新默认值。升级后检查标了 **Default updated** 的部件。
|
||||
|
||||
被更新版本迁移过的数据库无法被旧版本打开。因此回滚需要升级前的那份备份。升级 DeepSeek Harness 本身之后,重启运行中的实例,让面板加载相匹配的构件。
|
||||
|
||||
### agent 预设
|
||||
|
||||
worker 与 router 预设会把 shell 命令在 5 秒后移入后台。一个 Session 没有它的预设就无法加载。只要它们的 Sessions 还在使用,就保留 `cordis.patch.yml` 里的 `dsh-superagent-worker` 与 `dsh-superagent-router` 行;否则恢复会因 `agent-preset/not-found` 失败。
|
||||
|
||||
## 排障
|
||||
|
||||
设置问题带着修复链接出现在 overview 上,用面板的语言;日志与 API 显示下面这些英文文本。
|
||||
|
||||
| 症状 | 原因与修复 |
|
||||
| --- | --- |
|
||||
| `The agent GitHub CLI is not set.` | 在 GitHub 卡片上设置 agent 的 CLI |
|
||||
| `The agent GitHub CLI was not found.` | 设置包装器的绝对路径,或一个在 `PATH` 上的命令名 |
|
||||
| `The agent GitHub CLI cannot read its account` | 用 `auth login` 给包装器登录 |
|
||||
| `@… cannot read …` 或 `@… cannot push to …` | 给 agent 账号授予该仓库的写权限 |
|
||||
| `The GitHub Source names no repository.` | 在 GitHub 卡片上设置仓库,`owner/name` |
|
||||
| Ticket 因 `The prepare command failed: …` 失败 | 输出会点名原因,例如 clone 路径不对;修好命令,然后重试这个 Ticket |
|
||||
| `git does not read the github.com credential configuration dsh-superagent writes` | 安装 git 2.32 或更新 |
|
||||
| Ticket 因 `The git configuration of the repository in … sets …` 失败 | 从 clone 的 `.git/config` 里移除点名的凭据或 `http.*.extraheader` 设置 |
|
||||
| `… was not written by dsh-superagent` | 把点名的文件移出 `<stateDir>/worker-bin` 或 `<stateDir>/worker-gitconfig`,或另选一个 `stateDir` |
|
||||
| `The bot lark-cli allows user identities` | 把 Feishu Source 指向一个仅 bot 的配置,如上文 Feishu bot 所述 |
|
||||
| `The bot lark-cli does not work.` 或 `The bot identity of app … is …` | 检查配置里的 app id 与 secret |
|
||||
| `The Feishu bot is not a member of the chat.` | 在该 chat 的设置里把 bot 拉进群;下一次尝试就会成功 |
|
||||
| `status` 退出码 2 | 某个部件在报错,重启修不好:`errors` 点名每个部件(如 Management API 一节所列)及其细节;每个部件恢复正常后清掉自己的条目 |
|
||||
| `status` 退出码 3 | 设置在等待:`setup` 说明缺什么,overview 链接到能修复它的设置 |
|
||||
| `status` 退出码 1 | 服务没在跑、跑了双份、不回应、或其 runtime 报错;输出与最新的日志会说明是哪一种 |
|
||||
| `… timed out after 90 s`、`… was stopped by …`、`… exited with code …` 或 `… could not run: …` | 某个 CLI 失败而没说明原因;以操作者的用户手动运行被点名的程序,看它自己的报错 |
|
||||
| 面板说 dsh-superagent 不回应 | 它正在重启,或因配置问题没起来;DeepSeek Harness 日志会点名原因 |
|
||||
| `dsh-superagent configuration has keys it does not use`(日志) | 移除点名的键;消息会说每个值现在在哪里 |
|
||||
| 日志说插件与运行中的 dsh 不兼容 | 安装一个落在 `engines.dsh` 范围内的 DeepSeek Harness 版本 |
|
||||
| DeepSeek Harness 升级后,面板显示 "Failed to load plugins" | 重启运行中的实例 |
|
||||
| @ 之后什么都没发生 | 检查作者是否在该 Source 的允许名单上、Source 是否开着、处理是否被暂停。GitHub 上,靠编辑 Issue 正文补的 @ 不会被收到;请在评论里 @ agent |
|
||||
@@ -1,628 +0,0 @@
|
||||
# 007 Architecture
|
||||
|
||||
This is the developer guide to the implementation. It maps the concepts in [001 Domain model](001-domain.md) through [006 Deployment](006-deployment.md) to classes, interfaces and files. Start with the runtime and Engine, then use the module and test tables as a code index.
|
||||
|
||||
## Core classes and glue
|
||||
|
||||
Core classes own domain concepts and external boundaries behind small interfaces. Glue translates tool arguments, HTTP requests, Source payloads and DeepSeek Harness events into calls to those classes, then turns results into cards, views and messages.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
plugins[Source plugins] -->|SourceContext| engine
|
||||
tools[Session tools] --> engine
|
||||
http[Management routes] --> engine
|
||||
engine[Engine] -->|Repositories| store[Store]
|
||||
engine -->|Policy| policy[Policy]
|
||||
engine -->|SourceScope| registry[SourceRegistry]
|
||||
engine -->|Assessor| assessor[MasterAssessor]
|
||||
engine -->|Executor| executor[DshExecutor]
|
||||
assessor -->|decide| engine
|
||||
engine -->|Outbox| outbox[Outbox]
|
||||
executor -->|assignmentStarted, inputsDelivered, report| engine
|
||||
settings[Settings] -->|policyChanged| engine
|
||||
assessor & executor & sessions[Sessions] & outbox -->|enqueue| effects[Effects]
|
||||
effects --> store
|
||||
```
|
||||
|
||||
`Runtime` (`src/app/runtime.ts`) constructs the application, supplies dependencies, and owns startup and shutdown. `Engine` constructs its own internal parts. The Executor and Assessor call back into the Engine, so Runtime gives them getters rather than requiring construction in a circular order.
|
||||
|
||||
### Runtime
|
||||
|
||||
`Runtime` is the composition root; it contains no domain rules. Its constructor locks the state directory, opens the Store and builds each component. If construction fails, it closes the Store and releases the lock.
|
||||
|
||||
Startup registers management routes and runs Setup. After setup resolves, it installs Session and Executor hooks, ensures the main Session exists, starts Policy syncs, resumes interrupted Assignments, and starts passes and effects. `ready` marks completion of startup.
|
||||
|
||||
Shutdown cancels turns, waits for passes, effects and agents, restores the process environment, closes the Store and releases the lock. Source plugins inject Runtime as the `orchestrator` service through the narrow `SourceHost` interface.
|
||||
|
||||
| Part | Module | Does |
|
||||
| --- | --- | --- |
|
||||
| Setup | `src/app/setup.ts` | Resolves the repository and the accounts by probing their CLIs the way worker shells run them, writes and applies the workers' environment and command shims, and keeps the setup problems; Settings checks a changed account through it, and restarts through it, which runs setup again |
|
||||
| Passes | `src/app/passes.ts` | One pass at a time, every two seconds and whenever a command asks: the due effects, then `Engine.evaluateDue`; a Source's Trigger reaches the Engine through it |
|
||||
| Outbox | `src/app/outbox.ts` | The Engine's `Outbox` port, and the only writer of the receipts: `send` keeps a message's receipt and queues its `reply` effect in one transaction, and the effect's handler sends it through `SourceRegistry.send`; a message waits while its Source is off, and is dropped when the conversation is gone or not listened to |
|
||||
| Notices | `src/app/notices.ts` | Converts domain events and worker activity into main-Session messages. `eventNotice` handles domain notices; `reportRelay` forwards questions immediately, keeps ordinary progress on its Ticket, and relays other reports; `followupRelay` names the Tickets that own a human follow-up. Relays arriving during a turn are delivered together at its next step through the DeepSeek Harness inbox |
|
||||
|
||||
### Engine
|
||||
|
||||
`Engine` (`src/core/engine.ts`) is the only writer of Triggers, Threads, Decisions, Tickets and Assignments. It receives these ports from `src/core/ports.ts`: `Repositories`, a current-Policy getter, `SourceScope`, `Assessor`, `Executor` and `Outbox`. Optional dependencies are `prepareTicket`, `now` and `emit`.
|
||||
|
||||
All Engine methods are synchronous. Each command changes its aggregates in one transaction. `evaluateDue` uses a separate transaction for each Eval or Ticket it changes. Ports enqueue external work inside the transaction; adapters perform it after commit. Invalid requests throw `UserError`.
|
||||
|
||||
The Engine exposes one entry point and delegates to five internal parts. It owns each command's transaction. Commands spanning parts, such as `attach`, `evaluateDue` and `policyChanged`, call the parts in sequence.
|
||||
|
||||
Each part extends `EnginePart` (`src/core/part.ts`), which provides options and shared reads. Dependencies between the parts flow in one direction:
|
||||
|
||||
| Part | Responsibility | Depends on |
|
||||
| --- | --- | --- |
|
||||
| `Intake` | A Source's Trigger: stored once with its disposition, recorded on its Threads and their correlations, fanned out to the subscribed Tickets and, unless it is a person's message that reached one, queued for Eval on its primary Thread | Tickets |
|
||||
| `Evals` | The Evals of Threads: batching, the gate's strength, actor and basis with the Trigger that gave them (`primary`), the deadline from the candidate's delivery and its shift by a pause, the requeue of an expired Eval, candidates, replays and hand-backs, the requeue on a Policy change, and the Decision each ends with: accept, stop, ignore or unavailable | Tickets |
|
||||
| `Tickets` | The Ticket lifecycle: creation from an accept with its Mandate, Subscriptions and first Assignment, the Threads it follows and their closes (`followsDone`), attached Threads and Threads followed for context, Inputs and notes, resume and the messages a resume refuses, retry, restart, stop and end, the Subscriptions it drops, and the notice of a Policy change to the Tickets it bears on | Replies |
|
||||
| `Assignments` | What the Executor and its workers report: an Assignment started, Inputs reached it or failed to, a restart interrupted it, and a worker's Report with its question for a person | Tickets, Replies |
|
||||
| `Replies` | Channel writes through the `Outbox`: the acknowledgement, the answer, the result and the attention message, each by the rule that allows it | Nothing |
|
||||
|
||||
Commands use imperative names such as `stopTicket`; facts reported by adapters use past-tense names such as `assignmentStarted`. The query `candidate` uses a noun.
|
||||
|
||||
Callers receive only their role's `Pick` of the Engine: `EngineForSources`, `EngineForMainSession`, `EngineForPeople`, `EngineForSettings` or `EngineForExecutor`.
|
||||
|
||||
| Role | Method | Effect |
|
||||
| --- | --- | --- |
|
||||
| Sources | `receive(trigger)` | Stores a Trigger once and answers `stored`, `duplicate`, or `filtered` when no Source receives it; fans it out to the Tickets subscribed to its Threads, where a person's message resumes an ended Ticket at once unless processing is paused, and queues it for Eval on its primary Thread, unless it is a person's message that reached a Ticket, which stays that Ticket's |
|
||||
| Sources | `bindThread(ticketId, thread, own)` | Makes an open Ticket follow one of the Source's Threads, such as a pull request's, `own` when the Ticket opened it; a closed Thread bound again is open again. The Threads' closes, not a `ready` Report, end the Ticket |
|
||||
| Sources | `threadClosed(thread, outcome, at)` | A Thread closed: each open Ticket that follows it records the close, and ends once it is done with the Threads it follows (`followsDone`): no Thread it opened is open, or, when it opened none, every Thread it follows closed. It ends `merged` or `closed` as the latest of those closes did, unless work resumed after the close has not reported `ready`, or its Executor took up, by acknowledging or answering it, a person's follow-up it decides after its latest Report other than `progress`, or the end of its last turn; a `dropped` Report then ends it as the close did |
|
||||
| Main Session | `candidate(triggerId)` | The Eval that holds a Trigger, as its assessor sees it |
|
||||
| Main Session | `candidateDelivered(evalId)` | The Eval's candidate reached the main Session: its deadline counts from now |
|
||||
| Main Session | `decide(triggerId, outcome)` | Records an Eval Decision, or a direct Decision for an explicit request. Accept creates the Ticket, Mandate, Assignment, Subscriptions and acknowledgement atomically. An inferred candidate ignored from its card records `ignoredFromCard` before the supplied reason. While `followupOwners` still owns a human follow-up, only stop is allowed |
|
||||
| Main Session | `attach(ticketId, triggerId, note?)` | Routes a Trigger to a Ticket it points at, whose Executor then decides it as a follow-up, and ends the Eval that holds it, whose other Triggers wait for a fresh Eval; an ended Ticket resumes under its Mandate at once unless processing is paused. A follow-up an Executor still decides is refused, as is one routed back to the Ticket that handed it back |
|
||||
| Main Session | `requestStatus(ticketId)` | Asks a waiting Executor Session for its status, and answers whether it asked |
|
||||
| Main Session | `note(ticketId, text)` | Gives a Ticket's Executor a fact from other work as a text Input, which grants nothing |
|
||||
| Main Session, Executor | `reply(request)` | Queues a message for the Channel its purpose names: `acknowledgement`, `answer`, `result` or `attention` |
|
||||
| People | `stopTicket(ticketId, reason?)` | A person's stop, which withdraws the Mandate |
|
||||
| People | `retryTicket(ticketId)` | Ends the stalled Assignment of a Ticket that waits for a person after a `failed` or `blocked` Report, or whose Input delivery failed (`canRetry`), and starts a new one |
|
||||
| People | `reevaluate(triggerId)` | Queues a stored Trigger for a fresh Eval |
|
||||
| People | `unsubscribe(thread)` | Drops every Subscription on a Thread, and the Tickets that followed it follow it no more; each keeps deciding the follow-ups it received there |
|
||||
| People | `dismiss(decisionId)` | Dismisses the problem an `unavailable` Decision shows, which the Decision records |
|
||||
| Executor | `assignmentStarted(ticketId, assignmentId)` | An Assignment runs; its waiting Inputs go out |
|
||||
| Executor | `inputsDelivered(ticketId, assignmentId, inputIds)` | Inputs reached the Assignment's Executor Session, where each waits for a step to take it |
|
||||
| Executor | `inputsRead(ticketId, inputIds)` | The Executor Session took the messages that carry these Inputs into its history (a `user/message` Session event), from which its model has them; a step that claimed them and was cancelled or failed before does not count. Each counts as read once a Report or the end of a turn follows, also after a restart, and its Ticket's end does not let go of it; an Input delivered before reads were recorded counts as read once a Report or the end of a turn followed its delivery |
|
||||
| Executor | `turnEnded(ticketId, assignmentId)` | A turn of the Executor Session ended, also after a Report, once nothing runs for it in the background or as it hands off to a person. What a step took before, it read (`inputsRead`) |
|
||||
| Executor | `deliveryFailed(ticketId, assignmentId, error)` | Delivering Inputs failed; the Ticket shows it until a delivery succeeds, a new Assignment starts or the Ticket ends |
|
||||
| Executor | `report(ticketId, report)` | Records a Report and its question for a person; `dropped` ends the Ticket, and `ready` completes one that follows no Thread, and ends one done with the Threads it follows as they ended |
|
||||
| Executor | `markUnrelated(ticketId, thread, reason)` | Drops only that Ticket's Subscription to an attached Thread it found unrelated, and hands back, with the reason, the follow-ups there that the Ticket decides |
|
||||
| Executor | `followForContext(ticketId, thread)` | Subscribes an open Ticket to a related Thread for context: its messages reach the Ticket but lend it no follow-up strength and do not resume it |
|
||||
| Executor | `handback(ticketId, triggerId, reason)` | Records that the Ticket hands back a Trigger that reached it, with the reason its candidate carries, and queues it for a fresh Eval once no other Ticket decides it, if this Ticket did; it answers whether it queued it. A Ticket hands back each Trigger once and keeps its Subscription |
|
||||
| Executor | `assignmentInterrupted(ticketId, assignmentId)` | An Assignment was running when the host stopped: one interrupted mid-work, or one that received an Input after its latest Report, starts again with a recovery note and the Inputs delivered to it that its Executor Session did not take, as a retry does; one that had reported since keeps its Session. The DshExecutor reports each at start (`resume`), oldest Ticket first, and queues again a launch that waits |
|
||||
| Passes | `evaluateDue()` | Unless processing is paused: starts the Evals of the Threads whose queued Triggers wait, one per Thread with the Triggers queued meanwhile as one batch; queues the Triggers of an Eval past its deadline again, twice in a row, and records it `unavailable` the third time; and resumes the ended Tickets a person's message reached during a pause, if their Mandates still hold. An Eval whose candidate has not reached the main Session does not expire. While processing is paused, no Eval expires, and once it resumes each deadline moves later by the pause's length |
|
||||
| Settings | `policyChanged(from, to, effects)` | Tells each live Ticket the change bears on how (`policyEffects` in `src/policy.ts`: a rule it was accepted under changed or no longer applies, its requester may no longer start work, or proactive work is no longer accepted), and restarts the Evals in progress under the new version |
|
||||
|
||||
The Engine keeps batching, authorization, fan-out, resume and attention lifecycle rules private. Several shared predicates make these rules consistent:
|
||||
|
||||
- `waitsForPerson` (`src/core/model.ts`): the latest Assignment reported `blocked` or `failed`, and no human message has arrived since. Automatic Inputs and failed launches remain held while this is true.
|
||||
- `followupOwners`: the Tickets still responsible for a human follow-up received on their own Threads. A Ticket can be live or ended without a human stop and waiting to resume. Handing back the message ends that ownership.
|
||||
- `settled`: a Decision resolved the request; stop, unavailable, routed and refused-stop Decisions do not count.
|
||||
- `ticketStage` (`src/core/stage.ts`): implements the stage table in [003 Execution](003-execution.md).
|
||||
|
||||
When a Ticket ends, unread follow-ups return to Eval unless another Ticket still owns them or another disposition settled them. Acknowledged, answered and handed-back messages are excluded. For older records without acknowledgement dispositions, the outbox receipt supplies that fact. A final Report also covers messages waiting when the Assignment began, including the message that resumed it. The message that stopped a Ticket is not automatically re-evaluated.
|
||||
|
||||
After a change, the Engine emits an event from `DomainEvents` in `src/core/ports.ts`:
|
||||
|
||||
`trigger.received`, `eval.started`, `decision.recorded`, `ticket.created`, `ticket.resumed`, `ticket.ended`, `assignment.started`, `assignment.ended`, `thread.subscribed`, `thread.unsubscribed`, `trigger.requeued`, or `policy.changed`.
|
||||
|
||||
Runtime turns `ticket.ended`, `trigger.requeued` and `thread.unsubscribed` into notices. A human `trigger.received` delivered to Tickets becomes a follow-up relay. `pullEndNotices` sends one chat notice, using a stable id, when a Ticket ends merged or closed. Other events currently have no consumer.
|
||||
|
||||
### Model
|
||||
|
||||
`src/core/model.ts` holds the domain types, and `src/core/stage.ts` the Ticket stages. A Decision records why as `reason`, a list of `Reason`s (`src/core/reasons.ts`): each a `code` with its `values` and, for one Trigger of a batch, its `triggerId`.
|
||||
|
||||
The core's codes, such as `timeout`, `policyChanged`, `routed` and `startedTicket`, are `REASONS`, with the English that agents read through `reasonText`; a gate or a Source scope gives its own, such as `not_a_member`; and `said` carries words an assessor or a person wrote. The panel words each code by `reason.<code>`, and a Trigger a Source filtered records the Source's code the same way.
|
||||
|
||||
A Ticket keeps each fact once. It is open while it has no `ended`, which records when, by whom, why and with what outcome it ended; what it last said is its latest Assignment Report (`latestReport`); and its Executor Session is named after it (`workerSessionId` in `src/executor/dsh.ts`).
|
||||
|
||||
Its fields are typed: `origin`, the admissible Trigger that gave its Eval's batch its strength and the batch itself (`triggerId`, `triggerIds`); `execution`, what the Executor chose (`name`, `directory` and `assignment`); `delivery`, the last failed Input delivery (`error`, `at`), until a delivery succeeds, a new Assignment starts or the Ticket ends; and `follows`, the Threads a Source made it follow, each `{thread, own, closed?}`: `own` when the Ticket opened it, and `closed` with `at` and `outcome` once it closed. Tickets alone writes `follows`; the GitHub Source reads its pull requests with `pullsOf` and `pullOf` (`src/sources/github-pull.ts`).
|
||||
|
||||
An Input that carries a Trigger records whether a person wrote it, which `waitsForPerson` reads. A Thread also lists in `context` the subscribed Tickets that follow it only for context, and counts in `expired` the Evals of its queued Triggers that expired in a row; an Evaluation records its `primary` Trigger and when its candidate was `delivered`.
|
||||
|
||||
A Trigger's disposition records the Tickets it reached, by fan-out or attachment, a person's requeue, each Ticket that handed it back, with the reason, and each Ticket whose Executor acknowledged or answered it.
|
||||
|
||||
A Mandate's `basis` (`MandateBasis`) records how the gate admitted its Trigger. Revalidation reads the receiving `source`, `scope` and `row`, the listed `sender`, matched `rules`, and `via`:
|
||||
|
||||
| `via` | Admission basis |
|
||||
| --- | --- |
|
||||
| `addressed` | An allowed sender addressed the agent |
|
||||
| `followup` | A message arrived on a Thread with Tickets |
|
||||
| `admission` | An admission-table cell; `observation` marks an inferred candidate |
|
||||
| `bot` | A listed bot produced an inferred candidate |
|
||||
|
||||
Trigger and Thread `meta` belong to the Source. The core does not interpret them.
|
||||
|
||||
### Policy
|
||||
|
||||
`src/policy.ts` defines settings schemas, authorization and assessment rules. `settingsPolicy(settings, scope)` returns a fresh, stateless `Policy` on each use:
|
||||
|
||||
- `version`, `paused` and `evalTimeoutMs` describe its current state.
|
||||
- `gate(trigger, thread, tickets)` returns admission strength and basis.
|
||||
- `revalidate(mandate)` checks whether an existing Mandate still holds.
|
||||
- `admitsInferred(rules)` permits proactive acceptance only in auto mode with enabled matching rules.
|
||||
|
||||
`assessmentOutcome(assessment, settings, strength)` converts the model's assessment to an Engine outcome. It requires a judgement for every enabled rule, settled direction, a matching rule and evidence of intent. Outside auto mode, an inferred accept becomes a “would accept” record.
|
||||
|
||||
`policyEffects(tickets, old, now, scope)` identifies which open Tickets a change affects and how. `Engine.policyChanged` notifies only those Tickets. `policyVersion` hashes rules, accept mode and the Sources' Policy parts, including bot lists.
|
||||
|
||||
### Store
|
||||
|
||||
`Store` (`src/app/store.ts`) owns one SQLite database. Runtime acquires the directory lock before opening it, preventing a second process from migrating or writing the same state.
|
||||
|
||||
Its interfaces are grouped by purpose:
|
||||
|
||||
- **Domain repositories:** `atomic`, plus `triggers`, `threads`, `tickets` and `decisions`, each with `get`, `save` and list queries. Indexed working sets are `threads.active()`, `tickets.open()`, `tickets.active()` and `read.subscribedThreads`.
|
||||
- **Read queries:** `read` serves views, tools and Source polls through methods such as `threadTriggers`, `triggerStats`, `search` and `droppedReplies`. These methods never write.
|
||||
- **Source metadata:** `threads.describe` and `threads.patchMeta` update Source-owned Thread metadata.
|
||||
- **Settings:** `settings`, `saveSettings`, `blockRevision` and `settingsHistory`. Settings and prompts use numbered `Versions` (`src/app/versions.ts`).
|
||||
- **Outbox receipts:** `replyStatus` and `receipts`, written only by Outbox. Receipt operations are `get`, `waiting`, `behind`, `add`, `settle` and `withdraw`. `behind` checks whether an earlier message for the same Ticket and conversation is waiting with fewer than three failures. A withdrawn message settles with `channelGone` or `channelIgnored`. Read queries expose failed and dropped replies through `failingReplies`, `droppedReplies` and `droppedSince`.
|
||||
- **Typed key-value records:** `get`, `set`, `delete` and `keys`, using entries from `KV`.
|
||||
|
||||
`afterCommit` runs an action only after the transaction commits, never after rollback. Runtime reads records in their stored shape; the schema 11 migration converts older supported records at startup.
|
||||
|
||||
### Effects
|
||||
|
||||
`Effects` (`src/app/effects.ts`) is the durable side-effect queue. A command calls `enqueue(kind, key, payload)` inside its transaction; execution begins only after commit.
|
||||
|
||||
Each kind/key pair has one row. Enqueuing the same waiting key replaces its payload and makes it due immediately without changing queue order. `handle(kind, run, options)` registers one handler per kind. It processes due rows oldest first, up to the concurrency limit.
|
||||
|
||||
Failures retry with exponential backoff. Throwing `Later` defers work without counting a failure, for example when no worker slot is available. `start` begins processing after setup, `drain` stops it during shutdown while preserving pending rows, and `pending`/`idle` expose queue state.
|
||||
|
||||
| Kind | Handler | Does | At once | Retry |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `assessment` | MasterAssessor | Steers an Eval's candidate card into the main Session's turn, then tells the Engine it was delivered, which starts the Eval's deadline | 4, none while paused | 2 s doubling to 60 s |
|
||||
| `launch` | DshExecutor | Launches a pending Assignment: prepare command, Executor Session, assignment message | The free worker slots, none while paused | A passing failure of the model provider or the network runs again after 5 s doubling to 300 s, three times; any other failure, and a failed prepare command, fails its Ticket. One held by a pause or by full worker slots tries again after 5 s |
|
||||
| `delivery` | DshExecutor | Hands a Ticket's waiting Inputs to its running Assignment, in order | 4, none while paused | 2 s doubling to 60 s; one that waits for a worker slot tries again after 2 s |
|
||||
| `relay` | Sessions | Steers a worker's report or question, or a person's follow-up, into the main Session's turn | 1, in queue order | 2 s doubling to 60 s |
|
||||
| `notice` | Sessions | Injects a notice into the main Session without waking it | 1, in queue order | 2 s doubling to 60 s |
|
||||
| `reply` | Outbox | Sends a message for a Channel through its Source, after the earlier messages of its Ticket to its conversation that have not failed three times; withdrawing the message cancels it | 1, none while paused | 5 s doubling to 300 s; one whose Source is off, or behind an earlier message, tries again after 5 s |
|
||||
|
||||
Each kind has a health entry under its name. It reports an error once one of its effects failed three times in a row, except `delivery`, whose failures show on their Ticket instead. Relays keep their order, and so do notices; a failed one holds back the later ones of its kind.
|
||||
|
||||
### Sources
|
||||
|
||||
Sources extend the host through `SourceKind`, defined in `src/sources/kind.ts`. A kind declares its identity, title, description, configuration `schema` and `fields`, derived `facts`, and `health` entries. Optional behavior includes `scope`, a `channel` (`allowed`, `send`, `link`), `threadContext`, `find`, `policy`, `environment` and `refreshLabels`.
|
||||
|
||||
`SourceRegistry` (`registry.ts`) owns registered kinds and stored instances, and implements the Engine's `SourceScope`. Registration claims health entries on `HealthBoard`; duplicate claims and reserved `sync:` names are refused. A SourceContext writes only through its registration's claimed entries, so reports after unregistration do nothing.
|
||||
|
||||
Registry configuration methods include `kind`, `instances`, `instance`, `validate`, `validatePolicy`, `policyPart`, `channelAllowed`, `declarations`, `environments`, `find` and `scope`. `find` asks each kind that supports search.
|
||||
|
||||
Generic code accesses Source behavior through the registry. `send(message, commands)` calls the kind's Channel and returns `sent`, `gone` or `waiting` if the kind is not installed. `link` resolves its Channel link. `threadContext` pairs a kind's reader with its configuration, or returns nothing when the kind has no reader. Invalid stored configuration disables the instance.
|
||||
|
||||
Plugins receive `SourceHost`: the registry and `context(kind)`. The kind itself uses a narrower `SourceContext`, built by `sourceContextOf` in `src/app/source-context.ts`.
|
||||
|
||||
That context supplies deployment and commands, setup readiness (`ready`, `configured`, `problem`), configuration, `receive`, `atomic`, and typed `state`. It also exposes Thread reads and metadata writes, Ticket reads and binding/closure commands, Trigger reads, `notify` through Outbox, and `report` for declared health entries.
|
||||
|
||||
### DshExecutor
|
||||
|
||||
`DshExecutor` (`src/executor/dsh.ts`) provides one Session and directory per Ticket. `start` and `deliver` enqueue `launch` and `delivery` effects. `stop` cancels the turn through `Store.afterCommit`, so a rolled-back end does not stop the worker.
|
||||
|
||||
`prepare(ticket, outcome)` supplies the Engine's `prepareTicket` hook: short name, directory and `ticketExecution` assignment text. `workerSessionId(ticket.id)` derives the Session id.
|
||||
|
||||
`WorkerSlots` reserves capacity before a launch or a delivery that wakes an idle worker. A human message can use one slot beyond `maxWorkers`, and wakes have priority over launches. `activeCount` supplies panel load.
|
||||
|
||||
Transient provider and network failures get three retries. Launches use effect backoff; failed turns receive continuation messages after 15, 30 and 60 seconds. `inputMessage` renders each Input from current Ticket state, and `mandateInstructions` adds the matched-rule limits for inferred work.
|
||||
|
||||
`install` subscribes to Session events; `dispose` unsubscribes. A turn end normally reports waiting, except after readiness or while human action is required. Errors report failed. At startup, `resume` reports interrupted Assignments through `assignmentInterrupted` and requeues pending launches that do not wait for a person, oldest Ticket first.
|
||||
|
||||
### MasterAssessor
|
||||
|
||||
`MasterAssessor` (`src/app/assessment.ts`) lets the main Session assess proactive candidates. `request` queues an assessment effect; `release` drops cached state when its Eval ends.
|
||||
|
||||
Its `context`, `read` and `decide` methods serve `orchestrator_eval`, `orchestrator_eval_read` and `orchestrator_eval_decide`. They provide bounded Thread context through `SourceRegistry.threadContext`, repository context, and decision submission. A kind without a context reader supplies only the Trigger and batch.
|
||||
|
||||
`assessmentOutcome` converts the model's answer for `Engine.decide`. Evals checks inferred acceptance against the current Policy again.
|
||||
|
||||
### Settings
|
||||
|
||||
`Settings` (`src/app/settings.ts`) reads and writes revisioned settings blocks. `current` and `view` read them; `save(block, value, revision, origin)` validates a save; `pause(paused, origin)` changes the separate `paused` setting.
|
||||
|
||||
Every save declares its origin: `panel`, `tool` or `sync`. Only `panel` can change the execution environment or fields listed in `SETUP_FIELDS` (`src/deployment.ts`).
|
||||
|
||||
A Policy-version change calculates `policyEffects`, records which Tickets were notified, and calls `Engine.policyChanged`. `syncable` and `sync(id)` refresh a Source's Policy part. `refreshLabels(id)` refreshes display names.
|
||||
|
||||
### Prompts
|
||||
|
||||
`Prompts` (`src/app/prompts.ts`) stores configurations as `Versions`. `current`, `version` and `history` read them; `validate`, `preview` and `save(value, baseRevision, source)` check and save them. `compose` and `skills` build Session text.
|
||||
|
||||
`builtInParts` lists override keys, roles, default text and required template placeholders. Each override stores team `text` and a `base`: the `builtInBase` key of its original default. Saves preserve that default under `KV.prompts.builtIn`. `defaultChanged` then reports changed defaults, their old text when available, and removed parts.
|
||||
|
||||
Validation inherits an omitted base from the saved override, drops removed parts already present in saved data, and rejects blank text or other unknown keys. This allows the save tool to round-trip a configuration it read. `environmentSection` and `policySection` substitute template values once, using own keys only. Configurations without overrides use defaults.
|
||||
|
||||
### Sessions
|
||||
|
||||
`Sessions` (`src/app/sessions.ts`) manages DeepSeek Harness Sessions for both roles. `ensure` creates a Session with its role's model. `install` follows agent events and registers prompts, skills and tools for managed Sessions.
|
||||
|
||||
`prompt` delivers once per request id. `relay` and `notify` enqueue main-Session messages handled by the `relay` and `notice` effects. `src/app/notices.ts` defines their content; `delivery` checks whether a message arrived.
|
||||
|
||||
Each model request binds the Session to the current prompt revision. The revisions it used are stored durably: `promptVersion` identifies the active one and `mayRead` restricts which versions a worker can inspect. Registrations are tracked in one map per agent.
|
||||
|
||||
`status` and `promptVersion` serve prompt pages. Saves go through `Prompts.save` and notify the main Session. `modelRetry` exposes model-budget retry state.
|
||||
|
||||
### Glue
|
||||
|
||||
| Glue | Translates |
|
||||
| --- | --- |
|
||||
| `src/app/runtime.ts` | The composition root: builds the parts with the interfaces they use, registers the routes and the Source host, and starts and stops dsh-superagent |
|
||||
| `src/app/reclaim.ts` | Ended Tickets' workspace directories into what may be removed, and the disk they hold into the overview's readout |
|
||||
| `src/app/notices.ts` | Domain events and worker reports into notices and relays for the main Session |
|
||||
| `src/app/tools/main.ts`, `src/app/tools/executor.ts` | Tool arguments of the main Session and the Executor Sessions into calls of the core classes, and their results into cards |
|
||||
| `src/app/http.ts`, `src/app/views/`, `src/app/api.ts` | HTTP requests into calls, and Store reads into the panel's views; `api.ts` types every route for the panel |
|
||||
| `src/app/resources.ts`, `src/app/links.ts`, `src/app/cards.ts` | Resource names into reads, source messages into links, records into compact cards for models |
|
||||
| `src/sources/github*.ts`, `src/sources/feishu*.ts`, `src/sources/poll.ts` | GitHub and Feishu payloads into Triggers and Thread facts, and messages into CLI calls |
|
||||
| `src/deployment.ts`, `src/setup.ts`, `src/infra/` | Setup probes and problems, CLI transport, the agent's process environment |
|
||||
|
||||
## Dependency rules
|
||||
|
||||
- **Explicit, narrow dependencies.** Each class receives an interface that names what it uses, mostly as `Pick` types of other classes: `ExecutorHost`, `AssessorHost`, `SessionsHost`, `SettingsHost`, `SetupHost`, `PassesHost` and `OutboxHost` for the core classes, and `HttpHost`, `MainToolsHost`, `ExecutorToolsHost`, `ResourceHost`, `LinkHost` and `SourceContextDeps` for the glue, with the Engine as the `Pick` of the role that calls it. The Runtime passes itself only behind such an interface. A Source plugin injects the `orchestrator` service, which its type names as a `SourceHost`: the registry and `context(kind)`; its kind's code sees only its `SourceContext`.
|
||||
- **What may import what.** `src/core` imports nothing outside itself apart from Node's hashing, and does no I/O. `src/sources` imports nothing from `src/app`. The panel imports only types from the host side, and the plain values of `src/admission.ts`, `src/limits.ts`, `src/setup.ts`, `src/navigation.ts`, `src/sources/host.ts`, `src/core/errors.ts`, `src/core/reasons.ts` and the filter lists of `src/app/api.ts`.
|
||||
- **One writer per state.** Each kind of state has one writer, and the others read it or ask that writer.
|
||||
|
||||
| State | Writer |
|
||||
| --- | --- |
|
||||
| Triggers, Threads, Decisions, Tickets, Assignments | Engine |
|
||||
| A Source's read positions, and the meta it keeps on its Threads (`label`, `url`, `facts` and its own keys) | That Source, through `SourceContext` |
|
||||
| Settings and their history | Settings |
|
||||
| Prompt versions | Prompts |
|
||||
| Effect rows | Effects |
|
||||
| Outbox receipts | The Outbox: the Engine's messages and Sources' `notify` go through its `send`, and its `reply` effects mark them |
|
||||
| A health entry | The part that claimed it on the `HealthBoard` (`src/infra/health.ts`), which refuses a second claim: `runtime` the Runtime, which Passes report through, `evals` the Runtime after each pass, `models` Sessions, each effect kind Effects, `worker` the DshExecutor, a Source kind's entries the SourceRegistry for that kind's Source, `sync:<id>` Settings |
|
||||
|
||||
A Source changes a Ticket only through `SourceContext.tickets.bindThread` and `threadClosed`, which call the Engine.
|
||||
- **Requests and facts.** A method that asks for something is named by an imperative verb and its object, such as `stopTicket` or `bindThread`, and may be refused; a method that records what an adapter did is named in the past tense, such as `assignmentStarted` or `deliveryFailed`. Each host receives the Engine as the role type it needs.
|
||||
- **Typed fields, and Source data under the Source.** The code reads typed fields of Tickets and Mandates. A Source keeps nothing on a Ticket: which of its Threads a Ticket follows is the Ticket's `follows`, and what it keeps about a Trigger or a Thread is in their `meta`.
|
||||
- **Fixed refusals.** A refusal a person or an agent can cause is a `UserError(code, values)` from `USER_ERRORS` in `src/core/errors.ts`. The management API answers a `missing` code with 404 and the others with 400, and the panel words each code in its language. Setup problems are `SetupError` codes (`src/setup.ts`) and fixed health texts `HealthCode`s (`src/sources/host.ts`), worded the same way.
|
||||
### Source-specific dependencies
|
||||
|
||||
Some generic modules still refer directly to GitHub or Feishu:
|
||||
|
||||
- Outbox applies the Feishu chat word limit.
|
||||
- `src/app/links.ts` resolves Feishu message links and `repliedTo`. It and Runtime read settings with `feishuSettings`.
|
||||
- `src/app/resources.ts` reads the `feishu/` and `github/` originals; `src/app/cards.ts` uses `pullOf` for Ticket cards.
|
||||
- Runtime sends `pullEndNotices` for merged or closed Tickets.
|
||||
- Executor tools bind reported pull requests with `readPull` and `claimPull`, using the GitHub context exposed by Runtime.
|
||||
- MasterAssessor reads repository files with `readRepositoryPath`.
|
||||
- Sessions and Prompts register built-in skills only for GitHub and Feishu. Migration converts their schema-10 records.
|
||||
- Setup probes only GitHub and Feishu account fields (`SETUP_FIELDS`). Only those Sources receive account setup problems through `SourceContext.problem`.
|
||||
|
||||
|
||||
## Modules
|
||||
|
||||
| Path | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.ts` | The main plugin entry: reads the machine settings, starts the Runtime, and gives it the restart that a save of a Source's account uses |
|
||||
| `src/deployment.ts` | The plugin `Config` and `Machine`; `resolveDeployment` and `resolveFeishuBot` read the repository and the accounts from the Source settings, each CLI run as Executor shells run it; `SETUP_FIELDS` lists the Source fields setup reads. The code names no person, account or organization |
|
||||
| `src/setup.ts` | Setup problems: `SetupError`, with a code, the place that fixes it and the English text the log, the main Session and the panel share |
|
||||
| `src/policy.ts` | The Policy: the settings schemas by block, the Policy version, the gate and Mandate revalidation |
|
||||
| `src/admission.ts` | The admission table's values, each cell's options and defaults, shared with the panel |
|
||||
| `src/limits.ts` | The limits of the values a person sets, which the settings schemas check and the panel's fields offer |
|
||||
| `src/kv.ts` | `KV`: every family of `kv` records with its key, its value's type and its owner, and the `Key` and `Family` types the Store and `SourceContext.state` take |
|
||||
| `src/navigation.ts` | Opens a DeepSeek Harness Session from the panel once the host's catalog lists it |
|
||||
| `src/core/model.ts` | The domain types, among them the typed Ticket and `MandateBasis`, with `threadKey`, `ticketOutcome`, `reached`, `followupOwners`, `settled`, `waitsForPerson`, `liveTicket`, `runningAssignment`, `latestAssignment` and `latestReport` |
|
||||
| `src/core/ports.ts` | The ports: `Repositories`, `Policy`, `SourceScope`, `Assessor`, `Executor`, `Outbox`, and the domain events |
|
||||
| `src/core/engine.ts` | The Engine, the core's entry point, and the role interfaces `EngineForSources`, `EngineForMainSession`, `EngineForPeople`, `EngineForSettings` and `EngineForExecutor` |
|
||||
| `src/core/part.ts` | `EnginePart`, the base of the Engine's parts, and `EngineOptions` |
|
||||
| `src/core/intake.ts` | `Intake`: Triggers from Sources, to their Tickets and, unless a person's message reached one, to Eval |
|
||||
| `src/core/evals.ts` | `Evals`: the Evals of Threads and their Decisions |
|
||||
| `src/core/tickets.ts` | `Tickets`: the Ticket lifecycle, the Threads a Ticket follows, Subscriptions and Inputs |
|
||||
| `src/core/assignments.ts` | `Assignments`: the Executor's facts and its workers' Reports |
|
||||
| `src/core/replies.ts` | `Replies`: the Engine's Channel writes |
|
||||
| `src/core/stage.ts` | `ticketStage` and `canRetry`: the Ticket stages of [003 Execution](003-execution.md) |
|
||||
| `src/core/errors.ts` | `UserError` and `USER_ERRORS` |
|
||||
| `src/core/reasons.ts` | `Reason`, `REASONS` and `reasonText`: why the core recorded a Decision or set a Trigger aside, by code |
|
||||
| `src/core/ids.ts` | `key`: ids derived from their inputs, equal for equal inputs |
|
||||
| `src/core/memory.ts` | `MemoryRepositories`: the Store's repositories in memory, in the Store's order and with its nested rollback, for domain tests |
|
||||
| `src/core/index.ts` | The core's exports |
|
||||
| `src/app/runtime.ts` | The Runtime, the composition root, with its start and stop |
|
||||
| `src/app/setup.ts` | Setup, and `setupChanged`, whether a save changes a field setup reads |
|
||||
| `src/app/passes.ts` | Passes, the pass scheduler |
|
||||
| `src/app/reclaim.ts` | `Reclaimer`: hourly scans of the execution root that remove an Ended Ticket's directory once it outlives the retention, and the summary the overview reports |
|
||||
| `src/app/outbox.ts` | The Outbox: the Engine's `Outbox` port, the receipts and the `reply` effects that send them |
|
||||
| `src/app/notices.ts` | The main Session's notices and relays: `eventNotice`, `reportRelay`, `resultRelay` and `followupRelay` |
|
||||
| `src/app/store.ts` | The Store, and `Receipt`, a message of the outbox |
|
||||
| `src/app/store-read.ts` | `storeReads`: the Store's read queries for the views, the tools and the Sources' polls, which never write |
|
||||
| `src/app/migration.ts` | The schema 11 migration: one step from schema 10 that writes every stored record in its current shape |
|
||||
| `src/app/effects.ts` | Effects |
|
||||
| `src/app/versions.ts` | `Versions`: a record kept as numbered snapshots with a pointer to the current one, for the settings and the prompts |
|
||||
| `src/app/cache.ts` | `TextCache`: texts kept in memory within a size, the least recently used dropped first |
|
||||
| `src/app/settings.ts` | Settings |
|
||||
| `src/app/settings-history.ts` | `policyHistory`: each saved settings revision with what it changed from the one before |
|
||||
| `src/app/prompts.ts` | Prompts, `builtInParts`, the built-in parts a team may override, and `promptParts`, the order of a role's prompt sections |
|
||||
| `src/app/context.ts` | The built-in prompt text, read from `prompts/`, and the transcript entries Sessions read |
|
||||
| `src/app/skill-files.ts` | Packaged text: `promptFile`, which joins a file's one-sentence lines into paragraphs, and the bundled skills |
|
||||
| `src/app/sessions.ts` | Sessions |
|
||||
| `src/app/delivery.ts` | `AgentDelivery`: delivers a message to a DeepSeek Harness agent once per request id, also across a restored Session |
|
||||
| `src/app/retry-health.ts` | `RetryHealth`: which Sessions wait on a model budget retry, for the `models` health entry |
|
||||
| `src/app/role-models.ts` | Resolves the Eval and Executor models: seeds a new Session with its role's model and re-points a reused Session whose durable route drifted |
|
||||
| `src/app/default-model.ts` | `switchDefaultModel`: changes the deployment default model, optionally with a reasoning effort the catalog offers, through the Session Controller, which persists it into the profile |
|
||||
| `src/app/assessment.ts` | The MasterAssessor, the assessment schema and the window `orchestrator_eval` answers |
|
||||
| `src/app/execution-environment.ts` | The execution environment: its variables, a Ticket's values, the directory root and the lines of the prompt section, which `src/app/prompts.ts` fills into its template |
|
||||
| `src/app/source-context.ts` | `sourceContextOf`: each Source kind's `SourceContext` |
|
||||
| `src/app/links.ts` | `sourceLink`, the page of a Trigger's message, and `candidateCard` |
|
||||
| `src/app/cards.ts` | Compact cards of Triggers, Tickets, Threads and Decisions for models, the closed set of ticket card states `TICKET_STATES`, and `textPage` |
|
||||
| `src/app/resources.ts` | `RESOURCES` and `readResource`: what `orchestrator_read` reads, for each role |
|
||||
| `src/app/tools/tool.ts` | The `Tool` type, `readTool`, the `orchestrator_read` of both roles, and `AcknowledgementSchema` |
|
||||
| `src/app/tools/main.ts` | The main Session's tools |
|
||||
| `src/app/tools/executor.ts` | An Executor Session's tools |
|
||||
| `src/app/http.ts` | The `/orchestrator/api` routes: the same-origin check, request limits and one status per kind of failure |
|
||||
| `src/app/api.ts` | `Api`, every route with its parameters, query, body and response, and the filters of the lists; it imports only types, so the panel bundles it |
|
||||
| `src/app/views/text.ts` | The two forms of a view's text: `preview`, plain text on one line, and `markdown` without HTML; `plain` and `readable` |
|
||||
| `src/app/views/records.ts` | The rows the views share: Triggers, Tickets, Threads, Decisions and Channel links, and `ownersOf`, the owners of follow-ups as one request reads them |
|
||||
| `src/app/views/tickets.ts` | The Ticket lists by stage, a Ticket's page, the Mandate basis facts and the Executor's load |
|
||||
| `src/app/views/threads.ts` | The Thread lists, a Thread's timeline, the search and the Decision list |
|
||||
| `src/app/views/sources.ts` | The Source status: each Source instance's state, its last day's counts, and what it filtered and dropped in that day |
|
||||
| `src/app/views/settings.ts` | The settings page's view: each block with its revision, each Source's settings with its kind's form, and the execution environment |
|
||||
| `src/app/views/overview.ts` | The overview: Tickets that need a person, problems, the last day's flow and the Executor's load |
|
||||
| `src/executor/dsh.ts` | The DshExecutor, `WorkerSlots`, `ticketExecution`, `inputMessage` and `mandateInstructions` |
|
||||
| `src/executor/background.ts` | A worker's background work in DeepSeek Harness, the shell jobs its Session owns (`ctx.jobs`) and its subagents (`ctx.subagents`): `runningJobs`, `untilQuiet`, `killJobs`, `collectJobs`, `stopSubagents`, `stopBackground`, `until`, and `cancelled`, the workers whose turn a stop of their subagents cancelled |
|
||||
| `src/infra/commands.ts` | `Commands`, the CLI transport: `run`; `api` and `pages`, limited to the configured repository and validated; `lark` as the bot; `git`. `CliError` carries the code a command's output reports, such as a GitHub HTTP status, and a command that prints nothing fails with how it ended, never with its arguments; `validItems` skips and counts the malformed items of a list |
|
||||
| `src/infra/environment.ts` | The agent's process environment: command shims, the git configuration and credential helper, `runAsWorker`, and the checks of the environment and of a Ticket's directory |
|
||||
| `src/infra/paths.ts` | The package's root, where packaged files are |
|
||||
| `src/infra/text.ts` | Word counts and the chat message limit, `fill` and `sameName` |
|
||||
| `src/infra/health.ts` | The `HealthBoard`, every part's health entry, each claimed by its one writer |
|
||||
| `src/sources/kind.ts` | The `SourceKind` protocol |
|
||||
| `src/sources/registry.ts` | `SourceRegistry` |
|
||||
| `src/sources/host.ts` | `SourceHost`, `SourceContext`, `Health`, `health` and the fixed health texts |
|
||||
| `src/sources/poll.ts` | What GitHub and Feishu polling share: `POLL_HORIZON_MS`, `polled`, `cursorAt`, and `pollThreads`, which keeps a failure on its Thread as `pollError` |
|
||||
| `src/sources/github.ts` | The GitHub kind and plugin entry, exported as `./github-watch`: a poll round of notifications, the pull requests of open Tickets with their CI, and subscribed Threads |
|
||||
| `src/sources/github-trigger.ts` | GitHub comments, reviews, Issue bodies and CI changes as Triggers, and GitHub addresses |
|
||||
| `src/sources/github-pull.ts` | The pull request rules: which pull requests a Ticket follows (`claimPull`, `observePull`), the closes it reports to the Engine, and `pullEndNotices`, the chat notice of a Ticket that ended merged or closed |
|
||||
| `src/sources/github-channel.ts` | The GitHub Channel: comments and review thread replies, `sent` or `gone` |
|
||||
| `src/sources/github-read.ts` | CI snapshots, repository reads, the `github/` resources, and `githubThreadContext`: an Issue's or pull request's conversation by page |
|
||||
| `src/sources/feishu.ts` | The Feishu kind and plugin entry, exported as `./feishu-watch` |
|
||||
| `src/sources/feishu-trigger.ts` | Feishu messages as Triggers, by one rule for live events, history pages and topic pages |
|
||||
| `src/sources/feishu-loops.ts` | The Feishu loops: the event consumer, history recovery, chat names and polling of subscribed topics |
|
||||
| `src/sources/feishu-channel.ts` | The Feishu Channel: topic replies, `sent` or `gone` |
|
||||
| `src/sources/feishu-read.ts` | The `feishu/` reads of a Trigger's message, its root, parent and topic, and `feishuThreadContext`, which gives them to assessments with their images |
|
||||
| `src/sources/gitea.ts` | The Gitea kind and plugin entry, exported as `./gitea-watch`: a poll round of notifications, the observed repository streams, and subscribed Threads |
|
||||
| `src/sources/gitea-api.ts` | The minimal Gitea REST client: token auth from a named environment variable, zod-validated answers, pagination, an injectable fetch |
|
||||
| `src/sources/gitea-trigger.ts` | Gitea Issue bodies, comments and reviews as Triggers, and Gitea addresses |
|
||||
| `src/sources/gitea-channel.ts` | The Gitea Channel: Issue and PR comments, `sent` or `gone` |
|
||||
| `src/sources/admission-context.ts` | The bounded window an assessment starts from: the first pages of a Trigger's Thread, read through its kind, and the messages around the Trigger |
|
||||
| `src/sources/feishu-image.ts` | Reads a Feishu message's image as an attachment for the assessing Session |
|
||||
| `src/client/index.tsx` | The client plugin entry: the sidebar entry, the main panel and the page in DeepSeek Harness's Plugin Manager |
|
||||
| `src/client/app.tsx` | The panel: header, search, glossary, tabs and the page of the current route |
|
||||
| `src/client/provider.tsx` | What every page reads from its host: copy, calls, navigation, notices, the state poll, and `restart`, which reads the state quickly while dsh-superagent restarts |
|
||||
| `src/client/default-model.tsx` | The header's default-model switcher, and how a refused switch is worded |
|
||||
| `src/client/api.ts` | Calls and polls of the management API, typed by `src/app/api.ts`, and the Decision filters of the Eval status |
|
||||
| `src/client/route.ts` | The tabs, the settings groups, the typed routes that `parseRoute` reads, and the route memory with the settings group opened last |
|
||||
| `src/client/overview.tsx` | The overview |
|
||||
| `src/client/source.tsx`, `trigger.tsx`, `eval.tsx`, `execution.tsx` | One file per stage: its status, and its settings where it has some |
|
||||
| `src/client/thread.tsx`, `ticket.tsx` | The record pages: a Thread with its timeline, and a Ticket with its authorization, Inputs and Assignments |
|
||||
| `src/client/settings.tsx` | The Settings tab: the group navigation and the group on screen |
|
||||
| `src/client/settings-kit.tsx` | The building blocks of settings blocks: the settings the stage groups share, `useBlockDraft`, the fields, among them a Source kind's, and the history of saves |
|
||||
| `src/client/prompts.tsx` | The Prompts settings: the page owns the view it read and its editor the draft, and the Edit, Full prompt and History tabs render from them; a built-in part's editor shows a changed default and keeps or drops the override |
|
||||
| `src/client/prompt-config.ts` | The prompt configuration by section, by the team sections, skill names, built-in parts (`part/<key>`, with `withOverride`) and part headings the prompt views give, and the line diff of the full prompt, as pure functions |
|
||||
| `src/client/deployment.tsx` | The Deployment settings, also shown in DeepSeek Harness's Plugin Manager |
|
||||
| `src/client/draft.tsx` | Settings drafts: `draftReducer`, their states and their merge with a newer saved revision, `saveBarText`, the save bar's line, and the save bar |
|
||||
| `src/client/controls.tsx` | Local copies of DeepSeek Harness's controls, which follow its markup, keyboard behaviour and design tokens, since the bundle does not import `@deepseek-ai/dsh-client-ui-primitives`; they read no context |
|
||||
| `src/client/widgets.tsx` | The small components the pages share, which read the panel's context: sections, page heads, load states, times, links, confirmed actions, the setup check and Markdown |
|
||||
| `src/client/rows.tsx` | The rows the pages share: a Ticket, a Thread and a Decision, a Thread's chip, and the folding of runs of automated messages |
|
||||
| `src/client/markdown.ts` | The Markdown subset of reports and questions, and the bare addresses in messages, as blocks and inline parts |
|
||||
| `src/client/text.ts` | Copy by key and the wording of domain values, reasons, senders and times |
|
||||
| `src/client/marks.ts` | The marks of the states the views classify: a Ticket's or a Thread's stage, a Source's state and a Decision's outcome, each as a dot, an icon, a tone and a label |
|
||||
| `src/client/locales.ts` | All copy in both languages, and the glossary of [001 Domain model](001-domain.md) |
|
||||
| `src/client/client.css`, `css.d.ts` | The panel's stylesheet, loaded as text |
|
||||
|
||||
`src/sources/github.ts`, `src/sources/feishu.ts` and `src/sources/gitea.ts` are also the entries of the three Source plugins, built by `tsdown.config.ts` and exported in `package.json` as `./github-watch`, `./feishu-watch` and `./gitea-watch`. The bundle's `cordis.patch.yml` inserts the four plugins, with Feishu and Gitea disabled.
|
||||
|
||||
## Tests
|
||||
|
||||
| Test | Covers |
|
||||
| --- | --- |
|
||||
| `tests/core-engine.spec.ts` | Domain rules on in-memory repositories: receiving, Eval, fan-out, resume and stop, interrupted Assignments, the pause, Policy changes, reports and outcomes, attaching, replies, failed deliveries and dismissed problems |
|
||||
| `tests/stage.spec.ts` | The Ticket stages, row by row |
|
||||
| `tests/policy.spec.ts` | Source scope, the gate, the admission table, Mandate revalidation, the Policy version, settings migrations and assessment outcomes |
|
||||
| `tests/registry.spec.ts` | `SourceRegistry`: instances, validation, Policy parts, Channels and Thread context |
|
||||
| `tests/sources.spec.ts` | The Feishu and GitHub Sources on a `SourceContext`: translation, polls and read positions, the pull request rules, health |
|
||||
| `tests/gitea.spec.ts` | The Gitea Source on a `SourceContext` against a fetched stub of the Gitea API: translation, notification and stream polls, cursors, the Channel, health |
|
||||
| `tests/default-model.spec.ts` | The default-model switcher: the routes it refuses, the switch it makes through the Session Controller, and the wording of a refusal |
|
||||
| `tests/commands.spec.ts` | CLI calls as the deployment's accounts, the Channels' answers, and repository-scoped GitHub calls |
|
||||
| `tests/feishu-read.spec.ts`, `tests/github-read.spec.ts` | Feishu and GitHub reads |
|
||||
| `tests/admission-context.spec.ts`, `tests/feishu-image.spec.ts` | The Thread window of assessments, and Feishu image reads |
|
||||
| `tests/assessment.spec.ts` | What the MasterAssessor keeps per Eval, and when it lets go |
|
||||
| `tests/background.spec.ts` | Waiting for a worker's shell jobs and subagents: an end that woke it, and a host that stops |
|
||||
| `tests/bundle-patch.spec.ts` | The bundle's patch, read as DeepSeek Harness reads it, and the agent preset of Executor Sessions it defines |
|
||||
| `tests/effects.spec.ts` | The effect queue: coalescing, limits, retries with backoff, health and shutdown |
|
||||
| `tests/passes.spec.ts` | Passes: one at a time, the ones asked for during a pass run as one after it, and the runtime health a failed pass reports |
|
||||
| `tests/outbox.spec.ts` | The Outbox on a running Runtime: messages wait while processing is paused or their Source is not installed |
|
||||
| `tests/cache.spec.ts` | `TextCache` |
|
||||
| `tests/queue.spec.ts` | The Runtime's work through the effect queue, and what earlier versions stored |
|
||||
| `tests/followups.spec.ts` | A person's follow-up on a Ticket's Thread: its notice to the main Session, the tools that act on it, what a burst costs; a worker's hand-back and context follow, and the main Session's note |
|
||||
| `tests/health.spec.ts` | One health entry per part, each cleared by its own next success |
|
||||
| `tests/delivery.spec.ts` | `AgentDelivery` |
|
||||
| `tests/retry-health.spec.ts`, `tests/role-models.spec.ts` | Model budget retries, and the models of the two roles |
|
||||
| `tests/runtime.spec.ts` | The whole application on a simulated DeepSeek Harness host, through tools and HTTP |
|
||||
| `tests/tools.spec.ts` | The Sessions' tools: what `orchestrator_read` reads for each role, and the shapes and checks of the other main Session tools |
|
||||
| `tests/runtime-concurrency.spec.ts` | Work that overlaps: Triggers during a pass, Tickets that change during deliveries and launches |
|
||||
| `tests/http.spec.ts` | The same-origin and loopback checks, the status of each kind of failure, the body limit, path parameters and queries |
|
||||
| `tests/settings.spec.ts` | Settings saves that race a model or account check, a Policy sync that a save outran, and the refusals of the pause, a sync and a block |
|
||||
| `tests/views.spec.ts` | What the management views derive from the Store, such as the Thread a dropped message belongs to, and the overview's liveness and workspace readouts |
|
||||
| `tests/reclaim.spec.ts` | Which Ended Tickets' directories may be removed: the retention, the execution root, an open Ticket sharing one, and the end times that cannot be read |
|
||||
| `tests/timeline.spec.ts` | A Thread's pages and a Trigger's neighbours by time and then id, with every received Trigger's time in one form |
|
||||
| `tests/migration.spec.ts` | The schema 11 migration: every stored record in its current shape, and the refusal of older schemas |
|
||||
| `tests/store.spec.ts` | Repositories and their working sets, in SQLite as in memory, the kv area, with every key of `KV` spelled only in `src/kv.ts` and the migration, settings history and queries |
|
||||
| `tests/prompts.spec.ts` | Prompt validation, assembly and versions |
|
||||
| `tests/cards.spec.ts` | The rules the built-in text must state, file by file |
|
||||
| `tests/model-surface.spec.ts` | Everything models read: tools, prompts, skills and inputs, checked against the tool schemas, the execution environment and the glossary, within a size budget per role |
|
||||
| `tests/deployment.spec.ts`, `tests/environment.spec.ts`, `tests/index.spec.ts` | The plugin configuration and setup, the agent's process environment, the plugin entry |
|
||||
| `tests/service-script.spec.ts` | `scripts/dsh-superagent` against fake `ps`, `tmux` and `dsh` |
|
||||
| `tests/client.spec.tsx`, `tests/navigation.spec.ts`, `tests/controls.spec.tsx` | The client plugin's registration and polling, Session navigation and route memory, the local controls, and the panel's imports, which have no cycle |
|
||||
| `tests/panel-overview.spec.tsx`, `panel-source.spec.tsx`, `panel-trigger.spec.tsx`, `panel-eval.spec.tsx`, `panel-execution.spec.tsx` | Each page of the panel, against a fake management API answered by route |
|
||||
| `tests/panel-settings.spec.tsx`, `panel-prompts.spec.tsx`, `panel-deployment.spec.tsx`, `panel-panel.spec.tsx` | The settings groups, drafts and history, the prompts, the Deployment page, and the panel's search, glossary and route memory |
|
||||
| `tests/draft.spec.ts`, `tests/prompt-config.spec.ts`, `tests/markdown.spec.ts`, `tests/marks.spec.ts` | Draft states, merges and the save bar's line; the prompt configuration by section and the line diff; the Markdown subset and its links; the mark of every state the views classify |
|
||||
| `tests/locales.spec.ts`, `tests/styles.spec.ts` | The copy in both languages, the stylesheet |
|
||||
| `tests/class-docs.spec.ts` | Every class in `src` has a JSDoc block before it |
|
||||
| `tests/docs.spec.ts` | Numbered documents link only to earlier ones, every local link resolves, no document has merge conflict markers, and this document's tables list every module and every test |
|
||||
|
||||
`tests/fixtures/` holds what several tests share: `host.ts`, a simulated DeepSeek Harness host; `panel-server.ts` and `panel.tsx`, the fake management API and the panel around a page; `deployment.ts`, a resolved deployment; `git.ts`, the environment of every git the tests start, which names no repository; and `async.ts`.
|
||||
|
||||
## Persistence
|
||||
|
||||
One SQLite file, `<stateDir>/orchestrator.sqlite`, which only the Store opens.
|
||||
|
||||
| Table | Holds | Written by |
|
||||
| --- | --- | --- |
|
||||
| `trigger` | Each Trigger, its time `at` in the one form `Date#toISOString` gives, set when the Engine receives it, so times compare as text | Engine |
|
||||
| `disposition` | Where each Trigger went: `filtered` with the Source's reason code, its Eval and Decision, the Tickets it reached, by fan-out or attachment, whether it was requeued, the Tickets that handed it back with their reasons, and those whose Executor acknowledged or answered it | Engine |
|
||||
| `trigger_thread` | Which Threads each Trigger is on, with its time and whether a person wrote it, so a Thread's Triggers are read without scanning every Trigger, in order of time and then id: a page of a Thread's timeline continues from a `<at>|<id>` cursor, and Triggers that share a time are not skipped | Store, with each Trigger |
|
||||
| `thread` | Threads: Subscriptions, the Tickets that follow it for context, queued Triggers, the Eval in progress, the count of its expiries and `meta`; the generated columns `active`, set while Triggers are queued or an Eval runs, and `subscribed`, its Source while a Ticket subscribes to it | Engine; Sources write `meta` |
|
||||
| `ticket` | Tickets with their Mandate, origin, execution, Assignments, Channels, Inputs and the Threads they follow; the generated columns `open`, set while it has not ended, and `active`, set while Inputs are undelivered or the latest Assignment is pending or running | Engine |
|
||||
| `decision` | Decisions, by Thread | Engine |
|
||||
| `effect` | The effect queue: per kind and key, the payload, its order, failed attempts with the last error, and when it is due | Effects; the schema 11 migration queues what earlier versions kept for the main Session |
|
||||
| `outbox` | The receipt of each message for a Channel, by id, with the Ticket it is for and its `status`, `pending`, `sent` or `dropped`, kept so an id is never queued twice; for a dropped message, when and why, as the generated column `dropped`, its time, and a reason code. A message that waits has a `reply` effect, which holds its attempts | The Outbox |
|
||||
| `kv` | Every other record, by key | The part that names it |
|
||||
|
||||
A generated column is computed from its row's JSON and is empty outside its set, and each has an index over its set, so a pass reads only active work. A database that predates a column gets it when the Store opens it.
|
||||
|
||||
`KV` (`src/kv.ts`) defines each key-value record's owner, key and type. All readers and writers use these entries; only migration code names older keys.
|
||||
|
||||
| Records | Keys or contents |
|
||||
| --- | --- |
|
||||
| Schema | Current schema version |
|
||||
| Settings versions | `settings-revision`, `settings-version:<n>`, `settings-block-revision:<block>`; snapshots include origin, Policy version and affected Tickets |
|
||||
| Prompt versions | `prompt-revision`, `prompt-version:<n>` |
|
||||
| Main Session | `master-id` |
|
||||
| Session prompt history | `session-prompts:<session id>` |
|
||||
| Source state | Read positions such as `github-cursor:<number>`, PR CI fingerprints and seen Feishu-topic messages; keys begin with the kind or `thread-` plus a Thread key |
|
||||
| Display names | `source-labels:<source id>` |
|
||||
|
||||
Dismissal is stored on its Decision. Input text is rendered from the Ticket when read, so neither needs a separate key-value record.
|
||||
|
||||
Read caches use bounded in-memory `TextCache` instances and are lost on restart. A Session paging through an original Source record or prompt preview must reread offset 0 after a restart. Feishu links resolved after intake are cached the same way; link-based search finds them only after that link has been read during the current process lifetime.
|
||||
|
||||
Nothing deletes old records yet, so these grow without bound: the `trigger`, `disposition` and `decision` rows; ended Tickets and their Threads; the outbox's receipts; and in `kv`, every saved settings revision and prompt version, the prompt revisions each Session worked under, and the Sources' records per Thread and per Issue or pull request.
|
||||
|
||||
`SCHEMA_VERSION` only increases. A database of a newer version refuses to start; `src/app/migration.ts` upgrades a database of the pre-release schema 10 once, at start, and refuses an older one.
|
||||
|
||||
## Model-facing interfaces
|
||||
|
||||
### Main Session tools
|
||||
|
||||
The main Session has 14 application tools:
|
||||
|
||||
| Purpose | Tools |
|
||||
| --- | --- |
|
||||
| Read and route | `orchestrator_read`, `orchestrator_route` |
|
||||
| Manage Tickets | `orchestrator_ticket_retry`, `orchestrator_ticket_request_status`, `orchestrator_ticket_note` |
|
||||
| Re-evaluate | `orchestrator_trigger_requeue` |
|
||||
| Assess proactive work | `orchestrator_eval`, `orchestrator_eval_read`, `orchestrator_eval_decide` |
|
||||
| Communicate and unsubscribe | `orchestrator_reply`, `orchestrator_thread_unsubscribe` |
|
||||
| Configure | `orchestrator_settings_save`, `orchestrator_policy_sync`, `orchestrator_prompts_save` |
|
||||
|
||||
`orchestrator_route` accepts 1–20 decisions in order. Each accept, attach, stop or ignore runs as a separate Engine command. It also permits ignoring an inferred candidate directly from its card. `orchestrator_ticket_note` sends a fact as context, not authority.
|
||||
|
||||
Settings saves select `policy`, `eval`, `executor`, `pause`, or `source`; the last also names its Source id. A prompt save with `preview: true` composes text without saving.
|
||||
|
||||
The main Session also has DeepSeek Harness's `skill`, `bash`, `read`, `read_image`, `job_output`, `job_list` and `job_kill`. Avoiding code reads is a prompt rule, not a tool restriction.
|
||||
|
||||
### Presets and wait limits
|
||||
|
||||
New main Sessions use `dsh-superagent-router` (`ROUTER_PRESET`). Shell commands and job waits return after 5 seconds. If the Web UI selects another preset before the first turn, `Sessions` restores this preset, or `WORKER_PRESET` for a worker.
|
||||
|
||||
The `noWait` guard in `src/app/sessions.ts` refuses main-Session shell commands that invoke `sleep` at a command position, shell `timeoutMs` above 30 seconds, and `job_output` waits above 5 seconds. Reports and job results arrive automatically, so waiting only delays incoming candidates.
|
||||
|
||||
Existing main Sessions on `standard` also need explicit timeouts for foreground commands and job waits; otherwise that preset's defaults hold the step for 60 and 30 seconds respectively. The host determines the current preset through `presetForSession`, falls back to the Session projection or header, and assumes `standard` if it cannot read the value.
|
||||
|
||||
### Executor tools
|
||||
|
||||
Each Executor has six application tools:
|
||||
|
||||
| Tool | Purpose |
|
||||
| --- | --- |
|
||||
| `orchestrator_read` | Read resources available to its own Ticket |
|
||||
| `orchestrator_ack` | Acknowledge a request |
|
||||
| `orchestrator_report` | Report progress or an outcome, binding a pull request under the pull-request rules |
|
||||
| `orchestrator_thread_unrelated` | Drop an unrelated attached Thread, hand back its owned follow-ups and tell the main Session why |
|
||||
| `orchestrator_thread_follow` | Subscribe to a related Thread for context and tell the main Session |
|
||||
| `orchestrator_trigger_handback` | Hand back a message once, with a reason carried by its candidate |
|
||||
|
||||
A hand-back returns `queued: false` if another Ticket still owns the message, or this Ticket never owned it. For example, a bot message went to Eval independently.
|
||||
|
||||
The worker preset is defined in `cordis.patch.yml` and selected through `WORKER_PRESET`. `WORK_TOOLS` in `src/app/sessions.ts` retains available file, search, shell, job, web, skill, todo and subagent tools. Workers do not receive tools for asking or presenting to a person, changing the profile, scheduling work outside worker slots, starting or messaging other Sessions, or editing external documents. Tool calls are refused after the Ticket ends.
|
||||
|
||||
### Result shapes and assessment context
|
||||
|
||||
Routing results use `{decision?, reason, ticket?}`. `orchestrator_route` returns `{results}`, preserving item order and adding each item's `trigger_id` and `kind`. Invalid or refused items return `refused` and a reason without stopping the batch.
|
||||
|
||||
Settings and prompt saves return the saved configuration. Policy sync returns the Source, allowed-list size, and `added` and `removed` names.
|
||||
|
||||
`orchestrator_eval` returns context rather than instructions. Assessment instructions live in `runtime/admission` (`prompts/admission.md`), and rules in `runtime/policy`. Its context includes:
|
||||
|
||||
- The Trigger text, up to 4,000 characters, with image ids and other Triggers in the batch.
|
||||
- Up to eight `related_tickets`, open ones first, and `tickets_total`.
|
||||
- The Eval's `policy_version`.
|
||||
- A Thread window: the first five messages and five on either side of the Trigger. Messages use `{id, from, at, text}`, normally limited to 400 characters, or 1,200 for the two on either side. Truncated messages include `chars`; image references use `images`.
|
||||
- Other loaded messages as `{id, from, preview}` stubs of 50 characters. `orchestrator_eval_read` expands them by id.
|
||||
|
||||
Ticket cards use the closed `TICKET_STATES` set from `src/app/cards.ts`: an open stage such as `person/review` or `working/queued`, or an ended outcome. Acknowledgements are limited to 100 words whether submitted through accept, assessment or `orchestrator_ack`.
|
||||
|
||||
### Read resources
|
||||
|
||||
`RESOURCES` in `src/app/resources.ts` defines the resources and generates the tool's `resource` description.
|
||||
|
||||
| Resources | Content |
|
||||
| --- | --- |
|
||||
| `tickets`, `triggers`, `threads`, `decisions` | Paged lists as `{items, next_offset}`; activity lists put newest first |
|
||||
| `decision/<id>` | One Decision |
|
||||
| `thread/<key>` | Label, Tickets, recent Decisions and a page of Triggers |
|
||||
| `trigger/<id>` | Message text, replied-to Trigger and neighboring messages, excluding CI runs |
|
||||
| `ticket/<id>` | Related ids, source-batch Triggers, Threads, and first pages of Assignments and Inputs |
|
||||
| Ticket subresources | `/assignments`, `/inputs`, `/assignment`, `/result`, `/report/<assignment id>`, `/transcript`, `/message/<n>`, `/input/<id>` |
|
||||
| `settings`, `settings/<block>` | Revisions and summaries, or a complete block ready to save |
|
||||
| `prompts`, `prompts/<revision>`, `prompts/current`, `prompts/defaults`, `prompt-preview/<id>` | Prompt configurations and composed text |
|
||||
| `models`, `models/<provider>` | Available models |
|
||||
| `feishu/<trigger id>/…`, `github/…` | Original Source records, described in the Source skills |
|
||||
|
||||
Ticket Threads are marked `context` for context-only follows and `bound` for Source bindings. Assignment pages are newest first and include report previews. Continue lists with `next_offset`. `/input/<id>` accepts an Input's name or id. `settings` includes small blocks in full and summarizes Policy and Source blocks.
|
||||
|
||||
Source resources include message neighborhoods, `github/pulls/<n>/thread/<comment id>` and `github/commits/<sha>/checks`.
|
||||
|
||||
An original's messages read in one compact shape, `{id, from, at, text, reply_to?, path?, line?, url?}`, not as GitHub or Feishu records them.
|
||||
|
||||
An Executor Session reads only its own Ticket, its Threads, the Triggers that reached it or that its request held, the prompt versions it worked under and the originals, and its pages leave out the ids of what it cannot read, Decisions and other Tickets.
|
||||
|
||||
Every id a page shows reads through one of these, so a Session walks from one object to the next; hashes nothing reads by, such as an Eval's id or a Policy version, stay off the pages.
|
||||
|
||||
A text page has 4,000 UTF-16 code units by default and 8,000 at most, a list 20 items by default and 50 at most.
|
||||
|
||||
The names `event/`, `task/` and `observation/` of earlier versions still resolve.
|
||||
|
||||
### Messages delivered to Sessions
|
||||
|
||||
These messages are steered into the main Session's turn:
|
||||
|
||||
| Message | Content |
|
||||
| --- | --- |
|
||||
| Candidate | Batch Trigger cards, Thread, `related_tickets`, `tickets_total`, `delivered_to`, referenced Tickets from Issue/PR links, `handed_back` reasons and the next action |
|
||||
| `follow_up` | One message as its Executor sees it, `replies_to`, receiving Tickets, and the reminder that Executors decide it while the main Session may only stop work |
|
||||
| `worker_report` | Ticket card, originating Trigger, state and summary up to 600 characters; `chars` and a read pointer when truncated |
|
||||
| `worker_attention` | Ticket card carrying the question |
|
||||
|
||||
Other notices are injected without waking the main Session: `ticket_ended`, `settings_saved`, prompt saves, Trigger requeues, dropped Subscriptions and context follows. A settings notice names the block, changed paths and `read: settings/<block>`. The main Session receives no redundant notice for a save, requeue, unsubscribe or attach it performed itself.
|
||||
|
||||
Workers receive assignments and Inputs inline. The first message includes the title, up to 1,500 characters of assignment text and the source Trigger card. Inputs carry a Trigger card with up to 600 characters and `replies_to`, or plain Input text. Read pointers appear only for truncated content.
|
||||
|
||||
### Panel-only actions
|
||||
|
||||
There is no tool for dismissing an unavailable Decision, rerunning setup, refreshing Source display names, or sending an Executor-owned human follow-up to Eval. The last action is deliberately refused by `orchestrator_trigger_requeue`. Other panel actions have equivalent tools.
|
||||
|
||||
Most built-in instructions live in `prompts/*.md`, with one sentence per line joined by `promptFile`. Source skills live in `skills/dsh-superagent-*/SKILL.md`. Tool descriptions and fixed notices such as restart recovery notes remain in code. Assessment instructions live only in `prompts/admission.md`.
|
||||
|
||||
`tests/model-surface.spec.ts` checks these surfaces for drift. [004 Prompts](004-prompts.md) explains the team's editable parts.
|
||||
|
||||
## Configuration reload
|
||||
|
||||
Every plugin configuration key except `stateDir` is volatile. The panel's Deployment settings and DeepSeek Harness's Plugin Manager edit them through DeepSeek Harness's configuration form; the host writes `cordis.patch.yml` and updates the values in place, and dsh-superagent reads each limit when it uses it, so a change applies without a restart.
|
||||
|
||||
A settings save that changes a Source field setup reads, such as an account's CLI, and any save while setup waits, restarts dsh-superagent. DeepSeek Harness's settings service does not serve the entry during that restart, so after its own restart dsh-superagent asks it to read the entry again, and an open form becomes editable again.
|
||||
|
||||
## Reviewing a change against real data
|
||||
|
||||
Point `DSH_HOME` at a temporary directory and create a new profile there. Copy the database from a backup, disable every Source, pause processing and remove the main Session id. Give the GitHub and Feishu CLIs read-only stand-ins.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Trust: which Trigger fields are trusted, such as the Source's actor and Thread membership, and which are not, such as the body and quotes.
|
||||
- Deployment isolation: the agent's profile on its own pinned DeepSeek Harness installation, so upgrading DeepSeek Harness elsewhere does not break it.
|
||||
+628
@@ -0,0 +1,628 @@
|
||||
# 007 架构
|
||||
|
||||
这是实现层面的开发者指南。它把 [001 领域模型](001-领域模型.md) 至 [006 部署](006-部署.md) 的概念映射到类、接口与文件。先看 runtime 与 Engine,再把 module 表和 test 表当作代码索引使用。
|
||||
|
||||
## Core classes and glue
|
||||
|
||||
Core 类拥有领域概念,并在小接口之后持有外部边界。Glue 把工具参数、HTTP 请求、Source 载荷与 DeepSeek Harness 事件翻译成对这些类的调用,再把结果变成卡片、视图和消息。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
plugins[Source plugins] -->|SourceContext| engine
|
||||
tools[Session tools] --> engine
|
||||
http[Management routes] --> engine
|
||||
engine[Engine] -->|Repositories| store[Store]
|
||||
engine -->|Policy| policy[Policy]
|
||||
engine -->|SourceScope| registry[SourceRegistry]
|
||||
engine -->|Assessor| assessor[MasterAssessor]
|
||||
engine -->|Executor| executor[DshExecutor]
|
||||
assessor -->|decide| engine
|
||||
engine -->|Outbox| outbox[Outbox]
|
||||
executor -->|assignmentStarted, inputsDelivered, report| engine
|
||||
settings[Settings] -->|policyChanged| engine
|
||||
assessor & executor & sessions[Sessions] & outbox -->|enqueue| effects[Effects]
|
||||
effects --> store
|
||||
```
|
||||
|
||||
`Runtime`(`src/app/runtime.ts`)构建应用、供给依赖,并拥有启动与关闭。`Engine` 构建它自己的内部部件。Executor 与 Assessor 会回调 Engine,所以 Runtime 给它们的是 getter,而不是要求按循环顺序构造。
|
||||
|
||||
### Runtime
|
||||
|
||||
`Runtime` 是 composition root;它不含领域规则。它的构造函数锁住状态目录、打开 Store 并构建每个部件。若构造失败,它会关闭 Store 并释放锁。
|
||||
|
||||
启动注册 management 路由并运行 Setup。setup 解析完成后,它安装 Session 与 Executor 钩子、确保 main Session 存在、启动 Policy 同步、恢复被打断的 Assignments,并启动 passes 与 effects。`ready` 标记启动完成。
|
||||
|
||||
关闭取消回合、等待 passes、effects 与 agents、恢复进程环境、关闭 Store 并释放锁。Source 插件经由窄接口 `SourceHost` 把 Runtime 作为 `orchestrator` 服务注入。
|
||||
|
||||
| 部件 | 模块 | 做什么 |
|
||||
| --- | --- | --- |
|
||||
| Setup | `src/app/setup.ts` | 按 worker shell 运行 CLI 的方式探测 CLI,由此解析仓库与账号,写出并应用 worker 的环境与命令垫片,并保留 setup 问题;Settings 经由它检查变更的账号,也经由它重启,而重启会再次运行 setup |
|
||||
| Passes | `src/app/passes.ts` | 一次一趟,每两秒一次,以及每当有命令要求时:先到期 effects,再 `Engine.evaluateDue`;一个 Source 的 Trigger 经由它到达 Engine |
|
||||
| Outbox | `src/app/outbox.ts` | Engine 的 `Outbox` 端口,也是 receipts 的唯一写入者:`send` 在一个事务里保存消息的 receipt 并排队它的 `reply` effect,而该 effect 的 handler 经由 `SourceRegistry.send` 发送;消息在其 Source 停用时等待,在 conversation 已消失或不被监听时丢弃 |
|
||||
| Notices | `src/app/notices.ts` | 把领域事件与 worker 活动转换为 main-Session 消息。`eventNotice` 处理领域通知;`reportRelay` 立刻转发提问、把普通进度留在其 Ticket 上、并转发其他报告;`followupRelay` 点名拥有某条人类跟进的那些 Tickets。在一个回合进行中途到达的 relay,会在下一步经 DeepSeek Harness inbox 一起投递 |
|
||||
|
||||
### Engine
|
||||
|
||||
`Engine`(`src/core/engine.ts`)是 Triggers、Threads、Decisions、Tickets 与 Assignments 的唯一写入者。它从 `src/core/ports.ts` 接收这些端口:`Repositories`、一个当前-Policy getter、`SourceScope`、`Assessor`、`Executor` 与 `Outbox`。可选依赖是 `prepareTicket`、`now` 与 `emit`。
|
||||
|
||||
Engine 的所有方法都是同步的。每个命令在一个事务里改动它的聚合。`evaluateDue` 对它改动的每个 Eval 或 Ticket 使用单独的事务。端口在事务内 enqueue 外部工作;适配器在 commit 之后执行它们。非法的请求抛出 `UserError`。
|
||||
|
||||
Engine 对外暴露一个入口点,并委托给五个内部部件。它拥有每个命令的事务。跨部件的命令——如 `attach`、`evaluateDue` 与 `policyChanged`——按顺序调用各部件。
|
||||
|
||||
每个部件都继承 `EnginePart`(`src/core/part.ts`),后者提供选项与共享读取。部件之间的依赖单向流动:
|
||||
|
||||
| 部件 | 职责 | 依赖于 |
|
||||
| --- | --- | --- |
|
||||
| `Intake` | 一个 Source 的 Trigger:连同其处置结果存储一次、记录在其 Threads 及其 correlations 上、扇出到订阅的 Tickets,并且除非它是某条抵达了某个 Ticket 的人类消息,否则在其 primary Thread 上排队等待 Eval | Tickets |
|
||||
| `Evals` | Threads 的 Evals:批处理、gate 给出的强度、actor 与依据以及给出它们的 Trigger(`primary`)、依据候选的投递而定的 deadline 及其因暂停而后移、过期 Eval 的重新排队、候选、重放与交回、Policy 变更时的重新排队,以及各自最终的 Decision:accept、stop、ignore 或 unavailable | Tickets |
|
||||
| `Tickets` | Ticket 生命周期:从一次 accept 创建及其 Mandate、Subscriptions 与首个 Assignment,它跟随的 Threads 及其关闭(`followsDone`),附着的 Threads 与为上下文而跟随的 Threads,Inputs 与 notes,resume 及 resume 拒绝的消息,retry、restart、stop 与 end,它丢弃的 Subscriptions,以及向它所涉 Tickets 发出的 Policy 变更通知 | Replies |
|
||||
| `Assignments` | Executor 及其 workers 上报的事:Assignment 已启动、Inputs 抵达或未能抵达、restart 打断了它,以及 worker 的 Report 及其给人类的提问 | Tickets, Replies |
|
||||
| `Replies` | 经 `Outbox` 写 Channel:确认、回答、结果与求关注消息,各自按允许它的规则 | 无 |
|
||||
|
||||
命令用祈使式命名,如 `stopTicket`;适配器上报的事实用过去式命名,如 `assignmentStarted`。查询 `candidate` 用名词。
|
||||
|
||||
调用方只拿到它们角色的 Engine `Pick`:`EngineForSources`、`EngineForMainSession`、`EngineForPeople`、`EngineForSettings` 或 `EngineForExecutor`。
|
||||
|
||||
| 角色 | 方法 | 效果 |
|
||||
| --- | --- | --- |
|
||||
| Sources | `receive(trigger)` | 把一个 Trigger 存储一次并回答 `stored`、`duplicate`,或在没有 Source 接收它时 `filtered`;把它扇出到订阅其 Threads 的那些 Tickets——人类的消息除非处理处于暂停,会立刻 resume 一个已结束的 Ticket——并在其 primary Thread 上排队等待 Eval,除非它是某条抵达了某个 Ticket 的人类消息,那种情况它归属那个 Ticket |
|
||||
| Sources | `bindThread(ticketId, thread, own)` | 让一个开着的 Ticket 跟随该 Source 的某个 Thread,例如某个 pull request 的;`own` 表示该 Ticket 开启了它。一个已关闭的 Thread 再次绑定时就重新开启。结束 Ticket 的是那些 Threads 的关闭,而不是 `ready` Report |
|
||||
| Sources | `threadClosed(thread, outcome, at)` | 一个 Thread 关闭了:每个跟随它的开着 Ticket 记录这次关闭,并在它跟随完所有 Thread 时(`followsDone`)结束:它开启的 Thread 都不再开着;若它没开启任何 Thread,则它跟随的每个 Thread 都已关闭。它按这些关闭里最晚的那次以 `merged` 或 `closed` 结束——除非关闭之后恢复的工作还没有上报 `ready`,或它的 Executor 通过确认或回答,接管了某条它在其最新 `progress` 之外的 Report 之后、或其最后一个回合结束之时才决定的、属于人类的跟进;那样一条 `dropped` Report 会按那次关闭结束它 |
|
||||
| Main Session | `candidate(triggerId)` | 持有某 Trigger 的那个 Eval,按 assessor 看到的样貌 |
|
||||
| Main Session | `candidateDelivered(evalId)` | 该 Eval 的候选已抵达 main Session:它的 deadline 从现在起算 |
|
||||
| Main Session | `decide(triggerId, outcome)` | 记录一条 Eval Decision,或为 explicit 请求记录一条直接 Decision。Accept 原子地创建 Ticket、Mandate、Assignment、Subscriptions 与确认。一个 inferred 候选若从其卡片上被 ignore,会在给定理由之前先记录 `ignoredFromCard`。只要 `followupOwners` 还拥有某条人类跟进,就只允许 stop |
|
||||
| Main Session | `attach(ticketId, triggerId, note?)` | 把一个 Trigger 路由到它指向的那个 Ticket,其 Executor 随后把它当作跟进决定,并结束持有它的那个 Eval——该 Eval 的其他 Triggers 会等待一次新的 Eval。一个已结束的 Ticket 除非处理被暂停,会立刻在其 Mandate 下 resume。一条 Executor 仍在决定的跟进会被拒绝,一条被路由回当初交回它的那个 Ticket 的也会被拒绝 |
|
||||
| Main Session | `requestStatus(ticketId)` | 请求一个正在等待的 Executor Session 报告状态,并回答是否已请求 |
|
||||
| Main Session | `note(ticketId, text)` | 把来自其他工作的一条事实作为文本 Input 交给某个 Ticket 的 Executor,它不授予任何东西 |
|
||||
| Main Session, Executor | `reply(request)` | 为一个 Channel 排队其用途点名的消息:`acknowledgement`、`answer`、`result` 或 `attention` |
|
||||
| People | `stopTicket(ticketId, reason?)` | 来自人类的停止,它会撤回 Mandate |
|
||||
| People | `retryTicket(ticketId)` | 结束这样一个 Ticket 停滞的 Assignment——它在 `failed` 或 `blocked` Report 之后正等待人类,或其 Input 投递失败(`canRetry`)——并启动一个新的 |
|
||||
| People | `reevaluate(triggerId)` | 把一个已存储的 Trigger 排队等待一次新的 Eval |
|
||||
| People | `unsubscribe(thread)` | 丢弃某个 Thread 上的所有 Subscriptions,跟随它的 Tickets 从此不再跟随它;每个 Ticket 继续决定它在那里收到的跟进 |
|
||||
| People | `dismiss(decisionId)` | 解除某条 `unavailable` Decision 展示的问题,Decision 会记录这件事 |
|
||||
| Executor | `assignmentStarted(ticketId, assignmentId)` | 一个 Assignment 正在运行;它等待中的 Inputs 发出 |
|
||||
| Executor | `inputsDelivered(ticketId, assignmentId, inputIds)` | Inputs 已抵达该 Assignment 的 Executor Session,各自等待一个回合步骤来领取 |
|
||||
| Executor | `inputsRead(ticketId, inputIds)` | Executor Session 已把这些 Input 所承载的消息纳入其历史(一个 `user/message` Session 事件),其模型由此拥有它们;一个领取了它们但在结束前被取消或失败的步骤不算。每条在其 Report 或回合结束之后计为已读,重启之后同样如此,其 Ticket 的结束也不会把它放行;一条在开始记录读取之前投递的 Input,在其投递之后出现过 Report 或回合结束时计为已读 |
|
||||
| Executor | `turnEnded(ticketId, assignmentId)` | Executor Session 的一个回合结束了——Report 之后同样,只要没有东西在后台为其运行、或在其交接给人类时。一个回合开始前领走的东西,即为已读(`inputsRead`) |
|
||||
| Executor | `deliveryFailed(ticketId, assignmentId, error)` | 投递 Inputs 失败;Ticket 会展示它,直到一次投递成功、一个新 Assignment 开始或该 Ticket 结束 |
|
||||
| Executor | `report(ticketId, report)` | 记录一条 Report 及其给人类的提问;`dropped` 结束该 Ticket,`ready` 完成一个不跟随任何 Thread 的 Ticket,并在一个 Ticket 跟随完其 Threads(它们均已结束)时结束它 |
|
||||
| Executor | `markUnrelated(ticketId, thread, reason)` | 只丢弃该 Ticket 对一个被发现无关的附着 Thread 的 Subscription,并连同理由交回那里由该 Ticket 决定的跟进 |
|
||||
| Executor | `followForContext(ticketId, thread)` | 为一个开着的 Ticket 为上下文订阅一个相关的 Thread:它的消息会到达该 Ticket,但不给它带来跟进强度,也不会 resume 它 |
|
||||
| Executor | `handback(ticketId, triggerId, reason)` | 记录该 Ticket 交回一条抵达它的 Trigger,连同其候选携带的理由;若此前是本 Ticket 决定的,则在没有其他 Ticket 决定它时把它排队等待一次新的 Eval;它回答是否排了队。每个 Ticket 对每条 Trigger 只交回一次并保持其 Subscription |
|
||||
| Executor | `assignmentInterrupted(ticketId, assignmentId)` | host 停止时某个 Assignment 正在运行:一个在工作途中被打断,或一个在其最新 Report 之后收到过 Input 的,带着恢复备注和那些已投递给它但其 Executor Session 未领取的 Inputs 重新开始,如同 retry;一个在最新 Report 之后仍有上报的,保留其 Session。DshExecutor 在启动时逐一上报(`resume`),最旧的 Ticket 在前,并重新排队一个正在等待的 launch |
|
||||
| Passes | `evaluateDue()` | 除非处理被暂停:为那些排队 Trigger 正在等待的 Threads 启动 Evals,每个 Thread 一个、其间排队的 Triggers 合为一批;把过期 Eval 的 Triggers 重新排队,连续两次,第三次记录为 `unavailable`;并 resume 暂停期间被人类消息抵达的已结束 Tickets——若它们的 Mandates 仍然成立。候选尚未抵达 main Session 的 Eval 不过期。处理暂停期间没有 Eval 会过期,恢复后每个 deadline 按暂停时长后移 |
|
||||
| Settings | `policyChanged(from, to, effects)` | 告诉每个受变更影响的开着 Ticket 如何受影响(`src/policy.ts` 的 `policyEffects`:它据以接受的一条规则变了或不再适用、其请求者可能不再能启动工作、或主动工作不再被接受),并在新版本下重启进行中的 Evals |
|
||||
|
||||
Engine 让批处理、授权、扇出、resume 与求关注的生命周期规则保持私有。几个共享谓词让这些规则保持一致:
|
||||
|
||||
- `waitsForPerson`(`src/core/model.ts`):最新的 Assignment 上报了 `blocked` 或 `failed`,且此后没有人类消息到来。自动 Inputs 与失败的 launch 在此为真期间保持扣留。
|
||||
- `followupOwners`:仍对其自有 Threads 上收到的人类跟进负责的 Tickets。一个 Ticket 可以处于 live 状态,或未经人类 stop 而结束并等待 resume。交回那条消息即终结那份所有权。
|
||||
- `settled`:某条 Decision 已裁定该请求;stop、unavailable、routed 与 refused-stop 这些 Decision 不算。
|
||||
- `ticketStage`(`src/core/stage.ts`):实现 [003 执行](003-执行.md) 的阶段表。
|
||||
|
||||
当一个 Ticket 结束时,未读的跟进回到 Eval——除非另一个 Ticket 仍拥有它们,或别的处置结果已将其裁定。已确认、已回答与已交回的消息被排除。对于没有确认处置结果的旧记录,outbox receipt 提供该事实。一条最终 Report 还覆盖了 Assignment 开始时正在等待的消息,包括那条 resume 它的消息。使 Ticket 停止的那条消息不会被自动重判。
|
||||
|
||||
变更后,Engine 从 `src/core/ports.ts` 的 `DomainEvents` 发出一个事件:
|
||||
|
||||
`trigger.received`、`eval.started`、`decision.recorded`、`ticket.created`、`ticket.resumed`、`ticket.ended`、`assignment.started`、`assignment.ended`、`thread.subscribed`、`thread.unsubscribed`、`trigger.requeued` 或 `policy.changed`。
|
||||
|
||||
Runtime 把 `ticket.ended`、`trigger.requeued` 与 `thread.unsubscribed` 变成通知。投递到 Tickets 的人类 `trigger.received` 会变成一条跟进 relay。`pullEndNotices` 在一个 Ticket 以 merged 或 closed 结束时,使用一个稳定 id 发送一条 chat 通知。其他事件目前没有消费者。
|
||||
|
||||
### Model
|
||||
|
||||
`src/core/model.ts` 持有领域类型,`src/core/stage.ts` 持有 Ticket 阶段。一条 Decision 把理由记录为 `reason`,即一个 `Reason` 列表(`src/core/reasons.ts`):每个是一个 `code` 及其 `values`,对于一批中的某个 Trigger,还带其 `triggerId`。
|
||||
|
||||
core 的代码——如 `timeout`、`policyChanged`、`routed` 与 `startedTicket`——在 `REASONS` 里,配有 agents 通过 `reasonText` 读取的英文;gate 或 Source scope 给出自己的代码,如 `not_a_member`;而 `said` 承载 assessor 或人类写下的话。面板按 `reason.<code>` 给每个代码措辞,Source 过滤掉的 Trigger 也以同样方式记录 Source 的代码。
|
||||
|
||||
一个 Ticket 把每个事实保存一次。它没有 `ended` 时就是开着的,`it.ended` 记录它何时结束、被谁、为何、以什么结局;它最后说过的是它最新的 Assignment Report(`latestReport`);其 Executor Session 以它命名(`src/executor/dsh.ts` 里的 `workerSessionId`)。
|
||||
|
||||
它的字段有类型:`origin`,即给出其 Eval 批次之强度的可准入 Trigger 及批次本身(`triggerId`、`triggerIds`);`execution`,Executor 选择的东西(`name`、`directory` 与 `assignment` 赋值文本);`delivery`,最近一次失败的 Input 投递(`error`、`at`),直到一次投递成功、一个新 Assignment 开始或该 Ticket 结束;以及 `follows`,Source 让它跟随的那些 Threads,每个 `{thread, own, closed?}`:Ticket 开启了它时 `own` 为真,其关闭后 `closed` 带 `at` 与 `outcome`。只有 Tickets 写 `follows`;GitHub Source 用 `pullsOf` 与 `pullOf`(`src/sources/github-pull.ts`)读取它的 pull requests。
|
||||
|
||||
一个承载 Trigger 的 Input 记录是否由人类所写,`waitsForPerson` 会读取。一个 Thread 还在 `context` 里列出只为上下文而跟随它的已订阅 Tickets,并在 `expired` 里计数其排队 Triggers 的 Evals 连续过期次数;一次 Evaluation 记录其 `primary` Trigger 及其候选被 `delivered` 的时间。
|
||||
|
||||
一个 Trigger 的处置结果记录它抵达过的 Tickets——经扇出或附着、人类的 requeue、每个交回它的 Ticket 及其理由,以及每个其 Executor 确认或回答过它的 Ticket。
|
||||
|
||||
一个 Mandate 的 `basis`(`MandateBasis`)记录 gate 当初如何准入它的 Trigger。重新校验会读取接收的 `source`、`scope` 与 `row`、名单里的 `sender`、命中的 `rules`,以及 `via`:
|
||||
|
||||
| `via` | 准入依据 |
|
||||
| --- | --- |
|
||||
| `addressed` | 一个名单里的发送者 @ 了智能体 |
|
||||
| `followup` | 一条消息到达了一个有 Tickets 的 Thread |
|
||||
| `admission` | 一个准入表单元格;`observation` 标记一个 inferred 候选 |
|
||||
| `bot` | 一个名单里的 bot 产出了 inferred 候选 |
|
||||
|
||||
Trigger 与 Thread 的 `meta` 属于 Source。core 不解读它们。
|
||||
|
||||
### Policy
|
||||
|
||||
`src/policy.ts` 定义设置 schema、授权与研判规则。`settingsPolicy(settings, scope)` 每次使用都返回一个全新的、无状态的 `Policy`:
|
||||
|
||||
- `version`、`paused` 与 `evalTimeoutMs` 描述其当前状态。
|
||||
- `gate(trigger, thread, tickets)` 返回准入强度与依据。
|
||||
- `revalidate(mandate)` 检查一个既有 Mandate 是否仍成立。
|
||||
- `admitsInferred(rules)` 只在 auto 模式且有启用的匹配规则时允许主动接受。
|
||||
|
||||
`assessmentOutcome(assessment, settings, strength)` 把模型的研判转换为一个 Engine 结果。它要求每条启用的规则都有判断、确定的方向、一条匹配的规则与意图证据。auto 模式之外,一次 inferred 的 accept 会变成一条 “would accept” 记录。
|
||||
|
||||
`policyEffects(tickets, old, now, scope)` 找出一次变更影响哪些开着的 Tickets、如何影响。`Engine.policyChanged` 只通知那些 Tickets。`policyVersion` 对规则、accept 模式与各 Source 的 Policy 部分(含 bot 列表)做哈希。
|
||||
|
||||
### Store
|
||||
|
||||
`Store`(`src/app/store.ts`)拥有一个 SQLite 数据库。Runtime 在打开它之前先取得目录锁,阻止第二个进程迁移或写入同一份状态。
|
||||
|
||||
它的接口按用途分组:
|
||||
|
||||
- **领域 repositories:** `atomic`,加上 `triggers`、`threads`、`tickets` 与 `decisions`,各带 `get`、`save` 与列表查询。带索引的工作集是 `threads.active()`、`tickets.open()`、`tickets.active()` 与 `read.subscribedThreads`。
|
||||
- **读取查询:** `read` 通过 `threadTriggers`、`triggerStats`、`search` 与 `droppedReplies` 等方法服务视图、工具与 Source 轮询。这些方法从不写入。
|
||||
- **Source 元数据:** `threads.describe` 与 `threads.patchMeta` 更新 Source 拥有的 Thread 元数据。
|
||||
- **设置:** `settings`、`saveSettings`、`blockRevision` 与 `settingsHistory`。设置与提示词使用编号的 `Versions`(`src/app/versions.ts`)。
|
||||
- **Outbox receipts:** `replyStatus` 与 `receipts`,只由 Outbox 写入。Receipt 操作是 `get`、`waiting`、`behind`、`add`、`settle` 与 `withdraw`。`behind` 检查同一 Ticket 与 conversation 的一条更早消息是否在等待且失败未满三次。一条被撤回的消息以 `channelGone` 或 `channelIgnored` 结算。读取查询通过 `failingReplies`、`droppedReplies` 与 `droppedSince` 暴露失败与被丢弃的回复。
|
||||
- **有类型的 key-value 记录:** `get`、`set`、`delete` 与 `keys`,使用来自 `KV` 的条目。
|
||||
|
||||
`afterCommit` 只在事务 commit 之后执行动作,rollback 之后绝不执行。Runtime 以存储形态读取记录;schema 11 迁移在启动时转换受支持的更旧记录。
|
||||
|
||||
### Effects
|
||||
|
||||
`Effects`(`src/app/effects.ts`)是持久化的副作用队列。一个命令在其事务内调用 `enqueue(kind, key, payload)`;执行只在 commit 之后开始。
|
||||
|
||||
每个 kind/key 对有一行。重复 enqueue 同一个处于等待的 key 会替换其负载并使其立刻到期,但不改变队列顺序。`handle(kind, run, options)` 为每个 kind 注册一个 handler。它按最旧优先处理到期行,至多到并发上限。
|
||||
|
||||
失败以指数退避重试。抛出 `Later` 会推迟工作而不计一次失败,例如没有可用的 worker 槽位时。`start` 在 setup 之后开始处理,`drain` 在关闭期间停止处理但保留待处理行,`pending`/`idle` 暴露队列状态。
|
||||
|
||||
| Kind | Handler | 做什么 | 同时 | 重试 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `assessment` | MasterAssessor | 把一个 Eval 的候选卡片引入 main Session 的回合,然后告诉 Engine 它已投递,由此启动该 Eval 的 deadline | 4,暂停时无 | 2 秒倍增到 60 秒 |
|
||||
| `launch` | DshExecutor | 启动一个待处理的 Assignment:prepare command、Executor Session、赋值消息 | 空闲的 worker 槽位数,暂停时无 | 模型 provider 或网络的一次路过性失败在 5 秒后重跑,倍增到 300 秒,共三次;任何其他失败,以及失败的 prepare command,会使其 Ticket 失败。被暂停或被占满的 worker 槽位扣住的,5 秒后再试 |
|
||||
| `delivery` | DshExecutor | 把一个 Ticket 等待中的 Inputs 按序交给它运行中的 Assignment | 4,暂停时无 | 2 秒倍增到 60 秒;等待 worker 槽位的 2 秒后再试 |
|
||||
| `relay` | Sessions | 把一个 worker 的报告或提问、或一条人类跟进,引入 main Session 的回合 | 1,按队列顺序 | 2 秒倍增到 60 秒 |
|
||||
| `notice` | Sessions | 把一个通知注入 main Session 而不唤醒它 | 1,按队列顺序 | 2 秒倍增到 60 秒 |
|
||||
| `reply` | Outbox | 在一个 Ticket 发往其 conversation 的、尚未失败满三次的更早消息之后,经由其 Source 为一个 Channel 发送一条消息;撤回该消息会取消它 | 1,暂停时无 | 5 秒倍增到 300 秒;其 Source 停用的、或排在其更早消息之后的,5 秒后再试 |
|
||||
|
||||
每个 kind 以其名字在健康板上有一个条目。它在其某个 effect 连续失败三次后上报一个错误,`delivery` 除外——它的失败显示在其 Ticket 上。Relays 保持顺序,notices 也一样;一条失败的会扣住其后同 kind 的那些。
|
||||
|
||||
### Sources
|
||||
|
||||
Sources 通过定义在 `src/sources/kind.ts` 的 `SourceKind` 扩展 host。一个 kind 声明它的身份、title、description、配置 `schema` 与 `fields`、派生的 `facts`,以及 `health` 条目。可选行为包括 `scope`、一个 `channel`(`allowed`、`send`、`link`)、`threadContext`、`find`、`policy`、`environment` 与 `refreshLabels`。
|
||||
|
||||
`SourceRegistry`(`registry.ts`)拥有已注册的 kinds 与已存储的实例,并实现 Engine 的 `SourceScope`。注册会在 `HealthBoard` 上认领健康条目;重复认领与保留的 `sync:` 名字会被拒绝。SourceContext 只通过其注册所认领的条目写入,因此注销之后的上报什么都不做。
|
||||
|
||||
Registry 的配置方法包括 `kind`、`instances`、`instance`、`validate`、`validatePolicy`、`policyPart`、`channelAllowed`、`declarations`、`environments`、`find` 与 `scope`。`find` 会询问每个支持搜索的 kind。
|
||||
|
||||
泛型代码经由 registry 访问 Source 行为。`send(message, commands)` 调用该 kind 的 Channel,返回 `sent`、`gone`,或在 kind 未安装时返回 `waiting`。`link` 解析其 Channel 链接。`threadContext` 把一个 kind 的读取器与其配置配成一对,或在 kind 没有读取器时不返回任何东西。无效的设置配置会禁用该实例。
|
||||
|
||||
插件收到 `SourceHost`:registry 与 `context(kind)`。kind 本身使用一个更窄的 `SourceContext`,由 `src/app/source-context.ts` 里的 `sourceContextOf` 构建。
|
||||
|
||||
那个 context 供给 deployment 与 commands、setup 就绪状态(`ready`、`configured`、`problem`)、配置、`receive`、`atomic`,以及有类型的 `state`。它还暴露 Thread 读取与元数据写入、Ticket 读取与绑定/关闭命令、Trigger 读取、经 Outbox 的 `notify`,以及为已声明健康条目准备的 `report`。
|
||||
|
||||
### DshExecutor
|
||||
|
||||
`DshExecutor`(`src/executor/dsh.ts`)为每个 Ticket 提供一个 Session 与目录。`start` 与 `deliver` 排队 `launch` 与 `delivery` effects。`stop` 通过 `Store.afterCommit` 取消回合,于是一个被 rollback 的结束不会停掉 worker。
|
||||
|
||||
`prepare(ticket, outcome)` 提供 Engine 的 `prepareTicket` 钩子:短名、目录与 `ticketExecution` 赋值文本。`workerSessionId(ticket.id)` 派生 Session id。
|
||||
|
||||
`WorkerSlots` 在 launch 或会唤醒空闲 worker 的 delivery 之前预留容量。一条人类消息可以使用超出 `maxWorkers` 的一个槽位,且唤醒优先于 launch。`activeCount` 为面板提供负载。
|
||||
|
||||
暂时性的 provider 与网络故障获得三次重试。Launches 使用 effect 退避;失败的回合在 15、30 与 60 秒后收到继续消息。`inputMessage` 依据当前 Ticket 状态渲染每个 Input,`mandateInstructions` 为 inferred 工作追加匹配规则的限制。
|
||||
|
||||
`install` 订阅 Session 事件;`dispose` 退订。一个回合结束通常上报 waiting,除了就绪之后或需要人类行动时。错误上报 failed。启动时,`resume` 通过 `assignmentInterrupted` 上报被打断的 Assignments,并重新排队那些不等待人类的待处理 launches,最旧的 Ticket 在前。
|
||||
|
||||
### MasterAssessor
|
||||
|
||||
`MasterAssessor`(`src/app/assessment.ts`)让 main Session 研判 proactive 候选。`request` 排队一个研判 effect;其 Eval 结束时 `release` 丢弃缓存状态。
|
||||
|
||||
它的 `context`、`read` 与 `decide` 方法服务于 `orchestrator_eval`、`orchestrator_eval_read` 与 `orchestrator_eval_decide`。它们通过 `SourceRegistry.threadContext` 提供有界的 Thread 上下文、repository 上下文与决定提交。一个没有上下文读取器的 kind 只提供 Trigger 与批次。
|
||||
|
||||
`assessmentOutcome` 为 `Engine.decide` 转换模型的回答。Evals 会依当前 Policy 再次核对 inferred 的接受。
|
||||
|
||||
### Settings
|
||||
|
||||
`Settings`(`src/app/settings.ts`)读写带 revision 的设置 blocks。`current` 与 `view` 读取它们;`save(block, value, revision, origin)` 校验一次保存;`pause(paused, origin)` 更改独立的 `paused` 设置。
|
||||
|
||||
每次保存都声明其来源:`panel`、`tool` 或 `sync`。只有 `panel` 能更改执行环境或 `SETUP_FIELDS`(`src/deployment.ts`)里列出的字段。
|
||||
|
||||
一次 Policy 版本变更会计算 `policyEffects`、记录哪些 Tickets 被通知,并调用 `Engine.policyChanged`。`syncable` 与 `sync(id)` 刷新一个 Source 的 Policy 部分。`refreshLabels(id)` 刷新显示名。
|
||||
|
||||
### Prompts
|
||||
|
||||
`Prompts`(`src/app/prompts.ts`)把配置存为 `Versions`。`current`、`version` 与 `history` 读取它们;`validate`、`preview` 与 `save(value, baseRevision, source)` 检查并保存它们。`compose` 与 `skills` 构建 Session 文本。
|
||||
|
||||
`builtInParts` 列出覆盖键、角色、默认文本与必需的模板占位符。每个覆盖保存团队 `text` 与一个 `base`:其原始默认的 `builtInBase` 键。保存时把那个默认值保留在 `KV.prompts.builtIn` 下。`defaultChanged` 随后报告变更的默认值、可用时的旧文本,以及被移除的部件。
|
||||
|
||||
校验会从已保存的覆盖继承一个省略的 base、丢弃已存在于保存数据中的被移除部件,并拒绝空白文本或其他未知键。这让保存工具能对它读到的一份配置做往返。`environmentSection` 与 `policySection` 只用自己的键做一次模板替换。没有覆盖的配置使用默认值。
|
||||
|
||||
### Sessions
|
||||
|
||||
`Sessions`(`src/app/sessions.ts`)为两种角色管理 DeepSeek Harness Sessions。`ensure` 用一个角色的模型创建 Session。`install` 跟踪 agent 事件,并为受管 Sessions 注册提示词、技能与工具。
|
||||
|
||||
`prompt` 按请求 id 投递一次。`relay` 与 `notify` 排队由 `relay` 与 `notice` effects 处理的 main-Session 消息。`src/app/notices.ts` 定义其内容;`delivery` 检查一条消息是否已到达。
|
||||
|
||||
每次模型请求把 Session 绑定到当前的提示词 revision。它使用过的 revisions 会被持久保存:`promptVersion` 标识活动的那份,`mayRead` 限制 worker 能查看哪些版本。注册表按每个 agent 一个 map 跟踪。
|
||||
|
||||
`status` 与 `promptVersion` 服务于提示词页面。保存经由 `Prompts.save` 进行并通知 main Session。`modelRetry` 暴露模型预算重试状态。
|
||||
|
||||
### Glue
|
||||
|
||||
| Glue | 翻译什么 |
|
||||
| --- | --- |
|
||||
| `src/app/runtime.ts` | composition root:用各部件要用的接口构建它们,注册路由与 Source host,并启动与停止 dsh-superagent |
|
||||
| `src/app/reclaim.ts` | 已结束 Tickets 的工作区目录,哪些可移除;其占用的磁盘,变成 overview 上的读数 |
|
||||
| `src/app/notices.ts` | 领域事件与 worker 报告,变成给 main Session 的通知与 relay |
|
||||
| `src/app/tools/main.ts`、`src/app/tools/executor.ts` | main Session 与 Executor Sessions 的工具参数,变成对 core 类的调用,及其结果变成卡片 |
|
||||
| `src/app/http.ts`、`src/app/views/`、`src/app/api.ts` | HTTP 请求变成调用,Store 读取变成面板的视图;`api.ts` 为面板给每条路由定类型 |
|
||||
| `src/app/resources.ts`、`src/app/links.ts`、`src/app/cards.ts` | 资源名变成读取,源消息变成链接,记录变成给模型的紧凑卡片 |
|
||||
| `src/sources/github*.ts`、`src/sources/feishu*.ts`、`src/sources/poll.ts` | GitHub 与 Feishu 载荷变成 Triggers 与 Thread 事实,消息变成 CLI 调用 |
|
||||
| `src/deployment.ts`、`src/setup.ts`、`src/infra/` | Setup 探测与问题、CLI 传输、agent 的进程环境 |
|
||||
|
||||
## Dependency rules
|
||||
|
||||
- **显式、窄的依赖。** 每个类收到一个点名其所用内容的接口,大多做成其他类的 `Pick` 类型:core 类的 `ExecutorHost`、`AssessorHost`、`SessionsHost`、`SettingsHost`、`SetupHost`、`PassesHost` 与 `OutboxHost`,glue 的 `HttpHost`、`MainToolsHost`、`ExecutorToolsHost`、`ResourceHost`、`LinkHost` 与 `SourceContextDeps`,Engine 则是调用它那个角色的 `Pick`。Runtime 只在这样一个接口后面传自己。一个 Source 插件注入 `orchestrator` 服务,其类型把它命名为 `SourceHost`:registry 与 `context(kind)`;其 kind 的代码只见其 `SourceContext`。
|
||||
- **谁能 import 谁。** `src/core` 除 Node 的哈希之外什么都不 import 自自身之外,也不做 I/O。`src/sources` 不从 `src/app` import 任何东西。面板只从 host 侧 import 类型,以及 `src/admission.ts`、`src/limits.ts`、`src/setup.ts`、`src/navigation.ts`、`src/sources/host.ts`、`src/core/errors.ts`、`src/core/reasons.ts` 的普通值和 `src/app/api.ts` 的过滤列表。
|
||||
- **每种状态一个写入者。** 每种状态有一个写入者,其余要么读它,要么请求那个写入者。
|
||||
|
||||
| 状态 | 写入者 |
|
||||
| --- | --- |
|
||||
| Triggers、Threads、Decisions、Tickets、Assignments | Engine |
|
||||
| 一个 Source 的读取位置,及它在其 Threads 上保存的 meta(`label`、`url`、`facts` 及其自有键) | 那个 Source,经 `SourceContext` |
|
||||
| 设置及其历史 | Settings |
|
||||
| 提示词版本 | Prompts |
|
||||
| Effect 行 | Effects |
|
||||
| Outbox receipts | Outbox:Engine 的消息与 Sources 的 `notify` 都经它的 `send`,其 `reply` effects 标记它们 |
|
||||
| 一个健康条目 | 在 `HealthBoard`(`src/infra/health.ts`)上认领它、并拒绝第二次认领的那个部件:`runtime` 归 Runtime,Passes 经它上报,`evals` 归 Runtime(每趟之后),`models` 归 Sessions,每种 effect kind 归 Effects,`worker` 归 DshExecutor,一个 Source kind 的条目归代表该 kind 之 Source 的 SourceRegistry,`sync:<id>` 归 Settings |
|
||||
|
||||
Source 只能通过调用 Engine 的 `SourceContext.tickets.bindThread` 与 `threadClosed` 来改动一个 Ticket。
|
||||
- **请求与事实。** 索取某物的方法以祈使动词加其宾语命名,如 `stopTicket` 或 `bindThread`,可以被拒绝;记录适配器做过之事的方法以过去式命名,如 `assignmentStarted` 或 `deliveryFailed`。每个 host 收到 Engine 的它所必需的那个角色类型。
|
||||
- **有类型的字段,Source 数据放在 Source 名下。** 代码读取 Tickets 与 Mandates 的有类型字段。Source 不在 Ticket 上保存任何东西:一个 Ticket 跟随它的哪些 Threads 是 Ticket 的 `follows`,而它关于某个 Trigger 或 Thread 所保存的在它们的 `meta` 里。
|
||||
- **固定的拒绝。** 一个人或一个 agent 能引起的拒绝是 `src/core/errors.ts` 中来自 `USER_ERRORS` 的 `UserError(code, values)`。management API 对 `missing` 代码回答 404、其他回答 400,面板以其语言为每个代码措辞。Setup 问题是 `SetupError` 代码(`src/setup.ts`)与固定的健康文本 `HealthCode`(`src/sources/host.ts`),以同样的方式措辞。
|
||||
### Source 专属的依赖
|
||||
|
||||
一些泛型模块仍直接引用 GitHub 或 Feishu:
|
||||
|
||||
- Outbox 应用 Feishu 的 chat 字数上限。
|
||||
- `src/app/links.ts` 解析 Feishu 消息链接与 `repliedTo`。它与 Runtime 用 `feishuSettings` 读取设置。
|
||||
- `src/app/resources.ts` 读取 `feishu/` 与 `github/` 的原始记录;`src/app/cards.ts` 用 `pullOf` 制作 Ticket 卡片。
|
||||
- Runtime 对 merged 或 closed 的 Tickets 发送 `pullEndNotices`。
|
||||
- Executor 工具用 `readPull` 与 `claimPull` 绑定上报的 pull requests,使用 Runtime 暴露的 GitHub 上下文。
|
||||
- MasterAssessor 用 `readRepositoryPath` 读取 repository 文件。
|
||||
- Sessions 与 Prompts 只为 GitHub 与 Feishu 注册内建技能。迁移会转换它们的 schema-10 记录。
|
||||
- Setup 只探测 GitHub 与 Feishu 账号字段(`SETUP_FIELDS`)。只有那些 Sources 经由 `SourceContext.problem` 收到账号 setup 问题。
|
||||
|
||||
|
||||
## Modules
|
||||
|
||||
| Path | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.ts` | The main plugin entry: reads the machine settings, starts the Runtime, and gives it the restart that a save of a Source's account uses |
|
||||
| `src/deployment.ts` | The plugin `Config` and `Machine`; `resolveDeployment` and `resolveFeishuBot` read the repository and the accounts from the Source settings, each CLI run as Executor shells run it; `SETUP_FIELDS` lists the Source fields setup reads. The code names no person, account or organization |
|
||||
| `src/setup.ts` | Setup problems: `SetupError`, with a code, the place that fixes it and the English text the log, the main Session and the panel share |
|
||||
| `src/policy.ts` | The Policy: the settings schemas by block, the Policy version, the gate and Mandate revalidation |
|
||||
| `src/admission.ts` | The admission table's values, each cell's options and defaults, shared with the panel |
|
||||
| `src/limits.ts` | The limits of the values a person sets, which the settings schemas check and the panel's fields offer |
|
||||
| `src/kv.ts` | `KV`: every family of `kv` records with its key, its value's type and its owner, and the `Key` and `Family` types the Store and `SourceContext.state` take |
|
||||
| `src/navigation.ts` | Opens a DeepSeek Harness Session from the panel once the host's catalog lists it |
|
||||
| `src/core/model.ts` | The domain types, among them the typed Ticket and `MandateBasis`, with `threadKey`, `ticketOutcome`, `reached`, `followupOwners`, `settled`, `waitsForPerson`, `liveTicket`, `runningAssignment`, `latestAssignment` and `latestReport` |
|
||||
| `src/core/ports.ts` | The ports: `Repositories`, `Policy`, `SourceScope`, `Assessor`, `Executor`, `Outbox`, and the domain events |
|
||||
| `src/core/engine.ts` | The Engine, the core's entry point, and the role interfaces `EngineForSources`, `EngineForMainSession`, `EngineForPeople`, `EngineForSettings` and `EngineForExecutor` |
|
||||
| `src/core/part.ts` | `EnginePart`, the base of the Engine's parts, and `EngineOptions` |
|
||||
| `src/core/intake.ts` | `Intake`: Triggers from Sources, to their Tickets and, unless a person's message reached one, to Eval |
|
||||
| `src/core/evals.ts` | `Evals`: the Evals of Threads and their Decisions |
|
||||
| `src/core/tickets.ts` | `Tickets`: the Ticket lifecycle, the Threads a Ticket follows, Subscriptions and Inputs |
|
||||
| `src/core/assignments.ts` | `Assignments`: the Executor's facts and its workers' Reports |
|
||||
| `src/core/replies.ts` | `Replies`: the Engine's Channel writes |
|
||||
| `src/core/stage.ts` | `ticketStage` and `canRetry`: the Ticket stages of [003 执行](003-执行.md) |
|
||||
| `src/core/errors.ts` | `UserError` and `USER_ERRORS` |
|
||||
| `src/core/reasons.ts` | `Reason`, `REASONS` and `reasonText`: why the core recorded a Decision or set a Trigger aside, by code |
|
||||
| `src/core/ids.ts` | `key`: ids derived from their inputs, equal for equal inputs |
|
||||
| `src/core/memory.ts` | `MemoryRepositories`: the Store's repositories in memory, in the Store's order and with its nested rollback, for domain tests |
|
||||
| `src/core/index.ts` | The core's exports |
|
||||
| `src/app/runtime.ts` | The Runtime, the composition root, with its start and stop |
|
||||
| `src/app/setup.ts` | Setup, and `setupChanged`, whether a save changes a field setup reads |
|
||||
| `src/app/passes.ts` | Passes, the pass scheduler |
|
||||
| `src/app/reclaim.ts` | `Reclaimer`: hourly scans of the execution root that remove an Ended Ticket's directory once it outlives the retention, and the summary the overview reports |
|
||||
| `src/app/outbox.ts` | The Outbox: the Engine's `Outbox` port, the receipts and the `reply` effects that send them |
|
||||
| `src/app/notices.ts` | The main Session's notices and relays: `eventNotice`, `reportRelay`, `resultRelay` and `followupRelay` |
|
||||
| `src/app/store.ts` | The Store, and `Receipt`, a message of the outbox |
|
||||
| `src/app/store-read.ts` | `storeReads`: the Store's read queries for the views, the tools and the Sources' polls, which never write |
|
||||
| `src/app/migration.ts` | The schema 11 migration: one step from schema 10 that writes every stored record in its current shape |
|
||||
| `src/app/effects.ts` | Effects |
|
||||
| `src/app/versions.ts` | `Versions`: a record kept as numbered snapshots with a pointer to the current one, for the settings and the prompts |
|
||||
| `src/app/cache.ts` | `TextCache`: texts kept in memory within a size, the least recently used dropped first |
|
||||
| `src/app/settings.ts` | Settings |
|
||||
| `src/app/settings-history.ts` | `policyHistory`: each saved settings revision with what it changed from the one before |
|
||||
| `src/app/prompts.ts` | Prompts, `builtInParts`, the built-in parts a team may override, and `promptParts`, the order of a role's prompt sections |
|
||||
| `src/app/context.ts` | The built-in prompt text, read from `prompts/`, and the transcript entries Sessions read |
|
||||
| `src/app/skill-files.ts` | Packaged text: `promptFile`, which joins a file's one-sentence lines into paragraphs, and the bundled skills |
|
||||
| `src/app/sessions.ts` | Sessions |
|
||||
| `src/app/delivery.ts` | `AgentDelivery`: delivers a message to a DeepSeek Harness agent once per request id, also across a restored Session |
|
||||
| `src/app/retry-health.ts` | `RetryHealth`: which Sessions wait on a model budget retry, for the `models` health entry |
|
||||
| `src/app/role-models.ts` | Resolves the Eval and Executor models: seeds a new Session with its role's model and re-points a reused Session whose durable route drifted |
|
||||
| `src/app/default-model.ts` | `switchDefaultModel`: changes the deployment default model, optionally with a reasoning effort the catalog offers, through the Session Controller, which persists it into the profile |
|
||||
| `src/app/assessment.ts` | The MasterAssessor, the assessment schema and the window `orchestrator_eval` answers |
|
||||
| `src/app/execution-environment.ts` | The execution environment: its variables, a Ticket's values, the directory root and the lines of the prompt section, which `src/app/prompts.ts` fills into its template |
|
||||
| `src/app/source-context.ts` | `sourceContextOf`: each Source kind's `SourceContext` |
|
||||
| `src/app/links.ts` | `sourceLink`, the page of a Trigger's message, and `candidateCard` |
|
||||
| `src/app/cards.ts` | Compact cards of Triggers, Tickets, Threads and Decisions for models, the closed set of ticket card states `TICKET_STATES`, and `textPage` |
|
||||
| `src/app/resources.ts` | `RESOURCES` and `readResource`: what `orchestrator_read` reads, for each role |
|
||||
| `src/app/tools/tool.ts` | The `Tool` type, `readTool`, the `orchestrator_read` of both roles, and `AcknowledgementSchema` |
|
||||
| `src/app/tools/main.ts` | The main Session's tools |
|
||||
| `src/app/tools/executor.ts` | An Executor Session's tools |
|
||||
| `src/app/http.ts` | The `/orchestrator/api` routes: the same-origin check, request limits and one status per kind of failure |
|
||||
| `src/app/api.ts` | `Api`, every route with its parameters, query, body and response, and the filters of the lists; it imports only types, so the panel bundles it |
|
||||
| `src/app/views/text.ts` | The two forms of a view's text: `preview`, plain text on one line, and `markdown` without HTML; `plain` and `readable` |
|
||||
| `src/app/views/records.ts` | The rows the views share: Triggers, Tickets, Threads, Decisions and Channel links, and `ownersOf`, the owners of follow-ups as one request reads them |
|
||||
| `src/app/views/tickets.ts` | The Ticket lists by stage, a Ticket's page, the Mandate basis facts and the Executor's load |
|
||||
| `src/app/views/threads.ts` | The Thread lists, a Thread's timeline, the search and the Decision list |
|
||||
| `src/app/views/sources.ts` | The Source status: each Source instance's state, its last day's counts, and what it filtered and dropped in that day |
|
||||
| `src/app/views/settings.ts` | The settings page's view: each block with its revision, each Source's settings with its kind's form, and the execution environment |
|
||||
| `src/app/views/overview.ts` | The overview: Tickets that need a person, problems, the last day's flow and the Executor's load |
|
||||
| `src/executor/dsh.ts` | The DshExecutor, `WorkerSlots`, `ticketExecution`, `inputMessage` and `mandateInstructions` |
|
||||
| `src/executor/background.ts` | A worker's background work in DeepSeek Harness, the shell jobs its Session owns (`ctx.jobs`) and its subagents (`ctx.subagents`): `runningJobs`, `untilQuiet`, `killJobs`, `collectJobs`, `stopSubagents`, `stopBackground`, `until`, and `cancelled`, the workers whose turn a stop of their subagents cancelled |
|
||||
| `src/infra/commands.ts` | `Commands`, the CLI transport: `run`; `api` and `pages`, limited to the configured repository and validated; `lark` as the bot; `git`. `CliError` carries the code a command's output reports, such as a GitHub HTTP status, and a command that prints nothing fails with how it ended, never with its arguments; `validItems` skips and counts the malformed items of a list |
|
||||
| `src/infra/environment.ts` | The agent's process environment: command shims, the git configuration and credential helper, `runAsWorker`, and the checks of the environment and of a Ticket's directory |
|
||||
| `src/infra/paths.ts` | The package's root, where packaged files are |
|
||||
| `src/infra/text.ts` | Word counts and the chat message limit, `fill` and `sameName` |
|
||||
| `src/infra/health.ts` | The `HealthBoard`, every part's health entry, each claimed by its one writer |
|
||||
| `src/sources/kind.ts` | The `SourceKind` protocol |
|
||||
| `src/sources/registry.ts` | `SourceRegistry` |
|
||||
| `src/sources/host.ts` | `SourceHost`, `SourceContext`, `Health`, `health` and the fixed health texts |
|
||||
| `src/sources/poll.ts` | What GitHub and Feishu polling share: `POLL_HORIZON_MS`, `polled`, `cursorAt`, and `pollThreads`, which keeps a failure on its Thread as `pollError` |
|
||||
| `src/sources/github.ts` | The GitHub kind and plugin entry, exported as `./github-watch`: a poll round of notifications, the pull requests of open Tickets with their CI, and subscribed Threads |
|
||||
| `src/sources/github-trigger.ts` | GitHub comments, reviews, Issue bodies and CI changes as Triggers, and GitHub addresses |
|
||||
| `src/sources/github-pull.ts` | The pull request rules: which pull requests a Ticket follows (`claimPull`, `observePull`), the closes it reports to the Engine, and `pullEndNotices`, the chat notice of a Ticket that ended merged or closed |
|
||||
| `src/sources/github-channel.ts` | The GitHub Channel: comments and review thread replies, `sent` or `gone` |
|
||||
| `src/sources/github-read.ts` | CI snapshots, repository reads, the `github/` resources, and `githubThreadContext`: an Issue's or pull request's conversation by page |
|
||||
| `src/sources/feishu.ts` | The Feishu kind and plugin entry, exported as `./feishu-watch` |
|
||||
| `src/sources/feishu-trigger.ts` | Feishu messages as Triggers, by one rule for live events, history pages and topic pages |
|
||||
| `src/sources/feishu-loops.ts` | The Feishu loops: the event consumer, history recovery, chat names and polling of subscribed topics |
|
||||
| `src/sources/feishu-channel.ts` | The Feishu Channel: topic replies, `sent` or `gone` |
|
||||
| `src/sources/feishu-read.ts` | The `feishu/` reads of a Trigger's message, its root, parent and topic, and `feishuThreadContext`, which gives them to assessments with their images |
|
||||
| `src/sources/gitea.ts` | The Gitea kind and plugin entry, exported as `./gitea-watch`: a poll round of notifications, the observed repository streams, and subscribed Threads |
|
||||
| `src/sources/gitea-api.ts` | The minimal Gitea REST client: token auth from a named environment variable, zod-validated answers, pagination, an injectable fetch |
|
||||
| `src/sources/gitea-trigger.ts` | Gitea Issue bodies, comments and reviews as Triggers, and Gitea addresses |
|
||||
| `src/sources/gitea-channel.ts` | The Gitea Channel: Issue and PR comments, `sent` or `gone` |
|
||||
| `src/sources/admission-context.ts` | The bounded window an assessment starts from: the first pages of a Trigger's Thread, read through its kind, and the messages around the Trigger |
|
||||
| `src/sources/feishu-image.ts` | Reads a Feishu message's image as an attachment for the assessing Session |
|
||||
| `src/client/index.tsx` | The client plugin entry: the sidebar entry, the main panel and the page in DeepSeek Harness's Plugin Manager |
|
||||
| `src/client/app.tsx` | The panel: header, search, glossary, tabs and the page of the current route |
|
||||
| `src/client/provider.tsx` | What every page reads from its host: copy, calls, navigation, notices, the state poll, and `restart`, which reads the state quickly while dsh-superagent restarts |
|
||||
| `src/client/default-model.tsx` | The header's default-model switcher, and how a refused switch is worded |
|
||||
| `src/client/api.ts` | Calls and polls of the management API, typed by `src/app/api.ts`, and the Decision filters of the Eval status |
|
||||
| `src/client/route.ts` | The tabs, the settings groups, the typed routes that `parseRoute` reads, and the route memory with the settings group opened last |
|
||||
| `src/client/overview.tsx` | The overview |
|
||||
| `src/client/source.tsx`, `trigger.tsx`, `eval.tsx`, `execution.tsx` | One file per stage: its status, and its settings where it has some |
|
||||
| `src/client/thread.tsx`, `ticket.tsx` | The record pages: a Thread with its timeline, and a Ticket with its authorization, Inputs and Assignments |
|
||||
| `src/client/settings.tsx` | The Settings tab: the group navigation and the group on screen |
|
||||
| `src/client/settings-kit.tsx` | The building blocks of settings blocks: the settings the stage groups share, `useBlockDraft`, the fields, among them a Source kind's, and the history of saves |
|
||||
| `src/client/prompts.tsx` | The Prompts settings: the page owns the view it read and its editor the draft, and the Edit, Full prompt and History tabs render from them; a built-in part's editor shows a changed default and keeps or drops the override |
|
||||
| `src/client/prompt-config.ts` | The prompt configuration by section, by the team sections, skill names, built-in parts (`part/<key>`, with `withOverride`) and part headings the prompt views give, and the line diff of the full prompt, as pure functions |
|
||||
| `src/client/deployment.tsx` | The Deployment settings, also shown in DeepSeek Harness's Plugin Manager |
|
||||
| `src/client/draft.tsx` | Settings drafts: `draftReducer`, their states and their merge with a newer saved revision, `saveBarText`, the save bar's line, and the save bar |
|
||||
| `src/client/controls.tsx` | Local copies of DeepSeek Harness's controls, which follow its markup, keyboard behaviour and design tokens, since the bundle does not import `@deepseek-ai/dsh-client-ui-primitives`; they read no context |
|
||||
| `src/client/widgets.tsx` | The small components the pages share, which read the panel's context: sections, page heads, load states, times, links, confirmed actions, the setup check and Markdown |
|
||||
| `src/client/rows.tsx` | The rows the pages share: a Ticket, a Thread and a Decision, a Thread's chip, and the folding of runs of automated messages |
|
||||
| `src/client/markdown.ts` | The Markdown subset of reports and questions, and the bare addresses in messages, as blocks and inline parts |
|
||||
| `src/client/text.ts` | Copy by key and the wording of domain values, reasons, senders and times |
|
||||
| `src/client/marks.ts` | The marks of the states the views classify: a Ticket's or a Thread's stage, a Source's state and a Decision's outcome, each as a dot, an icon, a tone and a label |
|
||||
| `src/client/locales.ts` | All copy in both languages, and the glossary of [001 领域模型](001-领域模型.md) |
|
||||
| `src/client/client.css`, `css.d.ts` | The panel's stylesheet, loaded as text |
|
||||
|
||||
`src/sources/github.ts`、`src/sources/feishu.ts` 与 `src/sources/gitea.ts` 同时是三个 Source 插件的 entries,由 `tsdown.config.ts` 构建,并在 `package.json` 中以 `./github-watch`、`./feishu-watch` 与 `./gitea-watch` 导出。bundle 的 `cordis.patch.yml` insert 这四个插件,Feishu 与 Gitea 处于禁用状态。
|
||||
|
||||
## Tests
|
||||
|
||||
| Test | Covers |
|
||||
| --- | --- |
|
||||
| `tests/core-engine.spec.ts` | Domain rules on in-memory repositories: receiving, Eval, fan-out, resume and stop, interrupted Assignments, the pause, Policy changes, reports and outcomes, attaching, replies, failed deliveries and dismissed problems |
|
||||
| `tests/stage.spec.ts` | The Ticket stages, row by row |
|
||||
| `tests/policy.spec.ts` | Source scope, the gate, the admission table, Mandate revalidation, the Policy version, settings migrations and assessment outcomes |
|
||||
| `tests/registry.spec.ts` | `SourceRegistry`: instances, validation, Policy parts, Channels and Thread context |
|
||||
| `tests/sources.spec.ts` | The Feishu and GitHub Sources on a `SourceContext`: translation, polls and read positions, the pull request rules, health |
|
||||
| `tests/gitea.spec.ts` | The Gitea Source on a `SourceContext` against a fetched stub of the Gitea API: translation, notification and stream polls, cursors, the Channel, health |
|
||||
| `tests/default-model.spec.ts` | The default-model switcher: the routes it refuses, the switch it makes through the Session Controller, and the wording of a refusal |
|
||||
| `tests/commands.spec.ts` | CLI calls as the deployment's accounts, the Channels' answers, and repository-scoped GitHub calls |
|
||||
| `tests/feishu-read.spec.ts`, `tests/github-read.spec.ts` | Feishu and GitHub reads |
|
||||
| `tests/admission-context.spec.ts`, `tests/feishu-image.spec.ts` | The Thread window of assessments, and Feishu image reads |
|
||||
| `tests/assessment.spec.ts` | What the MasterAssessor keeps per Eval, and when it lets go |
|
||||
| `tests/background.spec.ts` | Waiting for a worker's shell jobs and subagents: an end that woke it, and a host that stops |
|
||||
| `tests/bundle-patch.spec.ts` | The bundle's patch, read as DeepSeek Harness reads it, and the agent preset of Executor Sessions it defines |
|
||||
| `tests/effects.spec.ts` | The effect queue: coalescing, limits, retries with backoff, health and shutdown |
|
||||
| `tests/passes.spec.ts` | Passes: one at a time, the ones asked for during a pass run as one after it, and the runtime health a failed pass reports |
|
||||
| `tests/outbox.spec.ts` | The Outbox on a running Runtime: messages wait while processing is paused or their Source is not installed |
|
||||
| `tests/cache.spec.ts` | `TextCache` |
|
||||
| `tests/queue.spec.ts` | The Runtime's work through the effect queue, and what earlier versions stored |
|
||||
| `tests/followups.spec.ts` | A person's follow-up on a Ticket's Thread: its notice to the main Session, the tools that act on it, what a burst costs; a worker's hand-back and context follow, and the main Session's note |
|
||||
| `tests/health.spec.ts` | One health entry per part, each cleared by its own next success |
|
||||
| `tests/delivery.spec.ts` | `AgentDelivery` |
|
||||
| `tests/retry-health.spec.ts`, `tests/role-models.spec.ts` | Model budget retries, and the models of the two roles |
|
||||
| `tests/runtime.spec.ts` | The whole application on a simulated DeepSeek Harness host, through tools and HTTP |
|
||||
| `tests/tools.spec.ts` | The Sessions' tools: what `orchestrator_read` reads for each role, and the shapes and checks of the other main Session tools |
|
||||
| `tests/runtime-concurrency.spec.ts` | Work that overlaps: Triggers during a pass, Tickets that change during deliveries and launches |
|
||||
| `tests/http.spec.ts` | The same-origin and loopback checks, the status of each kind of failure, the body limit, path parameters and queries |
|
||||
| `tests/settings.spec.ts` | Settings saves that race a model or account check, a Policy sync that a save outran, and the refusals of the pause, a sync and a block |
|
||||
| `tests/views.spec.ts` | What the management views derive from the Store, such as the Thread a dropped message belongs to, and the overview's liveness and workspace readouts |
|
||||
| `tests/reclaim.spec.ts` | Which Ended Tickets' directories may be removed: the retention, the execution root, an open Ticket sharing one, and the end times that cannot be read |
|
||||
| `tests/timeline.spec.ts` | A Thread's pages and a Trigger's neighbours by time and then id, with every received Trigger's time in one form |
|
||||
| `tests/migration.spec.ts` | The schema 11 migration: every stored record in its current shape, and the refusal of older schemas |
|
||||
| `tests/store.spec.ts` | Repositories and their working sets, in SQLite as in memory, the kv area, with every key of `KV` spelled only in `src/kv.ts` and the migration, settings history and queries |
|
||||
| `tests/prompts.spec.ts` | Prompt validation, assembly and versions |
|
||||
| `tests/cards.spec.ts` | The rules the built-in text must state, file by file |
|
||||
| `tests/model-surface.spec.ts` | Everything models read: tools, prompts, skills and inputs, checked against the tool schemas, the execution environment and the glossary, within a size budget per role |
|
||||
| `tests/deployment.spec.ts`, `tests/environment.spec.ts`, `tests/index.spec.ts` | The plugin configuration and setup, the agent's process environment, the plugin entry |
|
||||
| `tests/service-script.spec.ts` | `scripts/dsh-superagent` against fake `ps`, `tmux` and `dsh` |
|
||||
| `tests/client.spec.tsx`, `tests/navigation.spec.ts`, `tests/controls.spec.tsx` | The client plugin's registration and polling, Session navigation and route memory, the local controls, and the panel's imports, which have no cycle |
|
||||
| `tests/panel-overview.spec.tsx`, `panel-source.spec.tsx`, `panel-trigger.spec.tsx`, `panel-eval.spec.tsx`, `panel-execution.spec.tsx` | Each page of the panel, against a fake management API answered by route |
|
||||
| `tests/panel-settings.spec.tsx`, `panel-prompts.spec.tsx`, `panel-deployment.spec.tsx`, `panel-panel.spec.tsx` | The settings groups, drafts and history, the prompts, the Deployment page, and the panel's search, glossary and route memory |
|
||||
| `tests/draft.spec.ts`, `tests/prompt-config.spec.ts`, `tests/markdown.spec.ts`, `tests/marks.spec.ts` | Draft states, merges and the save bar's line; the prompt configuration by section and the line diff; the Markdown subset and its links; the mark of every state the views classify |
|
||||
| `tests/locales.spec.ts`, `tests/styles.spec.ts` | The copy in both languages, the stylesheet |
|
||||
| `tests/class-docs.spec.ts` | Every class in `src` has a JSDoc block before it |
|
||||
| `tests/docs.spec.ts` | Numbered documents link only to earlier ones, every local link resolves, no document has merge conflict markers, and this document's tables list every module and every test |
|
||||
|
||||
`tests/fixtures/` 存放若干测试共享的东西:`host.ts`,一个模拟的 DeepSeek Harness host;`panel-server.ts` 与 `panel.tsx`,围绕一个页面的假 management API 与面板;`deployment.ts`,一个已解析的 deployment;`git.ts`,测试启动的每个 git 的环境,其中不点名任何 repository;以及 `async.ts`。
|
||||
|
||||
## Persistence
|
||||
|
||||
一个 SQLite 文件,`<stateDir>/orchestrator.sqlite`,只有 Store 打开它。
|
||||
|
||||
| 表 | 存放 | 由谁写入 |
|
||||
| --- | --- | --- |
|
||||
| `trigger` | 每个 Trigger,其时间 `at` 用 `Date#toISOString` 给出的那一种形态,在 Engine 接收它时设置,因此时间按文本比较 | Engine |
|
||||
| `disposition` | 每个 Trigger 的去向:`filtered` 带 Source 的理由代码、它的 Eval 与 Decision、它抵达过的 Tickets(经扇出或附着)、它是否被 requeue、交回它的 Tickets 及其理由,以及其 Executor 确认或回答过它的那些 | Engine |
|
||||
| `trigger_thread` | 每个 Trigger 在哪些 Threads 上,带其时间与是否由人类所写,于是一个 Thread 的 Triggers 无需扫描所有 Trigger、按时间与随后 id 的顺序读取:一个 Thread 时间线的一页从 `<at>|<id>` 游标续读,共享同一时间的 Triggers 不会被跳过 | Store,随每个 Trigger |
|
||||
| `thread` | Threads:Subscriptions、为上下文跟随它的 Tickets、排队的 Triggers、进行中的 Eval、其过期计数与 `meta`;生成列 `active`(有 Triggers 排队或 Eval 运行时置位)与 `subscribed`(有 Ticket 订阅它时是其 Source) | Engine;Sources 写 `meta` |
|
||||
| `ticket` | Tickets 及其 Mandate、origin、execution、Assignments、Channels、Inputs 与它们跟随的 Threads;生成列 `open`(未结束时置位)与 `active`(有 Inputs 未投递或最新 Assignment 处于 pending 或 running 时置位) | Engine |
|
||||
| `decision` | Decisions,按 Thread | Engine |
|
||||
| `effect` | effect 队列:按 kind 与 key,其负载、其顺序、失败的尝试与最后的错误,以及其到期时间 | Effects;schema 11 迁移把早期版本为主 Session 保留的东西排队 |
|
||||
| `outbox` | 每条发往 Channel 的消息的 receipt,按 id,带它所属的 Ticket 与其 `status`——`pending`、`sent` 或 `dropped`——并保存以确保一个 id 不会被排队两次;对被丢弃的消息,何时与为何,即生成列 `dropped`、它的时间与一个理由代码。一条等待的消息有一个 `reply` effect,其持有它的尝试次数 | Outbox |
|
||||
| `kv` | 其他所有记录,按 key | 点名它的那个部件 |
|
||||
|
||||
一个生成列由所在行的 JSON 计算得出、在其集合之外为空,且每个都有一层覆盖其集合的索引,因此一趟只读取活跃的工作。早于某列的数据库在 Store 打开它时获得该列。
|
||||
|
||||
`KV`(`src/kv.ts`)定义每个 key-value 记录的属主、key 与类型。所有读取者与写入者都使用这些条目;只有迁移代码点名更旧的 keys。
|
||||
|
||||
| 记录 | 键或内容 |
|
||||
| --- | --- |
|
||||
| Schema | 当前 schema 版本 |
|
||||
| 设置版本 | `settings-revision`、`settings-version:<n>`、`settings-block-revision:<block>`;快照包含来源、Policy 版本与受影响的 Tickets |
|
||||
| 提示词版本 | `prompt-revision`、`prompt-version:<n>` |
|
||||
| Main Session | `master-id` |
|
||||
| Session 提示词历史 | `session-prompts:<session id>` |
|
||||
| Source 状态 | 读取位置,如 `github-cursor:<number>`、PR CI 指纹与已见过的 Feishu-topic 消息;keys 以 kind 或 `thread-` 加一个 Thread key 开头 |
|
||||
| 显示名 | `source-labels:<source id>` |
|
||||
|
||||
Dismissal 保存在其 Decision 上。Input 文本在读取时从 Ticket 渲染,所以两者都不需要单独的 key-value 记录。
|
||||
|
||||
读取缓存使用有界的内存 `TextCache` 实例,重启即失。一个 Session 若要翻页浏览一份原始 Source 记录或提示词预览,重启后必须重读 offset 0。Feishu 链接在收单后解析并以同样方式缓存;基于链接的搜索只在当前进程存续期内读过那个链接之后才能找到它们。
|
||||
|
||||
还没有任何东西删除旧记录,所以这些会无界增长:`trigger`、`disposition` 与 `decision` 行;已结束的 Tickets 及其 Threads;outbox 的 receipts;以及 `kv` 里每次保存的设置 revision 与提示词版本、每个 Session 工作于其下的提示词 revisions,以及各 Sources 按 Thread、按 Issue 或 pull request 的记录。
|
||||
|
||||
`SCHEMA_VERSION` 只增不减。一个更新版本的数据库拒绝启动;`src/app/migration.ts` 在启动时把 pre-release schema 10 的数据库升级一次,更旧的则拒绝。
|
||||
|
||||
## Model-facing interfaces
|
||||
|
||||
### Main Session tools
|
||||
|
||||
main Session 有 14 个应用工具:
|
||||
|
||||
| 用途 | 工具 |
|
||||
| --- | --- |
|
||||
| 读取与路由 | `orchestrator_read`、`orchestrator_route` |
|
||||
| 管理 Tickets | `orchestrator_ticket_retry`、`orchestrator_ticket_request_status`、`orchestrator_ticket_note` |
|
||||
| 重新研判 | `orchestrator_trigger_requeue` |
|
||||
| 研判主动工作 | `orchestrator_eval`、`orchestrator_eval_read`、`orchestrator_eval_decide` |
|
||||
| 沟通与退订 | `orchestrator_reply`、`orchestrator_thread_unsubscribe` |
|
||||
| 配置 | `orchestrator_settings_save`、`orchestrator_policy_sync`、`orchestrator_prompts_save` |
|
||||
|
||||
`orchestrator_route` 按序接受 1–20 条决定。每条 accept、attach、stop 或 ignore 作为独立的 Engine 命令运行。它还允许直接从一个 inferred 候选的卡片上 ignore 它。`orchestrator_ticket_note` 把一条事实作为上下文而非权威发送。
|
||||
|
||||
设置保存选择 `policy`、`eval`、`executor`、`pause` 或 `source`;最后那种还要点名其 Source id。带 `preview: true` 的提示词保存只组合文本而不保存。
|
||||
|
||||
main Session 还拥有 DeepSeek Harness 的 `skill`、`bash`、`read`、`read_image`、`job_output`、`job_list` 与 `job_kill`。避免读取代码是一条提示词规则,不是工具限制。
|
||||
|
||||
### 预设与等待上限
|
||||
|
||||
新的 main Sessions 使用 `dsh-superagent-router`(`ROUTER_PRESET`)。shell 命令与 job 等待在 5 秒后返回。若 Web UI 在第一个回合之前选了别的预设,`Sessions` 会恢复这个预设,worker 则恢复 `WORKER_PRESET`。
|
||||
|
||||
`src/app/sessions.ts` 里的 `noWait` guard 拒绝 main-Session 中位于命令位置的 `sleep`、`timeoutMs` 超过 30 秒的 shell,以及超过 5 秒的 `job_output` 等待。报告与 job 结果会自动到来,所以等待只会推迟即将到来的候选。
|
||||
|
||||
处于 `standard` 的既有 main Sessions 还需要为前台命令与 job 等待显式给出超时;否则该预设的默认值会把该步骤分别扣住 60 与 30 秒。host 经由 `presetForSession` 判定当前预设,回退到 Session 投影或头部,读不到值时假定 `standard`。
|
||||
|
||||
### Executor tools
|
||||
|
||||
每个 Executor 有六个应用工具:
|
||||
|
||||
| 工具 | 用途 |
|
||||
| --- | --- |
|
||||
| `orchestrator_read` | 读取对其自有 Ticket 可用的资源 |
|
||||
| `orchestrator_ack` | 确认一个请求 |
|
||||
| `orchestrator_report` | 报告进度或结果,按 pull-request 规则绑定一个 pull request |
|
||||
| `orchestrator_thread_unrelated` | 丢弃一个无关的附着 Thread,交回其拥有的跟进并告知 main Session 原因 |
|
||||
| `orchestrator_thread_follow` | 为上下文订阅一个相关的 Thread 并告知 main Session |
|
||||
| `orchestrator_trigger_handback` | 交回一条消息一次,带其候选承载的理由 |
|
||||
|
||||
一次 hand-back 在另一个 Ticket 仍拥有该消息时返回 `queued: false`,或本 Ticket 从未拥有过它。例如,一条 bot 消息独立地进入了 Eval。
|
||||
|
||||
worker 预设定义在 `cordis.patch.yml` 并经 `WORKER_PRESET` 选择。`src/app/sessions.ts` 里的 `WORK_TOOLS` 保留可用的文件、搜索、shell、job、web、skill、todo 与 subagent 工具。Workers 不会收到用于向人类提问或展示、更改 profile、在 worker 槽位之外排程工作、启动或给其他 Sessions 发消息、或编辑外部文档的工具。Ticket 结束后工具调用会被拒绝。
|
||||
|
||||
### 结果形状与研判上下文
|
||||
|
||||
路由结果使用 `{decision?, reason, ticket?}`。`orchestrator_route` 返回 `{results}`,保持条目顺序并给每条加上其 `trigger_id` 与 `kind`。无效或被拒绝的条目返回 `refused` 与一个理由,而不中断整批。
|
||||
|
||||
设置与提示词保存返回被保存的配置。Policy 同步返回该 Source、允许名单的大小,以及 `added` 与 `removed` 的名字。
|
||||
|
||||
`orchestrator_eval` 返回上下文而不是指令。研判指令位于 `runtime/admission`(`prompts/admission.md`),规则位于 `runtime/policy`。它的上下文包括:
|
||||
|
||||
- Trigger 文本,至多 4,000 字符,带图片 ids 与批次里的其他 Triggers。
|
||||
- 至多八个 `related_tickets`,开着的在前,以及 `tickets_total`。
|
||||
- 该 Eval 的 `policy_version`。
|
||||
- 一个 Thread 窗口:前五条消息,以及 Trigger 两边各五条。消息使用 `{id, from, at, text}`,通常限制在 400 字符,两侧相邻的两条为 1,200。被截断的消息包含 `chars`;图片引用使用 `images`。
|
||||
- 其他已加载的消息作为 50 字符的 `{id, from, preview}` 存根。`orchestrator_eval_read` 按 id 展开它们。
|
||||
|
||||
Ticket 卡片使用 `src/app/cards.ts` 里封闭的 `TICKET_STATES` 集合:一个开着的阶段,如 `person/review` 或 `working/queued`,或一个结束的结局。确认语无论经 accept、研判还是 `orchestrator_ack` 提交,都限于 100 个词。
|
||||
|
||||
### 读取资源
|
||||
|
||||
`RESOURCES` 在 `src/app/resources.ts` 里定义这些资源并生成工具的 `resource` 描述。
|
||||
|
||||
| 资源 | 内容 |
|
||||
| --- | --- |
|
||||
| `tickets`、`triggers`、`threads`、`decisions` | 分页列表,形如 `{items, next_offset}`;活动列表把最新的放最前 |
|
||||
| `decision/<id>` | 一条 Decision |
|
||||
| `thread/<key>` | 标签、Tickets、最近的 Decisions 与一页 Triggers |
|
||||
| `trigger/<id>` | 消息文本、被回复的 Trigger 与相邻消息,不含 CI 运行 |
|
||||
| `ticket/<id>` | 相关 ids、source-batch Triggers、Threads,以及 Assignments 与 Inputs 的第一页 |
|
||||
| Ticket 子资源 | `/assignments`、`/inputs`、`/assignment`、`/result`、`/report/<assignment id>`、`/transcript`、`/message/<n>`、`/input/<id>` |
|
||||
| `settings`、`settings/<block>` | revisions 与摘要,或一个可直接保存的完整 block |
|
||||
| `prompts`、`prompts/<revision>`、`prompts/current`、`prompts/defaults`、`prompt-preview/<id>` | 提示词配置与组合文本 |
|
||||
| `models`、`models/<provider>` | 可用的模型 |
|
||||
| `feishu/<trigger id>/…`、`github/…` | 原始 Source 记录,在 Source 技能里有描述 |
|
||||
|
||||
Ticket 的 Threads 对仅为上下文的跟随标记 `context`、对 Source 绑定标记 `bound`。Assignment 分页最新在前并包含报告预览。列表用 `next_offset` 续读。`/input/<id>` 接受一个 Input 的名字或 id。`settings` 完整包含小的 blocks,并对 Policy 与 Source blocks 做摘要。
|
||||
|
||||
Source 资源包括消息近邻、`github/pulls/<n>/thread/<comment id>` 与 `github/commits/<sha>/checks`。
|
||||
|
||||
原始记录的消息统一以一个紧凑形状读取,`{id, from, at, text, reply_to?, path?, line?, url?}`,而不是按 GitHub 或 Feishu 存储它们的样子。
|
||||
|
||||
一个 Executor Session 只能读取它自己的 Ticket、它的 Threads、抵达过它的或其请求所持有的 Triggers、它工作于其下的提示词版本与原始记录,它的页面会略去它读不到的东西的 ids——Decisions 与其他 Tickets。
|
||||
|
||||
一个页面展示的每个 id 都能经由这里面的某条读取,于是一个 Session 能从一个对象走到下一个;没有东西按其读取的哈希——比如一个 Eval 的 id 或一个 Policy 版本——不会出现在页面上。
|
||||
|
||||
一个文本页默认 4,000 个 UTF-16 code units、至多 8,000;一个列表默认 20 条、至多 50 条。
|
||||
|
||||
早期版本的 `event/`、`task/` 与 `observation/` 这些名字仍然可以解析。
|
||||
|
||||
### 投递给 Sessions 的消息
|
||||
|
||||
这些消息被引入 main Session 的回合:
|
||||
|
||||
| 消息 | 内容 |
|
||||
| --- | --- |
|
||||
| Candidate | 批次 Trigger 卡片、Thread、`related_tickets`、`tickets_total`、`delivered_to`、来自 Issue/PR 链接的被引用 Tickets、`handed_back` 的理由与下一步动作 |
|
||||
| `follow_up` | 一条消息按其 Executor 看到的样貌、`replies_to`、接收它的 Tickets,以及一条提醒:Executor 决定它,而 main Session 只能停止工作 |
|
||||
| `worker_report` | Ticket 卡片、发起的 Trigger、状态与至多 600 字符的摘要;截断时的 `chars` 与一个读取指针 |
|
||||
| `worker_attention` | 携带那个提问的 Ticket 卡片 |
|
||||
|
||||
其他通知在不唤醒 main Session 的情况下注入:`ticket_ended`、`settings_saved`、提示词保存、Trigger requeues、被丢弃的 Subscriptions 与上下文跟随。一条设置通知点名 block、变更的路径与 `read: settings/<block>`。对 main Session 自己执行的保存、requeue、unsubscribe 或 attach,它不会收到多余的通知。
|
||||
|
||||
Workers 就地收到赋值与 Inputs。第一条消息包含标题、至多 1,500 字符的赋值文本与 source Trigger 卡片。Inputs 携带一个至多 600 字符、带 `replies_to` 的 Trigger 卡片,或纯 Input 文本。读取指针只在内容被截断时出现。
|
||||
|
||||
### 面板专属的动作
|
||||
|
||||
没有工具用于解除一条 unavailable Decision、重跑 setup、刷新 Source 显示名,或把一个 Executor 拥有的人类跟进送进 Eval。最后那个动作被 `orchestrator_trigger_requeue` 刻意拒绝。其他面板动作都有等价的工具。
|
||||
|
||||
大多数内建指令位于 `prompts/*.md`,每行一句话、由 `promptFile` 拼接。Source 技能位于 `skills/dsh-superagent-*/SKILL.md`。工具描述与固定通知——如重启恢复备注——留在代码里。研判指令只位于 `prompts/admission.md`。
|
||||
|
||||
`tests/model-surface.spec.ts` 检查这些表面有无漂移。[004 提示词](004-提示词.md) 解释团队可编辑的部件。
|
||||
|
||||
## Configuration reload
|
||||
|
||||
除 `stateDir` 外,每个插件配置键都是 volatile 的。面板的 Deployment 设置与 DeepSeek Harness 的 Plugin Manager 经由 DeepSeek Harness 的配置表单编辑它们;host 写 `cordis.patch.yml` 并就地更新取值,dsh-superagent 在每次使用时读取每个限额,所以改动无需重启即生效。
|
||||
|
||||
一次更改了 setup 所读取的某个 Source 字段——例如账号的 CLI——的设置保存,以及 setup 等待期间的任何保存,会重启 dsh-superagent。DeepSeek Harness 的设置服务在那次重启期间不提供该条目,因此在它自己重启后,dsh-superagent 会请它重新读取该条目,一个打开的表单就能再次编辑。
|
||||
|
||||
## 用真实数据审查一次变更
|
||||
|
||||
把 `DSH_HOME` 指向一个临时目录并在那里创建一个新 profile。从一份备份复制数据库,禁用所有 Sources,暂停处理并移除 main Session id。给 GitHub 与 Feishu 的 CLI 只读的替身。
|
||||
|
||||
## Open questions
|
||||
|
||||
- 信任:哪些 Trigger 字段是被信任的——如 Source 的 actor 与 Thread 成员资格——哪些不是——如正文与引文。
|
||||
- 部署隔离:agent 的 profile 位于它自己钉住的 DeepSeek Harness 安装上,这样在其他地方升级 DeepSeek Harness 不会弄坏它。
|
||||
@@ -1,425 +0,0 @@
|
||||
# 008 Cookbook
|
||||
|
||||
This cookbook shows how to extend dsh-superagent. It uses the terms in [001 Domain model](001-domain.md) and [002 Authorization](002-authorization.md), and the components in [007 Architecture](007-architecture.md).
|
||||
|
||||
## Add a Source
|
||||
|
||||
This recipe implements a Source kind called `webhook`. It reads an imaginary message queue through an invented CLI, `hookq`, and converts messages to Triggers. All other interfaces are part of dsh-superagent.
|
||||
|
||||
The implementation has four responsibilities: translate events, define receiving and authorization rules, provide replies and context, and maintain a polling loop. The numbered steps build those pieces in order.
|
||||
|
||||
The example assumes this CLI contract:
|
||||
|
||||
- `hookq pull --after <n> --json` prints the messages after position `n`, oldest first: `{"me": "<the agent's id on the queue>", "items": […], "next": <position>}`. An item has `id`, `conversation`, `thread` (the id of the message that started its thread, absent on that message), `sender` with `id` and `bot`, `mentions_me`, `text`, `at` and `url`.
|
||||
- `hookq reply --conversation <id> --to <message id> --key <key> --text <text>` replies under a message, once per key. For a conversation that was deleted it fails with `{"error": {"code": 410, "message": "…"}}` on standard error.
|
||||
- `hookq thread --conversation <id> --root <message id> --json`, with `--after <cursor>` for a later page, prints a page of a thread, oldest first: `{"items": […], "next": "<cursor>"}`, without `next` on the last page.
|
||||
|
||||
Put the kind and its plugin entry in `src/sources/webhook.ts`, following `src/sources/github.ts`. If the Source grows, split translation, Channel and polling code into separate modules as the Feishu Source does.
|
||||
|
||||
### 1. Map the events onto the domain
|
||||
|
||||
Choose stable mappings before writing code. Trigger ids, Thread ids and Channel addresses are persisted, so changing them later can disconnect existing records.
|
||||
|
||||
| Concept | For the webhook queue |
|
||||
| --- | --- |
|
||||
| Trigger | One message |
|
||||
| Thread | A message and the replies to it in one conversation: `<conversation>:<id of the first message>` |
|
||||
| Actor | The sender's id on the queue, as the allowed list names it |
|
||||
| Channel address | The conversation and the message a reply goes under |
|
||||
| Receiving scope | A conversation, with its own row in the admission table |
|
||||
|
||||
### 2. Configuration: schema, fields, facts and health
|
||||
|
||||
The module imports what the steps below use:
|
||||
|
||||
```ts
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { setTimeout as delay } from 'node:timers/promises'
|
||||
import { z } from 'zod'
|
||||
import { key, threadKey, type OutboundMessage, type Trigger } from '../core/index.ts'
|
||||
import { CliError } from '../infra/commands.ts'
|
||||
import { KV } from '../kv.ts'
|
||||
import { health, sourceContext, type SourceContext } from './host.ts'
|
||||
import type { SourceKind } from './kind.ts'
|
||||
```
|
||||
|
||||
```ts
|
||||
const ConfigSchema = z.object({
|
||||
conversations: z.array(z.object({
|
||||
id: z.string().regex(/^[a-z0-9-]+$/),
|
||||
listen: z.enum(['mentions', 'all']),
|
||||
}).strict()).default([]),
|
||||
pollSeconds: z.number().int().min(10).max(3600).default(30),
|
||||
}).strict()
|
||||
export type WebhookConfig = z.infer<typeof ConfigSchema>
|
||||
|
||||
export const webhookKind: SourceKind<WebhookConfig> = {
|
||||
kind: 'webhook',
|
||||
title: { zh: 'Webhook 队列', en: 'Webhook queue' },
|
||||
description: { zh: '接收 webhook 队列里下列对话的消息。', en: 'Receives the messages of these conversations from the webhook queue.' },
|
||||
schema: ConfigSchema,
|
||||
fields: [
|
||||
{ key: 'conversations', type: 'rows', label: { zh: '接收的对话', en: 'Conversations' }, columns: [
|
||||
{ key: 'id', type: 'text', label: { zh: '对话', en: 'Conversation' }, pattern: '[a-z0-9-]+' },
|
||||
{ key: 'listen', type: 'select', label: { zh: '接收', en: 'Receive' }, options: [
|
||||
{ value: 'mentions', label: { zh: '仅 @', en: '@ only' } }, { value: 'all', label: { zh: '全部消息', en: 'All' } }] },
|
||||
] },
|
||||
{ key: 'pollSeconds', type: 'number', advanced: true, min: 10, max: 3600,
|
||||
label: { zh: '轮询间隔 / 秒', en: 'Polling interval / seconds' } },
|
||||
],
|
||||
health: ['webhook'],
|
||||
// find, scope, channel, threadContext, policy and environment follow in the next steps.
|
||||
}
|
||||
```
|
||||
|
||||
- `kind` is the stable Source id. It appears in Trigger `source`, Thread-key prefixes, the `source:webhook` settings block and the Source Policy.
|
||||
- `schema` validates stored configuration and supplies defaults. Before an operator saves an instance, the registry creates an enabled instance from those defaults. Make defaults receive nothing; this example defaults to no conversations.
|
||||
- `fields` generates the Source settings form without custom panel code. A `rows` field edits entries keyed by its first column; `advanced` hides less common settings. Implement `refreshLabels` if the Source can resolve ids to display names. Save those names under `KV.sources.labels(kind)`. Columns with `choose` and `refresh` can then offer them.
|
||||
- Optional `facts(deployment)` displays values derived by setup, such as the account behind a CLI. Setup currently checks only GitHub and Feishu fields listed in `SETUP_FIELDS`, so this example supplies no facts.
|
||||
- `health` declares the Source's health entries, initially `starting`. Registration claims each name and refuses collisions with entries such as `runtime`, `reply`, `worker` or effect kinds. Use the kind's name and reserve `sync:<id>` for Policy syncs. An `error` makes the status script exit 2 and the hourly check notify the operator, so report it only when human action may be needed.
|
||||
|
||||
### 3. Translate a message into a Trigger
|
||||
|
||||
Validate CLI responses before converting them:
|
||||
|
||||
```ts
|
||||
/** One message as `hookq pull` and `hookq thread` print it. */
|
||||
const MessageSchema = z.looseObject({
|
||||
id: z.string().min(1), conversation: z.string().min(1), thread: z.string().optional(),
|
||||
sender: z.looseObject({ id: z.string(), bot: z.boolean().default(false) }), mentions_me: z.boolean().default(false),
|
||||
text: z.string(), at: z.string(), url: z.string().optional(),
|
||||
})
|
||||
type QueueMessage = z.infer<typeof MessageSchema>
|
||||
/** The answer of `hookq pull`, whose items are read one by one, and of `hookq thread`. */
|
||||
const PageSchema = z.looseObject({ me: z.string(), items: z.array(z.unknown()), next: z.number().int() })
|
||||
const ThreadPageSchema = z.looseObject({ items: z.array(MessageSchema), next: z.string().optional() })
|
||||
```
|
||||
|
||||
```ts
|
||||
/** The Trigger of one queue message; the agent's own messages produce none. */
|
||||
export function webhookTrigger(message: QueueMessage, me: string): Trigger | undefined {
|
||||
if (message.sender.id === me) return undefined
|
||||
const thread = `${message.conversation}:${message.thread ?? message.id}`
|
||||
return {
|
||||
id: key('webhook', message.id),
|
||||
source: 'webhook',
|
||||
threads: [{ source: 'webhook', id: thread }],
|
||||
actor: { source: 'webhook', id: message.sender.id },
|
||||
human: !message.sender.bot,
|
||||
...(message.mentions_me ? { addressed: 'mention' as const } : {}),
|
||||
at: message.at,
|
||||
text: message.text,
|
||||
channel: { source: 'webhook', thread: `webhook:${thread}`,
|
||||
address: { conversation: message.conversation, message: message.id, ...(message.url ? { url: message.url } : {}) } },
|
||||
...(message.url ? { url: message.url } : {}),
|
||||
meta: { conversation: message.conversation, messageId: message.id, root: message.thread ?? message.id },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Stable ids.** Derive the Trigger id from the queue id through `key`. `Engine.receive` returns `duplicate` for stored ids, and duplicate accepts create only one Ticket. Re-reading messages is therefore safe. To treat edits as new events, include an edit version in the id, as GitHub does with comment `updated_at`.
|
||||
- **Threads.** The first Thread is primary and owns the Eval. Additional Threads add Correlation context. The Source chooses the id; its full key is `webhook:<id>`.
|
||||
- **Sender.** Use Source-native ids in `actor`. Set `human: false` for bots and CI. Listed bots can only become inferred candidates. An empty actor id, as for CI, uses the admission cell for unlisted, unaddressed senders. Set `addressed` to `mention` or `direct` only when the message actually addresses the agent; this gives an allowed person's message explicit strength. Never produce a Trigger for the agent's own message.
|
||||
- **Reply destination and metadata.** `channel` identifies where replies go; `url` links to the source message. `channel.thread` groups messages for ordered delivery: a Ticket's later messages wait behind its earlier ones until an earlier message has failed three times. Other Tickets do not wait. Keep Source-specific fields in `meta`, such as the receiving conversation and Thread root. Both `meta` and `text` are untrusted data, not authorization.
|
||||
|
||||
Agents can read the new kind through `orchestrator_read trigger/<id>` and `thread/<key>`. These expose message text, disposition and neighboring Triggers. A new kind does not automatically receive a built-in skill, resources for original service records or pull-request binding.
|
||||
|
||||
Text search works without extra code. Implement `find(query)` when people use Source-specific references such as message ids, URLs or `#12`. Return the Thread keys and search terms those references identify:
|
||||
|
||||
```ts
|
||||
find: query => ({ threads: [], terms: [...new Set(query.match(/\bm[0-9]+\b/g) ?? [])] }),
|
||||
```
|
||||
|
||||
Here a queue message id in the query, such as `m42`, searches the Triggers for it.
|
||||
|
||||
### 4. What the Source receives: scope
|
||||
|
||||
```ts
|
||||
scope({ meta }, config) {
|
||||
const conversation = config.conversations.find(item => item.id === meta.conversation)
|
||||
if (!conversation) return { listened: false, reason: 'conversation_not_listed' }
|
||||
return { listened: true, observed: conversation.listen === 'all', row: conversation.id }
|
||||
},
|
||||
```
|
||||
|
||||
The Engine calls `scope` through `SourceRegistry` for each incoming Trigger. Out-of-scope messages are stored as filtered with their reason and create no Thread. Pass messages to `receive` rather than silently dropping them in the polling loop, so the panel can explain filtering.
|
||||
|
||||
`observed: true` means the Source receives unaddressed messages in that scope, allowing admission-table decisions for them. `row` identifies the corresponding admission row.
|
||||
|
||||
Revalidation calls `scope` with the Mandate's stored metadata. Make it depend only on `meta` and configuration. Resuming requires `listened`, not `observed`. Without `scope`, a kind receives every Trigger it produces but observes none.
|
||||
|
||||
### 5. Replies: the Channel
|
||||
|
||||
```ts
|
||||
channel: {
|
||||
allowed: (address, config) => config.conversations.some(item => item.id === address.conversation),
|
||||
send: sendWebhook,
|
||||
link: address => ({ url: address.url ?? '',
|
||||
label: { zh: `Webhook 对话 ${address.conversation}`, en: `Webhook conversation ${address.conversation}` } }),
|
||||
},
|
||||
```
|
||||
|
||||
```ts
|
||||
export async function sendWebhook(host: Pick<SourceContext, 'commands'>, message: OutboundMessage): Promise<'sent' | 'gone'> {
|
||||
const { conversation, message: to } = message.channel.address
|
||||
if (!conversation || !to) throw new Error('Missing webhook reply target')
|
||||
try {
|
||||
await host.commands.run('hookq', ['reply', '--conversation', conversation, '--to', to, '--key', message.id,
|
||||
'--text', message.text])
|
||||
return 'sent'
|
||||
} catch (error) {
|
||||
if (error instanceof CliError && error.code === 410) return 'gone'
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Idempotent sends.** Outbox calls `SourceRegistry.send`, which invokes `channel.send`. Messages wait while the Source plugin is unloaded. A send may fail after the remote service received it, so retries must not duplicate messages. Use `message.id` as an idempotency key: `hookq` supports `--key`; GitHub stores a hidden comment marker; Feishu accepts an idempotency key.
|
||||
- **Permanent versus retryable failure.** Return `sent` on success and `gone` only when delivery is permanently impossible, such as the example's 410. A gone message is retained as dropped and never queued again; it appears in `GET sources` and on the Source page. Throw for other failures so Outbox retries after 5 seconds, doubling to 300 seconds. `CliError.code` is the code reported by the CLI output.
|
||||
- **Allowed destinations.** `allowed(address, config)` checks only the saved address and current configuration. Put every required fact in the address; for example, Feishu records whether the chat is direct. A destination no longer listened to is dropped, while a disabled Source leaves messages waiting. Without `allowed`, all addresses are permitted while the Source is enabled.
|
||||
- **Links.** `link(address, links)` returns the URL and label the panel displays through `SourceRegistry.link`.
|
||||
|
||||
### 6. Thread context for assessments
|
||||
|
||||
```ts
|
||||
threadContext: {
|
||||
messageId: trigger => String(trigger.meta.messageId ?? ''),
|
||||
// A queue thread is flat: its root, and the message a reply answers, are on its first page, as on GitHub.
|
||||
read: async (commands, trigger, _part, cursor) => {
|
||||
const page = ThreadPageSchema.parse(JSON.parse(await commands.run('hookq', ['thread', '--conversation',
|
||||
String(trigger.meta.conversation), '--root', String(trigger.meta.root), '--json', ...(cursor ? ['--after', cursor] : [])])))
|
||||
const messages = page.items.map(item => ({ id: item.id, from: item.sender.id, at: item.at, text: item.text,
|
||||
...(item.sender.bot ? { bot: true as const } : {}), ...(item.url ? { url: item.url } : {}) }))
|
||||
return { messages, ...(page.next ? { next: page.next } : {}), body: { messages } }
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Proactive assessment needs the conversation around a Trigger. `orchestrator_eval` reads an initial window; `orchestrator_eval_read` expands pages, the root or the parent message. Implement the optional `threadContext` interface to provide these reads:
|
||||
|
||||
- `read(commands, trigger, part, cursor, config)` returns a `ContextPart`. Its `messages` use `ContextMessage`: `{id, from, at, text}` plus applicable `bot`, `deleted`, `reply_to`, `url`, `path` and `line` fields. `next` is omitted on the last page. `body` is the content shown to the assessing Session, here the same messages. Optional `status` can describe special results such as Feishu's `chat_neighbours`. `part` is `thread`, `root` or `parent`.
|
||||
- `messageId(trigger)` finds the Trigger's own message among them.
|
||||
- `images(text)` and `image(…)` are for a kind whose messages carry images, as Feishu's do; the example leaves them out.
|
||||
|
||||
A kind without `threadContext` gives assessments no Thread context; the Trigger's own text stays readable.
|
||||
|
||||
If messages reply to earlier messages, add `repliesTo(trigger)`. It returns the corresponding Trigger id, allowing follow-up notices and worker Inputs to show the replied-to message when stored. For example, if `hookq` supplies `reply_to` and translation stores it as `meta.replyTo`:
|
||||
|
||||
```ts
|
||||
repliesTo: trigger => typeof trigger.meta.replyTo === 'string' ? key('webhook', trigger.meta.replyTo) : undefined,
|
||||
```
|
||||
|
||||
### 7. Who may ask: the Source's part of the Policy
|
||||
|
||||
```ts
|
||||
policy: {
|
||||
schema: z.object({ allowed: z.array(z.object({ id: z.string().min(1), name: z.string().trim().max(100).optional() }).strict())
|
||||
.default([]) }).strict(),
|
||||
fields: [{ key: 'allowed', type: 'rows', label: { zh: '能发起工作的发送者', en: 'Senders who may originate work' }, columns: [
|
||||
{ key: 'id', type: 'text', label: { zh: '发送者', en: 'Sender' } },
|
||||
{ key: 'name', type: 'text', label: { zh: '名字', en: 'Name' }, optional: true }] }],
|
||||
explicit: { zh: '名单里的人在消息里 @ 智能体。', en: 'Someone on the list mentions the agent in a message.' },
|
||||
rows: stored => ConfigSchema.parse(stored).conversations.map(conversation => {
|
||||
const all = conversation.listen === 'all'
|
||||
return { id: conversation.id, label: conversation.id, note: all ? { zh: '全部消息', en: 'All' } : { zh: '仅 @', en: '@ only' },
|
||||
cells: { listedUnaddressed: all, unlistedAddressed: true, unlistedUnaddressed: all } }
|
||||
}),
|
||||
},
|
||||
```
|
||||
|
||||
- `schema` validates the Source Policy, including `allowed` sender ids. Id comparison is case-insensitive. Admission tables are validated separately and shared across kinds. This Policy part is saved at `policy.sources.webhook` and contributes to the Policy version.
|
||||
- `fields` generates the form under **Eval → On each Source**, beginning with `allowed`. `explicit` explains how to address the agent. Optional `unnamed: {person, bot}` supplies display labels for opaque ids such as Feishu's `ou_…`; this example omits it because queue ids are readable names.
|
||||
- `rows(config)` lists admission scopes and their available cells. Each row id must match the `row` returned by `scope`. Unaddressed-message cells are available only where the Source observes those messages. Kinds without `rows` use default cells.
|
||||
- Optional `bots` lists bots eligible for inferred candidacy, using the same entry shape as `allowed`. Without it, no bot is admitted.
|
||||
- Optional `sync: {label, ready(part), run(host, part, signal)}` refreshes membership at startup and on **Sync now**. Feishu uses chat membership. Failures appear under `sync:<id>` and retry after one minute, doubling to one hour until successful.
|
||||
|
||||
### 8. Variables for the Executor
|
||||
|
||||
```ts
|
||||
environment: {
|
||||
variables: [{ key: 'DSH_SUPERAGENT_WEBHOOK_CONVERSATION', description: {
|
||||
zh: '接下工单的 webhook 对话;工单不是从这个来源来的时为空',
|
||||
en: 'The webhook conversation that started the ticket; empty when the ticket did not start on this Source' } }],
|
||||
values: ({ trigger }) => ({
|
||||
DSH_SUPERAGENT_WEBHOOK_CONVERSATION: trigger?.source === 'webhook' ? String(trigger.meta.conversation ?? '') : '' }),
|
||||
},
|
||||
```
|
||||
|
||||
Execution settings show declared `variables` beside the prepare command even when the Source is disabled. While enabled, `values` supplies Ticket-specific values using deployment, configuration and the originating Trigger.
|
||||
|
||||
The prepare command reads environment variables; the worker reads their values in its instructions. Use the `DSH_SUPERAGENT_<KIND>_…` prefix, return an empty value for Tickets from another Source, and never include credentials.
|
||||
|
||||
### 9. The plugin entry and its loop
|
||||
|
||||
```ts
|
||||
export const name = 'dsh-superagent-webhook'
|
||||
export const inject = ['orchestrator']
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.effect(() => ctx.orchestrator.sources.register(webhookKind), 'webhook: source kind')
|
||||
ctx.effect(() => {
|
||||
const source = sourceContext(ctx.orchestrator, webhookKind)
|
||||
const controller = new AbortController()
|
||||
// DSH ends its process on an unhandled rejection, so a failure that escapes the loop becomes the Source's health.
|
||||
const running = watch(source, controller.signal)
|
||||
.catch((error: unknown) => { source.report('webhook', { state: 'error', detail: String(error) }) })
|
||||
return async () => { controller.abort(); await running }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The plugin injects `orchestrator`. Its first effect registers the kind and unregisters it on disposal. The second runs the polling loop with a `SourceContext`. Disposal aborts the loop and awaits completion, including during application stop or restart.
|
||||
|
||||
```ts
|
||||
async function watch(ctx: SourceContext, signal: AbortSignal): Promise<void> {
|
||||
await ctx.ready
|
||||
// Setup restarts dsh-superagent once it resolves the accounts; until then the Source reads nothing.
|
||||
if (!ctx.configured()) { ctx.report('webhook', health('paused', 'setupPending')); return }
|
||||
let failures = 0
|
||||
while (!signal.aborted) {
|
||||
const config = ctx.config<WebhookConfig>('webhook')
|
||||
if (!config) ctx.report('webhook', health('paused', 'sourceOff'))
|
||||
else {
|
||||
const at = new Date().toISOString()
|
||||
try {
|
||||
const { read, unreadable } = await pollQueue(ctx, signal)
|
||||
failures = 0
|
||||
ctx.report('webhook', { state: 'ready', detail: `${read} messages, ${unreadable} unreadable`, at })
|
||||
} catch (error) {
|
||||
failures += 1
|
||||
// One network failure is retried on the next round; three in a row need a person.
|
||||
if (!signal.aborted && failures >= 3) {
|
||||
ctx.report('webhook', health('error', 'pollFailures', { count: String(failures), error: String(error) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
await delay((config?.pollSeconds ?? 30) * 1000, undefined, { signal }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
/** Read the queue after the cursor, take in each message, then move the cursor past them. */
|
||||
export async function pollQueue(ctx: SourceContext, signal: AbortSignal): Promise<{ read: number; unreadable: number }> {
|
||||
const after = ctx.state.get(KV.webhook.cursor) ?? 0
|
||||
const output = await ctx.commands.withSignal(signal).run('hookq', ['pull', '--after', String(after), '--json'])
|
||||
const page = PageSchema.parse(JSON.parse(output))
|
||||
let unreadable = 0
|
||||
for (const item of page.items) {
|
||||
if (signal.aborted) throw new Error('The webhook Source stopped during the read')
|
||||
const message = MessageSchema.safeParse(item)
|
||||
if (!message.success) { unreadable++; continue }
|
||||
const trigger = webhookTrigger(message.data, page.me)
|
||||
if (!trigger) continue
|
||||
await ctx.receive(trigger)
|
||||
const conversation = { label: { zh: '对话', en: 'Conversation' }, value: message.data.conversation }
|
||||
for (const ref of trigger.threads) ctx.threads.describe(threadKey(ref), { facts: { conversation } })
|
||||
}
|
||||
ctx.state.set(KV.webhook.cursor, page.next)
|
||||
return { read: page.items.length, unreadable }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- **Setup and the switch.** Wait for `ctx.ready`, and read nothing while `ctx.configured()` is false. Read `ctx.config(kind)` on every round: it is undefined while the Source is off.
|
||||
- **Cursors.** Store read positions in `ctx.state` using typed entries in `KV` (`src/kv.ts`), such as `webhook: {cursor: record<number>('webhook-cursor')}`. `tests/store.spec.ts` requires key literals to appear only in that registry. External packages can define keys with exported `record` and `family`. Setting `undefined` removes a record; `ctx.atomic` groups writes in a transaction.
|
||||
|
||||
Advance a cursor only after every covered Trigger has been stored. An interrupted round leaves the cursor unchanged; stable ids make replay safe. For timestamp cursors, `cursorAt(started)` from `src/sources/poll.ts` subtracts five seconds to allow for late remote writes.
|
||||
- **Thread metadata.** After receiving a Trigger, call `ctx.threads.describe(key, {label, url, facts})` to populate its display information. `ctx.threads.patch(key, meta)` delegates to `threads.patchMeta`, setting Source-owned keys or removing them when the value is `undefined`. Both ignore absent Threads, including those of filtered Triggers.
|
||||
- **Per-item errors.** Count malformed items and continue rather than failing the entire batch. `validItems(item, list, tally)` in `src/infra/commands.ts` supports this pattern. For independently polled Threads, use `pollThreads` from `src/sources/poll.ts`; it records each failure as `pollError`. Its `polled` predicate keeps live subscriptions and a two-week window after the later of the last Trigger and Ticket end.
|
||||
- **Health.** Write only declared entries through `ctx.report`. `health(state, code, values)` provides fixed messages such as `setupPending`, `sourceOff` and `pollFailures`. Count consecutive failures before reporting `error`.
|
||||
- **Abort.** Pass the signal to `ctx.commands.withSignal(signal)` and to `delay`, and check it between items, so a stop or a restart ends the loop at once.
|
||||
|
||||
This example does not send Source-originated notices. A Source that needs them calls `ctx.notify`; Outbox delivers them like other replies.
|
||||
|
||||
It also does not bind Threads to Ticket completion. Use `ctx.tickets.bindThread` and `threadClosed` only when a Source's Thread lifecycle determines whether work is done, as GitHub pull requests do. The Engine still decides when the Ticket ends.
|
||||
|
||||
### 10. Register the plugin
|
||||
|
||||
Build the module as its own entry in `tsdown.config.ts`:
|
||||
|
||||
```ts
|
||||
entry: { index: 'src/index.ts', 'github-watch': 'src/sources/github.ts', 'feishu-watch': 'src/sources/feishu.ts',
|
||||
'webhook-watch': 'src/sources/webhook.ts' },
|
||||
```
|
||||
|
||||
Then export the built file in `package.json` by adding `"./webhook-watch": "./lib/webhook-watch.js"` to `exports`, and add the plugin to the `insert` list of the bundle's `cordis.patch.yml`, disabled, as Feishu is:
|
||||
|
||||
```yaml
|
||||
- id: dsh-superagent-webhook
|
||||
name: dsh-superagent/webhook-watch
|
||||
disabled: true
|
||||
```
|
||||
|
||||
Enable the plugin in a profile's `cordis.patch.yml` with `id: dsh-superagent-webhook` and `disabled: false`. Build and restart. The new Source appears in Sources settings and under **Eval → On each Source**.
|
||||
|
||||
Add its module and spec to the module and test tables in [007 Architecture](007-architecture.md). `tests/docs.spec.ts` checks that both indexes are complete.
|
||||
|
||||
### 11. Tests
|
||||
|
||||
Create `tests/webhook.spec.ts` using the pattern in `tests/sources.spec.ts`: a real Store in a temporary directory, a `SourceRegistry` with `HealthBoard`, an Engine with stand-in ports, and a `SourceContext` from `sourceContextOf`.
|
||||
|
||||
Mock CLI calls by spying on `Commands.run`. Also spy on `withSignal`, because it normally returns a new `Commands` instance.
|
||||
|
||||
```ts
|
||||
function host(overrides: Partial<SourceContextDeps> = {}) {
|
||||
const store = new Store(join(mkdtempSync(join(tmpdir(), 'webhook-')), 'db.sqlite'))
|
||||
store.saveSettings(SettingsSchema.parse({
|
||||
sources: { webhook: { enabled: true, config: { conversations: [{ id: 'ops', listen: 'mentions' }] } } },
|
||||
policy: { sources: { webhook: { allowed: [{ id: 'alice' }] } } },
|
||||
}))
|
||||
// The registry claims the kind's health entries on the board, where the test reads them.
|
||||
const board = new HealthBoard()
|
||||
const sources = new SourceRegistry(() => store.settings(), board)
|
||||
sources.register(webhookKind)
|
||||
const engine = new Engine({ repo: store, scope: sources, policy: () => settingsPolicy(store.settings(), sources),
|
||||
assessor: { request: vi.fn() }, executor: { start: vi.fn(), deliver: vi.fn(), stop: vi.fn() },
|
||||
outbox: { send: vi.fn(), status: () => undefined, withdraw: vi.fn() } })
|
||||
const commands = new Commands(() => testDeployment(), {}, new AbortController().signal)
|
||||
const receive = vi.fn((trigger: Trigger) => { const result = engine.receive(trigger); engine.evaluateDue(); return result })
|
||||
// The Source's own notices go to the outbox, which keeps their receipts and sends nothing here.
|
||||
const deps: SourceContextDeps = { store, engine, outbox: { send: message => { store.receipts.add(message) } }, commands, sources,
|
||||
deployment: testDeployment(), ready: Promise.resolve(), configured: () => true, receive, ...overrides }
|
||||
return { ...deps, store, engine, commands, receive, health: board.all, ctx: sourceContextOf(deps, webhookKind) }
|
||||
}
|
||||
/** The queue as `hookq` answers it. */
|
||||
function queue(h: ReturnType<typeof host>, answer: () => unknown) {
|
||||
vi.spyOn(h.commands, 'withSignal').mockReturnValue(h.commands)
|
||||
return vi.spyOn(h.commands, 'run').mockImplementation(async () => JSON.stringify(answer()))
|
||||
}
|
||||
|
||||
it('takes in what the queue holds after the cursor, once, and moves the cursor after it', async () => {
|
||||
const h = host()
|
||||
queue(h, () => ({ me: 'agent', items: [message('m1'), { id: 'broken' }, message('m2', { conversation: 'elsewhere' })], next: 3 }))
|
||||
expect(await pollQueue(h.ctx, new AbortController().signal)).toEqual({ read: 3, unreadable: 1 })
|
||||
expect(await h.receive.mock.results[0]!.value).toBe('stored')
|
||||
expect(await h.receive.mock.results[1]!.value).toBe('filtered')
|
||||
expect(h.store.get(KV.webhook.cursor)).toBe(3)
|
||||
// An allowed sender who mentions the agent makes an explicit request.
|
||||
expect(h.engine.candidate(key('webhook', 'm1'))?.strength).toBe('explicit')
|
||||
await pollQueue(h.ctx, new AbortController().signal)
|
||||
expect(await h.receive.mock.results[2]!.value).toBe('duplicate')
|
||||
h.store.close()
|
||||
})
|
||||
```
|
||||
|
||||
`message(id, fields)` builds a queue item. Cover at least:
|
||||
|
||||
- the translation: a stable id, the Thread, the actor, `human` and `addressed`, and no Trigger for the agent's own message;
|
||||
- a round: Triggers stored once, one outside the scope `filtered`, the cursor moved after them, an allowed sender's mention evaluated as `explicit`;
|
||||
- a round cut short, which keeps its cursor;
|
||||
- `send`: `sent`, `gone` for a 410 only, and a throw for any other failure, with the CLI's error built by `commandError`;
|
||||
- the Thread context: the messages of a page and the cursor of the next, as `tests/registry.spec.ts` reads a kind's Threads through `SourceRegistry.threadContext`;
|
||||
- the plugin: `apply` registers the kind and reports `setupPending` while setup waits, its disposers unregister the kind, and a failure that escapes the loop becomes the Source's health instead of an unhandled rejection;
|
||||
- the Source's part of the Policy and its admission rows, as `tests/registry.spec.ts` and `tests/policy.spec.ts` check the Feishu Source's.
|
||||
|
||||
### Checklist
|
||||
|
||||
1. Map Triggers, Threads, actors, Channel addresses and receiving scopes.
|
||||
2. Write the kind: `schema`, `fields`, `health`, and `facts` if setup derives any.
|
||||
3. Translate events into Triggers with stable ids, and drop the agent's own.
|
||||
4. Decide what the Source receives in `scope`, from `meta` alone.
|
||||
5. Give it a Channel whose `send` posts each message once and answers `gone` only when the conversation can never receive it.
|
||||
6. Give assessments its Thread context, if it can read a Thread.
|
||||
7. Give it a part of the Policy with `allowed` and the admission rows.
|
||||
8. Register its execution environment variables.
|
||||
9. Write the plugin entry and a loop that waits for setup, follows the switch, moves cursors after `receive`, keeps errors per item, reports health and stops on abort; name the cursors in `KV`.
|
||||
10. Add the build entry, the export and the `cordis.patch.yml` insert, and list the module and its spec in the tables of 007.
|
||||
11. Test it as `tests/sources.spec.ts` tests the other Sources.
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
# 008 手册
|
||||
|
||||
这本手册展示如何扩展 dsh-superagent。它使用 [001 领域模型](001-领域模型.md) 与 [002 授权](002-授权.md) 里的术语,以及 [007 架构](007-架构.md) 里的构件。
|
||||
|
||||
## 添加一个 Source
|
||||
|
||||
这份食谱实现一个名为 `webhook` 的 Source kind。它通过一个虚构的 CLI `hookq` 读取一个假想的消息队列,并把消息转换为 Triggers。其他所有接口都是 dsh-superagent 已有的部分。
|
||||
|
||||
实现有四项职责:翻译事件、定义接收与授权规则、提供回复与上下文、维护轮询循环。下面编号的步骤按顺序构建这些部件。
|
||||
|
||||
示例假定这样的 CLI 契约:
|
||||
|
||||
- `hookq pull --after <n> --json` 打印位置 `n` 之后的消息,最旧的在前:`{"me": "<the agent's id on the queue>", "items": […], "next": <position>}`。一个 item 有 `id`、`conversation`、`thread`(开启其 Thread 的消息的 id,该消息本身没有此字段)、带 `id` 与 `bot` 的 `sender`、`mentions_me`、`text`、`at` 和 `url`。
|
||||
- `hookq reply --conversation <id> --to <message id> --key <key> --text <text>` 在某条消息下回复,每个 key 一次。对已被删除的 conversation,它失败并在标准错误上输出 `{"error": {"code": 410, "message": "…"}}`。
|
||||
- `hookq thread --conversation <id> --root <message id> --json`,用 `--after <cursor>` 取后一页,打印一个 Thread 的一页,最旧的在前:`{"items": […], "next": "<cursor>"}`,最后一页没有 `next`。
|
||||
|
||||
Kind 及其插件条目放在 `src/sources/webhook.ts`,仿照 `src/sources/github.ts`。若这个 Source 变大,就像 Feishu Source 那样把翻译、Channel 与轮询代码拆成独立模块。
|
||||
|
||||
### 1. 把事件映射到领域
|
||||
|
||||
写代码之前先定下稳定的映射。Trigger ids、Thread ids 和 Channel 地址都会被持久化,之后改动它们可能让既有记录失联。
|
||||
|
||||
| 概念 | 对 webhook 队列而言 |
|
||||
| --- | --- |
|
||||
| Trigger | 一条消息 |
|
||||
| Thread | 一个 conversation 里的一条消息及其回复:`<conversation>:<id of the first message>` |
|
||||
| Actor | 发送者在队列上的 id,按允许名单写它的方式 |
|
||||
| Channel 地址 | conversation,以及回复要挂在下面的那条消息 |
|
||||
| 接收范围 | 一个 conversation,在准入表里有自己的一行 |
|
||||
|
||||
### 2. 配置:schema、字段、事实与健康
|
||||
|
||||
模块 import 后面各步骤要用到的东西:
|
||||
|
||||
```ts
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { setTimeout as delay } from 'node:timers/promises'
|
||||
import { z } from 'zod'
|
||||
import { key, threadKey, type OutboundMessage, type Trigger } from '../core/index.ts'
|
||||
import { CliError } from '../infra/commands.ts'
|
||||
import { KV } from '../kv.ts'
|
||||
import { health, sourceContext, type SourceContext } from './host.ts'
|
||||
import type { SourceKind } from './kind.ts'
|
||||
```
|
||||
|
||||
```ts
|
||||
const ConfigSchema = z.object({
|
||||
conversations: z.array(z.object({
|
||||
id: z.string().regex(/^[a-z0-9-]+$/),
|
||||
listen: z.enum(['mentions', 'all']),
|
||||
}).strict()).default([]),
|
||||
pollSeconds: z.number().int().min(10).max(3600).default(30),
|
||||
}).strict()
|
||||
export type WebhookConfig = z.infer<typeof ConfigSchema>
|
||||
|
||||
export const webhookKind: SourceKind<WebhookConfig> = {
|
||||
kind: 'webhook',
|
||||
title: { zh: 'Webhook 队列', en: 'Webhook queue' },
|
||||
description: { zh: '接收 webhook 队列里下列对话的消息。', en: 'Receives the messages of these conversations from the webhook queue.' },
|
||||
schema: ConfigSchema,
|
||||
fields: [
|
||||
{ key: 'conversations', type: 'rows', label: { zh: '接收的对话', en: 'Conversations' }, columns: [
|
||||
{ key: 'id', type: 'text', label: { zh: '对话', en: 'Conversation' }, pattern: '[a-z0-9-]+' },
|
||||
{ key: 'listen', type: 'select', label: { zh: '接收', en: 'Receive' }, options: [
|
||||
{ value: 'mentions', label: { zh: '仅 @', en: '@ only' } }, { value: 'all', label: { zh: '全部消息', en: 'All' } }] },
|
||||
] },
|
||||
{ key: 'pollSeconds', type: 'number', advanced: true, min: 10, max: 3600,
|
||||
label: { zh: '轮询间隔 / 秒', en: 'Polling interval / seconds' } },
|
||||
],
|
||||
health: ['webhook'],
|
||||
// find, scope, channel, threadContext, policy and environment follow in the next steps.
|
||||
}
|
||||
```
|
||||
|
||||
- `kind` 是稳定的 Source id。它出现在 Trigger 的 `source`、Thread-key 前缀、`source:webhook` 设置 block 和 Source Policy 里。
|
||||
- `schema` 校验已保存的配置并提供默认值。在操作者保存一个实例之前,registry 会用那些默认值创建一个启用状态的实例。默认值应当什么都不接收;本示例默认为没有 conversation。
|
||||
- `fields` 无需自定义面板代码就生成 Source 设置表单。一个 `rows` 字段编辑以其第一列作键的条目;`advanced` 隐藏不常用的设置。若 Source 能把 ids 解析成显示名,就实现 `refreshLabels`。把这些名字存到 `KV.sources.labels(kind)` 下,带 `choose` 与 `refresh` 的列随即可提供它们。
|
||||
- 可选的 `facts(deployment)` 展示由 setup 推导的值,例如某个 CLI 背后的账号。目前 setup 只检查 `SETUP_FIELDS` 里列出的 GitHub 与 Feishu 字段,所以本示例不提供 facts。
|
||||
- `health` 声明这个 Source 的健康条目,初始为 `starting`。注册会认领每个名字,并拒绝与 `runtime`、`reply`、`worker` 或 effect 种类等条目的冲突。用 kind 的名字,并把 `sync:<id>` 留给 Policy 同步。一个 `error` 会让状态脚本以 2 退出、让每小时检查通知操作者,所以只在可能需要人来处理时上报它。
|
||||
|
||||
### 3. 把一条消息翻译成 Trigger
|
||||
|
||||
转换之前先校验 CLI 的应答:
|
||||
|
||||
```ts
|
||||
/** One message as `hookq pull` and `hookq thread` print it. */
|
||||
const MessageSchema = z.looseObject({
|
||||
id: z.string().min(1), conversation: z.string().min(1), thread: z.string().optional(),
|
||||
sender: z.looseObject({ id: z.string(), bot: z.boolean().default(false) }), mentions_me: z.boolean().default(false),
|
||||
text: z.string(), at: z.string(), url: z.string().optional(),
|
||||
})
|
||||
type QueueMessage = z.infer<typeof MessageSchema>
|
||||
/** The answer of `hookq pull`, whose items are read one by one, and of `hookq thread`. */
|
||||
const PageSchema = z.looseObject({ me: z.string(), items: z.array(z.unknown()), next: z.number().int() })
|
||||
const ThreadPageSchema = z.looseObject({ items: z.array(MessageSchema), next: z.string().optional() })
|
||||
```
|
||||
|
||||
```ts
|
||||
/** The Trigger of one queue message; the agent's own messages produce none. */
|
||||
export function webhookTrigger(message: QueueMessage, me: string): Trigger | undefined {
|
||||
if (message.sender.id === me) return undefined
|
||||
const thread = `${message.conversation}:${message.thread ?? message.id}`
|
||||
return {
|
||||
id: key('webhook', message.id),
|
||||
source: 'webhook',
|
||||
threads: [{ source: 'webhook', id: thread }],
|
||||
actor: { source: 'webhook', id: message.sender.id },
|
||||
human: !message.sender.bot,
|
||||
...(message.mentions_me ? { addressed: 'mention' as const } : {}),
|
||||
at: message.at,
|
||||
text: message.text,
|
||||
channel: { source: 'webhook', thread: `webhook:${thread}`,
|
||||
address: { conversation: message.conversation, message: message.id, ...(message.url ? { url: message.url } : {}) } },
|
||||
...(message.url ? { url: message.url } : {}),
|
||||
meta: { conversation: message.conversation, messageId: message.id, root: message.thread ?? message.id },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **稳定的 ids。** 通过 `key` 从队列 id 派生 Trigger id。`Engine.receive` 对已存储的 id 返回 `duplicate`,重复的 accept 也只创建一个 Ticket。因此重读消息是安全的。要把编辑当作新事件,就在 id 里包含编辑版本,如同 GitHub 对评论用 `updated_at` 那样。
|
||||
- **Threads。** 第一个 Thread 是 primary,拥有那个 Eval。额外的 Threads 增加 Correlation 上下文。id 由 Source 选定;它的完整 key 是 `webhook:<id>`。
|
||||
- **发送者。** 在 `actor` 里使用 Source 原生的 id。bot 与 CI 设 `human: false`。名单里的 bot 只能成为 inferred 候选。空的 actor id——如 CI 的情形——使用针对未列出、未被 at 的发送者的准入单元格。只有消息确实 at 了智能体时才设 `addressed` 为 `mention` 或 `direct`;这让名单中者的消息获得 explicit 强度。绝不为智能体自己的消息产出 Trigger。
|
||||
- **回复去向与元数据。** `channel` 指明回复去向;`url` 链接到源消息。`channel.thread` 为有序投递给消息分组:一个 Ticket 的较晚消息排在较早消息之后,直到某条较早消息失败三次。其他 Tickets 不排队。Source 专属字段留在 `meta`,比如接收它的 conversation 与 Thread root。`meta` 与 `text` 都是不可信数据,不是授权。
|
||||
|
||||
Agents 可以通过 `orchestrator_read trigger/<id>` 与 `thread/<key>` 读取新 kind。它们暴露消息文本、处置结果与相邻 Triggers。新 kind 不会自动获得内建技能、原始服务记录的资源或 pull-request 绑定。
|
||||
|
||||
文本搜索无需额外代码即可工作。当人们使用 Source 专属的指称——如消息 id、URL 或 `#12`——时才实现 `find(query)`。返回那些指称所标识的 Thread keys 与搜索词:
|
||||
|
||||
```ts
|
||||
find: query => ({ threads: [], terms: [...new Set(query.match(/\bm[0-9]+\b/g) ?? [])] }),
|
||||
```
|
||||
|
||||
这里,查询里的队列消息 id(如 `m42`)会去搜索具有它的 Triggers。
|
||||
|
||||
### 4. Source 接收什么:scope
|
||||
|
||||
```ts
|
||||
scope({ meta }, config) {
|
||||
const conversation = config.conversations.find(item => item.id === meta.conversation)
|
||||
if (!conversation) return { listened: false, reason: 'conversation_not_listed' }
|
||||
return { listened: true, observed: conversation.listen === 'all', row: conversation.id }
|
||||
},
|
||||
```
|
||||
|
||||
Engine 对每个进来的 Trigger 经由 `SourceRegistry` 调用 `scope`。范围之外的消息作为 filtered 存储并带上原因,不创建 Thread。把消息交给 `receive`,而不是在轮询循环里悄悄丢弃,这样面板才能解释过滤。
|
||||
|
||||
`observed: true` 表示 Source 接收该范围内未被 at 的消息,允许对它们做准入表判断。`row` 标识对应的准入行。
|
||||
|
||||
重新校验会用 Mandate 存储的元数据调用 `scope`。让它只依赖 `meta` 与配置。恢复要求的是 `listened`,不是 `observed`。没有 `scope` 时,一个 kind 接收它产出的每个 Trigger,但不 observe 任何消息。
|
||||
|
||||
### 5. 回复:Channel
|
||||
|
||||
```ts
|
||||
channel: {
|
||||
allowed: (address, config) => config.conversations.some(item => item.id === address.conversation),
|
||||
send: sendWebhook,
|
||||
link: address => ({ url: address.url ?? '',
|
||||
label: { zh: `Webhook 对话 ${address.conversation}`, en: `Webhook conversation ${address.conversation}` } }),
|
||||
},
|
||||
```
|
||||
|
||||
```ts
|
||||
export async function sendWebhook(host: Pick<SourceContext, 'commands'>, message: OutboundMessage): Promise<'sent' | 'gone'> {
|
||||
const { conversation, message: to } = message.channel.address
|
||||
if (!conversation || !to) throw new Error('Missing webhook reply target')
|
||||
try {
|
||||
await host.commands.run('hookq', ['reply', '--conversation', conversation, '--to', to, '--key', message.id,
|
||||
'--text', message.text])
|
||||
return 'sent'
|
||||
} catch (error) {
|
||||
if (error instanceof CliError && error.code === 410) return 'gone'
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **幂等的发送。** Outbox 调用 `SourceRegistry.send`,后者调用 `channel.send`。Source 插件被卸载期间,消息等待。发送可能在远端服务已收到之后才失败,所以重试不得重复发消息。用 `message.id` 作幂等键:`hookq` 支持 `--key`;GitHub 存一个隐藏的评论标记;Feishu 接受一个幂等键。
|
||||
- **永久失败与可重试失败。** 成功返回 `sent`,只有永久无法投递时才返回 `gone`,例如示例中的 410。一条 gone 消息作为 dropped 保留,不再排队;它出现在 `GET sources` 和 Source 页面上。其他失败要抛出,让 Outbox 在 5 秒后重试,倍增到 300 秒。`CliError.code` 是 CLI 输出报告的代码。
|
||||
- **允许的目的地。** `allowed(address, config)` 只检查已保存的地址与当前配置。把每个必需的事实放进地址;例如 Feishu 记录 chat 是否 direct。一个不再被监听的目的地会被丢弃,而被停用的 Source 让消息保持等待。没有 `allowed` 时,Source 启用期间所有地址都允许。
|
||||
- **链接。** `link(address, links)` 返回 `SourceRegistry.link` 供面板展示的 URL 与标签。
|
||||
|
||||
### 6. 供研判使用的 Thread 上下文
|
||||
|
||||
```ts
|
||||
threadContext: {
|
||||
messageId: trigger => String(trigger.meta.messageId ?? ''),
|
||||
// A queue thread is flat: its root, and the message a reply answers, are on its first page, as on GitHub.
|
||||
read: async (commands, trigger, _part, cursor) => {
|
||||
const page = ThreadPageSchema.parse(JSON.parse(await commands.run('hookq', ['thread', '--conversation',
|
||||
String(trigger.meta.conversation), '--root', String(trigger.meta.root), '--json', ...(cursor ? ['--after', cursor] : [])])))
|
||||
const messages = page.items.map(item => ({ id: item.id, from: item.sender.id, at: item.at, text: item.text,
|
||||
...(item.sender.bot ? { bot: true as const } : {}), ...(item.url ? { url: item.url } : {}) }))
|
||||
return { messages, ...(page.next ? { next: page.next } : {}), body: { messages } }
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
主动研判需要 Trigger 周围的对话。`orchestrator_eval` 读一个初始窗口;`orchestrator_eval_read` 展开分页、root 或父消息。实现可选的 `threadContext` 接口来提供这些读取:
|
||||
|
||||
- `read(commands, trigger, part, cursor, config)` 返回一个 `ContextPart`。它的 `messages` 使用 `ContextMessage`:`{id, from, at, text}` 加上适用的 `bot`、`deleted`、`reply_to`、`url`、`path` 与 `line` 字段。最后一页省略 `next`。`body` 是展示给研判 Session 的内容,这里是同样的消息。可选的 `status` 可描述特殊结果,如 Feishu 的 `chat_neighbours`。`part` 是 `thread`、`root` 或 `parent`。
|
||||
- `messageId(trigger)` 在这些消息中找到 Trigger 自己的那条。
|
||||
- `images(text)` 与 `image(…)` 供消息带图片的 kind 使用,如 Feishu;示例略去了它们。
|
||||
|
||||
没有 `threadContext` 的 kind 不给研判提供任何 Thread 上下文;Trigger 自己的文本仍可读。
|
||||
|
||||
若消息会回复更早的消息,就加 `repliesTo(trigger)`。它返回对应的 Trigger id,让跟进通知与 worker Inputs 能在已存储时展示被回复的消息。例如,若 `hookq` 提供 `reply_to` 且翻译把它存为 `meta.replyTo`:
|
||||
|
||||
```ts
|
||||
repliesTo: trigger => typeof trigger.meta.replyTo === 'string' ? key('webhook', trigger.meta.replyTo) : undefined,
|
||||
```
|
||||
|
||||
### 7. 谁可以提需求:Source 的 Policy 部分
|
||||
|
||||
```ts
|
||||
policy: {
|
||||
schema: z.object({ allowed: z.array(z.object({ id: z.string().min(1), name: z.string().trim().max(100).optional() }).strict())
|
||||
.default([]) }).strict(),
|
||||
fields: [{ key: 'allowed', type: 'rows', label: { zh: '能发起工作的发送者', en: 'Senders who may originate work' }, columns: [
|
||||
{ key: 'id', type: 'text', label: { zh: '发送者', en: 'Sender' } },
|
||||
{ key: 'name', type: 'text', label: { zh: '名字', en: 'Name' }, optional: true }] }],
|
||||
explicit: { zh: '名单里的人在消息里 @ 智能体。', en: 'Someone on the list mentions the agent in a message.' },
|
||||
rows: stored => ConfigSchema.parse(stored).conversations.map(conversation => {
|
||||
const all = conversation.listen === 'all'
|
||||
return { id: conversation.id, label: conversation.id, note: all ? { zh: '全部消息', en: 'All' } : { zh: '仅 @', en: '@ only' },
|
||||
cells: { listedUnaddressed: all, unlistedAddressed: true, unlistedUnaddressed: all } }
|
||||
}),
|
||||
},
|
||||
```
|
||||
|
||||
- `schema` 校验 Source Policy,包括 `allowed` 发送者 ids。id 比较不区分大小写。准入表单独校验、跨 kinds 共享。这部分 Policy 存于 `policy.sources.webhook`,并参与 Policy 版本。
|
||||
- `fields` 生成 **Eval → On each Source** 下的表单,从 `allowed` 开始。`explicit` 解释如何 at 智能体。可选的 `unnamed: {person, bot}` 为 Feishu 的 `ou_…` 这类不透明 ids 提供显示标签;示例省略它,因为队列 id 是可读的名字。
|
||||
- `rows(config)` 列出准入范围及其可用单元格。每行的 id 必须匹配 `scope` 返回的 `row`。未被 at 的消息的单元格,只在 Source observe 那些消息的地方可用。没有 `rows` 的 kind 使用默认单元格。
|
||||
- 可选的 `bots` 列出有 inferred 候选资格的 bots,使用与 `allowed` 相同的条目形状。没有它,任何 bot 都不被准入。
|
||||
- 可选的 `sync: {label, ready(part), run(host, part, signal)}` 在启动与 **Sync now** 时刷新成员名单。Feishu 用 chat 成员资格。失败出现在 `sync:<id>` 下,一分钟后重试,倍增到一小时直到成功。
|
||||
|
||||
### 8. 给 Executor 的变量
|
||||
|
||||
```ts
|
||||
environment: {
|
||||
variables: [{ key: 'DSH_SUPERAGENT_WEBHOOK_CONVERSATION', description: {
|
||||
zh: '接下工单的 webhook 对话;工单不是从这个来源来的时为空',
|
||||
en: 'The webhook conversation that started the ticket; empty when the ticket did not start on this Source' } }],
|
||||
values: ({ trigger }) => ({
|
||||
DSH_SUPERAGENT_WEBHOOK_CONVERSATION: trigger?.source === 'webhook' ? String(trigger.meta.conversation ?? '') : '' }),
|
||||
},
|
||||
```
|
||||
|
||||
Execution 设置即使在 Source 停用时,也会在 prepare command 旁展示声明的 `variables`。启用期间,`values` 用 deployment、配置与发起的 Trigger 提供 Ticket 专属的值。
|
||||
|
||||
prepare command 读取环境变量;worker 在其指令中读取这些值。使用 `DSH_SUPERAGENT_<KIND>_…` 前缀,对来自其他 Source 的 Tickets 返回空值,且永不包含凭据。
|
||||
|
||||
### 9. 插件条目及其循环
|
||||
|
||||
```ts
|
||||
export const name = 'dsh-superagent-webhook'
|
||||
export const inject = ['orchestrator']
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.effect(() => ctx.orchestrator.sources.register(webhookKind), 'webhook: source kind')
|
||||
ctx.effect(() => {
|
||||
const source = sourceContext(ctx.orchestrator, webhookKind)
|
||||
const controller = new AbortController()
|
||||
// DSH ends its process on an unhandled rejection, so a failure that escapes the loop becomes the Source's health.
|
||||
const running = watch(source, controller.signal)
|
||||
.catch((error: unknown) => { source.report('webhook', { state: 'error', detail: String(error) }) })
|
||||
return async () => { controller.abort(); await running }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
插件注入 `orchestrator`。它的第一个 effect 注册 kind,并在销毁时注销。第二个用 `SourceContext` 运行轮询循环。销毁会 abort 循环并等待其完成,包括应用停止或重启期间。
|
||||
|
||||
```ts
|
||||
async function watch(ctx: SourceContext, signal: AbortSignal): Promise<void> {
|
||||
await ctx.ready
|
||||
// Setup restarts dsh-superagent once it resolves the accounts; until then the Source reads nothing.
|
||||
if (!ctx.configured()) { ctx.report('webhook', health('paused', 'setupPending')); return }
|
||||
let failures = 0
|
||||
while (!signal.aborted) {
|
||||
const config = ctx.config<WebhookConfig>('webhook')
|
||||
if (!config) ctx.report('webhook', health('paused', 'sourceOff'))
|
||||
else {
|
||||
const at = new Date().toISOString()
|
||||
try {
|
||||
const { read, unreadable } = await pollQueue(ctx, signal)
|
||||
failures = 0
|
||||
ctx.report('webhook', { state: 'ready', detail: `${read} messages, ${unreadable} unreadable`, at })
|
||||
} catch (error) {
|
||||
failures += 1
|
||||
// One network failure is retried on the next round; three in a row need a person.
|
||||
if (!signal.aborted && failures >= 3) {
|
||||
ctx.report('webhook', health('error', 'pollFailures', { count: String(failures), error: String(error) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
await delay((config?.pollSeconds ?? 30) * 1000, undefined, { signal }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
/** Read the queue after the cursor, take in each message, then move the cursor past them. */
|
||||
export async function pollQueue(ctx: SourceContext, signal: AbortSignal): Promise<{ read: number; unreadable: number }> {
|
||||
const after = ctx.state.get(KV.webhook.cursor) ?? 0
|
||||
const output = await ctx.commands.withSignal(signal).run('hookq', ['pull', '--after', String(after), '--json'])
|
||||
const page = PageSchema.parse(JSON.parse(output))
|
||||
let unreadable = 0
|
||||
for (const item of page.items) {
|
||||
if (signal.aborted) throw new Error('The webhook Source stopped during the read')
|
||||
const message = MessageSchema.safeParse(item)
|
||||
if (!message.success) { unreadable++; continue }
|
||||
const trigger = webhookTrigger(message.data, page.me)
|
||||
if (!trigger) continue
|
||||
await ctx.receive(trigger)
|
||||
const conversation = { label: { zh: '对话', en: 'Conversation' }, value: message.data.conversation }
|
||||
for (const ref of trigger.threads) ctx.threads.describe(threadKey(ref), { facts: { conversation } })
|
||||
}
|
||||
ctx.state.set(KV.webhook.cursor, page.next)
|
||||
return { read: page.items.length, unreadable }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- **Setup 与开关。** 等待 `ctx.ready`,在 `ctx.configured()` 为 false 时什么都不读。每一轮都读 `ctx.config(kind)`:Source 停用时它是 undefined。
|
||||
- **游标。** 用 `KV`(`src/kv.ts`)里有类型的条目把读取位置存进 `ctx.state`,例如 `webhook: {cursor: record<number>('webhook-cursor')}`。`tests/store.spec.ts` 要求 key 字面量只出现在那个 registry 里。外部包可以用导出的 `record` 与 `family` 定义 keys。把 record 设为 `undefined` 会移除它;`ctx.atomic` 把写入归组进一个事务。
|
||||
|
||||
只有在每个被覆盖的 Trigger 都已存储之后才推进游标。被打断的一轮保持游标不变;稳定的 ids 让重放安全。对时间戳游标,`src/sources/poll.ts` 的 `cursorAt(started)` 会减去五秒,以容纳远端的迟到写入。
|
||||
- **Thread 元数据。** 收到一个 Trigger 后,调用 `ctx.threads.describe(key, {label, url, facts})` 填好它的展示信息。`ctx.threads.patch(key, meta)` 委托给 `threads.patchMeta`,设置 Source 拥有的键,或在值为 `undefined` 时移除它们。两者都会忽略不存在的 Threads,包括 filtered Triggers 的。
|
||||
- **逐条错误。** 计数畸形 item 并继续,而不是让整批失败。`src/infra/commands.ts` 里的 `validItems(item, list, tally)` 支持这种模式。对独立轮询的 Threads,用 `src/sources/poll.ts` 的 `pollThreads`;它把每次失败记为 `pollError`。它的 `polled` 谓词保留 live 订阅,以及较晚的 Trigger 与 Ticket 结束之后两周的窗口。
|
||||
- **健康。** 只通过 `ctx.report` 写声明过的条目。`health(state, code, values)` 提供固定消息,如 `setupPending`、`sourceOff` 与 `pollFailures`。上报 `error` 前先数连续失败次数。
|
||||
- **Abort。** 把 signal 传给 `ctx.commands.withSignal(signal)` 与 `delay`,并在条目之间检查它,这样停止或重启能立刻结束循环。
|
||||
|
||||
本示例不发送 Source 发起的通知。需要它们的 Source 调用 `ctx.notify`;Outbox 像投递其他回复一样投递它们。
|
||||
|
||||
它也不把 Threads 绑定到 Ticket 完成。只在 Source 的 Thread 生命周期决定工作是否完成时才用 `ctx.tickets.bindThread` 与 `threadClosed`,如 GitHub pull requests 那样。Ticket 何时结束仍由 Engine 决定。
|
||||
|
||||
### 10. 注册插件
|
||||
|
||||
在 `tsdown.config.ts` 里把这个模块构建为它自己的 entry:
|
||||
|
||||
```ts
|
||||
entry: { index: 'src/index.ts', 'github-watch': 'src/sources/github.ts', 'feishu-watch': 'src/sources/feishu.ts',
|
||||
'webhook-watch': 'src/sources/webhook.ts' },
|
||||
```
|
||||
|
||||
然后在 `package.json` 里导出构建产物:往 `exports` 加 `"./webhook-watch": "./lib/webhook-watch.js"`;再把该插件加进 bundle 的 `cordis.patch.yml` 的 `insert` 列表,处于禁用状态,就像 Feishu:
|
||||
|
||||
```yaml
|
||||
- id: dsh-superagent-webhook
|
||||
name: dsh-superagent/webhook-watch
|
||||
disabled: true
|
||||
```
|
||||
|
||||
在 profile 的 `cordis.patch.yml` 里用 `id: dsh-superagent-webhook` 与 `disabled: false` 启用该插件。构建并重启。新 Source 会出现在 Sources 设置和 **Eval → On each Source** 下。
|
||||
|
||||
把它的模块与 spec 加进 [007 架构](007-架构.md) 的 module 表和 test 表。`tests/docs.spec.ts` 检查两个索引是否完整。
|
||||
|
||||
### 11. 测试
|
||||
|
||||
仿照 `tests/sources.spec.ts` 的模式创建 `tests/webhook.spec.ts`:一个临时目录里的真实 Store、带 `HealthBoard` 的 `SourceRegistry`、用替身端口的 Engine,以及来自 `sourceContextOf` 的 `SourceContext`。
|
||||
|
||||
通过 spy `Commands.run` 来 mock CLI 调用。也要 spy `withSignal`,因为它通常返回一个新的 `Commands` 实例。
|
||||
|
||||
```ts
|
||||
function host(overrides: Partial<SourceContextDeps> = {}) {
|
||||
const store = new Store(join(mkdtempSync(join(tmpdir(), 'webhook-')), 'db.sqlite'))
|
||||
store.saveSettings(SettingsSchema.parse({
|
||||
sources: { webhook: { enabled: true, config: { conversations: [{ id: 'ops', listen: 'mentions' }] } } },
|
||||
policy: { sources: { webhook: { allowed: [{ id: 'alice' }] } } },
|
||||
}))
|
||||
// The registry claims the kind's health entries on the board, where the test reads them.
|
||||
const board = new HealthBoard()
|
||||
const sources = new SourceRegistry(() => store.settings(), board)
|
||||
sources.register(webhookKind)
|
||||
const engine = new Engine({ repo: store, scope: sources, policy: () => settingsPolicy(store.settings(), sources),
|
||||
assessor: { request: vi.fn() }, executor: { start: vi.fn(), deliver: vi.fn(), stop: vi.fn() },
|
||||
outbox: { send: vi.fn(), status: () => undefined, withdraw: vi.fn() } })
|
||||
const commands = new Commands(() => testDeployment(), {}, new AbortController().signal)
|
||||
const receive = vi.fn((trigger: Trigger) => { const result = engine.receive(trigger); engine.evaluateDue(); return result })
|
||||
// The Source's own notices go to the outbox, which keeps their receipts and sends nothing here.
|
||||
const deps: SourceContextDeps = { store, engine, outbox: { send: message => { store.receipts.add(message) } }, commands, sources,
|
||||
deployment: testDeployment(), ready: Promise.resolve(), configured: () => true, receive, ...overrides }
|
||||
return { ...deps, store, engine, commands, receive, health: board.all, ctx: sourceContextOf(deps, webhookKind) }
|
||||
}
|
||||
/** The queue as `hookq` answers it. */
|
||||
function queue(h: ReturnType<typeof host>, answer: () => unknown) {
|
||||
vi.spyOn(h.commands, 'withSignal').mockReturnValue(h.commands)
|
||||
return vi.spyOn(h.commands, 'run').mockImplementation(async () => JSON.stringify(answer()))
|
||||
}
|
||||
|
||||
it('takes in what the queue holds after the cursor, once, and moves the cursor after it', async () => {
|
||||
const h = host()
|
||||
queue(h, () => ({ me: 'agent', items: [message('m1'), { id: 'broken' }, message('m2', { conversation: 'elsewhere' })], next: 3 }))
|
||||
expect(await pollQueue(h.ctx, new AbortController().signal)).toEqual({ read: 3, unreadable: 1 })
|
||||
expect(await h.receive.mock.results[0]!.value).toBe('stored')
|
||||
expect(await h.receive.mock.results[1]!.value).toBe('filtered')
|
||||
expect(h.store.get(KV.webhook.cursor)).toBe(3)
|
||||
// An allowed sender who mentions the agent makes an explicit request.
|
||||
expect(h.engine.candidate(key('webhook', 'm1'))?.strength).toBe('explicit')
|
||||
await pollQueue(h.ctx, new AbortController().signal)
|
||||
expect(await h.receive.mock.results[2]!.value).toBe('duplicate')
|
||||
h.store.close()
|
||||
})
|
||||
```
|
||||
|
||||
`message(id, fields)` 构造一个队列 item。至少覆盖:
|
||||
|
||||
- 翻译:稳定的 id、Thread、actor、`human` 与 `addressed`,且智能体自己的消息不产生 Trigger;
|
||||
- 一轮:Triggers 只存储一次、一个范围之外的被 `filtered`、游标在其之后移动、名单中者的 mention 被研判为 `explicit`;
|
||||
- 被打断的一轮,其游标保持不变;
|
||||
- `send`:`sent`、仅对 410 返回 `gone`、任何其他失败抛出,CLI 的错误用 `commandError` 构造;
|
||||
- Thread 上下文:一页的消息与下一页的游标,如同 `tests/registry.spec.ts` 通过 `SourceRegistry.threadContext` 读取一个 kind 的 Threads;
|
||||
- 插件:`apply` 注册 kind,并在 setup 等待时上报 `setupPending`;它的 disposers 注销 kind;逃出循环的失败变成该 Source 的健康状态,而不是 unhandled rejection;
|
||||
- Source 的 Policy 部分及其准入行,如同 `tests/registry.spec.ts` 与 `tests/policy.spec.ts` 检查 Feishu Source 的那样。
|
||||
|
||||
### 检查清单
|
||||
|
||||
1. 映射 Triggers、Threads、actors、Channel 地址与接收范围。
|
||||
2. 写出 kind:`schema`、`fields`、`health`,若 setup 推导了任何值再加 `facts`。
|
||||
3. 把事件翻译成带稳定 id 的 Triggers,并丢掉智能体自己的。
|
||||
4. 在 `scope` 里决定 Source 接收什么,只依据 `meta`。
|
||||
5. 给它一个 Channel,其 `send` 每条消息只发一次,并且只在 conversation 永远无法接收时才回答 `gone`。
|
||||
6. 若能读取 Thread,给研判提供它的 Thread 上下文。
|
||||
7. 给它一个带 `allowed` 与准入行的 Policy 部分。
|
||||
8. 注册它的执行环境变量。
|
||||
9. 写好插件条目和一个循环:等待 setup、跟随开关、在 `receive` 之后移动游标、把错误保持在逐条层面、上报健康、abort 时停止;游标名字写进 `KV`。
|
||||
10. 添加构建 entry、导出和 `cordis.patch.yml` insert,并把模块及其 spec 列进 007 的表格。
|
||||
11. 像 `tests/sources.spec.ts` 测试其他 Sources 那样测试它。
|
||||
@@ -1,5 +1,5 @@
|
||||
# Example profile configuration ($DSH_HOME/profiles/<profile>/cordis.patch.yml).
|
||||
# Replace <placeholders> before first start; see docs/006-deployment.md.
|
||||
# Replace <placeholders> before first start; see docs/006-部署.md.
|
||||
- id: webserver
|
||||
config:
|
||||
host: 127.0.0.1
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ function setupView(runtime: HttpHost) {
|
||||
accounts: resolved ? { repository: resolved.repository, agent: resolved.github.login, feishu: resolved.feishu ? { appId: resolved.feishu.appId, brand: resolved.feishu.brand, botId: resolved.feishu.botId, name: resolved.feishu.name } : null } : null,
|
||||
}
|
||||
}
|
||||
/** Build the `/orchestrator/api/state` response consumed by the management panel and `scripts/dsh-superagent status` (`docs/006-deployment.md`). */
|
||||
/** Build the `/orchestrator/api/state` response consumed by the management panel and `scripts/dsh-superagent status` (`docs/006-部署.md`). */
|
||||
export function stateView(runtime: HttpHost) {
|
||||
const settings = runtime.store.settings()
|
||||
return { setup: setupView(runtime), load: { ...executorLoad(runtime.store, runtime.executor.activeCount()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Bilingual UI dictionaries (`[zh, en]`) and glossary groupings, following the domain terms in `docs/001-domain.md`.
|
||||
* Bilingual UI dictionaries (`[zh, en]`) and glossary groupings, following the domain terms in `docs/001-领域模型.md`.
|
||||
*/
|
||||
import { USER_ERRORS } from '../core/errors.ts'
|
||||
import { SETUP_PROBLEMS } from '../setup.ts'
|
||||
@@ -769,7 +769,7 @@ const TEXT = {
|
||||
'setup.feishuCliFailed': ['机器人的 lark-cli 不可用。', SETUP_PROBLEMS.feishuCliFailed[1]],
|
||||
'setup.feishuNoApp': ['机器人的 lark-cli 的 auth status 没有报告应用。', SETUP_PROBLEMS.feishuNoApp[1]],
|
||||
'setup.feishuBotNotReady': ['应用 {app} 的机器人身份状态是 {status};用 lark-cli 配置应用密钥。', SETUP_PROBLEMS.feishuBotNotReady[1]],
|
||||
'setup.feishuUserIdentity': ['机器人的 lark-cli 允许用户身份(strict mode 为 {mode});按 docs/006-deployment.md 给机器人一份 strict mode 为 bot 的单独配置。', SETUP_PROBLEMS.feishuUserIdentity[1]],
|
||||
'setup.feishuUserIdentity': ['机器人的 lark-cli 允许用户身份(strict mode 为 {mode});按 docs/006-部署.md 给机器人一份 strict mode 为 bot 的单独配置。', SETUP_PROBLEMS.feishuUserIdentity[1]],
|
||||
'setup.feishuNoBot': ['飞书机器人接口没有返回机器人的 open_id。', SETUP_PROBLEMS.feishuNoBot[1]],
|
||||
'setup.giteaCredentialFailed': ['git 没有读取 dsh-superagent 为 Gitea 写的凭据配置。', SETUP_PROBLEMS.giteaCredentialFailed[1]],
|
||||
'setup.giteaUrlUnset': ['Gitea 来源没有设置实例地址,例如 https://gitea.example.com。', SETUP_PROBLEMS.giteaUrlUnset[1]],
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ const MOVED: Record<string, string> = {
|
||||
/** Read machine settings and reject unknown configuration keys (naming where deprecated keys moved). */
|
||||
export function machineOf(config: DeploymentConfig): Machine {
|
||||
const unknown = Object.keys(config).filter(key => !(key in (Config.dict ?? {})))
|
||||
if (unknown.length) throw new Error(`dsh-superagent configuration has keys it does not use: ${unknown.map(key => MOVED[key] ? `${key} (now ${MOVED[key]})` : key).join(', ')}; docs/006-deployment.md lists the keys`)
|
||||
if (unknown.length) throw new Error(`dsh-superagent configuration has keys it does not use: ${unknown.map(key => MOVED[key] ? `${key} (now ${MOVED[key]})` : key).join(', ')}; docs/006-部署.md lists the keys`)
|
||||
const { stateDir, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs } = config
|
||||
return { stateDir, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs }
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export const SETUP_PROBLEMS = {
|
||||
feishuCliFailed: ['feishu', 'The bot lark-cli does not work.'],
|
||||
feishuNoApp: ['feishu', 'auth status of the bot lark-cli did not report the app.'],
|
||||
feishuBotNotReady: ['feishu', 'The bot identity of app {app} is {status}; configure the app secret with lark-cli.'],
|
||||
feishuUserIdentity: ['feishu', 'The bot lark-cli allows user identities (strict mode {mode}); give the bot its own lark-cli configuration in strict mode bot, as docs/006-deployment.md describes.'],
|
||||
feishuUserIdentity: ['feishu', 'The bot lark-cli allows user identities (strict mode {mode}); give the bot its own lark-cli configuration in strict mode bot, as docs/006-部署.md describes.'],
|
||||
feishuNoBot: ['feishu', 'The Feishu bot API did not return the bot\'s open_id.'],
|
||||
} as const satisfies Record<string, readonly [SetupPlace, string]>
|
||||
export type SetupCode = keyof typeof SETUP_PROBLEMS
|
||||
|
||||
+10
-7
@@ -1,10 +1,11 @@
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { dirname, join, relative, resolve } from 'node:path'
|
||||
import * as path from 'node:path'
|
||||
import { basename, dirname, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const NUMBERED = /^(\d{3})-[a-z0-9-]+\.md$/
|
||||
const NUMBERED = /^(\d{3})-[\p{L}0-9-]+\.md$/u
|
||||
const numbered = readdirSync(join(ROOT, 'docs')).filter(name => NUMBERED.test(name)).sort()
|
||||
const entries = ['README.md', 'SETUP.md']
|
||||
const documents = [...entries, ...readdirSync(join(ROOT, 'docs')).filter(name => name.endsWith('.md')).map(name => `docs/${name}`)]
|
||||
@@ -13,13 +14,15 @@ function links(file: string): string[] {
|
||||
return [...readFileSync(file, 'utf8').matchAll(/\]\(([^)\s]+)\)/g)].map(match => match[1]!.split('#')[0]!).filter(target => target && !/^[a-z]+:/.test(target))
|
||||
}
|
||||
const files = (directory: string): string[] => readdirSync(join(ROOT, directory), { recursive: true, withFileTypes: true })
|
||||
.filter(entry => entry.isFile()).map(entry => relative(ROOT, join(entry.parentPath, entry.name)))
|
||||
.filter(entry => entry.isFile()).map(entry => relative(ROOT, join(entry.parentPath, entry.name))
|
||||
.split(path.sep)
|
||||
.join('/'))
|
||||
/**
|
||||
* Extract repo-relative file paths from the first column of a table under `## <heading>` in `docs/007-architecture.md`.
|
||||
* Extract repo-relative file paths from the first column of a table under `## <heading>` in `docs/007-架构.md`.
|
||||
* Bare filenames inherit the directory of the preceding path in the same cell.
|
||||
*/
|
||||
function listed(heading: string): string[] {
|
||||
const lines = read('docs/007-architecture.md').split('\n')
|
||||
const lines = read('docs/007-架构.md').split('\n')
|
||||
const start = lines.indexOf(`## ${heading}`)
|
||||
const end = lines.findIndex((line, index) => index > start && line.startsWith('## '))
|
||||
// The first two rows are the header and its separator.
|
||||
@@ -27,7 +30,7 @@ function listed(heading: string): string[] {
|
||||
return rows.flatMap(row => {
|
||||
let directory = ''
|
||||
return [...row.split('|')[1]!.matchAll(/`([^`]+)`/g)].map(([, name]) => {
|
||||
if (!name!.includes('/')) return join(directory, name!)
|
||||
if (!name!.includes('/')) return join(directory, name!).split(path.sep).join('/')
|
||||
directory = dirname(name!)
|
||||
return name!
|
||||
})
|
||||
@@ -42,7 +45,7 @@ describe('documentation', () => {
|
||||
const violations = numbered.flatMap(name => links(join(ROOT, 'docs', name)).flatMap(target => {
|
||||
const linked = resolve(ROOT, 'docs', target)
|
||||
if (!linked.endsWith('.md')) return []
|
||||
const number = NUMBERED.exec(linked.slice(linked.lastIndexOf('/') + 1))?.[1]
|
||||
const number = NUMBERED.exec(basename(linked))?.[1]
|
||||
return dirname(linked) === join(ROOT, 'docs') && number !== undefined && number < name.slice(0, 3) ? [] : [`${name} → ${target}`]
|
||||
}))
|
||||
expect(violations).toEqual([])
|
||||
|
||||
Reference in New Issue
Block a user