19 · 实战二:用 profile 和 overlay 组成自己的 Agent¶
19.1 运行时工厂不等于另写应用入口¶
src/composition.ts 的 createNotesHarness() 负责整理宿主输入、建立临时 home、写 overlay,再调用公开 DeepSeekHarness SDK。SDK 实际启动同版本 dsh --profile sdk-minimal。它没有把一个自由 Cordis Context 当生产应用 launcher,也没有绕过官方 runtime 生命周期。
rc.2 的 sdk-minimal bundle 是完整 standalone tree,不是 base bundle 的少数隐藏 UI。它包含真正的模型路由、loop、Session、投影、JSONL 存储和 SDK server,但默认也有 persistent Shell 和 danger-full-access policy。因此名字“minimal”不等于低权限。
19.2 正确 patch 的 id 和配置覆盖¶
工厂生成的 overlay 首先禁用 persistent-bash、persistent-pwsh 两个 模型工具 plugin row,再用 llm-deepseek row 的完整 config 设置本次 endpoint、key 环境变量名、context window 与 5 秒 stream-idle timeout,最后 insert 自己的 provider 和 consumer。
[
{ "id": "persistent-bash", "disabled": true },
{ "id": "persistent-pwsh", "disabled": true },
{ "id": "llm-deepseek", "config": {
"apiKeyEnv": "DEEPSEEK_API_KEY",
"baseURL": "http://127.0.0.1:动态端口",
"defaultContextWindow": 1000000,
"streamIdleTimeoutMs": 5000
} },
{ "insert": [
{ "id": "course-note-provider", "name": "绝对路径/json-notes.js", "config": { "path": "绝对路径/notes.json" } },
{ "id": "course-note-tools", "name": "绝对路径/notes-tools.js", "config": { "maxSteps": 8 } }
] }
]
这是解释用结构,动态端口和路径必须由 composition.ts 填入,不能复制占位值运行。包的 config 键是 baseURL,不是 baseUrl;CLI/Python 参数的拼写又不同,见 14/20 章。Patch 的 row config 是 整份替换,不是自动深合并,所以我们把需要的字段显式写全。
禁用 persistent tool rows 后,terminal service/subprocess provider 仍可能作为 runtime 基础依赖存在。我们的实际模型请求断言只有两个笔记工具,说明模型可调用表面被收窄;不宣称整个插件树绝对不能创建进程或成为操作系统 sandbox。
19.3 home、cwd、processCwd 的用途¶
home 由 mkdtemp 放在系统临时目录。每次运行自己的 profile 初始化、Session 和 patch,不加载用户已有 home 的插件/凭据记录。cwd 是 SDK 创建 Session 的工作目录,processCwd 是 dsh 子进程启动目录;本例都显式设为解析后的 workspace。两者名字相近,但不是同一个配置作用点。
工厂要求 notes 和 notesPath 恰好选一项,防止两个 provider 同时发布相同 service。笔记 JSON 文件路径由宿主选择,不由模型选择;workspace 可以传自己的业务目录,但这不能自动赋予按租户隔离文件的保证。
实际 integration test 往目标 workspace 写入 AGENTS.md、SYSTEM.md sentinel,确认 sdk-minimal 的这个组合没有将它们放入模型请求。测试不声称穷尽所有文件发现规则,也不证明任意 profile 或额外 include plugin 都会忽略项目文件。若自己加回 agent-instructions/skills/context producer,必须重新测试数据边界。
19.4 子进程环境:两个 SDK 有实质区别¶
TypeScript SDK 中 env 对象替代子进程环境,工厂只传 PATH、显式 API key、显式 BASE URL 和 telemetry 关闭值。没有把宿主完整 process.env 复制给 Agent。PATH 必须保留,让 Node 的 shebang launcher 能找到解释器;自己的部署也要指定正确命令解析环境。
Python SDK 的 env 会与 os.environ 合并,所以给它 {} 并不会去掉父环境。Python demo 先由 Node execFile 用 allowlist 启动 Python 进程,再让 Python SDK 按公开参数启动 dsh。两层含义在实现和教程一致。
这仍不是对子进程可见文件、网络、动态 loader 或插件代码的强隔离。你选入 tree 的 JS/Python 插件是宿主运行的代码,应按自己的软件供应链管理。真正有不可信执行需要 sandbox/container/平台策略和单独测试。
19.5 步骤预算放在 agent/pre-step¶
笔记工具 plugin 用 WeakMap<Agent, { turn, count }> 跟踪每个 live Agent 本轮已准入 step。agent/pre-step 是 waterfall:达到上限返回 {kind: 'reject'};未达上限 await next(),只在 downstream 的 decision 是 enter 时加一,并将 完整原 decision 返回。
这保留其他监听者的 messages rewrite 与 startsRequestSeries 等标记,不把 next() 返回值替换成自己发明的固定 enter。budget reset 以新的 turn 为准,第二次 run 会重新开始计数。相同 turn 内多次 retry 不再经过 pre-step,所以这里限制 admitted steps,不限制 HTTP attempt 总数。
配置允许 1–100 的 safe integer,默认 8。测试连续工具调用、maxSteps 2,第三次请求没发出,最终 root turn reason 为 blocked。达到预算不会自动返回一段“成功”的文本,也不保证工具本轮已完成业务目标。
这个预算不能替代 token/cost/elapsed/request-attempt/concurrency 限额。maxTokens: 2048 限制单次请求输出,streamIdleTimeoutMs: 5000 限制流多久没新数据,requestTimeoutMs: 15000 限制部分 SDK 请求等待;特别是 SDK 的 run wait-for-idle 不等于有 15 秒硬总截止。要做生产任务,宿主要维护整体 deadline、进程终止与业务状态补偿。
19.6 关闭、flush 与业务结果¶
OwnedHarness.close() memoize 同一个 Promise,先 await harness.close(),再删除临时 home。demo 的 finally 同时关闭脚本 HTTP server。重复关闭应该等同一完成过程,而不是在前次进程还写日志时删除目录。
TS result 没有一个万能的 finishReason 字段。实验读取 durable turn/end.data.reason.kind,区别 completed、blocked 和 error。SDK 返回 idle 只说明当前 root 没有继续工作,HTTP 401 也会回到 idle;不能只检查 Promise resolve 或空 finalResponse 就写“任务成功”。
另一个实际测试发现:SDK 收到 turn/end 和 idle 时,文件 writer 可能仍未 flush。我们在检查 session.v4.jsonl 之前显式关闭 runtime,让正常收尾完成,再读取日志。这是正常关闭路径的验证,不是硬掉电/kill -9 耐久性测试。生产中的可靠交付还需要业务 commit/receipt 和明确 durable acknowledgement,见 16/17 章。
19.7 从实验改成自己的产品¶
先保留可重复本地 fixture,把 NoteStore 换成自己的 service;通过同样的 schema、拒绝、重试、副作用和 lifecycle 测试。然后用真实 API key 跑你自己的模型任务,观察实际 token、延迟与引用正确性。最后再加入 UI、审批、持久 home 和自动化,每加一个 producer 都复核模型可见数据与恢复行为。
临时 home 适合隔离实验,关闭后删除;需要跨宿主进程继续历史时必须换成应用拥有的持久 home,并设计并发 writer 与 retained data 清理。Session 是 durable evidence,业务数据库是交付状态,两者应建立明确关系,不能用“有一段回答”代替订单、文件或部署结果确认。