# PTC 模式(preset id: `code`)
## 一句话定位
> PTC = Programmatic Tool Calling(编程式工具调用):工具没变、权限没变、能力没变——变的只有**模型调用工具的方式**:不再一个个调用工具,而是写一个 TypeScript 程序,把多步操作一次执行完。
组装文件开头的注释原文:
> "Everything in `standard` is here unchanged. What is added is the `tool-presentation` row: instead of one tool call per action, the model writes a TypeScript program against a generated SDK and `run_code` executes it, so a sequence that would be five round trips becomes one."
## 与标准模式的关系
| | 标准模式 | PTC 模式 |
|---|---|---|
| 工具集 | 全部 | 完全相同(逐行继承) |
| 沙箱/审批 | 相同 | 相同(都在主机层) |
| 差异 | — | 尾部多一行:`tool-presentation: mode: code` |
## 核心机制:呈现 vs 注册表
为什么工具注册表不能搬进 preset?因为它的消费者全在主机层——`dsh-agent-loop` 读它调度、`dsh-apiproxy` 读它渲染工具卡。preset 能拥有的只是注册表的**呈现方式**:
```
native 模式(标准): 模型看到 20+ 个工具 schema,逐个调用
code 模式(PTC): 模型只看到 1 个工具 run_code + 系统提示词里一段生成的 TS SDK
```
`dsh-agent-tool-presentation` 三种取值:`native` / `code` / `both`,每个 Agent 只声明一次(同组装里声明两次 = 矛盾,直接拒绝)。
关键一致性保证:在 code 模式下,注册表把模型直呼任何其他工具名解析为 `UNKNOWN_TOOL`——**通告面 = 可调用面**,防止模型以为能用某个工具实际却调不了。
## 模型看到的世界(来自 `dsh-tools` 的 code-mode 投影)
唯一的工具 `run_code`,两个参数:
1. `code`:**async 函数的函数体**——只能用可擦除 TS 语法(类型注解等),`enum`/`namespace` 会被拒;顶层 `await` 和 `return` 直接可用
2. `description`:5–10 词的主动语态摘要,**显示在 UI 上**(例:"Count TODO markers across packages")
工具描述原文:
> "Execute a TypeScript program against the available tools... Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it."
也就是说模型写的是:
```ts
// 模型写一个"程序",把 5 次往返压缩成 1 次
const files = await tools.glob({ pattern: "tests/**/*.py" });
const results = [];
for (const f of files) {
const content = await tools.read({ file_path: f });
if (content.includes("TODO")) results.push(f);
}
return results; // 只有 return/print 的内容回到模型上下文,中间值不回流
```
## 执行机制(两层账本)
- 内层:程序里的每个 `tools.xxx()` = 一次绑定桥接,在原生并发契约下嵌套调度为真实工具调用,**逐次记录日志供重建**
- 外层:只有 `run_code` 的**策展后结果**进入模型历史——中间绑定值只存在于执行环境里,绝不进上下文(token 干净)
语言支持:TypeScript 有已发布后端;Python 只有 schema 声明(无后端)。
## 运行时:worker 线程(隔离 ≠ 安全边界)
`dsh-code-runtime-worker-thread` 的官方立场:**这是隔离措施,不是安全边界——信任立场与 bash 刻意等价**。但提供 bash 没有的隔离:
| 机制 | 细节 |
|------|------|
| 全新 worker | 每次运行一个全新 `worker_threads.Worker`,**不池化**——程序的世界随 worker 一起死,无跨运行状态、无泄漏 |
| 类型剥离 | 宿主侧用 Node 实验性 `stripTypeScriptTypes` 剥类型(只支持可擦除语法),按字节位置切回原内容;程序作为 `AsyncFunction` 执行 |
| 双预算 | `computeMs: 60000`(实测事件循环忙碌时间,25ms 采样,热循环藏不住)+ `maxWallMs: 600000`(挂钟上限,兜底永不 resolve 的 promise);双双触发 `worker.terminate()`——**同步死循环也能杀**;堆溢出 = `worker-exit` |
| 空环境 | `env: {}` + `execArgv: []`——比 spawn 的清理环境规则更严,**拿不到任何环境变量里的凭据** |
| 堆上限 | `maxOldGenerationSizeMb: 512` |
| 输出账本 | console/stdout/stderr 按序流式入账本——**超时/被杀的程序也能看到已打印内容**;64 MiB 组合上限;抛出的百万字节 stack 在 worker 边界变成固定 `output-limit` 诊断 |
| 对端不可信 | 模型代码能访问 `parentPort` 伪造消息 → 宿主校验形状、重建、null-prototype 命名空间、只解析自有属性(伪造的 `constructor` 无法走原型链) |
失败语义:所有程序失败通过结果 `error` 字段报告,正交 kind 分类(解析/转换失败、异常、无效完成值、输出溢出、预算到期、中止、worker 死)。`run_code` 抛 `CODE_RUN_FAILED` → 变成带失败 kind + 已捕获日志的结构化结果,**模型可以据此自我纠错重试**。
## 已知限制(官方清单)
- 程序派生的 OS 进程在程序终止后**仍存活**(比 bash 的进程组终止更弱,孤儿进程清理是部署职责)
- 中间绑定值无字节上限(可耗尽内存)
- 64 MiB 是**拒绝边界**,不是可恢复存储——被拒的字节永远到不了落盘层
- console 只有 5 个方法的 shim
- 目前只有 worker 线程后端;`process`/`container` 是声明未实现
- `run()` 一次性:无流式日志接口
## 什么时候用 PTC 模式
适合:长序列、确定性的多步操作——批量文件处理、流水线式数据收集、模式化重构("改每个 cordis.yml 里的某个键")。省往返 = 省延迟和费用,也省 UI 渲染。
不适合:
- 交互式探索/试探性工作(每步都想看中间结果的场景)
- 逐步可见性要求高:UI 上你只会看到一张 `run_code` 卡片,看不到内部每一步的工具卡(内部调用有日志但不渲染成卡片)
- 一步失败就整程序失败——重试代价是重跑整个程序(虽然日志和失败 kind 会帮助模型收敛)
## 对你的实际意义
你现在这个会话是标准模式。如果哪天你要在 `xxx工作区` 做大量机械化的批量操作(比如"遍历 133 台设备的 capability 缓存文件、统计某个字段"这类),切一个空白会话到 PTC 模式能明显提效。切换规则记得吗:**只能在空白会话切换**(会话一旦产出内容就锁定模式),改了默认值只影响之后创建的会话。
---
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删