diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2e63cc14..9b03729e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,8 +11,6 @@ this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - 原生工具构造与发布拒绝队列不再向组合根挂监听或生命周期 effect:证据观察者与 result admission 改为在调用方上下文订阅并声明 global 分发,拒绝队列在 root 拒绝 effect 时回落到调用方生命周期。dsh-tui 0.20 会拒绝插件激活期内的 `root.events.on` 与 `root.effect`,而原生工具行正是插件激活,导致每次 `dsh-tui --resume` 都在 agent 挂载前报 "cannot resume session"。 - The Bash tool description now states the parse contract of `stdout`/`stderr` where the model reads it: `text` may be a partial fragment, `truncated` marks one that must not be parsed as complete structured output, and `spillPath` is optional and not a completeness guarantee. The typed output schema is unchanged: repeating the contract there would cost every PTC request without saying anything the tool text does not. Command semantics, exit codes, result values, limits, spill files, permissions and rendering are unchanged. - Bash 工具说明现在在模型读取它的地方写明 `stdout`/`stderr` 的解析契约:文本可能只是片段,`truncated` 标记的片段不得当作完整结构化输出解析,`spillPath` 是可选项且不保证完整。输出类型 schema 保持不变:在那里重复这段契约只会让每个 PTC 请求更贵,并不会多说明什么。命令语义、退出码、结果值、上限、落盘、权限与渲染均不变。 -- Turning a PTC preset's `dynamicTools` off now publishes the configured pool (`allowedTools` plus `result`) as resident instead of leaving the scope with no reachable API. The loader is gone, yet every callable API stays visible; this is the cache-friendly choice for providers that break on dynamically added tools. `residentTools: false` still publishes nothing, so the declaration-surface guard is preserved. -- PTC 预设关闭 `dynamicTools` 后改为把配置池(`allowedTools` 与 `result`)作为常驻发布,而不是让作用域失去任何可达 API:加载器消失,但全部可调用 API 仍然可见,这是给不支持动态工具增删的提供商准备的缓存友好选项。`residentTools: false` 仍不发布任何启动 API,声明面守卫得以保留。 ## [0.5.5] — 2026-11-13 — Patch release diff --git a/docs/configuration-reference.md b/docs/configuration-reference.md index f77cc35b..97fd05ae 100644 --- a/docs/configuration-reference.md +++ b/docs/configuration-reference.md @@ -68,11 +68,11 @@ the host's `loadOverlayPatches`; there is no matching preset row in the plugin m | Key | Default | Meaning | | --- | --- | --- | | `enabled` | true | Publish this surface for sessions of the `ops-ptc` preset; page switch, also settable in YAML | -| `residentTools` | true | Publish the startup APIs. With dynamic loading on, the PTC resident surface is only `ops_inspect_local_file` and everything else loads on demand through `ops_load_sdk`; with dynamic loading off, this switch also publishes the whole configured pool as resident | +| `residentTools` | true | Publish the startup APIs. The PTC resident surface is only `ops_inspect_local_file`; everything else loads on demand through `ops_load_sdk` | | `bash` | true | Allow publishing `ops_bash` on Windows; POSIX sessions keep the host bash | | `hidePwsh` | true | Hide the official Windows shell while Bash is available; disabled entirely when `global.hidePwsh` is false | | `hideOfficialTools` | true | Hide the official file tools this mode replaces; disabled entirely when `global.hideOfficialTools` is false | -| `dynamicTools` | true | Publish the progressive SDK loader `ops_load_sdk`; effective only while `global.dynamicTools` is also on (AND gate). Turning it off drops the loader and publishes `allowedTools` plus `result` as resident instead, so every API stays callable without dynamic tool addition; `residentTools: false` keeps publishing nothing | +| `dynamicTools` | true | Publish the progressive SDK loader `ops_load_sdk`; effective only while `global.dynamicTools` is also on (AND gate) | | `batchingGuidance` | true for `ops-ptc`, false otherwise | Teach once at session start (logged history, system prompt untouched), then correct the shape of root `run_code` programs: `bypass`, `plan` and `shape` each latch once per turn and re-arm when the program complies; the notice rides the triggering result as `additionalContexts` and never blocks or rewrites the program. Text lives in the editable `prompts/batching-guidance.md`. YAML-only; an omitted `ops-ptc` section in a legacy whole-row profile override retains true; explicit false disables it | | `spillRetentionDays` | 30 | Host subprocess spill retention window; `0` reports only and never deletes | | `spillTotalBudgetMiB` | 256 | Total retained spill budget in MiB; `0` disables the budget | diff --git a/docs/dynamic-tool-directory.md b/docs/dynamic-tool-directory.md index 0b7bedb3..d1ec1773 100644 --- a/docs/dynamic-tool-directory.md +++ b/docs/dynamic-tool-directory.md @@ -79,13 +79,12 @@ Under the `ops-ptc` mode, therefore, the SDK loader is the only progressive-disc `global.dynamicTools` are on (an AND gate; the plugin page renders the bundle switch as the master switch). See the OPS PTC mode section of the [configuration reference](configuration-reference.md) for every key. -- Turning `dynamicTools` off removes the loader and publishes the configured pool as resident instead: - `allowedTools` plus `result` are visible from the first step, so a provider that cannot take dynamically added - tools still sees every API it may call. This is the cache-friendly alternative to progressive disclosure. -- `residentTools: false` stays authoritative: a scope that asked for no startup APIs keeps publishing none, - which is the state the declaration-surface guard protects. -- `result` is the loader's implicit selection exception (loadable even with `allowedTools: []`); with the loader - gone it arrives as resident together with the pool, so result references stay readable. +- Turning `dynamicTools` off removes the loader, so no on-demand API can be newly loaded. Resident and + explicitly published APIs are untouched (`ops_inspect_local_file` and any `alwaysLoaded` API stay visible), + and APIs already loaded in this session keep working. +- `result` is the loader's implicit selection exception (loadable even with `allowedTools: []`), but without a + loader there is no entry point. If result references must survive the switch, put `result` in that section's + `alwaysLoaded`. - **Loaded APIs appear on the next turn.** `ops_load_sdk` only queues names; they are absent from the currently frozen SDK snapshot and become visible only after the owning session commits `step/end` and the next model step re-assembles. Do not call an API you just loaded in the same `run_code` — it always fails; `return` first and diff --git "a/docs/\345\212\250\346\200\201\345\267\245\345\205\267\347\233\256\345\275\225.md" "b/docs/\345\212\250\346\200\201\345\267\245\345\205\267\347\233\256\345\275\225.md" index aa003ca2..efef5886 100644 --- "a/docs/\345\212\250\346\200\201\345\267\245\345\205\267\347\233\256\345\275\225.md" +++ "b/docs/\345\212\250\346\200\201\345\267\245\345\205\267\347\233\256\345\275\225.md" @@ -42,11 +42,10 @@ PTC 呈现下本来就不发布 Native 的 `ops_discover_tools`:这是设计 入口:`ops-ptc.dynamicTools` 与 bundle 级 `global.dynamicTools` 同时开启时才发布 `ops_load_sdk` (两者是与门;插件页把 bundle 开关渲染为总闸)。完整配置见[配置参考](配置参考.md)的 OPS PTC 模式节。 -- 关闭 `dynamicTools` 后不再发布 `ops_load_sdk`,改为把整个配置池作为常驻发布:`allowedTools` 与 `result` - 从第一步起就可见,因此不支持动态工具增删的提供商仍能拿到全部可调用 API。这是渐进式披露的缓存友好替代。 -- `residentTools: false` 仍然有效:同时关闭常驻与动态加载时不发布任何启动 API,这正是声明面守卫保护的状态。 -- `result` 是加载器的隐式选择例外(`allowedTools: []` 时仍可加载);加载器消失后它与池一起作为常驻发布, - 结果引用因此仍可读取。 +- 关闭 `dynamicTools` 后不再发布 `ops_load_sdk`,也就没有新加载按需 API 的入口;常驻与显式发布的 API 不受影响 + (`ops_inspect_local_file` 以及配置为 `alwaysLoaded` 的 API 继续可见),本会话已加载的 API 仍可调用。 +- `result` 是加载器的隐式选择例外(`allowedTools: []` 时仍可加载),但没有加载器就没有加载入口。若要求关闭 + 渐进式披露后仍能使用结果引用,把它放进该分区的 `alwaysLoaded: [result]`。 - **加载的 API 下一轮才可见。** `ops_load_sdk` 只把名称排队;当前轮次冻结的 SDK 快照里没有它们,必须等所属 会话提交 `step/end`、下一个模型步骤重新组装后才出现。不要在同一个 `run_code` 里调用刚加载的 API (它一定失败);先 `return`,下一轮再调用。系统提示组装、诊断组装与工具注册通知都不会提前激活排队请求。 diff --git "a/docs/\351\205\215\347\275\256\345\217\202\350\200\203.md" "b/docs/\351\205\215\347\275\256\345\217\202\350\200\203.md" index 4c23fe0f..aaf4561e 100644 --- "a/docs/\351\205\215\347\275\256\345\217\202\350\200\203.md" +++ "b/docs/\351\205\215\347\275\256\345\217\202\350\200\203.md" @@ -43,11 +43,11 @@ OPS 沿用单行插件页:bundle 只插入一个 `dsh-ops` 行。内置预设 | 键 | 默认 | 含义 | | --- | --- | --- | | `enabled` | true | 对 `ops-ptc` 预设的会话发布本模式工具面;插件页有开关,YAML 也可设置 | -| `residentTools` | true | 发布启动 API。动态加载开启时 PTC 常驻面只有 `ops_inspect_local_file`,其余按需通过 `ops_load_sdk` 加载;动态加载关闭时该开关同时把整个配置池作为常驻发布 | +| `residentTools` | true | 发布启动 API。PTC 常驻面只有 `ops_inspect_local_file`,其余按需通过 `ops_load_sdk` 加载 | | `bash` | true | 允许为 Windows 会话发布 `ops_bash`;POSIX 沿用宿主 bash | | `hidePwsh` | true | Bash 可用时隐藏 Windows 官方 shell;`global.hidePwsh` 为 false 时整体失效 | | `hideOfficialTools` | true | 隐藏本模式替代的官方文件工具;`global.hideOfficialTools` 为 false 时整体失效 | -| `dynamicTools` | true | 发布渐进式 SDK 加载器 `ops_load_sdk`;与 `global.dynamicTools` 同时开启才生效(与门)。关闭后不再发布加载器,改为把 `allowedTools` 与 `result` 作为常驻发布,工具仍可直接调用;`residentTools: false` 时仍不发布任何启动 API | +| `dynamicTools` | true | 发布渐进式 SDK 加载器 `ops_load_sdk`;与 `global.dynamicTools` 同时开启才生效(与门) | | `batchingGuidance` | `ops-ptc` 为 true,其它模式为 false | 会话首轮注入一次完整教学(进入历史、不改系统提示词),之后按根 `run_code` 程序形状纠偏:`bypass`/`plan`/`shape` 三类每类每轮各锁一次、合规即解锁;提醒以 `additionalContexts` 附在触发调用结果上,不阻断也不改写程序。文案来自可编辑的 `prompts/batching-guidance.md`。仅 YAML 可配;旧 profile 整行覆盖配置且缺少 `ops-ptc` 节时仍默认 true,显式 false 可关闭 | | `spillRetentionDays` | 30 | 宿主 subprocess spill 保留窗口;`0` 只报告、永不删除 | | `spillTotalBudgetMiB` | 256 | 保留总字节预算(MiB);`0` 关闭预算 | diff --git a/src/client/locales.ts b/src/client/locales.ts index 0f55b7e0..4c8979f3 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -13,7 +13,7 @@ export const en = { swResidentTools: 'Enable resident OPS tools', swResidentToolsHint: 'Keep this preset’s baseline OPS tools loaded. Independent of dynamic tool loading; Bash has its own switch.', dynamicTools: 'Enable dynamic OPS tool loading', - dynamicToolsHint: 'When off, no preset offers the discovery entry; a preset with dynamic loading off publishes its whole pool as resident tools.', + dynamicToolsHint: 'When off, no preset offers the discovery entry.', swForceDefaultOpsPtc: 'Force the default agent preset to OPS (PTC)', swForceDefaultOpsPtcHint: 'While this row is enabled, new sessions default to the OPS (PTC) preset. Turning it off restores the default saved before, or the official default when none was saved.', featureEnabled: 'Enabled', @@ -39,7 +39,7 @@ export const en = { swBash: 'Enable the Bash tool', swBashHint: 'Publishes ops_bash for this preset’s sessions.', swHidePwsh: 'Hide the host PowerShell', swHidePwshHint: 'Off keeps the host pwsh visible; with dsh-win-pwsh7 installed the pwsh tool points at pwsh7.', swHideOfficialToolsHint: 'Hides the official file tools this preset replaces. Applies only while the bundle switch is on.', - swDynamicToolsHint: 'Offers the discovery entry to this preset. Applies only while the bundle switch is on. With it off, the preset publishes its whole pool as resident tools.', + swDynamicToolsHint: 'Offers the discovery entry to this preset. Applies only while the bundle switch is on.', unavailable: 'Enable this row with the built-in switch to edit its settings. A writable host profile is required.', loading: 'Loading host settings…', executorTitle: 'Bash executor', executorHint: 'One choice for every mode below: the Niubash payload that ships with the package, or a Git Bash/MSYS executable you name. Saving writes bashPath on the rows that publish Bash.', @@ -99,7 +99,7 @@ export const zh: Record = { swResidentTools: '启用 OPS 常驻工具', swResidentToolsHint: '常驻加载该预设的基础 OPS 工具;与动态工具加载独立,Bash 由单独开关控制。', dynamicTools: '启用 OPS 动态工具加载', - dynamicToolsHint: '关闭后没有任何预设提供发现入口;关闭动态加载的预设改为把整个工具池作为常驻工具发布。', + dynamicToolsHint: '关闭后没有任何预设提供发现入口。', swForceDefaultOpsPtc: '强制默认模式为 ops_ptc', swForceDefaultOpsPtcHint: '开启后新会话默认使用 OPS PTC 预设(需本插件行已启用且预设注册成功);关闭后恢复开启前的默认模式,之前没有保存过则回退官方默认。', featureEnabled: '启用', @@ -125,7 +125,7 @@ export const zh: Record = { swBash: '开启 Bash 工具', swBashHint: '为该预设的会话发布 ops_bash。', swHidePwsh: '隐藏宿主 PowerShell', swHidePwshHint: '关闭则保留宿主 pwsh 可见;已安装 dsh-win-pwsh7 时会把 pwsh 工具指向为 pwsh7。', swHideOfficialToolsHint: '隐藏该预设替换掉的原版文件工具。仅在总开关开启时生效。', - swDynamicToolsHint: '为该预设提供发现入口。仅在总开关开启时生效;关闭后该预设改为把整个工具池作为常驻工具发布。', + swDynamicToolsHint: '为该预设提供发现入口。仅在总开关开启时生效。', unavailable: '请先用内置开关启用此行。编辑设置需要可写的宿主配置。', loading: '正在加载宿主设置…', executorTitle: 'Bash 执行器', executorHint: '所有模式统一选择:使用随包的 Niubash,或指定一个 Git Bash / MSYS 可执行文件。保存时写入发布 Bash 的模式行的 bashPath。', diff --git a/src/publication/ptc.ts b/src/publication/ptc.ts index 37add69b..f9167c8e 100644 --- a/src/publication/ptc.ts +++ b/src/publication/ptc.ts @@ -74,17 +74,9 @@ export function mountPtc(root: Context, rawConfig: unknown, gate: PtcGate = { al let presentationRevision = 0 ctx.on('system-prompt/change', () => { ++presentationRevision }) let ownership: Awaited> | undefined - // Progressive disclosure off: the pool that would otherwise wait behind - // `ops_load_sdk` is published as resident instead. A provider that cannot take - // dynamically added tools then sees every API the scope may call, instead of a - // loader nothing can reach. - // `resident` stays authoritative: a scope that asked for no startup APIs keeps - // publishing none, which is the state #887 protects. - const exposed: ApiName[] = gate.dynamic === false && gate.resident !== false - ? [...config.allowedTools, 'result' as ApiName] : [] - const resident = new Set(gate.resident === false ? [] : [...PTC_RESIDENT_TOOLS, ...config.alwaysLoaded, ...exposed]) - // A loader-less scope can never *select* a pool, so it registers none as - // selectable; the same names already arrived through `exposed` above. + const resident = new Set(gate.resident === false ? [] : [...PTC_RESIDENT_TOOLS, ...config.alwaysLoaded]) + // A loader-less scope can never select the allowed pool: registering it only + // lets a host that renders registered definitions declare unreachable APIs. const selectable: ApiName[] = gate.dynamic === false ? [] : [...config.allowedTools, 'result' as ApiName] const pool = new Set([...resident, ...selectable]) const owns = (agent: Agent): boolean => !stopped && gate.allows(agent) && Boolean(ownership?.owns(agent)) diff --git a/test/modes.test.mjs b/test/modes.test.mjs index 5bdb0a74..d0ff2ff3 100644 --- a/test/modes.test.mjs +++ b/test/modes.test.mjs @@ -389,14 +389,11 @@ await test('mode-ops-ptc dynamicTools off withdraws the loader but keeps the res const agent = await mint({ preset: 'ops-ptc', presentation: 'ptc', cwd }) await waitFor(() => names(agent).includes(publicToolName(PTC_RESIDENT_TOOLS[0])), { what: 'resident PTC API' }) await wait(300) - // The switch removes progressive disclosure, never the APIs: the pool that would - // have waited behind the loader is published as resident instead, so a provider - // that cannot take dynamically added tools still sees the whole surface. + // The switch gates the loader and its selectable pool: the resident API and + // the executor hand-off stay, and the Native discovery face is never part of a PTC presentation. assert.equal(names(agent).includes('ops_load_sdk'), false) for (const raw of PTC_RESIDENT_TOOLS) assert.ok(names(agent).includes(publicToolName(raw)), raw) - for (const raw of ['write_file', 'grep', 'glob', 'read_files', 'stat', 'result']) { - assert.ok(names(agent).includes(publicToolName(raw)), 'a loader-less scope publishes ' + raw + ' as resident') - } + assert.equal(names(agent).includes(publicToolName('write_file')), false, 'a loader-less scope publishes no selectable pool') assert.equal(names(agent).includes(publicToolName('discover_tools')), false) } finally { await shutdown(ctx) } }))