第 3 篇:运行机制与启动流程
本篇结论 :dsh 的运行分两幕 ------ 启动 是「把若干层配置合成一张插件清单,再按依赖顺序一个个挂上
ctx」;执行 是「Agent 主循环拿着这张清单,反复跑『组装提示词 → 调模型 → 执行工具』直到没人再欠谁」。读完你能得到:能完整说出从敲回车到拿到答案之间发生了什么,并且理解为什么「模型可见即已落日志」这条铁律要那样约束设计。
开篇:从开门营业到一桌客人吃完整顿饭
第 2 篇我们画了一张餐厅的岗位表(静态结构)。这一篇要看它动起来的样子,分两幕:
- 第一幕:开门营业。 早上到店,按今天的预订情况排班、备料、开火 ------ 对应 dsh 的启动流程,几秒钟内完成。
- 第二幕:一桌客人吃完整顿饭。 从点第一道菜到买单离开,中间可能加菜、催菜、退菜 ------ 对应 dsh 的一次 Turn(轮次)。
两幕的性格完全不同。第一幕是一次性的装配 ,第二幕是反复的循环。
第一幕:启动流程(六步)
假设你敲下这条命令:
sh
dsh --profile headless "帮我跑一下测试"
下面这六步在几秒内全部完成。
你的应用插件 Cordis Loader app-boot boot 库 profile-boot 编排层 dsh 启动器 apps/cli 你(终端) 你的应用插件 Cordis Loader app-boot boot 库 profile-boot 编排层 dsh 启动器 apps/cli 你(终端) #mermaid-svg-HABDP2DpNqJvkJL2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HABDP2DpNqJvkJL2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HABDP2DpNqJvkJL2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HABDP2DpNqJvkJL2 .error-icon{fill:#552222;}#mermaid-svg-HABDP2DpNqJvkJL2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HABDP2DpNqJvkJL2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HABDP2DpNqJvkJL2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HABDP2DpNqJvkJL2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HABDP2DpNqJvkJL2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HABDP2DpNqJvkJL2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HABDP2DpNqJvkJL2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HABDP2DpNqJvkJL2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HABDP2DpNqJvkJL2 .marker.cross{stroke:#333333;}#mermaid-svg-HABDP2DpNqJvkJL2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HABDP2DpNqJvkJL2 p{margin:0;}#mermaid-svg-HABDP2DpNqJvkJL2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HABDP2DpNqJvkJL2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-HABDP2DpNqJvkJL2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HABDP2DpNqJvkJL2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-HABDP2DpNqJvkJL2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-HABDP2DpNqJvkJL2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-HABDP2DpNqJvkJL2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-HABDP2DpNqJvkJL2 .sequenceNumber{fill:white;}#mermaid-svg-HABDP2DpNqJvkJL2 #sequencenumber{fill:#333;}#mermaid-svg-HABDP2DpNqJvkJL2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-HABDP2DpNqJvkJL2 .messageText{fill:#333;stroke:none;}#mermaid-svg-HABDP2DpNqJvkJL2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HABDP2DpNqJvkJL2 .labelText,#mermaid-svg-HABDP2DpNqJvkJL2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-HABDP2DpNqJvkJL2 .loopText,#mermaid-svg-HABDP2DpNqJvkJL2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-HABDP2DpNqJvkJL2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HABDP2DpNqJvkJL2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-HABDP2DpNqJvkJL2 .noteText,#mermaid-svg-HABDP2DpNqJvkJL2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-HABDP2DpNqJvkJL2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HABDP2DpNqJvkJL2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HABDP2DpNqJvkJL2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HABDP2DpNqJvkJL2 .actorPopupMenu{position:absolute;}#mermaid-svg-HABDP2DpNqJvkJL2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-HABDP2DpNqJvkJL2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HABDP2DpNqJvkJL2 .actor-man circle,#mermaid-svg-HABDP2DpNqJvkJL2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-HABDP2DpNqJvkJL2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} dsh --profile headless "任务" 1 ① 解析自己的 flag,其余原样透传 2 交给编排层 3 ② composeProfile:按序叠加 Bundle 的 patch 4 ③ boot() 5 new Context() → 装 Loader → 挂根 Include 6 ④ applyEntryPatches:合成最终插件清单 7 ⑤ 逐行实例化插件(inject 决定顺序) 8 全部 ACTIVE 9 ⑥ assertEntriesActivated 审计 10 appReady.commit() ------ 应用接管 11
① CLI 解析:启动器只认自己的参数
apps/cli/src/bin.ts 和 args.ts 负责。启动器只解析属于自己的 flag (--profile、--patch、--dump-config 等),遇到第一个不认识的参数,后面全部原样透传给被启动的应用。
sh
dsh --profile headless "帮我跑一下测试"
↑ 这一整句原样交给 headless 应用,启动器不解析
这也是为什么 dsh --profile web --help 看到的是 web 应用的帮助,而 dsh --help 看到的是启动器自己的帮助。
② 组合 Profile:把各层的补丁按顺序叠起来
apps/cli/src/profile-boot.ts 的 composeProfile() 干活。它把下面这些来源收集成一份有序的补丁列表(第 4 篇会详细讲每一层):
dsh.profile.bundles里每个 Bundle 的cordis.patch.yml(按列表顺序)- Profile 自己的
cordis.patch.yml - Harness Home 级的
$DSH_HOME/cordis.patch.yml - 命令行
--patch传进来的临时覆盖层
③ 启动 boot:new Context() → 装 Loader → 挂根 Include
packages/boot/app-boot/src/index.ts 的 boot() 是整个启动的中枢,核心就几行:
ts
const ctx = new Context() // 造一个空的共享上下文
await ctx.plugin(Loader) // 把「加载器」本身也当插件挂上
await prepare?.(ctx) // 注入环境快照、命令行参数
await mountRootInclude(ctx, configPath, patches) // 挂上根 Include
await ctx.get('loader')?.await() // 等所有插件落定
await assertEntriesActivated(ctx) // 审计
注意 ctx.plugin(Loader) ------ 连加载器自己都是个插件。这就是「一切皆插件」贯彻到底的样子。
一个反直觉的细节 :Profile 的根配置文件 cordis.yml 是空数组 []。
也就是说,启动时不是「读一份配置,然后往上打补丁」,而是「从一张白纸开始,把各层补丁打上去,长出整棵树」。
用餐厅的话说:不是「今天照昨天的班表微调」,而是「今天从零排班,把预订信息一层层喂进去,长出今天的班表」。
④ 合成插件清单:按 id 定位、整体替换、最后写入者胜
applyEntryPatches([], patches) 把那一叠补丁应用到空列表上。三条规则必须记住:
| 规则 | 含义 |
|---|---|
| 按 id 定位 | 补丁通过 id 找到目标行,比如 id: system-prompt |
| 整体替换 | 命中后替换整行的 config,不是合并字段 |
| 最后写入者胜 | 多层改同一行,后写的赢 |
第二条最容易踩坑。你想改 system-prompt 的 personaPrefix,但原来那行还有 personaSuffix,你的补丁里必须把 personaSuffix 也重写一遍,否则它会被整个替换掉、消失。
第三条保证了「上层覆盖下层」的直觉:Home 级配置能盖过 Profile 级,--patch 能盖过一切。
⑤ Loader 逐个实例化插件:inject 决定顺序
现在有一张最终的插件清单了。Loader 开始逐行实例化 ------ 但不是按清单顺序 ,而是按依赖关系。
回到第 1 篇的原理三:每个插件声明 inject(我需要哪些服务),框架自己算出拓扑顺序。声明了 inject: ['skills'] 的插件,会一直等到 ctx.skills 就绪才运行。
⑥ 审计 + 交接
assertEntriesActivated 检查:每个启用的插件都必须处于 ACTIVE 状态,否则报错并指出它在等谁(第 0 篇讲过的那行 labelled 报错)。
全部通过后 appReady.commit(),应用正式接管。
headless 这时候做什么? 它插的两行开始工作:
yaml
- id: headless-startup # 解析命令行,把 task 发布成服务
name: '@deepseek-ai/dsh-headless/startup'
- id: headless-runner # 读取 task,创建 Agent 并跑完
name: '@deepseek-ai/dsh-headless'
inject: [headlessStartup]
config:
task: !!js ctx.headlessStartup.task # ← 挂载时才求值
headless-runner 声明 inject: [headlessStartup],所以它一定在 startup 之后运行 ------ 这就是原理三在真实配置里的样子。
注意 !!js ctx.headlessStartup.task:这是一个 JavaScript 表达式,在挂载时才求值。所以它能读到运行时才存在的东西。
第二幕:一次 Turn 的完整旅程
启动完了,现在真正干活。先分清两个概念(docs/architecture.md:82):
Step(步) = 一次模型请求 + 它调用的工具Turn(轮) = 零个或多个 Step,直到没人再欠谁
类比:
- 一个 Step = 「点一次菜 → 厨房做 → 上桌」这一轮
- 一个 Turn = 一桌客人从点第一道菜到买单,中间可能加菜好几次(多个 Step),也可能进门发现没预订直接走了(零个 Step)
是的,Turn 可以有零个 Step ------ 比如第一条消息就被策略拒绝了,那这一轮就直接关掉,一次模型调用都不发生。
完整流程图
#mermaid-svg-btFA6dBjQmd2XZGM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-btFA6dBjQmd2XZGM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-btFA6dBjQmd2XZGM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-btFA6dBjQmd2XZGM .error-icon{fill:#552222;}#mermaid-svg-btFA6dBjQmd2XZGM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-btFA6dBjQmd2XZGM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-btFA6dBjQmd2XZGM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-btFA6dBjQmd2XZGM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-btFA6dBjQmd2XZGM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-btFA6dBjQmd2XZGM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-btFA6dBjQmd2XZGM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-btFA6dBjQmd2XZGM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-btFA6dBjQmd2XZGM .marker.cross{stroke:#333333;}#mermaid-svg-btFA6dBjQmd2XZGM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-btFA6dBjQmd2XZGM p{margin:0;}#mermaid-svg-btFA6dBjQmd2XZGM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-btFA6dBjQmd2XZGM .cluster-label text{fill:#333;}#mermaid-svg-btFA6dBjQmd2XZGM .cluster-label span{color:#333;}#mermaid-svg-btFA6dBjQmd2XZGM .cluster-label span p{background-color:transparent;}#mermaid-svg-btFA6dBjQmd2XZGM .label text,#mermaid-svg-btFA6dBjQmd2XZGM span{fill:#333;color:#333;}#mermaid-svg-btFA6dBjQmd2XZGM .node rect,#mermaid-svg-btFA6dBjQmd2XZGM .node circle,#mermaid-svg-btFA6dBjQmd2XZGM .node ellipse,#mermaid-svg-btFA6dBjQmd2XZGM .node polygon,#mermaid-svg-btFA6dBjQmd2XZGM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-btFA6dBjQmd2XZGM .rough-node .label text,#mermaid-svg-btFA6dBjQmd2XZGM .node .label text,#mermaid-svg-btFA6dBjQmd2XZGM .image-shape .label,#mermaid-svg-btFA6dBjQmd2XZGM .icon-shape .label{text-anchor:middle;}#mermaid-svg-btFA6dBjQmd2XZGM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-btFA6dBjQmd2XZGM .rough-node .label,#mermaid-svg-btFA6dBjQmd2XZGM .node .label,#mermaid-svg-btFA6dBjQmd2XZGM .image-shape .label,#mermaid-svg-btFA6dBjQmd2XZGM .icon-shape .label{text-align:center;}#mermaid-svg-btFA6dBjQmd2XZGM .node.clickable{cursor:pointer;}#mermaid-svg-btFA6dBjQmd2XZGM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-btFA6dBjQmd2XZGM .arrowheadPath{fill:#333333;}#mermaid-svg-btFA6dBjQmd2XZGM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-btFA6dBjQmd2XZGM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-btFA6dBjQmd2XZGM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-btFA6dBjQmd2XZGM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-btFA6dBjQmd2XZGM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-btFA6dBjQmd2XZGM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-btFA6dBjQmd2XZGM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-btFA6dBjQmd2XZGM .cluster text{fill:#333;}#mermaid-svg-btFA6dBjQmd2XZGM .cluster span{color:#333;}#mermaid-svg-btFA6dBjQmd2XZGM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-btFA6dBjQmd2XZGM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-btFA6dBjQmd2XZGM rect.text{fill:none;stroke-width:0;}#mermaid-svg-btFA6dBjQmd2XZGM .icon-shape,#mermaid-svg-btFA6dBjQmd2XZGM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-btFA6dBjQmd2XZGM .icon-shape p,#mermaid-svg-btFA6dBjQmd2XZGM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-btFA6dBjQmd2XZGM .icon-shape .label rect,#mermaid-svg-btFA6dBjQmd2XZGM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-btFA6dBjQmd2XZGM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-btFA6dBjQmd2XZGM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-btFA6dBjQmd2XZGM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拒绝 / 首轮被改写为空
放行
要
不要
是
否
turn/start
认领输入:
下一条待处理消息 + 一条排队消息
组装提示词段落 + 工具 schema
投影运行时上下文
agent/pre-step
(waterfall)
关闭本轮
(零个 step)
step/start
agent/request → prepareCall()
决定走哪条模型路由
写入 system/message、user/message
记录 request/header、request/context
从会话日志派生并冻结消息历史
流式调用模型
llm/stream → agent/assistant-stream
收到 assistant/message 或 assistant/attempt
模型要调工具吗?
tool/call → tools/pre-execute
→ tools/execute → tools/post-execute
→ tool/result
step/end
还欠一次请求吗?
(工具要求再来 / 又来了新输入)
agent/turn-stopping
turn/end
逐个环节讲
① 认领输入。 所有输入都通过**一个收件箱(inbox)**进来。有的消息会立刻唤醒循环,注入的上下文则在收件箱里排队,等别的消息来唤醒它。
② 组装提示词 + 投影上下文。 ctx.systemPrompt.assemble() 把各插件贡献的段落拼起来,同时把工具 schema 也加进去(这样模型才知道有哪些工具可用)。
这里有个优化细节:运行时上下文只在内容变化时才生成快照 ,不变就返回 undefined,避免每次请求都重发一大段没变的内容。
③ agent/pre-step(瀑布)。 这是第一个拦截点。监听器可以改写 或拒绝这条消息。如果首轮就被拒绝或改写为空,本轮直接关闭 ------ 零个 Step。
④ agent/request → prepareCall()。 决定实际走哪条模型路由。这里有个重要保证:在这个异步阶段取消,系统消息和用户消息都不会被提交。要的就是「要么完整地做,要么完全不做」。
⑤ 从日志派生并冻结消息历史。 关键一步 ------ 送给模型的历史不是从内存里拼的,是从会话日志派生出来的 (deriveMessages())。
⑥ 流式调用模型。 经过 llm/stream 瀑布(可以拦截/改写流),产出落到 assistant/message;失败的、重试的、取消的尝试则记为 assistant/attempt,不进入模型历史。
⑦ 执行工具(如果有)。 三段式管线:tools/pre-execute → tools/execute → tools/post-execute。前后两段是拦截点,可以在真正执行前做审批、在执行后做改写。
⑧ 判断要不要再来一轮。 如果工具说「我还欠一次请求」,或者收件箱里又来了新输入,就回到 ① 开始下一个 Step。否则进入 agent/turn-stopping,然后 turn/end。
循环本身不认识任何具体实现
这是整个第二幕最值得记住的一点。看 AgentLoop 的依赖声明:
ts
static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt', 'sessionProjections']
就这六个。没有 fs、没有 shell、没有 llm-deepseek、没有 tool-bash。
主循环只知道「有个工具柜、有个模型、有本账本」。至于工具柜里放的是文件工具还是 bash、模型是 DeepSeek 还是别的、账本存在本地磁盘还是数据库 ------ 它一概不知,也不关心。
用餐厅的话说:传菜动线不关心今天做的是川菜还是粤菜。你换菜系(换 Provider),传菜动线一个字都不用改。
一条铁律:模型可见即已落日志
第 1 篇提过,这里看它的工程后果。
Model-visible means logged. (
docs/architecture.md:121)任何到达模型请求的内容,都必须能从会话日志重建出来。dsh 有一个运行时断言在检查这件事。
注意流程图第 ⑤ 步 :消息历史是从日志派生的,不是从内存拼的。这就是这条铁律的落地方式 ------ 既然历史只能来自日志,那么「送了什么给模型」天然就是可复盘的。
代价是什么? 想给模型加一种新类型的可见输入,你必须:
- 在
SessionEventMap里新增一种事件类型 - 让渲染逻辑从日志里把它渲染出来
不能直接往提示词里偷偷塞一段文本。因为那样的话,这段文本不可解释、不可调试、不可复盘 ------ 你永远说不清「它当时到底看到了什么」。
用餐厅的话说:每一道端上桌的菜,都必须在流水账本上有记录。没记账的菜不许上桌。这样客人问「我这顿到底吃了什么」,你能翻账本一字不差地答出来。
动手试试
练习 1 跑一次 headless 任务,然后去看 $DSH_HOME 下生成的会话日志文件(session.vN.jsonl)。试着在里面找 turn/start、user/message、assistant/message、turn/end ------ 你会发现流程图里的每个节点,日志里都有对应的一条。这就是「模型可见即已落日志」的实物证据。
练习 2 在 --dump-config 的输出里找 headless-runner 那一行,看它的 inject: [headlessStartup]。然后想想:如果你把 headless-startup 那一行禁用掉,会发生什么?(提示:回到第 1 篇原理三和原理五 ------ headless-runner 会整个卸载,它注册的所有东西一起回滚。)
本篇小结
启动(一次性):
- CLI 只解析自己的 flag,其余透传
composeProfile按序收集各层补丁boot():new Context()→ 装 Loader(连加载器都是插件)→ 挂根 IncludeapplyEntryPatches:从空列表 长出插件树 ------ 按 id 定位、整体替换、最后写入者胜- Loader 按
inject依赖顺序实例化 assertEntriesActivated审计 →appReady.commit()
执行(循环):
- Step = 一次模型请求 + 它调用的工具;Turn = 零到多个 Step
- 主流程:认领输入 → 组装提示词 →
agent/pre-step(可改写/拒绝)→agent/request路由 → 从日志派生并冻结历史 → 流式调用 → 三段式执行工具 → 还欠就再来一轮 - 主循环只
inject六个抽象服务,不认识任何具体实现 - 铁律:模型可见即已落日志,靠「历史只能从日志派生」来保证
下篇预告
你现在知道 dsh 是怎么装配、怎么跑的了。但还缺最关键的一块:我怎么定制它?
第 0 篇你见过 --profile headless 和 $DSH_HOME/profiles/;第 3 篇你见过「各层补丁叠加」。现在把它们拼起来:
- Profile 到底是什么,里面装什么?
- Bundle 和 Profile 是什么关系?
- 那个
cordis.patch.yml到底怎么写?
👉 第 4 篇:Profile / Bundle / Patch 三件套 ------ 装修:套餐、建材包、图纸上的红笔修改。