《深入浅出 DeepSeek Harness》封面
全部小册

源码版本 dsh-v0.1.7-rc.2

深入浅出 DeepSeek Harness

从一次回车到完整 Agent 平台

沿 DeepSeek Harness 官方仓库的固定 commit,从你在终端敲下 dsh 或在浏览器按下回车讲起,追踪消息怎样穿过 Inbox 进入 Agent Loop,事件日志怎样同时支撑模型上下文、持久化与界面投影,并继续拆解工具治理、Goal、多 Agent、Cordis、MCP、SDK、安全与验证体系。

English reading guide

目录

56 章已发布

查阅索引

--session-id 只接已经存在的会话
Web 隔着网关等请求,headless 跑完就退
!!js 条件行:disabled: !!js "!ctx.get('profileContext')"——CLI 插件只在 profile 启动时加载
cordis.yml 组合:空根与五层 patch 栈
"最近的目录赢":同名技能按 rank 升序进层,先到先得,项目层压住全局层——项目目录覆盖全局是有意的,不是 bug
技能文件从哪几个目录冒出来,又怎样进模型视野
/api 的两道关:Host 围栏与浏览器 cookie
Web 隔着网关等请求,headless 跑完就退
$events 流转发 Host Cordis 事件,$events/result 回传客户端瀑布结果
浏览器到运行时,中间没有大表
19 个 gateway/* 基础设施错误码与业务错误身份分离,lookup 拒绝保留 session/not-found 等专属码
浏览器到运行时,中间没有大表
23 种确定性故障 + random:connection_reset/stream_disconnect/stall/malformed_json/rate_limit/context_overflow… 每种一种失败姿势
Mock Snapshot 测试:启动真实子进程,录制一次,重放一万次
按 id 找 Agent:活的直接用,冷的从日志恢复,同一个 id 只恢复一次
Web 隔着网关等请求,headless 跑完就退
版本号乐观并发:每次物化写入换新 randomUUID,ifVersion 对不上就是 version-conflict,不静默覆盖
Telemetry 是观测侧车,不是第二个会话引擎
包名单可过滤:package_allowlist/package_blocklist 正则在注册后筛掉不需要的包
Invariant 目录文档化:CI 怎样逼每个包自带守卫
被认领的消息不回收件箱,除非监听器自己放回去
被取走的消息不一定进历史
崩溃恢复:SessionLogScanner 把最后一条没写完的半截记录判为 torn tail,truncate 回最后一条完整事件
写一半崩了,已写的数据也不坏
必需清单决定启动成败
dsh 启动器管到哪里为止
边界即生命周期:进程归 ctx.subprocess 管,executor 重载不杀正在跑的子进程
能力接入的接缝:Service、Provider 与 Consumer
标记在追加时核对;恢复、分叉和离线读取按同一套规则重建清单
日志只增不减,历史有进有出
表变了,下一步写一条新的 request/header,名字有增减再写一条 developer/message;路由按 toolUpdate 决定模型收到什么
工具表是按 Agent 算出来的
表面不存消息内容,只存一串节点 seq(nodes: SessionSeq[]),消息按需从事件账本现取
模型看到的历史,是现折出来的
表面投影折出模型可见消息;SessionProjection 折出跨事件的业务状态(goal、todo、上下文快照)
事件是流水,状态是折叠出来的投影
表面折叠:analyzeEventLog 用 foldSurface 把消息合并折叠后,事件表面分 current/shadowed/log-only 三种归宿
轨迹查询与重放验证:readSession 为什么要把整个日志再跑一遍
捕获即脱敏:session-telemetry/record 瀑布由部署方挂载规则,服务自身零规则,innermost next() 原样放行
Telemetry 是观测侧车,不是第二个会话引擎
不是每个 commit 都要更新:只有改变核心叙事(owner/边界/恢复路径)才必须更新
从一个 commit 到下一版:这本书怎样更新并收束
参数的根是开放的对象,没声明的键不算错;default 只是注解,缺省值由工具自己补
拦下坏参数的不止一层
参数格式由 defineTool 在定义时编进 JSON Schema 子集,子集以外的写法当场报错;发给模型的格式和检查用的格式出自同一次编译
拦下坏参数的不止一层
仓库即插件:.dsh 不只是技能目录,它把仓库声明成一个可被自动装载的能力包
仓库里的 .dsh 目录,怎样变成整个工作区的能力包
策略按调用携带:SandboxPolicy 是 per-call 的,不是 provider 上固定的
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
策略属于路由:默认 normal,五个码、每一步最多 5 次、0.5 秒起翻倍、封顶 10 秒、±10% 的随机数;Retry-After 不超过上限就照办,超过就不重来
每一次失败都有下文
插件的消息投影只改已有消息的内容,节点和 id 不变,读历史从此离不开这个插件
日志只增不减,历史有进有出
插件的依赖声明 inject
插件跟着它等的服务起落
查不到适配器时不带句柄往下走,请求交给 llm/stream 监听器;两道手续里别的错误直接结束这一轮
请求离开进程之前
程序里的 tools 是桥:每个可见工具一个绑定函数,调用经注册表的镜像调度器当子调用执行,结果 curate 后交回
程序在子进程里,工具在宿主上
程序跑在全新 Node 进程里:类型先剥掉,环境从空开始,沙箱和 bash 用同一个服务,堆上限由参数定
程序在子进程里,工具在宿主上
池子的节奏:开始一个就提交一次、看一眼取消,落定一个就提交、再补池;名额数的是派发出去还没落定的调用
同时在跑的调用共用着什么
传输是换行分隔的 JSON-RPC 2.0:每行一个 JSON 对象,不是 LSP 风格的 Content-Length 头帧
stdio 上,程序对程序的 JSON-RPC
创建事务:准备、组装、补存发布前的事件、发布
Agent 没有自己的 id
次数从日志里折出来:llmRetry 投影按路由和策略计数,step/start、turn/end 清零;同一条链共用 retryId
每一次失败都有下文
错误是类型化的:TransportClosedError / RequestTimeoutError / SdkProtocolError,Python 侧 Pydantic 模型校验响应
两根语言,同一条线
带来源的启动环境快照
dsh 启动器管到哪里为止
单单调版本号:SESSION_FORMAT_VERSION=4,bump 只由 writer 发出的结构变化决定,加普通事件类型不 bump
旧会话打开时,版本号说了算
单行流式翻译:createRestore 逐行 decodeRow,compact run 不展开直传,inherited cut 预声明 finish 对账
旧会话打开时,版本号说了算
倒序启动、同时撤销;同一个 effect 里一步等一步
插件跟着它等的服务起落
第一步最后的决定里没有消息,这一轮以 completed 收场,驱动不再看队列
被取走的消息不一定进历史
调用方身份门禁:tool-session-query 先解 Caller(id/header/boundary),工作区授权可见血缘投影
轨迹查询与重放验证:readSession 为什么要把整个日志再跑一遍
顶替用一条新事件换下当前历史里的一段,来源列表要盖住换下的每一条,原事件留在日志里
日志只增不减,历史有进有出
冻结物理 codec:每个已发布版本一个 codec(decodeHeader + 逐行 createDecoder),发布后永不改动
旧会话打开时,版本号说了算
断开 = 结果与清理未知:不重连、不回滚,租约心跳兜底
同一个运行时,在另一台主机上
对话块投影把内容块分类成 text/reasoning/image/tool-call,未知块降级 other
日志是事实,对话是投影
发布前失败抛固定安全事实(product/stage/category),不泄 Host 内部;发布后 result 永不 reject,扁平化为 stopReason
另一个产品里的 Agent,当你的子 Agent
发布顺序:Session 进表、Agent 进表、宣告 Session、宣告 Agent
Agent 没有自己的 id
放行的消息里可以有从没进过收件箱的快照和说明
被取走的消息不一定进历史
放行先开 step,路由定下之后才提交系统提示词和 user/message
被取走的消息不一定进历史
放行之后还有守卫,守卫只能拒绝,批下来的调用守卫照样能拦;结果策略没有要问这一项
另一头有没有人
分层环境与 bootstrap-only 名字
dsh 启动器管到哪里为止
服务离开时依赖方卸载,回来时从头重跑
插件跟着它等的服务起落
服务只沿祖先链查找
插件跟着它等的服务起落
改配置改 cordis.patch.yml,不改根 cordis.yml
cordis.yml 组合:空根与五层 patch 栈
格式检查在 defineTool 包装的 execute 开头,放行策略、审批、守卫和 tools/execute 上的包装都在它之前
拦下坏参数的不止一层
格式写不出的规矩(正整数、非空、成对的参数、没声明的键)由工具主体自己查,报普通的错误,不带码
拦下坏参数的不止一层
根上下文不做声明检查
插件跟着它等的服务起落
根上下文的 dispose 是一次清场
插件跟着它等的服务起落
更新不是批量替换 verifiedCommit:批量换哈希是制造漂移最快的方式
从一个 commit 到下一版:这本书怎样更新并收束
更新粒度:单章可更新,但全部 verifiedCommit 要一起指向新 commit
从一个 commit 到下一版:这本书怎样更新并收束
工具的失败收成出错的结果;取消时不再开始,等已开始的,提交时成功的改记 ABORTED,没开始的补一对;调度器自己出错不编结果
几个工具能不能一起跑
工具调用和结果由写入的一方配对,推导不补也不查
日志只增不减,历史有进有出
工具注册表分层:一个全局层,每个作用域一层;register 只带定义,登记落在调用它的上下文所带的作用域
工具表是按 Agent 算出来的
挂着后台任务服务时,前台调用也先登记成 job;到点还在跑,就交回已有输出和编号,进程接着跑
命令可以比它的调用活得久
观测侧车定位:session-telemetry 只拥有 CAPTURE 一侧,batching/retry/loss policy 是 reporting SDK 的地盘,本包刻意不建模
Telemetry 是观测侧车,不是第二个会话引擎
管理类请求有保留配额:heartbeat/close 各 1,process.terminate/fs.streamClose 各 128
同一个运行时,在另一台主机上
管理面是版本化 JSON-RPC:4 字节大端长度前缀帧、64 MiB 帧上限、pending 128、写尾串行化
同一个运行时,在另一台主机上
归一化即脱敏:会话 id、运行 cwd、RPC id、时间戳、goal 时钟、hook 时长全部刷成占位符,快照才能跨机跨时一致
Mock Snapshot 测试:启动真实子进程,录制一次,重放一万次
规则翻译成 WebhookSessionRequest:workspacePath/title/prompt/agentPreset/permissionPreset/model 缺省走默认模型
一条投递,一个根会话
规则注册表是 fire-and-forget:dispatch 快照投递、按 kind 分发给所有匹配规则、不等回调结算
一条投递,一个根会话
恢复和分叉之后,新的循环实例从日志里现读系统消息,resume 请求头本身不另起一组
系统提示词也是一条消息
恢复沿用 id,fork 换新 id 并记下 parentSession
Agent 没有自己的 id
会变的事实做成运行时上下文快照,跟在历史后面,变了才发
系统提示词也是一条消息
会话空白时可选/重选 preset;一旦开了第一轮(turnBoundary),select 抛 agent-preset/locked,配置冻结
一组插件声明,怎样变成一个可共享的 Agent 配置
会唤醒 Agent 的写入都经 send;splice、prepend、replace、remove 直接改队列,不唤醒,也不改排
插话等下一步,排队等下一轮
技能是带 frontmatter 的 Markdown 文件:SKILL_NAME 校验 kebab-case,正文按需加载、目录清单常驻提示
技能文件从哪几个目录冒出来,又怎样进模型视野
加载失败先撤回,再停在 FAILED
插件跟着它等的服务起落
架构已迁移:"no API Proxy service remains",一元操作落在各业务 Remote 拥主方,不再有集中的 40+ 方法大表
浏览器到运行时,中间没有大表
监听器一层套一层,默认决定在最里层,不调 next() 就截断里层
被取走的消息不一定进历史
检查点包装在顶层调用的主体开跑前刷盘,同一个会话同时来的几次刷盘在一条写链上排队
同时在跑的调用共用着什么
建会话是一条事务:校验 → preset 作用域 → 工作区创建 → Agent 创建 → attach → 权限/标题 → 首条消息
一条投递,一个根会话
脚本不进父进程:共享 PTC 执行器每次调用新开一个 Node 进程,进程隔离与取消归执行器,vm 域只做脚本沙箱
工作流编排:脚本如何编排一批 Agent
接线顺序对了还要对号:构造里再校验 ctx.ptcRuntime.language === 'typescript'
能力接入的接缝:Service、Provider 与 Consumer
进程、job、终端都只在内存里,重开会话一个也接不回
命令可以比它的调用活得久
进程归共享子进程服务:spill/grace/managed range 与本地进程同一套
另一个产品里的 Agent,当你的子 Agent
进程没等 turn/end 落盘就没了,下次加载补 interrupted;循环在进程里从不写 interrupted 和 forked
每一次失败都有下文
进程内的子 Agent 在委派时被钉成 never,问不到人;提问服务只让运行时的根 Agent 问人
另一头有没有人
进程四步 prepare→start→done→terminate;prepare 先发私有流端点,全部通道认证完成才 start
同一个运行时,在另一台主机上
精确 live sender:ctx.agents.get(sender.id)!==sender 即拒,运行后顶替的 agent 不能冒名发信
Agent 之间的消息:send_message 的直邻路由
精确键集合解码:每个形状只许恰好那几个字段,多一少一都拒绝
Goal 不是状态对象,是一串可折叠的事件
拒绝让这一轮以 blocked 收场,没有单独的拒绝事件
被取走的消息不一定进历史
拒绝语义:任何迁移阶段错误都被 throwUnsupportedRefusal 包成"X refuses this format vN"
旧会话打开时,版本号说了算
壳与 Host 之间是私有 Node IPC:boot injections、readiness、shutdown、fatal 诊断、platform-session 都走它
桌面壳里,装着整个运行时
可复用的指令排在前面,本机路径、URL、工作目录排在后面
系统提示词也是一条消息
可见表每次现算:全局层和各上级由远到近叠,近的顶替远的;过滤只管继承来的;自己那一层不过滤、同名顶替;呈现方式不是 native 时放进 run_code
工具表是按 Agent 算出来的
客户端校验三样东西才放行:helper 摘要(helperHash)、PTC 入口摘要(bootstrapHash)、握手协议版本
同一个运行时,在另一台主机上
空存储值即"处处缺席":describe/listRecords 只报键名与元信息,永不回读值
密钥只在内存里转一圈,日志里永远只看到它的"名字"
空的提示词清掉所有版本,空的系统消息占位但不发给模型
系统提示词也是一条消息
空根哲学:cordis.yml 就是 [],每次启动重写,只作 Loader 的 baseUrl 锚点
cordis.yml 组合:空根与五层 patch 栈
控制通道按长度前缀的 JSON 帧走:ready/boot 握手后,程序发 call、log、done,宿主回 reply;协议、输出、并发都有硬限额
程序在子进程里,工具在宿主上
快照隔离:eventsSnapshot 全量快照 O(1) 复用,后续 append 不污染已发出的快照
会话里没有状态,只有一本只追加的流水账
扩展字段放在模型读不到的地方,2xx 之后才确认,会话日志以至少一次的方式送达
请求离开进程之前
了结的 job 由 tool-jobs 送通知:主人忙就旁注,闲就叫醒;模型已经知道结果的、主人正被撤下的不送
命令可以比它的调用活得久
冷接管先 query.observeSession 读快照做闸门,再 agents.resume;接管前再对活日志重读一次闸门,防 overlay 半路改 preset
命令行里,问一句话就退出
冷投递命中未驻留孩子 → coldResume 重建 Activation 后再投,descriptor 供全部重建输入
Agent 之间的消息:send_message 的直邻路由
历史第一位留给提示词,顶替它的只能是正好盖住它的 system/message
系统提示词也是一条消息
连接监督器:自动重连(指数退避、最多 10 次、30s 上限),生成代次 close 超时 5s 收尾
外部进程的工具,怎样进全局工具表又不撞名
两侧都只是 thinner client:spawn 进程、读写 NDJSON、配对 req_id、分发通知,不实现任何 Agent 逻辑
两根语言,同一条线
两道防线:shell maybeTruncate 硬截断(字符级)与 spill-policy bound 软截断(token 级)相互独立
工具输出太大时,全文落盘,上下文只留一张书签
两个入口共用 dsh-base 的运行时,差在各自 bundle 叠上去的几行
Web 隔着网关等请求,headless 跑完就退
两个入口共用 Harness home 下的会话目录,在写锁上相遇
Web 隔着网关等请求,headless 跑完就退
两阶段 syncTools:先 listTools(cacheMode:refresh) 拉全量,校验无重名,再一次性旧摘新挂——避免半新半旧的工具表
外部进程的工具,怎样进全局工具表又不撞名
两类方法十二个入口:SessionQueryEngine 只把 searchSessions/searchEvents 留给后端实现,其余十个(readSession/listEvents/traceSession…)全部 concrete
轨迹查询与重放验证:readSession 为什么要把整个日志再跑一遍
两条记录通道:ledger 通道逐事件镜像 session log,ops 通道只带 agent-error 与 shutdown 两个无日志归宿的信号
Telemetry 是观测侧车,不是第二个会话引擎
两种 change shape:GoalSnapshotChangeMeta 与 GoalClearChangeMeta 墓碑,version:1
Goal 不是状态对象,是一串可折叠的事件
两种传输:stdio 起子进程(env 走 scrubbedParentEnv 擦除凭据后再叠 config.env);streamable-http 连 URL
外部进程的工具,怎样进全局工具表又不撞名
浏览器走两条路:一元 POST /api 经 Connection 进 Gateway;流式经 Upgrade 到 WebSocket mux(/api/remote.mux)
浏览器到运行时,中间没有大表
流结束后写一条 assistant/message 或 assistant/attempt,stream 字段压紧存放收下的每一片块,展开后一片不差;回答事件不带 sourceEventSeqs
为什么不边流边记
流进行中循环不为这次尝试写日志;agent/assistant-stream 的帧只在进程里,监听器改不了拼出和记下的内容
为什么不边流边记
录制即 harvesting:record 模式在 graceful shutdown flush 之后,把持久化的 session JSONL 收割下来归一化
Mock Snapshot 测试:启动真实子进程,录制一次,重放一万次
路由声明 in-history、这一步不另起一组、文字不空时,变了的提示词追加在历史末尾;否则归并回第一条
系统提示词也是一条消息
轮次靠消息归属:user/message{source.kind=goal} 推进 roundsStarted,必须严格是下一个 admitted round
Goal 不是状态对象,是一串可折叠的事件
轮次预算:maxGoalRounds 上限,resume 时耗尽即拒
Goal 的相位机、revision CAS 与轮次预算
没有后台任务服务,或者登记被拒,前台命令才到点就被执行器杀掉
命令可以比它的调用活得久
每次 bash 调用起一个新的 bash -c:工具拼请求,执行器补缺省和时限,子进程服务起进程、按进程范围停进程
命令可以比它的调用活得久
每次尝试开头先走 agent/request 和 prepareCall,拿到模型能力再写日志
请求离开进程之前
每读一次流建一个 AssistantStreamAttempt:每片块记下到达时刻、只拷一次冻结快照,同时交给压紧记录、组装器和 chunk 帧
为什么不边流边记
每个 Agent 只有一个收件箱,里面是 next-turn 与 next-step 两个有序列表
插话等下一步,排队等下一轮
每个 preset 挂载时建一个独立 scope 的常驻树(PresetMount),多个 Agent 共享同一份插件实例,不每人一份
一组插件声明,怎样变成一个可共享的 Agent 配置
每会话串行化:operationTails 按 sessionId 排队完整读改写,disposing 期间拒收新写入
Telemetry 是观测侧车,不是第二个会话引擎
每条事件是一个结构化信封:type + seq + time + data,外加可选的 ignorable 与 surface 两字段
每条事件都装在带五格字段的信封里
每一步开头认领 next-step 的全部,一轮的第一步再加 next-turn 队首一条
插话等下一步,排队等下一轮
密钥文件权限:0600(文件)/0700(目录),assertOwnerOnly 发现权限太开就拒,提示 chmod 600
密钥只在内存里转一圈,日志里永远只看到它的"名字"
命名空间是可移植契约:全局名、错误类名都要过一份跨后端的合法标识符清单,一个后端合法的名字在所有后端合法
程序在子进程里,工具在宿主上
模型写给相邻 Agent 的信两个方向都走 steer,运行时的结束通知按父 Agent 的状态选动词
插话等下一步,排队等下一轮
模型只能点名加载 isModelInvocable 的技能;斜杠手势 /skill-name 在用户消息里被识别,模型主动调用走 ctx.skills.get
技能文件从哪几个目录冒出来,又怎样进模型视野
默认 FEEDBACK_ONLY:部署默认不抓会话内容,只有显式 feedback 提交才授权回放 canonical log;fork seed 不算数
Telemetry 是观测侧车,不是第二个会话引擎
默认的取消清空队列并记为取消,keepInbox 只停这一轮
插话等下一步,排队等下一轮
目标先验检查:给消息打分前必须证明该 assistant/message 事件真实存在于日志,否则 target-not-found
Telemetry 是观测侧车,不是第二个会话引擎
能不能一起跑由工具声明:isConcurrencySafe 返回的正好是 true 才算并行;没声明、抛错、别的返回值、调用者看不见的工具都是独占
几个工具能不能一起跑
能不能一起跑只在一个调用开始之前问,开始以后不再问;注册表在一步中间变了,没开始的按新定义判断,一组可能被切短
同时在跑的调用共用着什么
能产生消息的只有五种事件,写入时必须带 surfaceOp 标记,别的事件不许带
日志只增不减,历史有进有出
能力接缝三件套:abstract Service 定义契约 → declare module 挂载 Context 类型 → static inject 声明消费依赖
能力接入的接缝:Service、Provider 与 Consumer
平台文档用持久 WebContentsView 分区,凭据走私有 Node IPC,renderer 收不到 token
桌面壳里,装着整个运行时
评分是二元的:rating 只有 positive/negative,不是 1-5 星;另挂 7 个 category 与可选 note
Telemetry 是观测侧车,不是第二个会话引擎
启动器参数与应用参数的分界
dsh 启动器管到哪里为止
签名阶段用账号级独占文件句柄串行:硬件令牌签名互斥,公共密钥检查最多四个进程、32 个一批
桌面壳里,装着整个运行时
签名资源里带一整套 dsh:独立 Node/Python/pnpm 发行,锁文件钉住归档与 wheel 哈希,runtime.json 记录载荷摘要
桌面壳里,装着整个运行时
清单上的每个节点投影成一条消息,空的系统、开发者、助手消息不产生消息
日志只增不减,历史有进有出
取消/失败后 settleRunResult:cancelled → aborted,其它 rejection → error,诊断 4 KiB 上限不拆 UTF-8
另一个产品里的 Agent,当你的子 Agent
取消不撤销已完成的远端变更:completed remote mutation is not rolled back
同一个运行时,在另一台主机上
取消落在哪里,日志就停在哪里:尝试之间取消,有 llm/retry 而没有 llm/retry-started;尝试之中取消,按第 09 章结算
每一次失败都有下文
取消之后、收尾之前到达的唤醒改排下一轮,并被记下;disposed 的取消之后只改排、不记
插话等下一步,排队等下一轮
全快照变更:goal/change 每次写"改完之后的样子",不写 delta
Goal 不是状态对象,是一串可折叠的事件
缺省走 agents.create(随机 session-id + meta.cwd);带 --session-id 走 resolveAgent 接管已持久化会话
命令行里,问一句话就退出
认领之后、写进历史之前,agent/pre-step 是这一步的最后一道关
被取走的消息不一定进历史
认证在 provider 层:GitHub 适配器用 x-hub-signature-256 验签名,body 有界 UTF-8,secret 从凭据服务解析
一条投递,一个根会话
三处独立闸门:配置层不写值、文件层权限锁死、日志层抹掉 role('secret'),任何一处漏了另两处兜
密钥只在内存里转一圈,日志里永远只看到它的"名字"
三类声明证据等级:配置声明只能证明可组合,类型声明只能证明允许传什么,只有调用点与状态写入证明实际运行
从一个 commit 到下一版:这本书怎样更新并收束
三种模式:replay(默认)/ record / refresh,由 DSH_SNAPSHOT 环境变量切换
Mock Snapshot 测试:启动真实子进程,录制一次,重放一万次
沙箱模式和审批策略是会话日志里的两个旋钮,只认最后一条,重开会话时照日志重放;预设只是两个旋钮的组合,执行时不读预设的名字
另一头有没有人
深冻结账本:每个 event 被 deepFreeze,读到的 event.data 是落盘值而非调用方可变输入
会话里没有状态,只有一本只追加的流水账
审批从两处发出:tools/pre-execute 上的放行策略答 ask,或者工具主体要升权;默认的随附组合里只有后一处
另一头有没有人
审批服务先看策略再找人:never 谁也不问、一律 rejected;没人接、应答者出错或答非所问都是 unavailable;等着时中止是 cancelled
另一头有没有人
声明可以并行的承诺:不改父 Agent 的状态,共享的东西容得下并发,记录的先后要么无所谓、要么错了会被拦下
同时在跑的调用共用着什么
省略理由有法定句式:OMITTED_COMPANION_REASON 正则要求"No … companion is published because…",不是随口一句话
Invariant 目录文档化:CI 怎样逼每个包自带守卫
失败不吞不炸兄弟:child/registry/handle 三类失败聚成一条 ACTIVATION_TEARDOWN_FAILED,多边界用 AggregateError
多 Agent 拆除:cancel 与 dispose 的两个方向
失败回滚与正常撤下走同一个只执行一次的撤销函数
Agent 没有自己的 id
失败回滚只回滚已做的:attached 之后失败 detach + dispose,回滚失败只记日志不覆盖原失败
一条投递,一个根会话
失败纪律:parallel/pipeline 普通错误归一成 per-item null,fatal WorkflowError 穿透 combinator
工作流编排:脚本如何编排一批 Agent
失败是包可归因的:InvariantError code=INVARIANT,消息格式 invariant violated by "包名": …
Invariant 目录文档化:CI 怎样逼每个包自带守卫
失败是一类结果:exception、timeout、abort、worker-exit、invalid-output、output-limit、protocol、sandbox-unavailable
程序在子进程里,工具在宿主上
失败在哪里浮出,决定留下什么:两道手续里出错,这次尝试什么也没写;组请求时出错,已经写下的留着,这次尝试不结算;适配器以下的失败收成带码的结束块,整段流写成 assistant/attempt 再问要不要重来;读流时抛出的错先结算,再原样抛出,不问要不要重来
每一次失败都有下文
时限包装不和主体赛跑:到点只发信号,主体收场之前名额一直占着
同时在跑的调用共用着什么
实时的那份可能作废,要作数的输出从日志取;计量和回放读嵌入的流
为什么不边流边记
事件分两大阵营:surface-eligible(5 种,产生模型可见消息)与 log-only(边界/追踪/元数据)
每条事件都装在带五格字段的信封里
事件先 enqueueLive 进写后批量,200ms 一个 timer flush,不是每条事件立刻 fsync
写一半崩了,已写的数据也不坏
事件沿同一条链往上送:主机层的监听器收所有 Agent 的调用,preset 里的监听器收加入这一版的 Agent 的调用
工具表是按 Agent 算出来的
适配器把提供方的流翻成七种中性的块:index 关联交错的块,block-end 带完整内容,finish 带结束原因和回放状态
为什么不边流边记
适配器以下的失败收成一个收尾块,监听器和消费方的错误照样抛
请求离开进程之前
收件箱是会话日志里 agent/inbox/spliced 事件的折叠,ReactLoopInbox 自己不存列表
插话等下一步,排队等下一轮
收敛六步:resident.delete → releaseSlot → notifySettlement → releaseOwnership → observer.settle → 有 failure 才 throw
多 Agent 拆除:cancel 与 dispose 的两个方向
收尾必经 sessions.flush 把会话日志落盘,再 summarize;appExit 由 launcher 注入,completed 退 0、其余退 1
命令行里,问一句话就退出
收尾通知有 announced 闸门:从没被投递过的孩子不欠父一个交代
Agent 之间的消息:send_message 的直邻路由
输出到上限时丢工具调用和裁回放条目是同一次取舍
为什么不边流边记
输入脚本按序执行:newSession 把服务器发的随机 id 收进 {{sessionId}} 变量,后续步骤引用它
Mock Snapshot 测试:启动真实子进程,录制一次,重放一万次
数据库自家人权限:打开索引库用 wx 模式 + 0o600,PRAGMA application_id 与 user_version 双重识别防错文件
轨迹查询与重放验证:readSession 为什么要把整个日志再跑一遍
数据面每流独立:ssh -O forward -L + TLS-PSK(PSK-AES256-GCM-SHA384),每流 randomBytes(32) 能力密钥
同一个运行时,在另一台主机上
双路径耐久性核对:live session append 后重开 handle 读回,header 三元组(id/createdAt/cwd)深等才算落盘
Telemetry 是观测侧车,不是第二个会话引擎
私有 Node 启动器环境只作用于包操作,不污染 Host PATH——PTC 与 agent shell 的环境是另一张白纸
桌面壳里,装着整个运行时
四层防线互不信任:Trust 决定问不问人,Permission 决定放行什么,Sandbox 决定文件写到哪,Network 决定联网到哪
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
四道门一次过:checkManifest/checkBuild/checkSource/checkOmissionReason,结构不全就是 violation
Invariant 目录文档化:CI 怎样逼每个包自带守卫
四个服务器到客户端通知:session.event、session.status、subagent.started、subagent.finished
stdio 上,程序对程序的 JSON-RPC
四类证据分别刷新:源码引用 / 可复现实验 / 叙事审计 / 最小更新闭包
从一个 commit 到下一版:这本书怎样更新并收束
算前台还是后台,看调用方等不等 result(),不看进程怎么起
命令可以比它的调用活得久
随附的 DeepSeek 适配器在回答结束时要求参数解析得出 JSON 对象,否则整条回答报 MALFORMED_RESPONSE,没有工具结果
拦下坏参数的不止一层
随附模板与 --from-default-profile
dsh 启动器管到哪里为止
随附组合挂着的工具里,声明了可以并行的只有读文件、读图片、联网搜索和抓取、委派子 Agent 这几样,写法都是一律 true;写、改、bash、grep、glob 都独占
几个工具能不能一起跑
锁 commit = 可复核性:每条 SourceEvidence 的 path/行号/符号在该 commit 上都能复跑
从一个 commit 到下一版:这本书怎样更新并收束
提供方进入 ACTIVE,服务才可见
插件跟着它等的服务起落
提交指针:结果先进格子,按单子顺序提交,写 tool/result、交出附带的消息、看 concludesTurn;名额在跑完时让出,不等提交
几个工具能不能一起跑
提示词里的工具、按名字查定义、调度器分类、派发前找定义,查的是同一张可见表
工具表是按 Agent 算出来的
提示词作为 system/message 写进会话,请求从日志推出,循环发出的请求不带 system 字段
系统提示词也是一条消息
提问没有策略、没有成对的审计,问题和回答就是 tool/call 和 tool/result;计划评审走的也是提问
另一头有没有人
停一条命令按进程范围停:POSIX 先 SIGTERM,宽限 3 秒后 SIGKILL;Windows 直接结束
命令可以比它的调用活得久
通知订阅两派:TS 是 subscribe() 返回异步可迭代流,Python 是 next_notification() 轮询拉取
两根语言,同一条线
同一个 id 的先后几次生命周期,按对象区分
Agent 没有自己的 id
同一个 id 同一时刻只有一次创建在路上
Web 隔着网关等请求,headless 跑完就退
投递被接纳后 webhook 所有权结束:Agent 归 ctx 生命周期,走普通会话行为
一条投递,一个根会话
投递历史保留上限:days + records 双约束
目标之下的三件套——plan、todo、schedule
投递两态:steer 插最近一步,queue 排独立新 turn;父收尾时 idle→queue、忙→steer
Agent 之间的消息:send_message 的直邻路由
投递消息带来源痕迹:kind/provider/source/deliveryId/ruleId/form notice,进消息 source
一条投递,一个根会话
投递与 disposal 共享临界区:closing 已开就等 disposal 结束再重试;不静默吞信
Agent 之间的消息:send_message 的直邻路由
推理流走 stderr(streamReasoning),最终文本走 stdout;--json 时 stdout 变成换行分隔的运行事件流
命令行里,问一句话就退出
退出都是幂等:TS disposeRuntimeProcess,Python close() 先发 shutdown 再关 stdin
两根语言,同一条线
退出前 Host 报两个事实:会打断的活动任务、已武装的定时提醒;都没有才静默退出
桌面壳里,装着整个运行时
外部 Agent 作子 Agent:provider 契约 + 五面能力旗(agentOptions/outputSchema/depthLimit/toolFilter/persona),启动前逐面校验
另一个产品里的 Agent,当你的子 Agent
外部 provider 全部声明 NO_START_CAPABILITIES(五旗全 false)、inheritsParentContext=false:子进程是新环境
另一个产品里的 Agent,当你的子 Agent
唯一读父会话的是 workspace cwd:resolveChildCwd(config cwd 优先,否则父会话 cwd,都没有即拒绝)
另一个产品里的 Agent,当你的子 Agent
未注册 provider → NO_PROVIDER;要求了不支的能力 → UNSUPPORTED_CAPABILITY,都在 start 前响亮失败
另一个产品里的 Agent,当你的子 Agent
未注册方法回 -32601,handler 异常回 -32603;客户端用 req_id 配对 pending 请求
stdio 上,程序对程序的 JSON-RPC
文件监听器读到 observed 里的路径就跳过:宿主自己写的,不触发重载,避免自激环
仓库里的 .dsh 目录,怎样变成整个工作区的能力包
文件命名世代:session.jsonl=v0,session.vN.jsonl=v1..v4,正则只认 canonical 名
旧会话打开时,版本号说了算
无法强制即 SANDBOX_UNAVAILABLE:没有后端能执行时 fail-closed,绝不裸奔
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
无密钥录制测试:dsh-session-snapshot 服务于 pnpm run test:snapshot,不改一行产品代码、不需要任何 API key
Mock Snapshot 测试:启动真实子进程,录制一次,重放一万次
五层 patch 栈:bundle layers → profile cordis.patch.yml → $DSH_HOME/cordis.patch.yml → --patch overlays → telemetryPatch
cordis.yml 组合:空根与五层 patch 栈
五维度比较:package graph / public symbols / event vocabulary / config schema / 产品入口
从一个 commit 到下一版:这本书怎样更新并收束
系统提示词由注册表把各插件登记的段落拼成:同名的由近的作用域胜出,按一张集中的排序表排,瀑布之后 complete 段落独占
系统提示词也是一条消息
先写 llm/retry 再等,等完再写 llm/retry-started:等待本身留在日志里,排了却没开始的重试读得出来
每一次失败都有下文
先追加事件、后发 end 帧;回答拼不成或日志拒收,end 帧写已放弃
为什么不边流边记
相邻迁移链:defineSessionFormatMigration 强制 to=from+1,编译期四边缺一即拒、多一边也拒
旧会话打开时,版本号说了算
相位机:active/paused/blocked/complete 四态,每条边在 fold 与服务层双重校验
Goal 的相位机、revision CAS 与轮次预算
项目根的 .dsh/skills 是 rank 100 的技能层:随仓库提交、随仓库走,进这个仓库就先吃这个仓库的技能
仓库里的 .dsh 目录,怎样变成整个工作区的能力包
消费方不 import 实现,只 import 类型:PtcWorkflowEngine.inject = ['subagents','ptcRuntime','sandboxPolicy']
能力接入的接缝:Service、Provider 与 Consumer
写入侧:encodeCurrentHeader/encodeCurrentEvent 永远产出 v4,旧磁盘文件一个字节不改
旧会话打开时,版本号说了算
新会话钉进默认预设:设置里存的那个,没有就按组合的默认值推出;不设 DSH_PERMISSION_MODE 时是 workspace-write 加 ask
另一头有没有人
循环只认路由名,LlmRuntime 把名字对到登记的适配器;一个名字只有一个适配器,整组替换没有空窗
请求离开进程之前
严格插值:变量没登记、这次没有值或写法不对,都直接报错
系统提示词也是一条消息
一步的工具调用按模型写下的顺序排成单子,参数只解析一次,调用开始时先写 tool/call,再做放行检查
几个工具能不能一起跑
一次点击的路径:浏览器、Connection、API Gateway、会话控制器、Agent 工厂
Web 隔着网关等请求,headless 跑完就退
一次会话里目录真变了才重发清单:JSON 摘要 + catalogHistory 回扫历史,未变化轮次静默
技能文件从哪几个目录冒出来,又怎样进模型视野
一个 context 只许一个实现:加载第二个抛 cordis 标准 duplicate-service
能力接入的接缝:Service、Provider 与 Consumer
一个 id 同一时刻只有一个驱动者
Agent 没有自己的 id
一个线程里的"一起跑"是等待的重叠:一个主体停在 await 上时,别的主体接着往下走
同时在跑的调用共用着什么
一轮收尾先看信号:已中止记 aborted 和取消原因的拷贝,不发 agent/error;否则记 error,LlmError 原样,其余经 errorChain 记成 UNKNOWN
每一次失败都有下文
一轮一个 AbortController;信号交给这一轮要等的事件、随请求到适配器、给重试的等待和工具调度器,循环自己在固定的检查点停下
每一次失败都有下文
一轮正常收尾且队列不空才开下一轮;被拒、出错、被取消,或第一步的决定为空时,驱动停下
插话等下一步,排队等下一轮
引用而非值:配置里写 ${VAR_NAME},credentialRef 把它解析成内存引用,运行时才取值,值不进配置文件
密钥只在内存里转一圈,日志里永远只看到它的"名字"
用量在结束块之前交、结束块之后不再交块,是适配器要守的约定;适配器抛错由运行时补一个 error 或 aborted 的结束块
为什么不边流边记
游离的 Session:只供读取,不进表,没有 Agent
Agent 没有自己的 id
预设服务必须躲在 isolate realm:leakedServices 发现预设往 root realm 发服务,mountPreset 直接拒绝
一组插件声明,怎样变成一个可共享的 Agent 配置
原子发布用 tmp(wx 0o600)+ link()+unlink(),而非 rename(),防并发覆盖
写一半崩了,已写的数据也不坏
远端 helper 用同一套本地实现:SandboxedFileSystem、LocalSubprocessRuntime、LocalSandboxProvider
同一个运行时,在另一台主机上
运行时 owner、持久的来历、同进程的发起者,三种关系不占用 id
Agent 没有自己的 id
运行时守卫服务 InvariantRegistry:register 即占名,重复注册直接抛错;每个包的 install 跑在自己的 child fiber
Invariant 目录文档化:CI 怎样逼每个包自带守卫
运行时在适配器边界改写请求副本:文件、纯文本模型的图、工具增减、别家的回放状态
请求离开进程之前
账本两类消息刻意分家:agent-message/relay(孩子的话)与 subagent-settled/notice(runtime 旁白)
Agent 之间的消息:send_message 的直邻路由
真子进程真协议:ACP adapter 用 cordis Loader boot 真实 agent bin,经 ACP JSON-RPC over stdio 驱动,不是进程内假对象
Mock Snapshot 测试:启动真实子进程,录制一次,重放一万次
证据自证:每个场景自带足够的落盘证据证明模型可见输出与文件系统效果,不采信 agent 自己的报告
Mock Snapshot 测试:启动真实子进程,录制一次,重放一万次
直邻通信:驻留子→父、父→子;非驻留子发给父直接 UNAUTHORIZED
Agent 之间的消息:send_message 的直邻路由
只有 allowed-once 放行,而且只放这一次;approval/asked 和 approval/decided 只进日志,模型只看到那个调用的结果
另一头有没有人
只有派发重叠:放不放行、结果策略都按单子顺序一个个跑,放行这一关停下来等,后面的调用都开始不了
几个工具能不能一起跑
只有三个 wire 方法:initialize、session/prompt、shutdown;会话在首次 session/prompt 时惰性创建
stdio 上,程序对程序的 JSON-RPC
只有循环组装提示词,每一步在 pre-step 里重拼一次,同一步的重试沿用同一份文字
系统提示词也是一条消息
重来回到这一步开头:重走 agent/request 和 prepareCall,不再写 user/message,从改过的日志重新推出消息
每一次失败都有下文
重选走 recompose → emit tools/change,工具 schema 换了要通知依赖它的东西
一组插件声明,怎样变成一个可共享的 Agent 配置
注册表把参数定成存得成 JSON 的冻结快照;溢出成无穷大的数和负零存不成,调用直接收成出错的结果,不经过放行和结果策略
拦下坏参数的不止一层
注册表在主体之前把调用方的信号并回来,包装换掉信号也断不了这一轮的取消
同时在跑的调用共用着什么
桌面默认端口 19387(区别于 Web 的 3080),进程内转发应用 HTTP 请求到已认证 Web Host
桌面壳里,装着整个运行时
桌面应用是包着完整 dsh Web 应用的 Electron 壳:RunAsNode 子进程跑共享 profile runner,主窗口加载 dsh-app://app/
桌面壳里,装着整个运行时
子调用调度与原生循环同一套规则:提交序、head-of-line、exclusive 屏障、maxParallel,注册表变化时重新分类
程序在子进程里,工具在宿主上
字段整值替换,config 不合并
插件树是一层层补丁叠出来的
自改边界:宿主用 write/edit 改自己的技能文件时,observeHostMutation 把这次写记入 observed 集合
仓库里的 .dsh 目录,怎样变成整个工作区的能力包
组边跑边切:独占的单独一组;能并行的往后延伸,开始每个之前再问一次,碰到第一个不能并行的就停;同时在跑的最多 maxParallelToolCalls 个,默认 10
几个工具能不能一起跑
作用域链:Web 的会话把 Agent 挂到它那一版 preset 下面,子 Agent 加入父 Agent 挂的那一版;headless 的 Agent 没有上一级
工具表是按 Agent 算出来的
abort 即 cancelled:用户撤销提问,请求结算为 cancelled,不留下半截审批
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
ACP:独立 process/session/model,permission 默认 reject 自动应答不浮提示
另一个产品里的 Agent,当你的子 Agent
activation 副作用:pause/block/complete 一律 disarm,只有 resume armed,edit 保持
Goal 的相位机、revision CAS 与轮次预算
activation 进程本地:armed/disarmed 不落盘,GoalProjection 故意不含它
Goal 不是状态对象,是一串可折叠的事件
Agent 层反向 teardown:machine.cancel({kind:''disposed''}) → whenIdle → scope.dispose → handle.close → detach → untrack
多 Agent 拆除:cancel 与 dispose 的两个方向
Agent 的 id 就是它驱动的 Session 的 id
Agent 没有自己的 id
Agent inbox 是唯一 turn 队列:编排服务不建 Task、不建中间包装,只往 inbox 投
Agent 之间的消息:send_message 的直邻路由
Agent.cancel(cause):默认 inbox.clear + abort 进行中 phase;dispose 路径 keepInbox:true 保留收件箱
多 Agent 拆除:cancel 与 dispose 的两个方向
agent() 并发槽 FIFO:默认 min(16, cores-2),排队等槽不无限生成;三道上限 AGENT_CAP/ITEM_CAP/syncTimeout
工作流编排:脚本如何编排一批 Agent
agent/request-error 是瀑布:每一层可以交回 retry 接手,可以调 next() 先听里层的,也可以不往里问、直接交回空;最后没人要重来,就抛出 LlmError,这一轮以 error 收场
每一次失败都有下文
AgentPresetRegistry 是一个 TypertRemoteService:preset 不是磁盘目录发现,而是插件用 AgentPreset 行注册进来的声明
一组插件声明,怎样变成一个可共享的 Agent 配置
agents 与 sessions 两张表都以 SessionId 为键
Agent 没有自己的 id
always 不看码、不限次数、先问里层;碰上永远不会好的错会一直重来到有人取消,随附的配置没有用它
每一次失败都有下文
append-only 日志是唯一事实源;Surface fold 在浏览器安全的子路径里把事件折成可见消息块
日志是事实,对话是投影
append() 是唯一入口:没有 update、没有 delete、没有 splice,纠错只能靠追加更正事件
会话里没有状态,只有一本只追加的流水账
appExit 缺失时插件直接抛错——headless 不自己决定进程何时结束,它只请求退出
命令行里,问一句话就退出
ask 必须落在开放 turn 内:turn/start 未闭合时才允许挂起等人,越界即拒
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
assertAdoptable 是接管闸门:拒跑在 agent preset 下的会话、拒 subagent/forked 会话、拒 cwd 缺失或不符的会话
命令行里,问一句话就退出
BlockAssembler 容忍缺开始和结束的块,以 block-end 为准,关上以后的增量不理,没收到 finish 按正常停止算
为什么不边流边记
blockedReason:lower-kebab code + 非空 message,随快照落盘
Goal 的相位机、revision CAS 与轮次预算
bound() 的预算会计:先按最坏情况预留 GAP 与 notice 的 token,再决定原文留多少
工具输出太大时,全文落盘,上下文只留一张书签
buildForkSeed = 父前缀 + end-seed{inherited:true} + openTurnClosers{forked}
断点、分叉与压缩:不重写历史的三种接管
bundle 层、profile 层、home 层、命令行 overlay 与遥测开关
插件树是一层层补丁叠出来的
cancel 与 dispose 是两个方向:cancel 自上而下中止传播,dispose 自下而上收敛释放
多 Agent 拆除:cancel 与 dispose 的两个方向
ChildLock 按 childId 串行化投递/释放/销毁,失败不传染下一个调用者
Agent 之间的消息:send_message 的直邻路由
Claude Code:官方 Agent SDK 起 CLI,权限默认 dontAsk
另一个产品里的 Agent,当你的子 Agent
clear 墓碑:revision+1 不删历史,goal 置空但 lastRef 保留
Goal 的相位机、revision CAS 与轮次预算
Codex:包内固定命令 app-server --stdio,先 initialize 再 thread/start 建临时线程后才 publish,权限默认 never
另一个产品里的 Agent,当你的子 Agent
compaction/start 同步相邻追加即持久锁;失败恰好补一次 compaction/end{error},故意留 unmatched 可察觉
断点、分叉与压缩:不重写历史的三种接管
Connection 层管浏览器认证(dsh-auth cookie/HMAC)、Host/Origin 信任、请求信封与精确 Fetch 路由
浏览器到运行时,中间没有大表
ConnectionController 指数退避重连,generation 就绪帧带 Host home
浏览器到运行时,中间没有大表
contain() 兜底:脚本钩子丢的 promise 挂 noop 回收,宿主不被 unhandled rejection 带走
工作流编排:脚本如何编排一批 Agent
create 特例:revision 必须 1、phase 必须 active、roundsStarted 必须 0、当前 goal 要么没有要么已 complete
Goal 不是状态对象,是一串可折叠的事件
credentialKey 只认标识符形状:REF_PATTERN 要求 ^[A-Za-z_][A-Za-z0-9_]*$,不是随便一段文本都算引用
密钥只在内存里转一圈,日志里永远只看到它的"名字"
DeepSeek 适配器把中性历史拼成 Messages 请求体,发出前核对工具调用和结果的配对
请求离开进程之前
deepseek-official 和 deepseek-account 是同一套适配器实现的两个实例:线格式和扩展字段相同,凭证、出错时的改写、模型发现和设置各自分开,凭证每次请求现取
请求离开进程之前
delegationDepth:会话头单调地板 + runtime 可选加深
子代理的血缘——subagent 怎么知道自己是谁的孩子
deliveryId 只作痕迹,不做内置去重:重复投递由规则自己判断
一条投递,一个根会话
deriveEventMessage 是逐字透传:user/system/developer/assistant/tool 五类映射,其余事件投影成 null
模型看到的历史,是现折出来的
deriveMessages() 只投影新节点,contentGeneration 变了才整份重建,交出冻结的快照
日志只增不减,历史有进有出
displayFailure 对 AUTH 码屏蔽凭据原文,只留 code——诊断留在日志,UI 不留
日志是事实,对话是投影
dispose = inbox.close(() => finishDisposal):memoized 共享事务,"拆两遍"观察到同一个 quiescence
多 Agent 拆除:cancel 与 dispose 的两个方向
dispose 幂等:摘 abort 监听 → 本地取消 → 后端 teardown 到静默
另一个产品里的 Agent,当你的子 Agent
doc 同步 gate 群:doc-typecheck/docs-site-build/verify-doc-graphs/verify-md-links/verify-type-equiv/verify-cordis-catalog
Invariant 目录文档化:CI 怎样逼每个包自带守卫
doc-sync gate:20+ 文档验证 gate,跑已生成产物与源码的结构比对
从一个 commit 到下一版:这本书怎样更新并收束
drain 三兄弟:drain(全树)/drainDescendants(按父链)/drainChildren(精选),之后 admission 闸门 DRAINING
多 Agent 拆除:cancel 与 dispose 的两个方向
enforcement 分 full/partial:partial 不等于 full,要绝对边界的调用不能当 full 用
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
env 显式层叠在凭据擦除后副本,环境是显式给的,不是隐式继承的
另一个产品里的 Agent,当你的子 Agent
fiber 与它的六个状态
插件跟着它等的服务起落
findProjectRoot 向上找 .git/package.json:项目级能力根锚定在仓库根,不是当前 cwd
仓库里的 .dsh 目录,怎样变成整个工作区的能力包
finishDisposal child-first:先 cancel 并并行 dispose 全部 ownedChildren,再 whenIdle、flushFinalState、capture、handle.dispose
多 Agent 拆除:cancel 与 dispose 的两个方向
flattenLineage 按父子关系缩进,孤儿降级 root、成环告警,顺序不重排
日志是事实,对话是投影
fold 即校验:replay 解码器同时是 append 期 write gate
Goal 不是状态对象,是一串可折叠的事件
followup、steer、inject 都经 send 写入,差别在进哪个列表、唤不唤醒
插话等下一步,排队等下一轮
fork 四拒绝码:SESSION_NOT_FOUND / SESSION_NOT_LIVE / SESSION_ALREADY_EXISTS / INVALID_BOUNDARY
断点、分叉与压缩:不重写历史的三种接管
FTS5 双表:persisted_docs 常驻虚拟表与 temp.live_docs 临时虚拟表,活会话内容默认不进全文索引
轨迹查询与重放验证:readSession 为什么要把整个日志再跑一遍
fused AbortSignal:调用方取消、owner fiber 卸载、工厂 teardown 三路合成,永不挂死后端
断点、分叉与压缩:不重写历史的三种接管
Gateway 层只管分发:invoke() 一元调用、stream() 流式方法、descriptor 解析与 zod 校验
浏览器到运行时,中间没有大表
generation 用 users 引用计数:最后一个 joined Agent 离开且树已 retired,collect 才 dispose 这一代
一组插件声明,怎样变成一个可共享的 Agent 配置
header 冻结,之后的变化记在日志里
Agent 没有自己的 id
headless 的树里挂着 Gateway,却没有把请求送到它面前的 connection 与 Web 服务器
Web 隔着网关等请求,headless 跑完就退
headless 的一次运行独占一段日志区间
Web 隔着网关等请求,headless 跑完就退
headless 是同一个 Agent 运行时的一次性面孔:commander 解析 task/--json/--session-id,publish 一个 headlessStartup 服务,runner 惰性消费它
命令行里,问一句话就退出
home 层压过 profile 层:机器级偏好对所有 profile 生效
cordis.yml 组合:空根与五层 patch 栈
host 是唯一计算点:客户端只存整值、按 seq 合并,不做域折叠;新投影域零客户端代码
日志是事实,对话是投影
host bindings 是唯一回桥:begin/startChild/childResult/disposeChild/progress,真孩子走 ctx.subagents.start
工作流编排:脚本如何编排一批 Agent
initialize 是就绪边界:先 await loader 再配 provider/model/reasoningEffort,然后才置 initialized=true
stdio 上,程序对程序的 JSON-RPC
invariant companion 是包级守卫:每个 workspace 包要么发 ./invariant companion,要么 README 写清省略理由
Invariant 目录文档化:CI 怎样逼每个包自带守卫
job 记在发起调用的 Agent 名下:活过这次调用和这一轮,Agent 撤下时被停
命令可以比它的调用活得久
LLM 调用前 assertUsableApiKey:空 key 或含非法字符直接报错,且错误消息不回显 key 本体
密钥只在内存里转一圈,日志里永远只看到它的"名字"
llm/stream 监听器能观察、包一层、整个接下调用,改不了请求
请求离开进程之前
materializeFromRealm:vm 域值过界只能是 lossless JSON——class 实例/稀疏数组/symbol 键/bigint 全拒
工作流编排:脚本如何编排一批 Agent
maxParallelToolCalls 设成 1,日志的先后和全部独占时一样;池子按组现建,别的 Agent、子 Agent、run_code 的程序各有各的队列
同时在跑的调用共用着什么
mcp-client 是一个 Cordis 插件,inject=['tools']:按 server 名注册一个 MCP server 连接,把它的工具同步进全局工具表
外部进程的工具,怎样进全局工具表又不撞名
MessageFeedback 是日志事件不是历史:feedback/message-put 标注"Log-only human feedback; never enters model history"
Telemetry 是观测侧车,不是第二个会话引擎
minimal preset 和 sdk-minimal 的 bash 是一个 Agent 一个常驻 shell:跨调用、跨轮,超时、被停就整个重置
命令可以比它的调用活得久
mutationToolName:知道是哪个工具动了技能,才能把"宿主写"和"外部改"区分开
仓库里的 .dsh 目录,怎样变成整个工作区的能力包
Network 层只放 HTTP(S)、URL 长度封顶、禁内嵌凭证、同源才跟随重定向、不带浏览器 ambient cookie
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
nextMutationTime:Math.max(Date.now, updatedAt) 钳住时钟回拨
Goal 的相位机、revision CAS 与轮次预算
Notifier 微任务/动画帧批,受控输入 notifyNow 同步通知,防 React 把 caret 弹回末尾
日志是事实,对话是投影
one-shot vs continuable:终止式委派 vs 可冷启恢复的会话
子代理的血缘——subagent 怎么知道自己是谁的孩子
openTurnClosers 双原因:interrupted(崩溃恢复)与 forked(中切分叉)共用算法,只差措辞与 turn/end reason
断点、分叉与压缩:不重写历史的三种接管
packages 下挂在这个瀑布上的只有三个监听器:llm-retry 按码,compaction-basic 接 CONTEXT_WINDOW_EXCEEDED,compaction-image-offload 接 IMAGE_OFFLOAD_REQUIRED;组合挂了哪个,决定哪种失败会被重来
每一次失败都有下文
patch 语义:整行替换 config(不 merge),行顺序不承载语义,activation 由服务可用性驱动
cordis.yml 组合:空根与五层 patch 栈
pending:选了但未生效的模式,配对 command/done 结算
目标之下的三件套——plan、todo、schedule
persistContiguous:ensureLease → assertContiguous → 先修 torn-tail / 重写 recoveredTail → persistBatch → 推进 cursor
写一半崩了,已写的数据也不坏
plan-mode:二进制投影 {active, pending},plan/mode 事件 + /plan 命令生命周期
目标之下的三件套——plan、todo、schedule
prepareCall 让模型信息和派发取自适配器配置的同一代,句柄只能用一次,请求配置必须和冻结的逐项相同
请求离开进程之前
PresetDefinition = {id, name?, description?, order?, plugins: Cordis entry list}:一组子插件声明就是一个预设
一组插件声明,怎样变成一个可共享的 Agent 配置
profile 简写与保留名
dsh 启动器管到哪里为止
profile runner 接管进程生命周期
dsh 启动器管到哪里为止
ProjectionDefinition 是一份纯函数契约:key + stateSchema + init + apply + wire + stateVersion
事件是流水,状态是折叠出来的投影
ProjectionValueStore 每会话存两类行:sequenced(高 seq 胜)与 cached(持久化快照视图,让位 sequenced)
日志是事实,对话是投影
prompt 强制 initialized、跨 await 两次断言活 Agent、经附件库接纳编码图片
stdio 上,程序对程序的 JSON-RPC
provider 失败只作废该层缓存并 warn,不炸整个收集;只有可收集时才写 cacheable 项
技能文件从哪几个目录冒出来,又怎样进模型视野
PTC 模式把模型面折叠成一个 run_code:别的工具只能从程序里调,直接调就报错;both 模式才保留原生入口
程序在子进程里,工具在宿主上
publicToolName 给外部工具加 server 前缀防撞车:超 64 字符或含非法字符时截短后拼 12 位哈希
外部进程的工具,怎样进全局工具表又不撞名
Python 侧 DeepSeekHarness 是 context manager:Popen 拉起 server、initialize 握手、Session.run 收集通知到 final 响应
两根语言,同一条线
readHeader 四态:current / migration-required / unsupported / malformed,只读首行不碰事件体
旧会话打开时,版本号说了算
readSession 是重放验证不是读取:corpus.load 全量事件后 Session.create 重放,能跑完才返回快照
轨迹查询与重放验证:readSession 为什么要把整个日志再跑一遍
redactSecrets 在日志层兜底:对象里 role('secret') 的字段 walk 到即替换成 RedactedSecret,不进日志
密钥只在内存里转一圈,日志里永远只看到它的"名字"
register 只查输出格式;原样登记的工具(比如 MCP 工具)参数格式没人查,由工具自己处理
拦下坏参数的不止一层
resolve 三级优先级:request.mode ?? session override ?? defaultMode
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
resolveChildDepth = parent.depth + 1,超过 maxDepth 抛 SubagentDepthError
子代理的血缘——subagent 怎么知道自己是谁的孩子
result 永不 reject:脚本跑完 completed、炸了 error、取消 cancelled,全部走 stopReason
工作流编排:脚本如何编排一批 Agent
resume 三件事:write 夺门(排除并发)、冷读物理合法日志、interruptedTurnClosers 补尾 turn
断点、分叉与压缩:不重写历史的三种接管
retainContent 头尾双向保留,中间插入 GAP 占位,omittedBytes/omittedImages 驱动 notice
工具输出太大时,全文落盘,上下文只留一张书签
return guidance 只在 send_message 工具在场时注入,孩子才知道要主动发结果
Agent 之间的消息:send_message 的直邻路由
revision CAS 牌:每次变更恰好 +1,错牌 GOAL_STALE_REVISION
Goal 不是状态对象,是一串可折叠的事件
revision CAS:每次 durable 变更恰好 +1,对牌错 GOAL_STALE_REVISION
Goal 的相位机、revision CAS 与轮次预算
run 一了结就把子调度全部中止并排干,图片结果经 deferContext 带回,模型历史只记外层
程序在子进程里,工具在宿主上
run_code 不归任何一层:不能登记、不能写进过滤条件,只放给呈现方式不是 native 的 Agent
工具表是按 Agent 算出来的
run_code 自己的参数也由 defineTool 查;程序里的调用先要存得成 JSON,再走同一条路,被拒时程序收到 ToolCallError
拦下坏参数的不止一层
run() 先 await loader 再取 agents/sessions,缺服务就静默返回;空白 sessionId 直接抛错
命令行里,问一句话就退出
runtime-closure 兜底:部署 manifest 必须含 preset 引用的每个插件与每个必需 workspace peer,否则 cordis 加载时才炸就晚了
Invariant 目录文档化:CI 怎样逼每个包自带守卫
Sandbox 三模式:read-only / workspace-write / danger-full-access,默认 read-only 是 fail-safe
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
schedule CAS 更新:expected 整条旧记录对牌
目标之下的三件套——plan、todo、schedule
schedule miss 三码:not_found / ended / conflict
目标之下的三件套——plan、todo、schedule
schedule/change:版本化变更事件,六种记录 kind
目标之下的三件套——plan、todo、schedule
seed 契约:isSeeded 必须显式 seed 与 inheritedEventCount;end-seed{inherited:true} 标记切分点
断点、分叉与压缩:不重写历史的三种接管
seenGoalIds:建过的 id 永不复用,clear 后重建也不许同名
Goal 不是状态对象,是一串可折叠的事件
send_message 是全局唯一的控制面工具:薄适配把参数交给 ctx.subagents.sendMessage,不自带路由与授权
Agent 之间的消息:send_message 的直邻路由
server 名是受管资源:activeServerNames 是 WeakMap<ctx, Set>,同名 server 在同一上下文里二次注册直接拒绝
外部进程的工具,怎样进全局工具表又不撞名
server instructions 被包一层、截到 32KB,作为该 server 工具的附加说明喂给模型
外部进程的工具,怎样进全局工具表又不撞名
Session 的 id 只在 header 里存一份
Agent 没有自己的 id
Session 的心脏是 private readonly log:一个只追加、永不修改、永不删除的事件数组
会话里没有状态,只有一本只追加的流水账
session companion 的记账本:每会话 SessionTrace 记 lastSeq/openTurn/openStep/nextTurn/nextStep/pendingCalls
Invariant 目录文档化:CI 怎样逼每个包自带守卫
Session.create 与 Session.fromRestore:snapshot 模式逐事件快照深冻,detached/shared-frozen 模式零拷贝只校验
断点、分叉与压缩:不重写历史的三种接管
SessionProjectionRegistry 监听 session/created 初始化 cell、session/event 急切 drive 每个注册单元
事件是流水,状态是折叠出来的投影
ShellExecutor extends Service:super(ctx,'shell') 把抽象类注册成 ctx.shell
能力接入的接缝:Service、Provider 与 Consumer
shutdown 是 memoized 单次退出:flush 传输 → dispose 根 fiber → exit(0),竞态只退出一次
stdio 上,程序对程序的 JSON-RPC
SkillFilesystem 把六个磁盘根目录按固定 rank 排序成候选层:项目 .dsh/skills=100、项目 .agents=200、自定义=300、用户 .dsh=400、用户 .agents=500,运行期注册=250,打包技能=600
技能文件从哪几个目录冒出来,又怎样进模型视野
SkillRegistry 用 scopeChainOf 作 collect 缓存键:工作区变了缓存键就变,跨会话串味被结构上排除
技能文件从哪几个目录冒出来,又怎样进模型视野
snake_case 与 camelCase 的边界在客户端:Python 的 max_tokens 映射成线上的 maxTokens,reasoning_effort→reasoningEffort
两根语言,同一条线
sourceEventSeqs 是因果链:压缩节点必须把它遮蔽的每个旧节点都列进来源
每条事件都装在带五格字段的信封里
spill 不是截断而是搬运:全文落盘 + 模型视野只留带 locator 的书签
工具输出太大时,全文落盘,上下文只留一张书签
SSH 执行环境把文件系统、子进程和沙箱整体搬到远端:一条 OpenSSH 控制主控承载管理面,数据面每进程每通道独立转发
同一个运行时,在另一台主机上
start() 预检在父进程:validateMeta、assertBodyParses(只 parse 不执行)、provider 注册检查,失败同步抛 WorkflowError 且没有 run
工作流编排:脚本如何编排一批 Agent
startsRequestSeries 让这一步另起一组请求,只在第一次尝试时生效
被取走的消息不一定进历史
stdout/stderr 通道再套一层 RPC 传输出快照,stdin/control 是原始 duplex
同一个运行时,在另一台主机上
subagent 身份投影:null ⟺ 无合法 descriptor
子代理的血缘——subagent 怎么知道自己是谁的孩子
SUBAGENT_DESCRIPTOR_VERSION=3:字段集精确锁定,加字段必升版本
子代理的血缘——subagent 怎么知道自己是谁的孩子
subagent/descriptor:孩子出生时写的身份事件,log-only,过 compaction
子代理的血缘——subagent 怎么知道自己是谁的孩子
subagentCatalog:直接孩子目录,剔除 fork 继承事实
子代理的血缘——subagent 怎么知道自己是谁的孩子
surface replace 不变量:sourceEventSeqs 覆盖全部遮蔽节点、tool/result 只改 content、节点 0 system 保护
断点、分叉与压缩:不重写历史的三种接管
SurfaceManager 增量折叠:validateNext 预演计划,_processDelta 把新事件尾追折进表面状态
模型看到的历史,是现折出来的
surfaceOp 只有两种取值:append 追尾,或 {op:replace,startSeq,endSeq} 替换一段表面节点
每条事件都装在带五格字段的信封里
SystemPromptProjection 决定系统提示是 append 新节点还是 replace 旧 head
模型看到的历史,是现折出来的
telemetry 开关:DSH_TELEMETRY_DISABLED 任何非空值都关(off-by-mistake 优先)
cordis.yml 组合:空根与五层 patch 栈
todo/write:整条列表快照 last-wins,TodoItem 无 id 无优先级
目标之下的三件套——plan、todo、schedule
TodoItem 三态:pending / in_progress / completed
目标之下的三件套——plan、todo、schedule
tool-cordis 是只读运行时勘探:cordis_inspect_list/query 让模型读 Host/Client 的服务与 schema,但不能改
仓库里的 .dsh 目录,怎样变成整个工作区的能力包
tool-pairing 平衡:N 个节点 N+1 个切分点,增量 BalanceCache 挂在 surface.replaceGeneration 上
断点、分叉与压缩:不重写历史的三种接管
tool-skill 在 pre-step 阶段先于任何工具加载注入完整目录清单:"This complete catalog replaces every earlier available-skills list"
技能文件从哪几个目录冒出来,又怎样进模型视野
ToolArgsError(INVALID_ARGS)收成普通的出错结果,经过结果策略、按顺序提交;码记进 tool/result,模型只读到文字
拦下坏参数的不止一层
tools/execute 上的包装同时处在几个调用里:只许换 exec.signal,要换回,交回之前要停稳
同时在跑的调用共用着什么
traceEvent 替换链:一条被替换的 assistant/message 可沿 replacement 指针追出 replacementChain 与派生事件集 derivedEventSeqs
轨迹查询与重放验证:readSession 为什么要把整个日志再跑一遍
traceSession 血缘:沿 parentSession 向上走,遇到环抛 SESSION_QUERY_INVALID_LINEAGE,断链停在最近可解析祖先
轨迹查询与重放验证:readSession 为什么要把整个日志再跑一遍
Trust 层 user-approval:ApprovalPolicy 只有 ask/never 两值,ask 在没有 answerer 时 fail-closed 落 unavailable
Trust / Permission / Sandbox / Network:四层防线为什么不能合成一个开关
TS 侧 DeepSeekHarness 是 AsyncDisposable:spawn 子进程、memoized initialize 握手、HarnessSession.run 推一个 prompt 并等到整轮静止
两根语言,同一条线
turn/end 写在 finally 里;max-tokens 粘住以后,后面的步改不回来;turn/end 不带用量
每一次失败都有下文
unmatched start 越过最新 end-seed 边界即豁免(属于上一生命周期)
断点、分叉与压缩:不重写历史的三种接管
useProjection(key) 是第五个框架钩子座,身份稳定 face,组件订阅一次
日志是事实,对话是投影
v3→v4 特例:裸迁移拒绝开工,必须 createSessionFormatCatalogWithChildren 显式传入历史 child facts
旧会话打开时,版本号说了算
vm.Context 只挂六个冻结全局:agent/parallel/pipeline/phase/log/args——脚本拿不到任何 Node API
工作流编排:脚本如何编排一批 Agent
wake 纪律:disposal 永不 latch,teardown 不等任何模型 turn
多 Agent 拆除:cancel 与 dispose 的两个方向
watchSettlement:孩子自然跑完,poke+idle 后 runMaintenance 里长出一次 dispose
多 Agent 拆除:cancel 与 dispose 的两个方向
Web 的 Agent 归会话控制器所有,留在主机进程里
Web 隔着网关等请求,headless 跑完就退
Web 让每个会话挂自己的 preset,headless 用进程级的一套工具与提示词
Web 隔着网关等请求,headless 跑完就退
Web 由浏览器回答,请求经 api-remotes 转出,后连上的浏览器也会收到;ACP 由客户端回答;headless 和 SDK 没有应答者;sdk-minimal 不挂审批服务
另一头有没有人
Web 在主机层关掉按 Agent 组合的工具行,改由 preset 挂;三个 preset 里的 subagent 装在每个 Agent 自己那一层
工具表是按 Agent 算出来的
webhook 会话是不从对话开始的根会话:外部投递经 provider 层认证、规则翻译成 Session 请求、事务建会话
一条投递,一个根会话
WebSocket mux 一条 socket 多路复用逻辑流:open/item/end/cancel 帧,uplink inbox 有界字节数
浏览器到运行时,中间没有大表
whole-value event rule:apply 不感兴趣就必须返回同一个 state 引用(Object.is),零下游工作
事件是流水,状态是折叠出来的投影
Windows 打包先扫描 PE 内容,保留有效供应商签名、签署未签名代码,密封运行时哈希前先过硬件令牌签名
桌面壳里,装着整个运行时
workflow 是脚本编排:父写一段脚本声明 parallel/pipeline,由 runtime 起一批子 Agent,不是父自己跑多回合
工作流编排:脚本如何编排一批 Agent
WorkflowEngine 契约:result 永不 reject,workflow/end 恰好一次,listener 失败 contained
能力接入的接缝:Service、Provider 与 Consumer