ARTICLE · 1334247
dsh-superagent 文档|007 架构

本文是该实现的开发者指南。它把 001 领域模型 到 006 部署 中阐述的概念映射到具体的类、接口和文件。请先阅读 runtime 与 Engine 部分,然后把模块表和测试表当作代码索引来使用。
核心类与胶水层
核心类通过小型接口持有领域概念与外部边界。胶水层(Glue)则把工具参数、HTTP 请求、来源(Source)载荷以及 DeepSeek Harness 事件翻译成对这些类的调用,再把结果转换成卡片、视图和消息。
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 并释放锁。
启动会注册管理路由并运行 Setup。Setup 解析完成后,它安装 Session 与执行器钩子、确保主 Session 存在、启动策略(Policy)同步、恢复被中断的派工(Assignment),并启动 pass 与副作用(effect)。ready 标志启动完成。
关闭会取消各轮(turn)、等待 pass/effect/agent 收尾、恢复进程环境、关闭 Store 并释放锁。来源插件通过窄接口 SourceHost,以 orchestrator(编排器)服务的形式被注入 Runtime。
src/app/setup.ts | ||
src/app/passes.ts | Engine.evaluateDue;来源的触发(Trigger)通过它送达 Engine | |
src/app/outbox.ts | Outbox 端口,也是回执(receipt)的唯一写入者:send 在一个事务内保存消息的回执并排队它的 reply effect,而该 effect 的处理函数通过 SourceRegistry.send 把消息发出去;当某消息所属的来源处于关闭状态时它会等待,当会话(conversation)已不存在或不再被监听时它会被丢弃 | |
src/app/notices.ts | eventNotice 处理领域通知;reportRelay 立即转发提问,把普通进展留在其工单(Ticket)上,并转发其他报告;followupRelay 指明拥有某条人类追问(follow-up)的那些工单。在某一轮进行期间到达的转发消息,会在该轮的下一步通过 DeepSeek Harness 收件箱(inbox)一并投递 |
Engine
Engine(src/core/engine.ts)是触发、线程(Thread)、决定(Decision)、工单和派工的唯一写入者。它从 src/core/ports.ts 接收这些端口:Repositories、一个获取当前策略的 getter、SourceScope、Assessor、Executor 和 Outbox。可选依赖是 prepareTicket、now 和 emit。
Engine 的所有方法都是同步的。每条命令在一个事务内修改它的聚合(aggregate)。evaluateDue 对每一个它所修改的评估(Eval)或工单使用单独的事务。端口在事务内部把外部工作排队;适配器(adapter)在提交(commit)之后执行它。无效请求会抛出 UserError。
Engine 只暴露一个入口点,并委派给五个内部组成部分。它掌管每条命令的事务。跨多个组成部分的命令,例如 attach、evaluateDue 和 policyChanged,会按顺序调用各个组成部分。
每个组成部分都继承 EnginePart(src/core/part.ts),后者提供配置项和共享的读取能力。组成部分之间的依赖是单向流动的:
Intake | ||
Evals | primary);从候选(candidate)投递时起算的截止时间(deadline)及其因暂停(pause)而产生的顺延;过期评估的重新排队;候选、重放(replay)与交还(hand-back);因策略变更而重新排队;以及每个评估最终得出的决定:accept、stop、ignore 或 unavailable | |
Tickets | followsDone);附加的线程与仅为上下文而跟随的线程;输入(Input)与备注(note);resume 以及 resume 所拒绝的消息;retry、restart、stop 和 end;它所丢弃的订阅;以及向受某次策略变更影响的工单发出通知 | |
Assignments | ||
Replies | Outbox 进行的渠道(Channel)写入:确认(acknowledgement)、答复(answer)、结果(result)和提醒(attention)消息,每一种都按其对应的允许规则发出 |
命令使用祈使语气的名字,例如 stopTicket;适配器上报的事实使用过去式的名字,例如 assignmentStarted。查询 candidate 使用名词。
调用方只会拿到 Engine 中属于其角色的 Pick:EngineForSources、EngineForMainSession、EngineForPeople、EngineForSettings 或 EngineForExecutor。
receive(trigger) | stored、duplicate 或 filtered;把它扇出给订阅了其线程的那些工单,其中一条人类消息会立即 resume 一个已结束的工单(除非处理处于暂停),并在其主线程上为一个评估排队——除非它是一条已经抵达某个工单的人类消息,那样的消息仍归该工单所有 | |
bindThread(ticketId, thread, own) | own 为真;一个已关闭的线程若被再次绑定,就重新变为打开状态。结束工单的是这些线程的关闭,而不是一份 ready 的报告 | |
threadClosed(thread, outcome, at) | followsDone)——即它自己打开的线程没有一个还开着;若它一个都没有打开,则它所跟随的每一个线程都已关闭。它按这些关闭中最近一次的结局而结束为 merged 或 closed——除非出现下列情形之一:关闭之后恢复的工作尚未上报 ready;或其执行器通过确认或答复,接手了某条人类追问,且这条追问是在该工单最近一份非 progress 的报告之后、或在其最后一轮结束之后才被它决定的。若此时来了一份 dropped 的报告,则该报告会像那次关闭一样让工单结束 | |
candidate(triggerId) | ||
candidateDelivered(evalId) | ||
decide(triggerId, outcome) | ignoredFromCard,再记录所给出的理由。只要 followupOwners 仍拥有某个属于人类的追问,就只允许 stop | |
attach(ticketId, triggerId, note?) | ||
requestStatus(ticketId) | ||
note(ticketId, text) | ||
reply(request) | acknowledgement、answer、result 或 attention | |
stopTicket(ticketId, reason?) | ||
retryTicket(ticketId) | failed 或 blocked 的报告之后正等待人类,或其输入投递失败(canRetry) | |
reevaluate(triggerId) | ||
unsubscribe(thread) | ||
dismiss(decisionId) | unavailable 决定所呈现的问题,这一点由该决定记录 | |
assignmentStarted(ticketId, assignmentId) | ||
inputsDelivered(ticketId, assignmentId, inputIds) | ||
inputsRead(ticketId, inputIds) | user/message 会话事件),其模型由此得到它们;一个曾领取它们、但在此之前被取消或失败的步骤不算数。当一份报告或一轮结束随之而来时,每个输入即计为已读取,restart 之后也如此;其工单的结束也不会使其脱手。一个在"读取记录"机制出现之前就已投递的输入,在一份报告或一轮结束跟随其投递而来时,即计为已读取 | |
turnEnded(ticketId, assignmentId) | inputsRead) | |
deliveryFailed(ticketId, assignmentId, error) | ||
report(ticketId, report) | dropped 会结束该工单;ready 会完成一个不跟随任何线程的工单,并按所跟随线程各自的结局,结束一个对其所跟随的线程都已处理完毕的工单 | |
markUnrelated(ticketId, thread, reason) | ||
followForContext(ticketId, thread) | ||
handback(ticketId, triggerId, reason) | ||
assignmentInterrupted(ticketId, assignmentId) | resume),最年长的工单优先,并再次为一个仍在等待的 launch 排队 | |
evaluateDue() | unavailable;并把暂停期间被人类消息抵达的那些已结束工单(若其授权仍然成立)恢复。一个候选尚未抵达主 Session 的评估不会过期。处理处于暂停期间没有任何评估会过期,而一旦恢复,每个截止时间都按暂停的时长相应顺延 | |
policyChanged(from, to, effects) | src/policy.ts 的 policyEffects:它当初被 accept 所依据的某条规则发生了变化或不再适用、其请求者可能不再可以开始工作,或不再接受主动工作),并在新版本下重启正在进行的那些评估 |
Engine 把批处理、授权、扇出、resume 以及提醒生命周期等规则都作为私有实现。若干共享谓词(predicate)使这些规则保持一致:
waitsForPerson( src/core/model.ts):最近的派工上报了blocked或failed,且自那以后没有任何人类消息抵达。当此条件为真时,自动生成的输入与失败的 launch 都保持搁置。followupOwners:仍然对其自身线程上收到的人类追问负责的工单。一个工单可以处于活动状态,或在没有人类 stop 的情况下已结束并等待 resume。把该消息交还会终结这种所有权。 settled:某个决定已解决该请求;stop、unavailable、routed 以及 refused-stop 这几类决定不计入。 ticketStage( src/core/stage.ts):实现 003 执行 中的阶段表。
当一个工单结束时,未读取的追问会退回评估——除非另有工单仍拥有它们,或另有处置已使其落定。已被确认、已答复以及已交还的消息被排除在外。对于没有确认处置的旧记录,outbox 的回执提供了这一事实。一份最终报告也涵盖了派工开始时正处于等待的消息,包括那个 resume 了它的那条消息。使某个工单被 stop 的那条消息不会被自动重新评估。
在一次变更之后,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 转换成通知。一条投递给工单的人类 trigger.received 会变成一次追问转发。当某个工单以 merged 或 closed 结束时,pullEndNotices 会用一个稳定的 id 发送一条聊天通知。其余事件目前没有消费者。
模型(Model)
src/core/model.ts 持有领域类型,src/core/stage.ts 持有工单阶段。一个决定把原因记录为 reason——一个 Reason 列表(src/core/reasons.ts):每一项是一个 code 连同其 values,并且对于批次中的某个触发,还有其 triggerId。
核心的这些 code,例如 timeout、policyChanged、routed 和 startedTicket,都属于 REASONS,并带有供 agent 通过 reasonText 阅读的英文文案;准入闸门或某个来源 scope 会给出自己的 code,例如 not_a_member;而 said 则承载评估器或某个人写下的文字。面板按 reason.<code> 为每个 code 生成文案,被来源过滤掉的触发也以同样方式记录该来源的 code。
一个工单对每一项事实只保存一次。只要它没有 ended,它就处于打开状态;该记录保存它于何时、被谁、因何原因、以何种结局结束;它最后一次"说过"的内容是其最近一份派工报告(latestReport);其执行器 Session 以它的名字命名(见 src/executor/dsh.ts 的 workerSessionId)。
它的字段都是有类型的:origin,即赋予其评估批次以强度的那条可准入触发,以及批次本身(triggerId、triggerIds);execution,执行器所选择的(name、directory 和 assignment);delivery,最近一次失败的输入投递(error、at),直到某次投递成功、一个新的派工启动或该工单结束为止;以及 follows,即某个来源让它去跟随的那些线程,每一项形如 {thread, own, closed?}:当该工单打开了此线程时 own 为真,一旦此线程关闭,closed 就带上 at 与 outcome。只有工单会写入 follows;GitHub 来源通过 pullsOf 和 pullOf 读取它自己的 pull request(src/sources/github-pull.ts)。
一个承载触发的输入会记录它是否由人所写,waitsForPerson 读取这一点。一个线程还在 context 中列出那些已订阅、但仅为上下文而跟随它的工单,并在 expired 中统计其排队触发连续过期的评估个数;一个评估会记录它的 primary 触发及其候选何时被 delivered。
一个触发的处置记录它曾抵达的工单(通过扇出或附加)、某人的重新排队、每一个把它交还的工单(连同理由),以及其执行器曾确认或答复过它的每一个工单。
一个授权的 basis(MandateBasis)记录准入闸门是以何种方式准入其触发的。重新校验(Revalidation)会读取承载它的 source、scope 与 row、被列出的 sender、匹配到的 rules,以及 via:
via | |
|---|---|
addressed | |
followup | |
admission | observation 标记一个推断出的候选 |
bot |
触发和线程的 meta 属于该来源。核心不会去解释它们。
Policy
src/policy.ts 定义设置的模式(schema)、授权与评估规则。settingsPolicy(settings, scope) 每次调用都返回一个全新的、无状态的 Policy:
version、 paused和evalTimeoutMs描述其当前状态。gate(trigger, thread, tickets)返回准入强度与依据。 revalidate(mandate)检查一个已存在的授权是否仍然成立。 admitsInferred(rules)只有在 auto 模式、且存在已启用的匹配规则时,才允许主动接受。
assessmentOutcome(assessment, settings, strength) 把模型的评估结果转换成 Engine 的结果。它要求对每一条已启用的规则都作出判断、落定的方向、一条匹配的规则,以及意图的证据。在 auto 模式之外,一个推断出的 accept 会变成一条"本会接受(would accept)"的记录。
policyEffects(tickets, old, now, scope) 判定一次变更影响哪些处于打开状态的工单、以及以何种方式影响。Engine.policyChanged 只通知这些工单。policyVersion 对规则、accept 模式以及各来源的策略部分(包括 bot 列表)做哈希。
Store
Store(src/app/store.ts)持有一个 SQLite 数据库。Runtime 在打开它之前先获取目录锁,从而阻止第二个进程去迁移或写入同一份状态。
它的接口按用途分组:
- 领域仓储(Domain repositories):
atomic,加上 triggers、threads、tickets和decisions,每个都带有get、save以及列表查询。带索引的工作集有threads.active()、tickets.open()、tickets.active()和read.subscribedThreads。 - 读取查询(Read queries):
read通过诸如 threadTriggers、triggerStats、search和droppedReplies这些方法,为视图、工具和来源轮询提供服务。这些方法从不写入。 - 来源元数据:
threads.describe和 threads.patchMeta更新归属于来源的线程元数据。 - 设置(Settings):
settings、 saveSettings、blockRevision和settingsHistory。设置与提示词都使用带编号的Versions(src/app/versions.ts)。 - Outbox 回执:
replyStatus和 receipts,仅由 Outbox 写入。回执操作包括get、waiting、behind、add、settle和withdraw。behind检查针对同一工单与同一会话的一条更早消息是否正在等待,且失败次数少于三次。一条被撤回的消息以channelGone或channelIgnored落定。读取查询通过failingReplies、droppedReplies和droppedSince暴露失败与被丢弃的回复。 - 带类型的键值记录:
get、 set、delete和keys,使用来自KV的条目。
afterCommit 只在事务提交之后运行某个动作,绝不在回滚之后运行。Runtime 以记录的存储形态读取它们;schema 11 迁移会在启动时转换较早的、仍受支持的记录。
Effects
Effects(src/app/effects.ts)是持久化的副作用队列。一条命令在其事务内部调用 enqueue(kind, key, payload);执行只在提交之后才开始。
每个 kind/key 对只有一行。为一个已处于等待的相同 key 再次入队,会替换其载荷并使其立即到期,而不改变队列顺序。handle(kind, run, options) 为每个 kind 注册一个处理函数。它按最旧优先处理已到期的行,直到达到并发上限。
失败会以指数退避(exponential backoff)重试。抛出 Later 会在不计为一次失败的前提下推迟工作,例如没有可用的 worker 槽位时。start 在 setup 之后开始处理,drain 在关闭期间停止处理但保留处于待处理的行,pending/idle 则暴露队列状态。
assessment | ||||
launch | ||||
delivery | ||||
relay | ||||
notice | ||||
reply |
每个 kind 都在其名下有一个健康状态条目(health entry)。当其中某个 effect 连续失败三次时它就上报一个错误,唯独 delivery 例外——它的失败改在其工单上显示。转发保持其顺序,通知也是如此;一个失败项会阻挡其同类中较晚的那些。
来源(Sources)
来源通过定义在 src/sources/kind.ts 的 SourceKind 扩展宿主。一个 kind 声明其标识(identity)、标题、描述、配置 schema 与 fields、派生的 facts,以及 health 条目。可选行为包括 scope、一个 channel(allowed、send、link)、threadContext、find、policy、environment 和 refreshLabels。
SourceRegistry(registry.ts)持有已注册的 kind 与已存储的实例,并实现 Engine 的 SourceScope。注册会在 HealthBoard 上认领(claim)健康条目;重复认领以及被保留的 sync: 名称都会被拒绝。一个 SourceContext 只通过其注册所认领的条目进行写入,因此注销之后的上报不会有任何效果。
注册表的配置方法包括 kind、instances、instance、validate、validatePolicy、policyPart、channelAllowed、declarations、environments、find 和 scope。find 会询问每一个支持搜索的 kind。
通用代码通过注册表访问来源的行为。send(message, commands) 调用该 kind 的渠道;若该 kind 未安装,则返回 sent、gone 或 waiting。link 解析其渠道链接。threadContext 把一个 kind 的读取器与其配置配对,或在该 kind 没有读取器时返回空。无效的存储配置会使该实例被禁用。
插件会收到 SourceHost:注册表以及 context(kind)。kind 本身使用一个更窄的 SourceContext,由 src/app/source-context.ts 中的 sourceContextOf 构建。
该 context 提供部署与命令、setup 就绪情况(ready、configured、problem)、配置、receive、atomic 以及带类型的 state。它还暴露线程读取与元数据写入、工单读取与绑定/关闭命令、触发读取、通过 Outbox 的 notify,以及针对已声明健康条目的 report。
DshExecutor
DshExecutor(src/executor/dsh.ts)为每个工单提供一个 Session 和一个目录。start 与 deliver 分别把 launch 和 delivery effect 入队。stop 通过 Store.afterCommit 取消该轮,因此一次被回滚的结束不会停止 worker。
prepare(ticket, outcome) 提供 Engine 的 prepareTicket 钩子:短名称、目录以及 ticketExecution 派工文本。workerSessionId(ticket.id) 推导出 Session id。
WorkerSlots 在一次 launch、或一次会唤醒空闲 worker 的 delivery 之前预留容量。一条人类消息可以使用超出 maxWorkers 的一个槽位,且唤醒优先于 launch。activeCount 为面板提供负载信息。
临时的提供方与网络失败会获得三次重试。launch 使用 effect 退避;失败的轮会分别在第 15、30 和 60 秒后收到续跑消息。inputMessage 依据当前工单状态渲染每一个输入,mandateInstructions 为推断出的工作添加匹配规则的限制。
install 订阅 Session 事件;dispose 取消订阅。一轮的结束通常上报 waiting,除非是在就绪之后,或是在需要人类操作之时。错误上报为 failed。在启动时,resume 通过 assignmentInterrupted 上报被中断的派工,并重新排队那些不等待人类的待处理 launch,最年长的工单优先。
MasterAssessor
MasterAssessor(src/app/assessment.ts)让主 Session 能够评估主动(proactive)候选。request 把一个评估 effect 入队;release 在其评估结束时丢弃缓存的状态。
它的 context、read 和 decide 方法服务于 orchestrator_eval、orchestrator_eval_read 和 orchestrator_eval_decide。它们通过 SourceRegistry.threadContext 提供有界的线程上下文,外加仓库上下文和决定提交。一个没有上下文读取器的 kind 只提供触发与批次。
assessmentOutcome 为 Engine.decide 转换模型的回答。Evals 会针对当前策略再次校验推断出的接受。
Settings
Settings(src/app/settings.ts)读取和写入带修订版本(revision)的设置块。current 与 view 读取它们;save(block, value, revision, origin) 校验一次保存;pause(paused, origin) 修改独立的 paused 设置。
每一次保存都声明其来源(origin):panel、tool 或 sync。只有 panel 才能更改执行环境,或更改 SETUP_FIELDS(src/deployment.ts)中列出的字段。
一次策略版本变更会计算 policyEffects、记录哪些工单被通知,并调用 Engine.policyChanged。syncable 和 sync(id) 刷新某个来源的策略部分。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 继承已保存的覆盖,丢弃那些已存在于已保存数据中的被移除部分,并拒绝空白文本或其他未知键。这使得保存工具能够对其读取到的配置做往返(round-trip)。environmentSection 和 policySection 只使用各自的键、替换一次模板取值。没有覆盖的配置使用默认值。
Sessions
Sessions(src/app/sessions.ts)为两种角色管理 DeepSeek Harness Session。ensure 用该角色的模型创建一个 Session。install 跟随 agent 事件,并为受管理的 Session 注册提示词、技能和工具。
prompt 对每个请求 id 只投递一次。relay 和 notify 把主 Session 消息入队,由 relay 和 notice effect 处理。src/app/notices.ts 定义其内容;delivery 检查某条消息是否已抵达。
每一次模型请求都会把该 Session 绑定到当前的提示词修订版本。它所使用的修订版本被持久存储:promptVersion 标识当前生效的那个,mayRead 限制 worker 可以查看哪些版本。注册情况按每个 agent 一张 map 来跟踪。
status 和 promptVersion 服务于提示词页面。保存都经 Prompts.save 进行,并通知主 Session。modelRetry 暴露模型额度重试状态。
胶水层(Glue)
src/app/runtime.ts | |
src/app/notices.ts | |
src/app/tools/main.tssrc/app/tools/executor.ts | |
src/app/http.tssrc/app/views/、src/app/api.ts | api.ts 为面板给每条路由加上类型 |
src/app/resources.tssrc/app/links.ts、src/app/cards.ts | |
src/sources/github*.tssrc/sources/feishu*.ts、src/sources/poll.ts | |
src/deployment.tssrc/setup.ts、src/infra/ |
依赖规则
- 显式、窄依赖。
每个类都收到一个点明其所用内容的接口,多数是其他类的 Pick类型:面向核心类的ExecutorHost、AssessorHost、SessionsHost、SettingsHost、SetupHost、PassesHost与OutboxHost,面向胶水层的HttpHost、MainToolsHost、ExecutorToolsHost、ResourceHost、LinkHost与SourceContextDeps,而 Engine 则以其调用者角色的Pick呈现。Runtime 只在这种接口之后传递自身。一个来源插件注入orchestrator服务,其类型把它命名为SourceHost:注册表以及context(kind);其 kind 的代码只能看到自己的SourceContext。 - 谁能 import 谁。
src/core除了 Node 的哈希之外不 import 自身以外的任何东西,也不做任何 I/O。 src/sources不从src/appimport 任何东西。面板只从宿主侧 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的过滤器列表。 - 每种状态只有一个写入者。
每一类状态都只有一个写入者,其余各方要么读取它,要么向该写入者发出请求。
label、url、facts 及其自有键) | SourceContext |
notify 都经它的 send 送出,其 reply effect 标记它们 | |
HealthBoard(src/infra/health.ts)上认领它的那个部分,它会拒绝第二次认领:runtime 是 Runtime(Passes 通过它上报),evals 是 Runtime 在每次 pass 之后,models 是 Sessions,每个 effect kind 由 Effects,worker 是 DshExecutor,某个来源 kind 的条目由 SourceRegistry 为该 kind 的来源,sync:<id> 是 Settings |
来源只能通过 SourceContext.tickets.bindThread 和 threadClosed 来修改工单,二者都会调用 Engine。 - 请求与事实。 一个"请求某事"的方法以祈使动词加其宾语命名,例如 stopTicket 或 bindThread,且可能被拒绝;一个"记录适配器做了什么"的方法以过去式命名,例如 assignmentStarted 或 deliveryFailed。每个宿主都以它所需的角色类型收到 Engine。 - 带类型的字段,以及来源数据归来源。 代码读取工单与授权的带类型字段。来源不在工单上保存任何东西:某个工单跟随其哪些线程记录在工单的 follows 里,而它关于某个触发或线程所保存的内容则放在它们的 meta 里。 - 固定的拒绝。 一个人或一个 agent 能够引发的拒绝,是来自 src/core/errors.ts 中 USER_ERRORS 的 UserError(code, values)。管理 API 对 missing code 用 404 应答,对其余的用 400,面板按其语言为每个 code 生成文案。setup 问题则用 SetupError code(src/setup.ts)和固定的健康文案 HealthCode(src/sources/host.ts),并以同样方式生成文案。
来源专属依赖
一些通用模块仍直接引用 GitHub 或飞书:
Outbox 施加飞书聊天的字数上限。 src/app/links.ts解析飞书消息链接与 repliedTo。它与 Runtime 都用feishuSettings读取设置。src/app/resources.ts读取 feishu/与github/原件;src/app/cards.ts在工单卡片中使用pullOf。Runtime 为 merged 或 closed 的工单发送 pullEndNotices。执行器工具用 readPull和claimPull绑定所报告的 pull request,使用 Runtime 暴露的 GitHub 上下文。MasterAssessor 用 readRepositoryPath读取仓库文件。Sessions 与 Prompts 只为 GitHub 和飞书注册内置技能。迁移会转换它们的 schema-10 记录。 Setup 只探测 GitHub 和飞书的账号字段( SETUP_FIELDS)。只有那些来源会通过SourceContext.problem收到账号 setup 问题。
模块
src/index.ts | |
src/deployment.ts | Config 与 Machine;resolveDeployment 与 resolveFeishuBot 像执行器 shell 那样运行每条 CLI,从来源设置中解析出仓库与账号;SETUP_FIELDS 列出 setup 读取的来源字段。代码不点名任何个人、账号或组织 |
src/setup.ts | SetupError,带一个 code、修复它的地方,以及日志、主 Session 与面板共用的英文文案 |
src/policy.ts | |
src/admission.ts | |
src/limits.ts | |
src/kv.ts | KVkv 记录族,连同其键、其值的类型及其所有者,以及 Store 与 SourceContext.state 所采用的 Key 和 Family 类型 |
src/navigation.ts | |
src/core/model.ts | MandateBasis,并带有 threadKey、ticketOutcome、reached、followupOwners、settled、waitsForPerson、liveTicket、runningAssignment、latestAssignment 和 latestReport |
src/core/ports.ts | Repositories、Policy、SourceScope、Assessor、Executor、Outbox,以及领域事件 |
src/core/engine.ts | EngineForSources、EngineForMainSession、EngineForPeople、EngineForSettings 和 EngineForExecutor |
src/core/part.ts | EnginePartEngineOptions |
src/core/intake.ts | Intake |
src/core/evals.ts | Evals |
src/core/tickets.ts | Tickets |
src/core/assignments.ts | Assignments |
src/core/replies.ts | Replies |
src/core/stage.ts | ticketStagecanRetry:003 执行 中的工单阶段 |
src/core/errors.ts | UserErrorUSER_ERRORS |
src/core/reasons.ts | ReasonREASONS 与 reasonText:以 code 表明核心为何记录一个决定、或将一个触发搁置 |
src/core/ids.ts | key |
src/core/memory.ts | MemoryRepositories |
src/core/index.ts | |
src/app/runtime.ts | |
src/app/setup.ts | setupChanged——判断一次保存是否更改了 setup 读取的某个字段 |
src/app/passes.ts | |
src/app/outbox.ts | Outbox 端口、回执,以及发送这些回执的 reply effect |
src/app/notices.ts | eventNotice、reportRelay、resultRelay 和 followupRelay |
src/app/store.ts | Receipt——outbox 中的一条消息 |
src/app/store-read.ts | storeReads |
src/app/migration.ts | |
src/app/effects.ts | |
src/app/versions.ts | Versions |
src/app/cache.ts | TextCache |
src/app/settings.ts | |
src/app/settings-history.ts | policyHistory |
src/app/prompts.ts | builtInParts(一个团队可覆盖的内置部分),以及 promptParts(某角色的提示词各节的顺序) |
src/app/context.ts | prompts/ 读取,以及 Sessions 读取的转录条目 |
src/app/skill-files.ts | promptFile——把一个文件中一句一行的内容合并成段落,以及随附的技能 |
src/app/sessions.ts | |
src/app/delivery.ts | AgentDelivery |
src/app/retry-health.ts | RetryHealthmodels 健康条目使用 |
src/app/role-models.ts | |
src/app/assessment.ts | orchestrator_eval 作答的窗口 |
src/app/execution-environment.ts | src/app/prompts.ts 填入其模板 |
src/app/source-context.ts | sourceContextOfSourceContext |
src/app/links.ts | sourceLinkcandidateCard |
src/app/cards.ts | TICKET_STATES,以及 textPage |
src/app/resources.ts | RESOURCESreadResource:每种角色下 orchestrator_read 所读取的内容 |
src/app/tools/tool.ts | ToolreadTool、两种角色各自的 orchestrator_read,以及 AcknowledgementSchema |
src/app/tools/main.ts | |
src/app/tools/executor.ts | |
src/app/http.ts | /orchestrator/api |
src/app/api.ts | Api |
src/app/views/text.ts | preview,以及不含 HTML 的 markdown;plain 与 readable |
src/app/views/records.ts | ownersOf——把追问的所有者作为一次请求读取 |
src/app/views/tickets.ts | |
src/app/views/threads.ts | |
src/app/views/sources.ts | |
src/app/views/settings.ts | |
src/app/views/overview.ts | |
src/executor/dsh.ts | WorkerSlots、ticketExecution、inputMessage 与 mandateInstructions |
src/executor/background.ts | ctx.jobs)及其子代理(ctx.subagents):runningJobs、untilQuiet、killJobs、collectJobs、stopSubagents、stopBackground、until,以及 cancelled——其子代理被停止打断(从而取消了其轮次)的那些 worker |
src/infra/commands.ts | Commandsrun;api 与 pages,限定在所配置的仓库内并经过校验;作为 bot 的 lark;git。CliError 携带某命令输出所报告的 code,例如 GitHub 的 HTTP 状态;一个不打印任何内容的命令会以它的结束方式失败,而绝不会带上其参数而失败;validItems 会跳过一个列表中的畸形条目并计数 |
src/infra/environment.ts | runAsWorker,以及对环境和某个工单目录的检查 |
src/infra/paths.ts | |
src/infra/text.ts | fill 与 sameName |
src/infra/health.ts | HealthBoard |
src/sources/kind.ts | SourceKind |
src/sources/registry.ts | SourceRegistry |
src/sources/host.ts | SourceHostSourceContext、Health、health,以及固定的健康文案 |
src/sources/poll.ts | POLL_HORIZON_MS、polled、cursorAt,以及 pollThreads——它把一次失败以 pollError 保留在其线程上 |
src/sources/github.ts | ./github-watch 导出:一轮对通知的轮询、处于打开状态的工单的 pull request 及其 CI,以及被订阅的线程 |
src/sources/github-trigger.ts | |
src/sources/github-pull.ts | claimPull、observePull)、它上报给 Engine 的关闭,以及 pullEndNotices——某个以 merged 或 closed 结束的工单的聊天通知 |
src/sources/github-channel.ts | sent 或 gone |
src/sources/github-read.ts | github/ 资源,以及 githubThreadContext:按页读取某个 Issue 或 pull request 的对话 |
src/sources/feishu.ts | ./feishu-watch 导出 |
src/sources/feishu-trigger.ts | |
src/sources/feishu-loops.ts | |
src/sources/feishu-channel.ts | sent 或 gone |
src/sources/feishu-read.ts | feishu/feishuThreadContext——把它们连同其图片提供给评估 |
src/sources/admission-context.ts | |
src/sources/feishu-image.ts | |
src/client/index.tsx | |
src/client/app.tsx | |
src/client/provider.tsx | restart——在 dsh-superagent 重启期间快速读取状态 |
src/client/api.ts | src/app/api.ts 加上类型,以及评估状态的决定过滤器 |
src/client/route.ts | parseRoute 所读取的带类型路由,以及记录了最近一次打开的设置分组的导航记忆 |
src/client/overview.tsx | |
src/client/source.tsxtrigger.tsx、eval.tsx、execution.tsx | |
src/client/thread.tsxticket.tsx | |
src/client/settings.tsx | |
src/client/settings-kit.tsx | useBlockDraft、各字段(其中含某个来源 kind 的字段),以及保存的历史 |
src/client/prompts.tsx | |
src/client/prompt-config.ts | part/<key>,连同 withOverride)与各部分标题,以及完整提示词的逐行 diff |
src/client/deployment.tsx | |
src/client/draft.tsx | draftReducer、草稿的状态及其与某个较新已保存修订版本的合并、saveBarText、保存栏那一行的文案,以及保存栏本身 |
src/client/controls.tsx | @deepseek-ai/dsh-client-ui-primitives;它们不读取任何 context |
src/client/widgets.tsx | |
src/client/rows.tsx | |
src/client/markdown.ts | |
src/client/text.ts | |
src/client/marks.ts | |
src/client/locales.ts | |
src/client/client.csscss.d.ts |
src/sources/github.ts 与 src/sources/feishu.ts 同时也是这两个来源插件的入口,由 tsdown.config.ts 构建,并在 package.json 中以 ./github-watch 和 ./feishu-watch 导出。该 bundle 的 cordis.patch.yml 插入这三个插件,其中飞书处于禁用状态。
测试
tests/core-engine.spec.ts | |
tests/stage.spec.ts | |
tests/policy.spec.ts | |
tests/registry.spec.ts | SourceRegistry |
tests/sources.spec.ts | SourceContext 上的飞书与 GitHub 来源:转换、轮询与读取位置、pull request 规则、健康状态 |
tests/commands.spec.ts | |
tests/feishu-read.spec.tstests/github-read.spec.ts | |
tests/admission-context.spec.tstests/feishu-image.spec.ts | |
tests/assessment.spec.ts | |
tests/background.spec.ts | |
tests/bundle-patch.spec.ts | |
tests/effects.spec.ts | |
tests/passes.spec.ts | |
tests/outbox.spec.ts | |
tests/cache.spec.ts | TextCache |
tests/queue.spec.ts | |
tests/followups.spec.ts | |
tests/health.spec.ts | |
tests/delivery.spec.ts | AgentDelivery |
tests/retry-health.spec.tstests/role-models.spec.ts | |
tests/runtime.spec.ts | |
tests/tools.spec.ts | orchestrator_read 读取的内容,以及其他主 Session 工具的形状与检查 |
tests/runtime-concurrency.spec.ts | |
tests/http.spec.ts | |
tests/settings.spec.ts | |
tests/views.spec.ts | |
tests/timeline.spec.ts | |
tests/migration.spec.ts | |
tests/store.spec.ts | KV 的每一个键都只在 src/kv.ts 与迁移中拼写;设置历史与查询 |
tests/prompts.spec.ts | |
tests/cards.spec.ts | |
tests/model-surface.spec.ts | |
tests/deployment.spec.tstests/environment.spec.ts、tests/index.spec.ts | |
tests/service-script.spec.ts | ps、tmux 与 dsh 测试 scripts/dsh-superagent |
tests/client.spec.tsxtests/navigation.spec.ts、tests/controls.spec.tsx | |
tests/panel-overview.spec.tsxpanel-source.spec.tsx、panel-trigger.spec.tsx、panel-eval.spec.tsx、panel-execution.spec.tsx | |
tests/panel-settings.spec.tsxpanel-prompts.spec.tsx、panel-deployment.spec.tsx、panel-panel.spec.tsx | |
tests/draft.spec.tstests/prompt-config.spec.ts、tests/markdown.spec.ts、tests/marks.spec.ts | |
tests/locales.spec.tstests/styles.spec.ts | |
tests/class-docs.spec.ts | src |
tests/docs.spec.ts |
tests/fixtures/ 存放多个测试共享的内容:host.ts,一个模拟的 DeepSeek Harness 宿主;panel-server.ts 与 panel.tsx,伪造的管理 API,以及围绕一个页面的面板;deployment.ts,一个已解析的部署;git.ts,测试所启动的每一个 git 的环境,其中不点名任何仓库;以及 async.ts。
持久化
一个 SQLite 文件,<stateDir>/orchestrator.sqlite,只有 Store 会打开它。
trigger | at 采用 Date#toISOString 所给出的那一种形态,在 Engine 接收它时设定,从而使时间可按文本比较 | |
disposition | filtered 连同该来源的原因 code、其评估与决定、它经由扇出或附加所抵达的工单、它是否被重新排队、把它交还的那些工单(连同各自理由),以及其执行器曾确认或答复过的那些工单 | |
trigger_thread | <at>|<id> 游标续读,共享同一时间的触发不会被跳过 | |
thread | meta;生成列 active(在触发排队或评估运行时置位),以及 subscribed(当某个工单订阅它时为其来源) | meta |
ticket | open(在它尚未结束时置位),以及 active(在输入尚未投递、或最近一个派工处于 pending 或 running 时置位) | |
decision | ||
effect | ||
outbox | status——pending、sent 或 dropped;保存它是为了一个 id 绝不会被排队两次;对于一条被丢弃的消息,还会记录其丢弃时间与原因 code,体现在生成列 dropped 上。一条正在等待的消息会有一个 reply effect,其中保存其尝试次数 | |
kv |
生成列由其所在行的 JSON 计算得出,在其集合之外为空,且每个生成列都针对其集合建有索引,因此一次 pass 只读取处于活动状态的工作。一个早于某列而存在的数据库,会在 Store 打开它时获得该列。
KV(src/kv.ts)定义每一项键值记录的所有者、键与类型。所有读取者与写入者都使用这些条目;只有迁移代码会点名更早的键。
settings-revisionsettings-version:<n>、settings-block-revision:<block>;快照包含来源、策略版本与受影响的工单 | |
prompt-revisionprompt-version:<n> | |
master-id | |
session-prompts:<session id> | |
github-cursor:<number>、PR 的 CI 指纹,以及已见过的飞书话题消息;键以 kind 名开头,或以 thread- 加上一个线程 key 开头 | |
source-labels:<source id> |
驳回存储在其决定上。输入文本在读取时由工单渲染得出,因此二者都不需要单独的键值记录。
读取缓存使用有界的内存 TextCache 实例,并在重启时丢失。某个 Session 在逐页翻阅一条来源原件记录或提示词预览时,必须在重启后从 offset 0 重新读取。在接收之后解析出的飞书链接以同样方式缓存;基于链接的搜索只有在当前进程存续期内读取过那条链接之后才能找到它。
目前尚没有任何东西会删除旧记录,因此以下各项会无限增长:trigger、disposition 和 decision 的行;已结束的工单及其线程;outbox 的回执;以及在 kv 中,每一次已保存的设置修订版本与提示词版本、每个 Session 工作于其下的提示词修订版本,以及各来源按线程、按 Issue 或 pull request 记录的那些记录。
SCHEMA_VERSION 只增不减。一个版本更新的数据库会拒绝启动;src/app/migration.ts 会在启动时对预发布的 schema 10 数据库升级一次,并拒绝更旧的数据库。
面向模型的接口
主 Session 工具
主 Session 共有 14 个应用工具:
orchestrator_readorchestrator_route | |
orchestrator_ticket_retryorchestrator_ticket_request_status、orchestrator_ticket_note | |
orchestrator_trigger_requeue | |
orchestrator_evalorchestrator_eval_read、orchestrator_eval_decide | |
orchestrator_replyorchestrator_thread_unsubscribe | |
orchestrator_settings_saveorchestrator_policy_sync、orchestrator_prompts_save |
orchestrator_route 按顺序接受 1–20 个决定。每一次 accept、attach、stop 或 ignore 都作为一条独立的 Engine 命令运行。它也允许直接从某个推断候选的卡片忽略它。orchestrator_ticket_note 把一项事实作为上下文而非权威(authority)发送出去。
设置保存会选择 policy、eval、executor、pause 或 source;最后一种还要点名其来源 id。一次带 preview: true 的提示词保存会合成文本而不保存。
主 Session 还拥有 DeepSeek Harness 的 skill、bash、read、read_image、job_output、job_list 和 job_kill。避免读取代码是一条提示词规则,而非工具层面的限制。
预设与等待上限
新建的主 Session 使用 dsh-superagent-router(ROUTER_PRESET)。shell 命令与任务等待在 5 秒后返回。如果在首轮之前 Web UI 选定了另一个预设,Sessions 会恢复为本预设;对 worker 则恢复为 WORKER_PRESET。
src/app/sessions.ts 中的 noWait 守卫会拒绝这类主 Session shell 命令:在命令位置调用 sleep 的、shell timeoutMs 超过 30 秒的,以及 job_output 等待超过 5 秒的。报告与任务结果会自动抵达,因此等待只会延迟即将到来的候选。
运行于 standard 预设上的既有主 Session,同样需要为前台命令与任务等待设置显式超时;否则该预设的默认值会分别把某个步骤留住 60 秒和 30 秒。宿主通过 presetForSession 确定当前预设,退化到 Session 投影(projection)或头部(header),若读不到该值则假定其为 standard。
执行器工具
每个执行器有六个应用工具:
orchestrator_read | |
orchestrator_ack | |
orchestrator_report | |
orchestrator_thread_unrelated | |
orchestrator_thread_follow | |
orchestrator_trigger_handback |
若另一个工单仍拥有该消息,或本工单从未拥有过它,则交还返回 queued: false。例如,一条 bot 消息独立地进入了评估。
worker 预设定义在 cordis.patch.yml 中,并通过 WORKER_PRESET 选定。src/app/sessions.ts 中的 WORK_TOOLS 保留可用的文件、搜索、shell、任务、web、技能、待办和子代理工具。worker 不会收到用于向人类提问或呈报、更改配置文件、在 worker 槽位之外调度工作、启动其他 Session 或向其发消息、或编辑外部文档的工具。在工单结束之后就拒绝工具调用。
结果形态与评估上下文
路由结果使用 {decision?, reason, ticket?}。orchestrator_route 返回 {results},保持条目顺序,并为每一项加上其 trigger_id 与 kind。无效或被拒绝的项返回 refused 和一个理由,而不中断整批。
设置与提示词保存返回被保存的配置。策略同步返回该来源、允许列表的大小,以及 added 和 removed 名称。
orchestrator_eval 返回的是上下文而非指令。评估指令位于 runtime/admission(prompts/admission.md),规则位于 runtime/policy。其上下文包括:
触发文本,最多 4,000 字符,连同图片 id 和批次中的其他触发。 最多八个 related_tickets,处于打开状态的排在前面,以及tickets_total。该评估的 policy_version。一个线程窗口:前五条消息,以及触发两侧各五条。消息使用 {id, from, at, text},通常限 400 字符,两侧那两条则为 1,200 字符。被截断的消息带有chars;图片引用使用images。其他被载入的消息以 50 字符的 {id, from, preview}存根形式呈现。orchestrator_eval_read按 id 将其展开。
工单卡片使用 src/app/cards.ts 中封闭的 TICKET_STATES 集合:一个处于打开状态的阶段,例如 person/review 或 working/queued,或一个已结束的结局。无论经 accept、评估还是 orchestrator_ack 提交,确认都限于 100 词。
读取资源
src/app/resources.ts 中的 RESOURCES 定义各资源,并生成该工具的 resource 描述。
ticketstriggers、threads、decisions | {items, next_offset};活动列表把最新的排在前面 |
decision/<id> | |
thread/<key> | |
trigger/<id> | |
ticket/<id> | |
/assignments/inputs、/assignment、/result、/report/<assignment id>、/transcript、/message/<n>、/input/<id> | |
settingssettings/<block> | |
promptsprompts/<revision>、prompts/current、prompts/defaults、prompt-preview/<id> | |
modelsmodels/<provider> | |
feishu/<trigger id>/…github/… |
工单的线程会针对仅为上下文的跟随标记为 context,针对来源绑定标记为 bound。派工的页面把最新的排在前面,并包含报告的预览。用 next_offset 续读列表。/input/<id> 接受一个输入的名称或 id。settings 完整包含较小的块,并对策略与来源块给出摘要。
来源资源包括消息邻域、github/pulls/<n>/thread/<comment id> 和 github/commits/<sha>/checks。
一个原件的消息以一种紧凑形态读取,形如 {id, from, at, text, reply_to?, path?, line?, url?},而非按 GitHub 或飞书记录它们的原样。
某个执行器 Session 只能读取其自身工单、其线程、曾抵达它的或其所持请求包含的触发、它工作于其下的提示词版本及原件;其页面会省略它无法读取之物的 id、决定以及其他工单。
某页面所显示的每一个 id 都能通过上述之一读取,于是 Session 能从一个对象走到下一个对象;没有任何东西以之为据来读取的哈希,例如评估的 id 或策略版本,都不会出现在页面上。
一个文本页面默认为 4,000 个 UTF-16 码元,最多 8,000 个;一个列表默认为 20 项,最多 50 项。
较早版本中的 event/、task/ 和 observation/ 这些名称仍可解析。
投递给 Session 的消息
以下消息会被引导进主 Session 的一轮:
related_tickets、tickets_total、delivered_to、由 Issue/PR 链接引用的工单、handed_back 理由,以及下一步动作 | |
follow_up | replies_to、接收方工单,外加提醒:由执行器来决定它,而主 Session 只能停止工作 |
worker_report | chars 和一个读取指针 |
worker_attention |
其他通知则在不唤醒主 Session 的前提下被注入:ticket_ended、settings_saved、提示词保存、触发重新排队、被丢弃的订阅以及上下文跟随。一条设置通知会点名该块、发生变更的路径,以及 read: settings/<block>。对于主 Session 自己执行的保存、重新排队、退订或附加,它不会收到多余的通知。
worker 以内联方式接收派工和输入。第一条消息包含标题、最多 1,500 字符的派工文本,以及来源触发卡片。输入携带一张最多 600 字符、并带 replies_to 的触发卡片,或纯输入文本。读取指针仅针对被截断的内容出现。
仅面板可执行的操作
没有哪个工具可用于:驳回一个 unavailable 的决定、重新运行 setup、刷新来源显示名称,或把一个由执行器拥有的人类追问发往评估。最后这一操作被 orchestrator_trigger_requeue 刻意拒绝。其余面板操作都有等价的工具。
大多数内置指令位于 prompts/*.md 中,一句一行,由 promptFile 拼接。来源技能位于 skills/dsh-superagent-*/SKILL.md。工具描述以及诸如 restart 恢复备注之类的固定通知仍留在代码中。评估指令仅位于 prompts/admission.md。
tests/model-surface.spec.ts 会检查这些接口面是否发生漂移。004 提示词 解释了团队可编辑的部分。
配置重载
除 stateDir 之外的每一个插件配置键都是易变的。面板的 Deployment 设置与 DeepSeek Harness 的插件管理器,通过 DeepSeek Harness 的配置表单来编辑它们;宿主写入 cordis.patch.yml 并就地更新这些取值,而 dsh-superagent 在每次用到某个限额时才读取它,因此变更无需重启即可生效。
一次更改了 setup 所读取的某个来源字段的设置保存(例如某账号的 CLI),以及在 setup 处于等待期间的任何保存,都会重启 dsh-superagent。在此期间,DeepSeek Harness 的设置服务不会提供该条目,所以在其自身重启之后,dsh-superagent 会要求再次读取该条目,一个已打开的表单便重新可编辑。
用真实数据复核改动
把 DSH_HOME 指向一个临时目录,并在其中新建一个 profile。从一个备份复制数据库,禁用每一个来源,暂停处理,并移除主 Session id。为 GitHub 与飞书 CLI 提供只读的替身(stand-in)。
悬而未决的问题
信任:哪些触发字段是可信的,例如该来源的 actor 与线程成员关系;哪些不可信,例如正文与引用。 部署隔离:agent 的 profile 位于其自身固定的 DeepSeek Harness 安装之上,从而在其他地方升级 DeepSeek Harness 不会破坏它。















































































