Parcourir la source

refactor(pty): rename model-facing tools to terminal_* and harden teardown

Rename the six model-facing tools pty_* -> terminal_* and align every
description, guidance section, ACP card title, and rendered result to
terminal terminology. Package and service internals keep their technical
PTY names (PtyService, "unknown PTY session", node-pty).

Harden the local backend teardown:
- a failed close is retryable: drop the memoized rejection so a later
  terminal_close re-runs against the live process table
- service disposal clears the backend, reservation, and owner-cleanup
  registries even when a close fails
- stop readiness polling before teardown so an in-flight send settles as
  session_exit instead of a mis-inferred wait reason
- bound the sanitizer's pending buffer against unterminated escape runs

Update the tool catalog, package READMEs, the bilingual Agent Note, and the
acp/headless pty-tools snapshots to match.
NI0317 il y a 1 mois
Parent
commit
a7bfade7eb
28 fichiers modifiés avec 569 ajouts et 402 suppressions
  1. 2 2
      .agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml
  2. 12 12
      .agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md
  3. 12 12
      .agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md
  4. 1 1
      docs/core-data-structures/pty.md
  5. 46 46
      docs/tool-catalog.md
  6. 27 27
      examples/acp-agent/tests/snapshots/pty-tools/session.jsonl
  7. 9 9
      examples/acp-agent/tests/snapshots/pty-tools/stdout.expected.jsonl
  8. 1 1
      examples/acp-agent/tests/snapshots/pty-tools/system-prompt.expected.md
  9. 129 129
      examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json
  10. 27 27
      examples/headless-agent/tests/snapshots/pty-tools/session.jsonl
  11. 27 27
      examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl
  12. 1 1
      packages/core/tools/tests/gen-tool-catalog.spec.ts
  13. 1 1
      packages/pty/README.md
  14. 1 1
      packages/pty/pty-local/README.md
  15. 54 1
      packages/pty/pty-local/src/sanitize.ts
  16. 19 5
      packages/pty/pty-local/src/session.ts
  17. 37 2
      packages/pty/pty-local/tests/sanitize.spec.ts
  18. 40 2
      packages/pty/pty-local/tests/session.spec.ts
  19. 12 6
      packages/pty/pty/src/index.ts
  20. 1 1
      packages/pty/pty/src/types.ts
  21. 21 0
      packages/pty/pty/tests/service.spec.ts
  22. 4 4
      packages/pty/tool-pty/README.md
  23. 34 34
      packages/pty/tool-pty/src/index.ts
  24. 3 3
      packages/pty/tool-pty/src/render.ts
  25. 5 5
      packages/pty/tool-pty/tests/loader-composition.spec.ts
  26. 2 2
      packages/pty/tool-pty/tests/render.spec.ts
  27. 40 40
      packages/pty/tool-pty/tests/tools.spec.ts
  28. 1 1
      scripts/gen-tool-catalog.ts

+ 2 - 2
.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-2026-07-16-persistent-pty-sessions.md: 1be87fcd8275b493bc0c552fb34a500a2c8bcce4
-2026-07-16-persistent-pty-sessions.zh.md: ffb0c490197120b6065ddeaf0584263a65ffd61c
+2026-07-16-persistent-pty-sessions.md: 76354891f557974b953a1b0b805d94330c9f6e69
+2026-07-16-persistent-pty-sessions.zh.md: 86200d70c6eda815a440c44e8b55457b0424edb6

+ 12 - 12
.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md

@@ -32,7 +32,7 @@ Idle detection is backend behavior, not a second public seam. A remote or contai
 
 `PtyService` stores live sessions process-locally, but every session is owned by the exact `Agent` passed through the tool execution context. The service mints an opaque `PtySessionId`; an optional model-chosen `name` is display metadata and is unique only within that owner. Every operation targets `sessionId`, and `list`/`read`/`signal`/`kill` reject callers other than the owner.
 
-There are no plugin-load auto-start sessions. `pty_spawn` creates a session only during an agent tool call, when ownership and the owning event-sourced session are known. A future declarative startup feature must compose through unpublished agent setup rather than create shared global terminals.
+There are no plugin-load auto-start sessions. `terminal_open` creates a session only during an agent tool call, when ownership and the owning event-sourced session are known. A future declarative startup feature must compose through unpublished agent setup rather than create shared global terminals.
 
 Agent-scope disposal closes registrations first, then awaits quiescent teardown of every owned PTY. Backend or tool-plugin reload does not orphan sessions: ownership lives in `PtyService` until the agent ends, following the same service-owned-record pattern as [`ctx.tasks`](../../../../packages/tasks/tasks/README.md).
 
@@ -51,22 +51,22 @@ The implementation uses only public `node-pty` capabilities: child PID, `data` a
 
 | Tool | Purpose | Result |
 |---|---|---|
-| `pty_spawn` | Create an owner-scoped session from a registered backend type | `{ sessionId, name, type, motd }` |
-| `pty_send` | Send text, optionally submit Enter, and wait for readiness or register a background task | bounded viewport plus wait and session status; background also returns `taskId` |
-| `pty_read` | Read a bounded page from retained scrollback | `{ text, totalLines, lineBegin, lineEnd, truncated }` |
-| `pty_signal` | Send one allowed signal to the current foreground process group | `{ delivered, targetPgid }` |
-| `pty_kill` | Close one session and await process-tree quiescence | `{ killed }` |
-| `pty_list` | List the caller's live sessions | owner-scoped session summaries |
+| `terminal_open` | Create an owner-scoped session from a registered backend type | `{ sessionId, name, type, motd }` |
+| `terminal_send` | Send text, optionally submit Enter, and wait for readiness or register a background task | bounded viewport plus wait and session status; background also returns `taskId` |
+| `terminal_read` | Read a bounded page from retained scrollback | `{ text, totalLines, lineBegin, lineEnd, truncated }` |
+| `terminal_signal` | Send one allowed signal to the current foreground process group | `{ delivered, targetPgid }` |
+| `terminal_close` | Close one session and await process-tree quiescence | `{ killed }` |
+| `terminal_list` | List the caller's live sessions | owner-scoped session summaries |
 
-`pty_send({ sessionId, text, submit?, run_in_background? })` treats `text` as UTF-8 bytes and resolves `submit` to `true` in the tool implementation. When `submit` is true it writes the platform Enter sequence after the text; when false it writes only the text, allowing control characters and REPL fragments without hidden content heuristics.
+`terminal_send({ sessionId, text, submit?, run_in_background? })` treats `text` as UTF-8 bytes and resolves `submit` to `true` in the tool implementation. When `submit` is true it writes the platform Enter sequence after the text; when false it writes only the text, allowing control characters and REPL fragments without hidden content heuristics.
 
 Foreground sends return a bounded rendered delta and two independent facts: `waitReason` (`stdin_read | inferred_idle | timeout | session_exit`) and `sessionStatus` (`running` or `exited` with exit code or signal). `session_exit` refers to the PTY's top-level shell process, not an arbitrary foreground command whose status the shell consumes. A timeout never implies process exit.
 
 With `run_in_background: true`, `dsh-tool-pty` registers the in-flight send on `ctx.tasks` and returns immediately with `taskId`. `task_output(wait: true)` waits, reads incremental output, and records the final result; `task_kill` forwards cancellation as `SIGINT` and escalates only through the PTY backend's owned teardown path. If the task surface is absent, background mode fails before writing input. No PTY-specific `sleep` tool or general wake-up seam is added.
 
-`pty_read` pages backward from the newest retained line. The backend enforces both line and UTF-8 byte caps on retained scrollback and the complete returned value, so one oversized line cannot bypass the bound. `truncated` distinguishes retention loss from an ordinary viewport delta.
+`terminal_read` pages backward from the newest retained line. The backend enforces both line and UTF-8 byte caps on retained scrollback and the complete returned value, so one oversized line cannot bypass the bound. `truncated` distinguishes retention loss from an ordinary viewport delta.
 
-`pty_signal` accepts the closed set `SIGINT | SIGTERM | SIGKILL | SIGTSTP | SIGHUP`. The backend resolves the terminal foreground process group at execution time. `SIGKILL` is rejected when that group is the top-level shell, directing the caller to `pty_kill`; a failed group lookup fails the operation instead of signaling a guessed PID.
+`terminal_signal` accepts the closed set `SIGINT | SIGTERM | SIGKILL | SIGTSTP | SIGHUP`. The backend resolves the terminal foreground process group at execution time. `SIGKILL` is rejected when that group is the top-level shell, directing the caller to `terminal_close`; a failed group lookup fails the operation instead of signaling a guessed PID.
 
 ### Local readiness detection
 
@@ -82,7 +82,7 @@ Tier 2 returns `inferred_idle` after `idleSilenceMs` without output. A sleeping
 
 ### Model-visible output and durability
 
-The existing durable `tool/call` and `tool/result` events are the source of truth for text sent by the model and rendered output returned to it. `pty_spawn` returns its MOTD through the logged tool result; foreground `send`/`read`/`list`/`signal`/`kill` results are logged the same way. The PTY packages do not duplicate raw byte streams into custom session events.
+The existing durable `tool/call` and `tool/result` events are the source of truth for text sent by the model and rendered output returned to it. `terminal_open` returns its MOTD through the logged tool result; foreground `send`/`read`/`list`/`signal`/`close` results are logged the same way. The PTY packages do not duplicate raw byte streams into custom session events.
 
 Background sends use the existing task completion notice and `task_output` result path, so any output that reaches a later model request is likewise durable. Raw terminal bytes remain bounded process-local state and are neither persisted nor restorable. A future opt-in transcript sink would need its own retention, credential, and privacy contract.
 
@@ -90,7 +90,7 @@ Background sends use the existing task completion notice and `task_output` resul
 
 The top-level `node-pty` child is the ownership anchor. On close, the backend stops callbacks, snapshots that PID and its transitive descendants by parent PID in children-first order, sends `SIGTERM`, closes the PTY, waits for quiescence, then sends `SIGKILL` to verified survivors after configurable `disposeGraceMs` and waits for them to leave the process table. Every captured PID includes process-start identity so reuse cannot redirect escalation.
 
-Teardown reports root exit and survivor cleanup independently. It does not claim success merely because the shell exited; disposal resolves only after no captured tree member remains or returns a structured cleanup failure naming the survivors. It never broadens ownership to every member of the root PID's POSIX session.
+Teardown reports root exit and survivor cleanup independently. It does not claim success merely because the shell exited; disposal resolves only after no captured tree member remains or returns a structured cleanup failure naming the survivors. Service disposal still clears its backend, reservation, and owner-detacher registries when a close fails. It never broadens ownership to every member of the root PID's POSIX session.
 
 ### Composition and rollout
 

+ 12 - 12
.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md

@@ -32,7 +32,7 @@ idle 检测属于后端行为,不是第二条公共 seam。远程或容器后
 
 `PtyService` 在进程内保存活会话,但每个会话都由工具执行上下文传入的确切 `Agent` 拥有。服务铸造不透明的 `PtySessionId`;模型可选填的 `name` 只是显示元数据,仅在该 owner 内唯一。所有操作都以 `sessionId` 为目标,`list`/`read`/`signal`/`kill` 会拒绝 owner 之外的调用方。
 
-实现不提供插件加载期 auto-start 会话。`pty_spawn` 只在 agent 工具调用期间创建会话,此时所有权和所属的事件溯源会话都已确定。未来的声明式启动功能必须通过尚未发布的 agent setup 组合,而不能创建全局共享终端。
+实现不提供插件加载期 auto-start 会话。`terminal_open` 只在 agent 工具调用期间创建会话,此时所有权和所属的事件溯源会话都已确定。未来的声明式启动功能必须通过尚未发布的 agent setup 组合,而不能创建全局共享终端。
 
 agent scope dispose 时先关闭注册,再等待全部所属 PTY 静默退出。后端或工具插件 reload 不会遗留会话:所有权持续存放在 `PtyService` 中,直到 agent 结束,与 [`ctx.tasks`](../../../../packages/tasks/tasks/README.md) 的服务持有记录模式一致。
 
@@ -51,22 +51,22 @@ agent scope dispose 时先关闭注册,再等待全部所属 PTY 静默退出
 
 | 工具 | 用途 | 结果 |
 |---|---|---|
-| `pty_spawn` | 从已注册的后端类型创建按 owner 隔离的会话 | `{ sessionId, name, type, motd }` |
-| `pty_send` | 发送文本、可选提交 Enter,并等待就绪或注册一个后台任务 | 有界 viewport、等待状态和会话状态;后台模式还返回 `taskId` |
-| `pty_read` | 从保留的 scrollback 读取一个有界页 | `{ text, totalLines, lineBegin, lineEnd, truncated }` |
-| `pty_signal` | 向当前前台进程组发送一种允许的信号 | `{ delivered, targetPgid }` |
-| `pty_kill` | 关闭一个会话并等待进程树静默退出 | `{ killed }` |
-| `pty_list` | 列出调用方的活会话 | 按 owner 隔离的会话摘要 |
+| `terminal_open` | 从已注册的后端类型创建按 owner 隔离的会话 | `{ sessionId, name, type, motd }` |
+| `terminal_send` | 发送文本、可选提交 Enter,并等待就绪或注册一个后台任务 | 有界 viewport、等待状态和会话状态;后台模式还返回 `taskId` |
+| `terminal_read` | 从保留的 scrollback 读取一个有界页 | `{ text, totalLines, lineBegin, lineEnd, truncated }` |
+| `terminal_signal` | 向当前前台进程组发送一种允许的信号 | `{ delivered, targetPgid }` |
+| `terminal_close` | 关闭一个会话并等待进程树静默退出 | `{ killed }` |
+| `terminal_list` | 列出调用方的活会话 | 按 owner 隔离的会话摘要 |
 
-`pty_send({ sessionId, text, submit?, run_in_background? })` 将 `text` 视为 UTF-8 字节,并由工具实现在解析阶段把 `submit` 默认成 `true`。`submit` 为 true 时先写入文本,再写入平台 Enter 序列;为 false 时只写文本,使控制字符和 REPL 片段无需隐藏的内容启发式即可发送。
+`terminal_send({ sessionId, text, submit?, run_in_background? })` 将 `text` 视为 UTF-8 字节,并由工具实现在解析阶段把 `submit` 默认成 `true`。`submit` 为 true 时先写入文本,再写入平台 Enter 序列;为 false 时只写文本,使控制字符和 REPL 片段无需隐藏的内容启发式即可发送。
 
 前台发送返回有界的渲染增量和两个独立事实:`waitReason`(`stdin_read | inferred_idle | timeout | session_exit`)与 `sessionStatus`(`running`,或携带退出码或信号的 `exited`)。`session_exit` 指 PTY 顶层 shell 进程退出,不指由 shell 消费状态的任意前台命令。timeout 从不意味着进程已经退出。
 
 当 `run_in_background: true` 时,`dsh-tool-pty` 在 `ctx.tasks` 上注册进行中的发送,并立即返回 `taskId`。`task_output(wait: true)` 负责等待、读取增量输出并记录最终结果;`task_kill` 将取消转发为 `SIGINT`,只有 PTY 后端拥有的 teardown 路径可以升级信号。若 task 对外接口不存在,后台模式必须在写入输入前失败。设计不新增 PTY 专用的 `sleep` 工具或通用唤醒 seam。
 
-`pty_read` 从最新保留行向后分页。后端同时对保留的 scrollback 和完整返回值执行行数与 UTF-8 字节上限,因此单个超长行无法绕过限制。`truncated` 用于区分保留数据丢失与普通 viewport 增量。
+`terminal_read` 从最新保留行向后分页。后端同时对保留的 scrollback 和完整返回值执行行数与 UTF-8 字节上限,因此单个超长行无法绕过限制。`truncated` 用于区分保留数据丢失与普通 viewport 增量。
 
-`pty_signal` 接受闭合集 `SIGINT | SIGTERM | SIGKILL | SIGTSTP | SIGHUP`。后端在执行时解析终端前台进程组。当目标组是顶层 shell 时拒绝 `SIGKILL`,并指引调用方使用 `pty_kill`;进程组解析失败时操作直接失败,而不是向猜测的 PID 发送信号。
+`terminal_signal` 接受闭合集 `SIGINT | SIGTERM | SIGKILL | SIGTSTP | SIGHUP`。后端在执行时解析终端前台进程组。当目标组是顶层 shell 时拒绝 `SIGKILL`,并指引调用方使用 `terminal_close`;进程组解析失败时操作直接失败,而不是向猜测的 PID 发送信号。
 
 ### 本地就绪检测
 
@@ -82,7 +82,7 @@ Tier 2 在持续 `idleSilenceMs` 没有输出后返回 `inferred_idle`,因此
 
 ### 模型可见输出与持久性
 
-现有持久化 `tool/call` 与 `tool/result` 事件是模型发送文本和返回给模型的渲染输出的真源。`pty_spawn` 通过已记录的工具结果返回 MOTD;前台 `send`/`read`/`list`/`signal`/`kill` 结果走同一路径记录。PTY 包不会把原始字节流重复写入自定义会话事件。
+现有持久化 `tool/call` 与 `tool/result` 事件是模型发送文本和返回给模型的渲染输出的真源。`terminal_open` 通过已记录的工具结果返回 MOTD;前台 `send`/`read`/`list`/`signal`/`close` 结果走同一路径记录。PTY 包不会把原始字节流重复写入自定义会话事件。
 
 后台发送复用现有后台任务完成通知和 `task_output` 结果路径,因此进入后续模型请求的任何输出同样持久化。原始终端字节只作为有界的进程内状态存在,既不持久化也不可恢复。未来的 opt-in transcript sink 必须拥有独立的保留、凭证和隐私契约。
 
@@ -90,7 +90,7 @@ Tier 2 在持续 `idleSilenceMs` 没有输出后返回 `inferred_idle`,因此
 
 顶层 `node-pty` 子进程是所有权锚点。关闭时,后端先停止 callback,再按父 PID 以子进程优先顺序捕获该 PID 及其传递子进程、发送 `SIGTERM`、关闭 PTY 并等待静默,然后在可配置的 `disposeGraceMs` 后向已验证的存活者发送 `SIGKILL`,并等待它们离开进程表。每个捕获的 PID 都包含进程启动身份,避免 PID 复用把升级信号发给无关进程。
 
-teardown 独立报告根进程退出与存活进程清理。它不会只因 shell 退出就声称成功;dispose 只有在已捕获的进程树成员全部消失后才完成,否则返回结构化清理失败并列出存活者。所有权绝不会扩大到根 PID 所属 POSIX 会话的全部成员。
+teardown 独立报告根进程退出与存活进程清理。它不会只因 shell 退出就声称成功;dispose 只有在已捕获的进程树成员全部消失后才完成,否则返回结构化清理失败并列出存活者。即使某个 close 失败,服务 dispose 仍会清空其后端、预留与 owner detacher 注册表。所有权绝不会扩大到根 PID 所属 POSIX 会话的全部成员。
 
 ### 组合与推行
 

+ 1 - 1
docs/core-data-structures/pty.md

@@ -37,7 +37,7 @@ interface PtyBackend {
 ```ts type-equiv
 /** Backend-owned live session retained by {@link PtyService}. */
 interface PtyBackendSession {
-  /** Initial bounded terminal output returned from `pty_spawn`. */
+  /** Initial bounded terminal output returned from `terminal_open`. */
   readonly motd: string
   /** Top-level process id when one exists. */
   readonly pid?: number

+ 46 - 46
docs/tool-catalog.md

@@ -21,7 +21,7 @@ This table connects model-visible tool names to the plugin package and service s
 | `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect`, `cordis_mount`, `cordis_unmount` | `ctx.tools` | `tool/call`, `tool/result`, `live plugin-tree mutations (mount/unmount)` | - | Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes. |
 | `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after successful file operations`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin. |
 | `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.bash`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. |
-| `@deepseek-ai/dsh-tool-pty` | `pty_kill`, `pty_list`, `pty_read`, `pty_send`, `pty_signal`, `pty_spawn` | `ctx.tools`, `ctx.pty`, `ctx.systemPrompt`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The six PTY tools are opt-in and complement one-shot bash/filesystem tools. `pty_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. |
+| `@deepseek-ai/dsh-tool-pty` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.tools`, `ctx.pty`, `ctx.systemPrompt`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot bash/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. |
 | `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.skills` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/repl-agent/cordis.yml` and `examples/acp-agent/cordis.yml`. |
 | `@deepseek-ai/dsh-tool-tasks` | `task_kill`, `task_list`, `task_output` | `ctx.tools`, `ctx.tasks`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `context/message via agent.inject() for background completion notices` | - | The kind-agnostic background-task control surface: a background bash command and a background subagent are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`. |
@@ -396,9 +396,9 @@ glob and grep are conditional bash-backed discovery tools: they register only wh
 
 ## `@deepseek-ai/dsh-tool-pty`
 
-### `pty_kill`
+### `terminal_close`
 
-Close one persistent PTY and wait until its captured owned process tree is gone.
+Close one persistent terminal and wait until its captured owned process tree is gone.
 
 ```json
 {
@@ -406,7 +406,7 @@ Close one persistent PTY and wait until its captured owned process tree is gone.
   "properties": {
     "sessionId": {
       "type": "string",
-      "description": "PTY session id."
+      "description": "Terminal session id."
     }
   },
   "required": [
@@ -417,9 +417,9 @@ Close one persistent PTY and wait until its captured owned process tree is gone.
 
 Source: [`packages/pty/tool-pty/src/index.ts`](../packages/pty/tool-pty/src/index.ts)
 
-### `pty_list`
+### `terminal_list`
 
-List persistent PTY sessions owned by the current agent.
+List persistent terminal sessions owned by the current agent.
 
 ```json
 {
@@ -430,9 +430,38 @@ List persistent PTY sessions owned by the current agent.
 
 Source: [`packages/pty/tool-pty/src/index.ts`](../packages/pty/tool-pty/src/index.ts)
 
-### `pty_read`
+### `terminal_open`
 
-Read a bounded page of retained output from a persistent PTY without sending input.
+Create a persistent, owner-isolated terminal session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "type": {
+      "type": "string",
+      "description": "Registered terminal backend type, usually \"shell\"."
+    },
+    "name": {
+      "type": "string",
+      "description": "Optional owner-local display name such as \"main\" or \"gdb\"."
+    },
+    "cwd": {
+      "type": "string",
+      "description": "Initial working directory. Defaults to the deployment workspace root."
+    }
+  },
+  "required": [
+    "type"
+  ]
+}
+```
+
+Source: [`packages/pty/tool-pty/src/index.ts`](../packages/pty/tool-pty/src/index.ts)
+
+### `terminal_read`
+
+Read a bounded page of retained output from a persistent terminal without sending input.
 
 ```json
 {
@@ -440,7 +469,7 @@ Read a bounded page of retained output from a persistent PTY without sending inp
   "properties": {
     "sessionId": {
       "type": "string",
-      "description": "PTY session id."
+      "description": "Terminal session id."
     },
     "offset": {
       "type": "number",
@@ -459,9 +488,9 @@ Read a bounded page of retained output from a persistent PTY without sending inp
 
 Source: [`packages/pty/tool-pty/src/index.ts`](../packages/pty/tool-pty/src/index.ts)
 
-### `pty_send`
+### `terminal_send`
 
-Send text to a persistent PTY. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.
+Send text to a persistent terminal. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.
 
 ```json
 {
@@ -469,7 +498,7 @@ Send text to a persistent PTY. By default Enter is submitted and the call waits
   "properties": {
     "sessionId": {
       "type": "string",
-      "description": "PTY session id returned by pty_spawn or pty_list."
+      "description": "Terminal session id returned by terminal_open or terminal_list."
     },
     "text": {
       "type": "string",
@@ -493,9 +522,9 @@ Send text to a persistent PTY. By default Enter is submitted and the call waits
 
 Source: [`packages/pty/tool-pty/src/index.ts`](../packages/pty/tool-pty/src/index.ts)
 
-### `pty_signal`
+### `terminal_signal`
 
-Send an allowed signal to the current foreground process group of a persistent PTY.
+Send an allowed signal to the current foreground process group of a persistent terminal.
 
 ```json
 {
@@ -503,11 +532,11 @@ Send an allowed signal to the current foreground process group of a persistent P
   "properties": {
     "sessionId": {
       "type": "string",
-      "description": "PTY session id."
+      "description": "Terminal session id."
     },
     "signal": {
       "type": "string",
-      "description": "Signal to deliver. Shell-targeted SIGKILL is rejected; use pty_kill.",
+      "description": "Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.",
       "enum": [
         "SIGINT",
         "SIGTERM",
@@ -526,36 +555,7 @@ Send an allowed signal to the current foreground process group of a persistent P
 
 Source: [`packages/pty/tool-pty/src/index.ts`](../packages/pty/tool-pty/src/index.ts)
 
-### `pty_spawn`
-
-Create a persistent, owner-isolated PTY session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "type": {
-      "type": "string",
-      "description": "Registered PTY backend type, usually \"shell\"."
-    },
-    "name": {
-      "type": "string",
-      "description": "Optional owner-local display name such as \"main\" or \"gdb\"."
-    },
-    "cwd": {
-      "type": "string",
-      "description": "Initial working directory. Defaults to the deployment workspace root."
-    }
-  },
-  "required": [
-    "type"
-  ]
-}
-```
-
-Source: [`packages/pty/tool-pty/src/index.ts`](../packages/pty/tool-pty/src/index.ts)
-
-The six PTY tools are opt-in and complement one-shot bash/filesystem tools. `pty_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema.
+The six terminal tools are opt-in and complement one-shot bash/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema.
 
 ## `@deepseek-ai/dsh-tool-skill`
 

+ 27 - 27
examples/acp-agent/tests/snapshots/pty-tools/session.jsonl

@@ -4,63 +4,63 @@
 {"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}}
 {"type":"request/header","seq":3,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
 {"type":"assistant/chunk","seq":4,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"pty_spawn","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}}
-{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}}
+{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"terminal_open","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}}
+{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}}
 {"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":9,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}
-{"type":"tool/call","seq":10,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}
-{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","content":[{"type":"text","text":"started PTY session pty-1 (main) [type: shell]\ndsh> "}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"}
+{"type":"assistant/message","seq":9,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}
+{"type":"tool/call","seq":10,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}
+{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","content":[{"type":"text","text":"started terminal session pty-1 (main) [type: shell]\ndsh> "}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"}
 {"type":"step/end","seq":12,"time":0,"data":{"turn":1,"step":1}}
 {"type":"step/start","seq":13,"time":0,"data":{"turn":1,"step":2}}
 {"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-send","name":"pty_send","argumentsDelta":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}
-{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}}
+{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-send","name":"terminal_send","argumentsDelta":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}
+{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}}
 {"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
-{"type":"tool/call","seq":20,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}
+{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
+{"type":"tool/call","seq":20,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}
 {"type":"tool/result","seq":21,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","content":[{"type":"text","text":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[wait: stdin_read]\n[session: running]"}],"isError":false,"meta":{"viewport":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> ","waitReason":"stdin_read","sessionStatus":{"kind":"running"},"truncated":false}},"sourceEventSeqs":[20],"surfaceOp":"append"}
 {"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":2}}
 {"type":"step/start","seq":23,"time":0,"data":{"turn":1,"step":3}}
 {"type":"assistant/chunk","seq":24,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-read","name":"pty_read","argumentsDelta":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}
-{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}}
+{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-read","name":"terminal_read","argumentsDelta":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}
+{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}}
 {"type":"assistant/chunk","seq":27,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":29,"time":0,"data":{"turn":1,"step":3,"content":[{"type":"tool-call","id":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
-{"type":"tool/call","seq":30,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}
+{"type":"assistant/message","seq":29,"time":0,"data":{"turn":1,"step":3,"content":[{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
+{"type":"tool/call","seq":30,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}
 {"type":"tool/result","seq":31,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","content":[{"type":"text","text":"dsh> printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[lines: 0-3 of 3]"}],"isError":false},"sourceEventSeqs":[30],"surfaceOp":"append"}
 {"type":"step/end","seq":32,"time":0,"data":{"turn":1,"step":3}}
 {"type":"step/start","seq":33,"time":0,"data":{"turn":1,"step":4}}
 {"type":"assistant/chunk","seq":34,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-signal","name":"pty_signal","argumentsDelta":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}
-{"type":"assistant/chunk","seq":36,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}}
+{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-signal","name":"terminal_signal","argumentsDelta":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}
+{"type":"assistant/chunk","seq":36,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}}
 {"type":"assistant/chunk","seq":37,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":38,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":39,"time":0,"data":{"turn":1,"step":4,"content":[{"type":"tool-call","id":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[34,35,36,37,38],"surfaceOp":"append"}
-{"type":"tool/call","seq":40,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}
+{"type":"assistant/message","seq":39,"time":0,"data":{"turn":1,"step":4,"content":[{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[34,35,36,37,38],"surfaceOp":"append"}
+{"type":"tool/call","seq":40,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}
 {"type":"tool/result","seq":41,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","content":[{"type":"text","text":"Error: unknown PTY session pty-missing"}],"isError":true},"sourceEventSeqs":[40],"surfaceOp":"append"}
 {"type":"step/end","seq":42,"time":0,"data":{"turn":1,"step":4}}
 {"type":"step/start","seq":43,"time":0,"data":{"turn":1,"step":5}}
 {"type":"assistant/chunk","seq":44,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-kill","name":"pty_kill","argumentsDelta":"{\"sessionId\":\"pty-1\"}"}}}
-{"type":"assistant/chunk","seq":46,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}}}}
+{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-kill","name":"terminal_close","argumentsDelta":"{\"sessionId\":\"pty-1\"}"}}}
+{"type":"assistant/chunk","seq":46,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}}}
 {"type":"assistant/chunk","seq":47,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":48,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":49,"time":0,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[44,45,46,47,48],"surfaceOp":"append"}
-{"type":"tool/call","seq":50,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}}
-{"type":"tool/result","seq":51,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","content":[{"type":"text","text":"killed PTY session pty-1"}],"isError":false},"sourceEventSeqs":[50],"surfaceOp":"append"}
+{"type":"assistant/message","seq":49,"time":0,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[44,45,46,47,48],"surfaceOp":"append"}
+{"type":"tool/call","seq":50,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}
+{"type":"tool/result","seq":51,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","content":[{"type":"text","text":"closed terminal session pty-1"}],"isError":false},"sourceEventSeqs":[50],"surfaceOp":"append"}
 {"type":"step/end","seq":52,"time":0,"data":{"turn":1,"step":5}}
 {"type":"step/start","seq":53,"time":0,"data":{"turn":1,"step":6}}
 {"type":"assistant/chunk","seq":54,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-list","name":"pty_list","argumentsDelta":"{}"}}}
-{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-list","name":"pty_list","arguments":"{}"}}}}
+{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-list","name":"terminal_list","argumentsDelta":"{}"}}}
+{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}}}}
 {"type":"assistant/chunk","seq":57,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":58,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":59,"time":0,"data":{"turn":1,"step":6,"content":[{"type":"tool-call","id":"pty-list","name":"pty_list","arguments":"{}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[54,55,56,57,58],"surfaceOp":"append"}
-{"type":"tool/call","seq":60,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","name":"pty_list","arguments":"{}"}}
-{"type":"tool/result","seq":61,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","content":[{"type":"text","text":"(no PTY sessions)"}],"isError":false},"sourceEventSeqs":[60],"surfaceOp":"append"}
+{"type":"assistant/message","seq":59,"time":0,"data":{"turn":1,"step":6,"content":[{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[54,55,56,57,58],"surfaceOp":"append"}
+{"type":"tool/call","seq":60,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","name":"terminal_list","arguments":"{}"}}
+{"type":"tool/result","seq":61,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","content":[{"type":"text","text":"(no terminal sessions)"}],"isError":false},"sourceEventSeqs":[60],"surfaceOp":"append"}
 {"type":"step/end","seq":62,"time":0,"data":{"turn":1,"step":6}}
 {"type":"step/start","seq":63,"time":0,"data":{"turn":1,"step":7}}
 {"type":"assistant/chunk","seq":64,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}

+ 9 - 9
examples/acp-agent/tests/snapshots/pty-tools/stdout.expected.jsonl

@@ -1,16 +1,16 @@
 {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
 {"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-spawn","title":"Start PTY main","kind":"execute","status":"in_progress"}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-spawn","status":"completed","content":[{"type":"content","content":{"type":"text","text":"started PTY session pty-1 (main) [type: shell]\ndsh> "}}]}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-send","title":"printf 'PTY_OK\\n'","kind":"execute","status":"in_progress","rawInput":"printf 'PTY_OK\\n'","content":[{"type":"content","content":{"type":"text","text":"PTY pty-1"}},{"type":"terminal","terminalId":"pty-send"}],"_meta":{"terminal_info":{"terminal_id":"pty-send","cwd":"{{cwd}}"}}}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-spawn","title":"Open terminal main","kind":"execute","status":"in_progress"}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-spawn","status":"completed","content":[{"type":"content","content":{"type":"text","text":"started terminal session pty-1 (main) [type: shell]\ndsh> "}}]}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-send","title":"printf 'PTY_OK\\n'","kind":"execute","status":"in_progress","rawInput":"printf 'PTY_OK\\n'","content":[{"type":"content","content":{"type":"text","text":"Terminal pty-1"}},{"type":"terminal","terminalId":"pty-send"}],"_meta":{"terminal_info":{"terminal_id":"pty-send","cwd":"{{cwd}}"}}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-send","status":"completed","_meta":{"terminal_output":{"terminal_id":"pty-send","data":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[wait: stdin_read]\n[session: running]"}}}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-read","title":"Read PTY pty-1","kind":"read","status":"in_progress","rawInput":{"sessionId":"pty-1","offset":0,"count":20}}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-read","title":"Read terminal pty-1","kind":"read","status":"in_progress","rawInput":{"sessionId":"pty-1","offset":0,"count":20}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-read","status":"completed","content":[{"type":"content","content":{"type":"text","text":"dsh> printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[lines: 0-3 of 3]"}}]}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-signal","title":"Signal PTY pty-missing","kind":"execute","status":"in_progress","rawInput":{"sessionId":"pty-missing","signal":"SIGINT"}}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-signal","title":"Signal terminal pty-missing","kind":"execute","status":"in_progress","rawInput":{"sessionId":"pty-missing","signal":"SIGINT"}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-signal","status":"failed","content":[{"type":"content","content":{"type":"text","text":"Error: unknown PTY session pty-missing"}}]}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-kill","title":"Kill PTY pty-1","kind":"delete","status":"in_progress"}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-kill","status":"completed","content":[{"type":"content","content":{"type":"text","text":"killed PTY session pty-1"}}]}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-list","title":"List PTY sessions","kind":"read","status":"in_progress"}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-list","status":"completed","content":[{"type":"content","content":{"type":"text","text":"(no PTY sessions)"}}]}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-kill","title":"Close terminal pty-1","kind":"delete","status":"in_progress"}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-kill","status":"completed","content":[{"type":"content","content":{"type":"text","text":"closed terminal session pty-1"}}]}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"pty-list","title":"List terminal sessions","kind":"read","status":"in_progress"}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"pty-list","status":"completed","content":[{"type":"content","content":{"type":"text","text":"(no terminal sessions)"}}]}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
 {"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}

+ 1 - 1
examples/acp-agent/tests/snapshots/pty-tools/system-prompt.expected.md

@@ -13,7 +13,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
 
 Check the [exit code: N] marker on every bash result; investigate failures before moving on.
 
-Use PTY only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every PTY session id and kill sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.
+Use a terminal session only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every terminal session id and close sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.
 
 Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
 

+ 129 - 129
examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json

@@ -87,135 +87,6 @@
         ]
       }
     },
-    {
-      "name": "pty_kill",
-      "description": "Close one persistent PTY and wait until its captured owned process tree is gone.",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "sessionId": {
-            "type": "string",
-            "description": "PTY session id."
-          }
-        },
-        "required": [
-          "sessionId"
-        ]
-      }
-    },
-    {
-      "name": "pty_list",
-      "description": "List persistent PTY sessions owned by the current agent.",
-      "parameters": {
-        "type": "object",
-        "properties": {}
-      }
-    },
-    {
-      "name": "pty_read",
-      "description": "Read a bounded page of retained output from a persistent PTY without sending input.",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "sessionId": {
-            "type": "string",
-            "description": "PTY session id."
-          },
-          "offset": {
-            "type": "number",
-            "description": "Newest-relative line offset (default 0)."
-          },
-          "count": {
-            "type": "number",
-            "description": "Requested line count (default 500; backend caps apply)."
-          }
-        },
-        "required": [
-          "sessionId"
-        ]
-      }
-    },
-    {
-      "name": "pty_send",
-      "description": "Send text to a persistent PTY. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "sessionId": {
-            "type": "string",
-            "description": "PTY session id returned by pty_spawn or pty_list."
-          },
-          "text": {
-            "type": "string",
-            "description": "UTF-8 text to write to the terminal."
-          },
-          "submit": {
-            "type": "boolean",
-            "description": "Submit Enter after text (default true). Set false for control characters or incomplete REPL input."
-          },
-          "run_in_background": {
-            "type": "boolean",
-            "description": "Return a task id immediately; collect with task_output or stop with task_kill."
-          }
-        },
-        "required": [
-          "sessionId",
-          "text"
-        ]
-      }
-    },
-    {
-      "name": "pty_signal",
-      "description": "Send an allowed signal to the current foreground process group of a persistent PTY.",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "sessionId": {
-            "type": "string",
-            "description": "PTY session id."
-          },
-          "signal": {
-            "type": "string",
-            "description": "Signal to deliver. Shell-targeted SIGKILL is rejected; use pty_kill.",
-            "enum": [
-              "SIGINT",
-              "SIGTERM",
-              "SIGKILL",
-              "SIGTSTP",
-              "SIGHUP"
-            ]
-          }
-        },
-        "required": [
-          "sessionId",
-          "signal"
-        ]
-      }
-    },
-    {
-      "name": "pty_spawn",
-      "description": "Create a persistent, owner-isolated PTY session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "description": "Registered PTY backend type, usually \"shell\"."
-          },
-          "name": {
-            "type": "string",
-            "description": "Optional owner-local display name such as \"main\" or \"gdb\"."
-          },
-          "cwd": {
-            "type": "string",
-            "description": "Initial working directory. Defaults to the deployment workspace root."
-          }
-        },
-        "required": [
-          "type"
-        ]
-      }
-    },
     {
       "name": "read",
       "description": "Read a UTF-8 text file and return line-numbered content.",
@@ -358,6 +229,135 @@
         ]
       }
     },
+    {
+      "name": "terminal_close",
+      "description": "Close one persistent terminal and wait until its captured owned process tree is gone.",
+      "parameters": {
+        "type": "object",
+        "properties": {
+          "sessionId": {
+            "type": "string",
+            "description": "Terminal session id."
+          }
+        },
+        "required": [
+          "sessionId"
+        ]
+      }
+    },
+    {
+      "name": "terminal_list",
+      "description": "List persistent terminal sessions owned by the current agent.",
+      "parameters": {
+        "type": "object",
+        "properties": {}
+      }
+    },
+    {
+      "name": "terminal_open",
+      "description": "Create a persistent, owner-isolated terminal session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.",
+      "parameters": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "description": "Registered terminal backend type, usually \"shell\"."
+          },
+          "name": {
+            "type": "string",
+            "description": "Optional owner-local display name such as \"main\" or \"gdb\"."
+          },
+          "cwd": {
+            "type": "string",
+            "description": "Initial working directory. Defaults to the deployment workspace root."
+          }
+        },
+        "required": [
+          "type"
+        ]
+      }
+    },
+    {
+      "name": "terminal_read",
+      "description": "Read a bounded page of retained output from a persistent terminal without sending input.",
+      "parameters": {
+        "type": "object",
+        "properties": {
+          "sessionId": {
+            "type": "string",
+            "description": "Terminal session id."
+          },
+          "offset": {
+            "type": "number",
+            "description": "Newest-relative line offset (default 0)."
+          },
+          "count": {
+            "type": "number",
+            "description": "Requested line count (default 500; backend caps apply)."
+          }
+        },
+        "required": [
+          "sessionId"
+        ]
+      }
+    },
+    {
+      "name": "terminal_send",
+      "description": "Send text to a persistent terminal. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.",
+      "parameters": {
+        "type": "object",
+        "properties": {
+          "sessionId": {
+            "type": "string",
+            "description": "Terminal session id returned by terminal_open or terminal_list."
+          },
+          "text": {
+            "type": "string",
+            "description": "UTF-8 text to write to the terminal."
+          },
+          "submit": {
+            "type": "boolean",
+            "description": "Submit Enter after text (default true). Set false for control characters or incomplete REPL input."
+          },
+          "run_in_background": {
+            "type": "boolean",
+            "description": "Return a task id immediately; collect with task_output or stop with task_kill."
+          }
+        },
+        "required": [
+          "sessionId",
+          "text"
+        ]
+      }
+    },
+    {
+      "name": "terminal_signal",
+      "description": "Send an allowed signal to the current foreground process group of a persistent terminal.",
+      "parameters": {
+        "type": "object",
+        "properties": {
+          "sessionId": {
+            "type": "string",
+            "description": "Terminal session id."
+          },
+          "signal": {
+            "type": "string",
+            "description": "Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.",
+            "enum": [
+              "SIGINT",
+              "SIGTERM",
+              "SIGKILL",
+              "SIGTSTP",
+              "SIGHUP"
+            ]
+          }
+        },
+        "required": [
+          "sessionId",
+          "signal"
+        ]
+      }
+    },
     {
       "name": "todo_write",
       "description": "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).",

+ 27 - 27
examples/headless-agent/tests/snapshots/pty-tools/session.jsonl

@@ -4,63 +4,63 @@
 {"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}}
 {"type":"request/header","seq":3,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
 {"type":"assistant/chunk","seq":4,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"pty_spawn","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}}
-{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}}
+{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"terminal_open","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}}
+{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}}
 {"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":9,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}
-{"type":"tool/call","seq":10,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}
-{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","content":[{"type":"text","text":"started PTY session pty-1 (main) [type: shell]\ndsh> "}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"}
+{"type":"assistant/message","seq":9,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}
+{"type":"tool/call","seq":10,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}
+{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","content":[{"type":"text","text":"started terminal session pty-1 (main) [type: shell]\ndsh> "}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"}
 {"type":"step/end","seq":12,"time":0,"data":{"turn":1,"step":1}}
 {"type":"step/start","seq":13,"time":0,"data":{"turn":1,"step":2}}
 {"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-send","name":"pty_send","argumentsDelta":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}
-{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}}
+{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-send","name":"terminal_send","argumentsDelta":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}
+{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}}
 {"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
-{"type":"tool/call","seq":20,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}
+{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
+{"type":"tool/call","seq":20,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}
 {"type":"tool/result","seq":21,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","content":[{"type":"text","text":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[wait: stdin_read]\n[session: running]"}],"isError":false,"meta":{"viewport":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> ","waitReason":"stdin_read","sessionStatus":{"kind":"running"},"truncated":false}},"sourceEventSeqs":[20],"surfaceOp":"append"}
 {"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":2}}
 {"type":"step/start","seq":23,"time":0,"data":{"turn":1,"step":3}}
 {"type":"assistant/chunk","seq":24,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-read","name":"pty_read","argumentsDelta":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}
-{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}}
+{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-read","name":"terminal_read","argumentsDelta":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}
+{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}}
 {"type":"assistant/chunk","seq":27,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":29,"time":0,"data":{"turn":1,"step":3,"content":[{"type":"tool-call","id":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
-{"type":"tool/call","seq":30,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}
+{"type":"assistant/message","seq":29,"time":0,"data":{"turn":1,"step":3,"content":[{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
+{"type":"tool/call","seq":30,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}
 {"type":"tool/result","seq":31,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","content":[{"type":"text","text":"dsh> printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[lines: 0-3 of 3]"}],"isError":false},"sourceEventSeqs":[30],"surfaceOp":"append"}
 {"type":"step/end","seq":32,"time":0,"data":{"turn":1,"step":3}}
 {"type":"step/start","seq":33,"time":0,"data":{"turn":1,"step":4}}
 {"type":"assistant/chunk","seq":34,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-signal","name":"pty_signal","argumentsDelta":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}
-{"type":"assistant/chunk","seq":36,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}}
+{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-signal","name":"terminal_signal","argumentsDelta":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}
+{"type":"assistant/chunk","seq":36,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}}
 {"type":"assistant/chunk","seq":37,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":38,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":39,"time":0,"data":{"turn":1,"step":4,"content":[{"type":"tool-call","id":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[34,35,36,37,38],"surfaceOp":"append"}
-{"type":"tool/call","seq":40,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}
+{"type":"assistant/message","seq":39,"time":0,"data":{"turn":1,"step":4,"content":[{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[34,35,36,37,38],"surfaceOp":"append"}
+{"type":"tool/call","seq":40,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}
 {"type":"tool/result","seq":41,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","content":[{"type":"text","text":"Error: unknown PTY session pty-missing"}],"isError":true},"sourceEventSeqs":[40],"surfaceOp":"append"}
 {"type":"step/end","seq":42,"time":0,"data":{"turn":1,"step":4}}
 {"type":"step/start","seq":43,"time":0,"data":{"turn":1,"step":5}}
 {"type":"assistant/chunk","seq":44,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-kill","name":"pty_kill","argumentsDelta":"{\"sessionId\":\"pty-1\"}"}}}
-{"type":"assistant/chunk","seq":46,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}}}}
+{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-kill","name":"terminal_close","argumentsDelta":"{\"sessionId\":\"pty-1\"}"}}}
+{"type":"assistant/chunk","seq":46,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}}}
 {"type":"assistant/chunk","seq":47,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":48,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":49,"time":0,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[44,45,46,47,48],"surfaceOp":"append"}
-{"type":"tool/call","seq":50,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}}
-{"type":"tool/result","seq":51,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","content":[{"type":"text","text":"killed PTY session pty-1"}],"isError":false},"sourceEventSeqs":[50],"surfaceOp":"append"}
+{"type":"assistant/message","seq":49,"time":0,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[44,45,46,47,48],"surfaceOp":"append"}
+{"type":"tool/call","seq":50,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}
+{"type":"tool/result","seq":51,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","content":[{"type":"text","text":"closed terminal session pty-1"}],"isError":false},"sourceEventSeqs":[50],"surfaceOp":"append"}
 {"type":"step/end","seq":52,"time":0,"data":{"turn":1,"step":5}}
 {"type":"step/start","seq":53,"time":0,"data":{"turn":1,"step":6}}
 {"type":"assistant/chunk","seq":54,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-list","name":"pty_list","argumentsDelta":"{}"}}}
-{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-list","name":"pty_list","arguments":"{}"}}}}
+{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-list","name":"terminal_list","argumentsDelta":"{}"}}}
+{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}}}}
 {"type":"assistant/chunk","seq":57,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","seq":58,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":59,"time":0,"data":{"turn":1,"step":6,"content":[{"type":"tool-call","id":"pty-list","name":"pty_list","arguments":"{}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[54,55,56,57,58],"surfaceOp":"append"}
-{"type":"tool/call","seq":60,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","name":"pty_list","arguments":"{}"}}
-{"type":"tool/result","seq":61,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","content":[{"type":"text","text":"(no PTY sessions)"}],"isError":false},"sourceEventSeqs":[60],"surfaceOp":"append"}
+{"type":"assistant/message","seq":59,"time":0,"data":{"turn":1,"step":6,"content":[{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[54,55,56,57,58],"surfaceOp":"append"}
+{"type":"tool/call","seq":60,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","name":"terminal_list","arguments":"{}"}}
+{"type":"tool/result","seq":61,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","content":[{"type":"text","text":"(no terminal sessions)"}],"isError":false},"sourceEventSeqs":[60],"surfaceOp":"append"}
 {"type":"step/end","seq":62,"time":0,"data":{"turn":1,"step":6}}
 {"type":"step/start","seq":63,"time":0,"data":{"turn":1,"step":7}}
 {"type":"assistant/chunk","seq":64,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}

+ 27 - 27
examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl

@@ -3,63 +3,63 @@
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":3,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":4,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"pty_spawn","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"terminal_open","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":9,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":10,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","name":"pty_spawn","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","content":[{"type":"text","text":"started PTY session pty-1 (main) [type: shell]\ndsh> "}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":9,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":10,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"pty-spawn","content":[{"type":"text","text":"started terminal session pty-1 (main) [type: shell]\ndsh> "}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":12,"time":0,"data":{"turn":1,"step":1}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":13,"time":0,"data":{"turn":1,"step":2}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-send","name":"pty_send","argumentsDelta":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-send","name":"terminal_send","argumentsDelta":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":20,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","name":"pty_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":19,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":20,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","name":"terminal_send","arguments":"{\"sessionId\":\"pty-1\",\"text\":\"printf 'PTY_OK\\\\n'\"}"}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":21,"time":0,"data":{"turn":1,"step":2,"callId":"pty-send","content":[{"type":"text","text":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[wait: stdin_read]\n[session: running]"}],"isError":false,"meta":{"viewport":"printf 'PTY_OK\\n'\nPTY_OK\ndsh> ","waitReason":"stdin_read","sessionStatus":{"kind":"running"},"truncated":false}},"sourceEventSeqs":[20],"surfaceOp":"append"}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":2}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":23,"time":0,"data":{"turn":1,"step":3}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":24,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-read","name":"pty_read","argumentsDelta":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-read","name":"terminal_read","argumentsDelta":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":27,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":29,"time":0,"data":{"turn":1,"step":3,"content":[{"type":"tool-call","id":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":30,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","name":"pty_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":29,"time":0,"data":{"turn":1,"step":3,"content":[{"type":"tool-call","id":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":30,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","name":"terminal_read","arguments":"{\"sessionId\":\"pty-1\",\"offset\":0,\"count\":20}"}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":31,"time":0,"data":{"turn":1,"step":3,"callId":"pty-read","content":[{"type":"text","text":"dsh> printf 'PTY_OK\\n'\nPTY_OK\ndsh> \n[lines: 0-3 of 3]"}],"isError":false},"sourceEventSeqs":[30],"surfaceOp":"append"}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":32,"time":0,"data":{"turn":1,"step":3}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":33,"time":0,"data":{"turn":1,"step":4}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":34,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-signal","name":"pty_signal","argumentsDelta":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":36,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-signal","name":"terminal_signal","argumentsDelta":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":36,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":37,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":38,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":39,"time":0,"data":{"turn":1,"step":4,"content":[{"type":"tool-call","id":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[34,35,36,37,38],"surfaceOp":"append"}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":40,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","name":"pty_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":39,"time":0,"data":{"turn":1,"step":4,"content":[{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[34,35,36,37,38],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":40,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":41,"time":0,"data":{"turn":1,"step":4,"callId":"pty-signal","content":[{"type":"text","text":"Error: unknown PTY session pty-missing"}],"isError":true},"sourceEventSeqs":[40],"surfaceOp":"append"}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":42,"time":0,"data":{"turn":1,"step":4}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":43,"time":0,"data":{"turn":1,"step":5}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":44,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-kill","name":"pty_kill","argumentsDelta":"{\"sessionId\":\"pty-1\"}"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":46,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-kill","name":"terminal_close","argumentsDelta":"{\"sessionId\":\"pty-1\"}"}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":46,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":47,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":48,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":49,"time":0,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[44,45,46,47,48],"surfaceOp":"append"}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":50,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","name":"pty_kill","arguments":"{\"sessionId\":\"pty-1\"}"}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":51,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","content":[{"type":"text","text":"killed PTY session pty-1"}],"isError":false},"sourceEventSeqs":[50],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":49,"time":0,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[44,45,46,47,48],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":50,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":51,"time":0,"data":{"turn":1,"step":5,"callId":"pty-kill","content":[{"type":"text","text":"closed terminal session pty-1"}],"isError":false},"sourceEventSeqs":[50],"surfaceOp":"append"}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":52,"time":0,"data":{"turn":1,"step":5}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":53,"time":0,"data":{"turn":1,"step":6}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":54,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-list","name":"pty_list","argumentsDelta":"{}"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-list","name":"pty_list","arguments":"{}"}}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-list","name":"terminal_list","argumentsDelta":"{}"}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":57,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":58,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":59,"time":0,"data":{"turn":1,"step":6,"content":[{"type":"tool-call","id":"pty-list","name":"pty_list","arguments":"{}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[54,55,56,57,58],"surfaceOp":"append"}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":60,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","name":"pty_list","arguments":"{}"}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":61,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","content":[{"type":"text","text":"(no PTY sessions)"}],"isError":false},"sourceEventSeqs":[60],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":59,"time":0,"data":{"turn":1,"step":6,"content":[{"type":"tool-call","id":"pty-list","name":"terminal_list","arguments":"{}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[54,55,56,57,58],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":60,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","name":"terminal_list","arguments":"{}"}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":61,"time":0,"data":{"turn":1,"step":6,"callId":"pty-list","content":[{"type":"text","text":"(no terminal sessions)"}],"isError":false},"sourceEventSeqs":[60],"surfaceOp":"append"}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":62,"time":0,"data":{"turn":1,"step":6}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":63,"time":0,"data":{"turn":1,"step":7}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":64,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}

+ 1 - 1
packages/core/tools/tests/gen-tool-catalog.spec.ts

@@ -23,7 +23,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
   it('boots every shipped tool package and harvests its model-facing schemas', async () => {
     const catalog = await collectToolCatalog()
     const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
-    expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'edit', 'glob', 'grep', 'pty_kill', 'pty_list', 'pty_read', 'pty_send', 'pty_signal', 'pty_spawn', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'todo_write', 'web_fetch', 'web_search', 'workflow', 'write'])
+    expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'edit', 'glob', 'grep', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'web_fetch', 'web_search', 'workflow', 'write'])
     // Every tool carries a JSON-Schema `parameters` object (what the model sees).
     for (const entry of catalog) {
       for (const schema of entry.schemas) {

+ 1 - 1
packages/pty/README.md

@@ -1,6 +1,6 @@
 # pty/ — persistent PTY capability family
 
-Persistent, owner-scoped terminal sessions for workflows that require state across tool calls or interactive stdin. PTY complements the one-shot bash and filesystem tools; it does not replace their stronger per-operation contracts.
+`PTY` stands for **Pseudo-Terminal**(伪终端). This capability provides persistent, owner-scoped terminal sessions for workflows that require state across tool calls or interactive stdin. PTY complements the one-shot bash and filesystem tools; it does not replace their stronger per-operation contracts.
 
 | Package | Role | ctx key |
 |---|---|---|

+ 1 - 1
packages/pty/pty-local/README.md

@@ -6,7 +6,7 @@ Local `node-pty` backend for `ctx.pty`. It starts an interactive shell under the
 
 The plugin injects `pty`, `sandbox`, and `sandboxPolicy`, then registers the configured backend type (`shell`). `danger-full-access` starts the shell directly; confined modes wrap the exact shell argv through `ctx.sandbox`. The current session-level sandbox override is resolved at spawn and remains fixed for the PTY lifetime.
 
-Linux readiness combines a private bash prompt marker, foreground-process-group syscall inspection, silence fallback, and absolute timeout. macOS uses the prompt marker plus silence/timeout because it has no `/proc` syscall surface. Unrecognized or unreadable process state is never a positive exact-idle signal. During unpublished startup, a fallback requires observed output; zero-output silence cannot publish an empty session, and timeout rejects the spawn.
+Linux readiness combines a foreground-verified private bash prompt marker, foreground-process-group syscall inspection, silence fallback, and absolute timeout. macOS uses the verified prompt marker plus silence/timeout because it has no `/proc` syscall surface. Unrecognized or unreadable process state is never a positive exact-idle signal. During unpublished startup, a fallback requires observed output; zero-output silence cannot publish an empty session, and timeout rejects the spawn. Incomplete terminal-control sequences are bounded by `maxReadBytes` and discarded through their terminator after crossing that limit.
 
 ## Model Experience
 

+ 54 - 1
packages/pty/pty-local/src/sanitize.ts

@@ -1,5 +1,7 @@
 /** Streaming terminal-control sanitizer for the line-oriented first release. */
 
+import { Buffer } from 'node:buffer'
+
 /** OSC marker emitted by the controlled bash before each prompt. */
 export const PROMPT_MARKER_PREFIX = '133;D;'
 
@@ -16,6 +18,10 @@ export interface SanitizedChunk {
  */
 export class TerminalSanitizer {
   private pending = ''
+  private discardMode: 'osc' | 'csi' | undefined
+  private discardOscEscape = false
+
+  constructor(private readonly maxPendingBytes: number) {}
 
   /**
    * Consume one decoded `node-pty` data chunk.
@@ -23,7 +29,7 @@ export class TerminalSanitizer {
    * @returns Printable text and whether the private prompt marker completed.
    */
   push(chunk: string): SanitizedChunk {
-    this.pending += chunk
+    this.pending += this.discardPrefix(chunk)
     let text = ''
     let prompt = false
     let index = 0
@@ -75,6 +81,7 @@ export class TerminalSanitizer {
       index = escape + 2
     }
     this.pending = this.pending.slice(index)
+    this.enforcePendingBound()
     return { text: normalizeTerminalText(text), prompt }
   }
 
@@ -85,8 +92,54 @@ export class TerminalSanitizer {
   flush(): string {
     const text = this.pending.startsWith('\x1b') ? '' : this.pending
     this.pending = ''
+    this.discardMode = undefined
+    this.discardOscEscape = false
     return normalizeTerminalText(text)
   }
+
+  private enforcePendingBound(): void {
+    if (Buffer.byteLength(this.pending) <= this.maxPendingBytes) return
+    this.discardMode = this.pending[1] === ']' ? 'osc' : 'csi'
+    this.pending = ''
+  }
+
+  private discardPrefix(chunk: string): string {
+    if (this.discardMode === undefined) return chunk
+    if (this.discardMode === 'csi') {
+      for (let index = 0; index < chunk.length; index += 1) {
+        const code = chunk.charCodeAt(index)
+        if (code >= 0x40 && code <= 0x7e) {
+          this.discardMode = undefined
+          return chunk.slice(index + 1)
+        }
+      }
+      return ''
+    }
+
+    let index = 0
+    if (this.discardOscEscape) {
+      this.discardOscEscape = false
+      if (chunk.startsWith('\\')) {
+        this.discardMode = undefined
+        return chunk.slice(1)
+      }
+    }
+    while (index < chunk.length) {
+      if (chunk[index] === '\x07') {
+        this.discardMode = undefined
+        return chunk.slice(index + 1)
+      }
+      if (chunk[index] === '\x1b') {
+        if (chunk[index + 1] === '\\') {
+          this.discardMode = undefined
+          return chunk.slice(index + 2)
+        }
+        if (index + 1 === chunk.length) this.discardOscEscape = true
+      }
+      index += 1
+    }
+    return ''
+  }
 }
 
 /**

+ 19 - 5
packages/pty/pty-local/src/session.ts

@@ -138,7 +138,7 @@ function signalName(number: number | undefined): NodeJS.Signals | null {
 export class LocalPtySession implements PtyBackendSession {
   motd = ''
   readonly pid: number
-  private readonly sanitizer = new TerminalSanitizer()
+  private readonly sanitizer: TerminalSanitizer
   private readonly scrollback: BoundedTextBuffer
   private readonly exitPromise: PromiseWithResolvers<void> = Promise.withResolvers<void>()
   private readonly dataDisposable: IDisposable
@@ -148,6 +148,7 @@ export class LocalPtySession implements PtyBackendSession {
   private activeTimer: NodeJS.Timeout | undefined
   private activeAbort: (() => void) | undefined
   private promptSeen = false
+  private shellPgid: number | undefined
   private initializing = false
   private lastOutputAt = Date.now()
   private closePromise: Promise<void> | undefined
@@ -158,6 +159,7 @@ export class LocalPtySession implements PtyBackendSession {
     private readonly config: ResolvedConfig,
   ) {
     this.pid = terminal.pid
+    this.sanitizer = new TerminalSanitizer(config.maxReadBytes)
     this.scrollback = new BoundedTextBuffer(config.scrollbackMaxBytes, config.scrollbackLines)
     this.dataDisposable = terminal.onData((data) => { this.onData(data) })
     this.exitDisposable = terminal.onExit(({ exitCode, signal }) => {
@@ -253,7 +255,7 @@ export class LocalPtySession implements PtyBackendSession {
       const pgid = this.inspector.foregroundPgid(this.pid)
       if (pgid === undefined) throw new Error(`cannot resolve foreground process group for PTY ${this.pid}`)
       if (signal === 'SIGKILL' && pgid === this.pid) {
-        throw new Error('refusing to SIGKILL the PTY shell; use pty_kill')
+        throw new Error('refusing to SIGKILL the PTY shell; use terminal_close')
       }
       this.inspector.signalGroup(pgid, signal)
       return { delivered: true, targetPgid: pgid }
@@ -273,8 +275,12 @@ export class LocalPtySession implements PtyBackendSession {
     const sanitized = this.sanitizer.push(data)
     this.appendOutput(sanitized.text)
     if (sanitized.prompt) {
-      this.promptSeen = true
-      this.lastOutputAt = Date.now()
+      const foregroundPgid = this.inspector.foregroundPgid(this.pid)
+      if (this.shellPgid === undefined) this.shellPgid = foregroundPgid
+      if (foregroundPgid !== undefined && foregroundPgid === this.shellPgid) {
+        this.promptSeen = true
+        this.lastOutputAt = Date.now()
+      }
     }
   }
 
@@ -319,9 +325,13 @@ export class LocalPtySession implements PtyBackendSession {
     operation.settle(waitReason, this.statusValue, scrollbackTruncated)
   }
 
-  private clearActive(): void {
+  private stopPolling(): void {
     if (this.activeTimer !== undefined) clearInterval(this.activeTimer)
     this.activeTimer = undefined
+  }
+
+  private clearActive(): void {
+    this.stopPolling()
     this.activeAbort?.()
     this.activeAbort = undefined
     this.active = undefined
@@ -329,6 +339,10 @@ export class LocalPtySession implements PtyBackendSession {
 
   private async closeOnce(reason: string): Promise<void> {
     this.dataDisposable.dispose()
+    // Stop readiness polling but retain the active operation: teardown settles
+    // it as session_exit below, so an in-flight send is never mis-settled as
+    // stdin_read/inferred_idle/timeout during the grace period.
+    this.stopPolling()
     const members = this.inspector.processTree(this.pid)
     for (const member of members) {
       try {

+ 37 - 2
packages/pty/pty-local/tests/sanitize.spec.ts

@@ -3,7 +3,7 @@ import { normalizeTerminalText, TerminalSanitizer } from '@deepseek-ai/dsh-pty-l
 
 describe('TerminalSanitizer', () => {
   it('removes split CSI and owned OSC prompt markers', () => {
-    const sanitizer = new TerminalSanitizer()
+    const sanitizer = new TerminalSanitizer(64)
     expect(sanitizer.push('red\x1b[3')).toEqual({ text: 'red', prompt: false })
     expect(sanitizer.push('1m text\x1b[0m\r\n')).toEqual({ text: ' text\n', prompt: false })
     expect(sanitizer.push('\x1b]133;')).toEqual({ text: '', prompt: false })
@@ -11,7 +11,7 @@ describe('TerminalSanitizer', () => {
   })
 
   it('drops unrelated OSC, short escapes, BEL, and incomplete trailing escape', () => {
-    const sanitizer = new TerminalSanitizer()
+    const sanitizer = new TerminalSanitizer(64)
     expect(sanitizer.push('a\x1b]0;title\x1b\\b\x1b7c\x07')).toEqual({ text: 'abc', prompt: false })
     expect(sanitizer.push('tail\x1b')).toEqual({ text: 'tail', prompt: false })
     expect(sanitizer.flush()).toBe('')
@@ -24,4 +24,39 @@ describe('TerminalSanitizer', () => {
   it('normalizes CRLF and standalone carriage returns', () => {
     expect(normalizeTerminalText('a\r\nb\rc\x07')).toBe('a\nb\nc')
   })
+
+  it('bounds and discards unterminated control sequences through their terminators', () => {
+    const oscBel = new TerminalSanitizer(8)
+    expect(oscBel.push(`\x1b]0;${'x'.repeat(16)}`)).toEqual({ text: '', prompt: false })
+    expect(oscBel.push('more\x07tail')).toEqual({ text: 'tail', prompt: false })
+
+    const oscSt = new TerminalSanitizer(8)
+    oscSt.push(`\x1b]0;${'x'.repeat(16)}`)
+    expect(oscSt.push('more\x1b')).toEqual({ text: '', prompt: false })
+    expect(oscSt.push('\\tail')).toEqual({ text: 'tail', prompt: false })
+
+    const oscDirectSt = new TerminalSanitizer(8)
+    oscDirectSt.push(`\x1b]0;${'x'.repeat(16)}`)
+    expect(oscDirectSt.push('more\x1b\\tail')).toEqual({ text: 'tail', prompt: false })
+
+    const oscFalseSt = new TerminalSanitizer(8)
+    oscFalseSt.push(`\x1b]0;${'x'.repeat(16)}`)
+    oscFalseSt.push('\x1b')
+    expect(oscFalseSt.push('more')).toEqual({ text: '', prompt: false })
+    expect(oscFalseSt.push('\x07tail')).toEqual({ text: 'tail', prompt: false })
+
+    const oscNonTerminatingEscape = new TerminalSanitizer(8)
+    oscNonTerminatingEscape.push(`\x1b]0;${'x'.repeat(16)}`)
+    expect(oscNonTerminatingEscape.push('more\x1bxmore\x07tail')).toEqual({ text: 'tail', prompt: false })
+
+    const csi = new TerminalSanitizer(8)
+    expect(csi.push(`\x1b[${'1'.repeat(16)}`)).toEqual({ text: '', prompt: false })
+    expect(csi.push('123')).toEqual({ text: '', prompt: false })
+    expect(csi.push('mtext')).toEqual({ text: 'text', prompt: false })
+
+    const flushed = new TerminalSanitizer(8)
+    flushed.push(`\x1b]0;${'x'.repeat(16)}`)
+    expect(flushed.flush()).toBe('')
+    expect(flushed.push('text')).toEqual({ text: 'text', prompt: false })
+  })
 })

+ 40 - 2
packages/pty/pty-local/tests/session.spec.ts

@@ -124,9 +124,9 @@ describe('LocalPtySession readiness and output', () => {
     vi.useFakeTimers()
     const terminal = new FakeTerminal()
     const inspector = new FakeInspector()
-    inspector.pgid = undefined
     const session = new LocalPtySession(terminal.asPty(), inspector, config())
     await initialize(session, terminal)
+    inspector.pgid = undefined
 
     const inferred = session.startSend({ text: 'sleep', submit: false })
     terminal.emitData('working')
@@ -238,6 +238,27 @@ describe('LocalPtySession readiness and output', () => {
     await vi.advanceTimersByTimeAsync(100)
     await timedOut
   })
+
+  it('trusts prompt markers only while the startup shell owns the foreground group', async () => {
+    vi.useFakeTimers()
+    const terminal = new FakeTerminal()
+    const inspector = new FakeInspector()
+    const session = new LocalPtySession(terminal.asPty(), inspector, config())
+    await initialize(session, terminal)
+
+    const operation = session.startSend({ text: 'run', submit: true })
+    let settled = false
+    void operation.done.then(() => { settled = true })
+    inspector.pgid = 789
+    terminal.emitData('\x1b]133;D;0\x07spoofed')
+    await vi.advanceTimersByTimeAsync(10)
+    expect(settled).toBe(false)
+
+    inspector.pgid = 456
+    terminal.emitData('\x1b]133;D;0\x07dsh> ')
+    await vi.advanceTimersByTimeAsync(10)
+    expect((await operation.done).waitReason).toBe('stdin_read')
+  })
 })
 
 describe('LocalPtySession bounds, signals, and teardown', () => {
@@ -278,7 +299,7 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
     const session = new LocalPtySession(terminal.asPty(), inspector, config())
     expect(await session.signal('SIGINT')).toEqual({ delivered: true, targetPgid: 456 })
     inspector.pgid = terminal.pid
-    await expect(session.signal('SIGKILL')).rejects.toThrow('use pty_kill')
+    await expect(session.signal('SIGKILL')).rejects.toThrow('use terminal_close')
     inspector.pgid = undefined
     await expect(session.signal('SIGTERM')).rejects.toThrow('cannot resolve')
   })
@@ -297,6 +318,23 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
     expect(() => session.startSend({ text: '', submit: false })).toThrow('closing')
   })
 
+  it('settles an active send as session_exit when closed mid-operation', async () => {
+    vi.useFakeTimers()
+    const terminal = new FakeTerminal()
+    const session = new LocalPtySession(terminal.asPty(), new FakeInspector(), config({ disposeGraceMs: 50 }))
+    await initialize(session, terminal)
+    const operation = session.startSend({ text: 'run', submit: true })
+    // The shell returns to its prompt while the send is active; a running
+    // readiness poll would otherwise mis-settle this as stdin_read once close
+    // begins, so teardown must stop polling before its grace period.
+    terminal.emitData('\x1b]133;D;0\x07dsh> ')
+    terminal.throwKill = true
+    const closing = session.close('mid-send')
+    await vi.advanceTimersByTimeAsync(60)
+    expect((await operation.done).waitReason).toBe('session_exit')
+    await closing
+  })
+
   it('waits for SIGKILL recipients to leave the process table after the shell exits', async () => {
     vi.useFakeTimers()
     const terminal = new FakeTerminal()

+ 12 - 6
packages/pty/pty/src/index.ts

@@ -331,12 +331,18 @@ export class PtyService extends Service {
   private async disposeAll(): Promise<void> {
     this.disposing = true
     const records = [...this.sessions.values()]
-    await this.closeRecords(records, 'PTY service disposed')
-    this.backends.clear()
-    this.reservedNames.clear()
-    const cleanups = [...this.ownerCleanups.values()]
-    this.ownerCleanups.clear()
-    await Promise.all(cleanups.map(cleanup => Promise.resolve(cleanup())))
+    // Teardown is best-effort: a close failure still clears registries and runs
+    // owner cleanups before the aggregated error propagates, so one stuck
+    // session cannot orphan backends, reservations, or owner detachers.
+    try {
+      await this.closeRecords(records, 'PTY service disposed')
+    } finally {
+      this.backends.clear()
+      this.reservedNames.clear()
+      const cleanups = [...this.ownerCleanups.values()]
+      this.ownerCleanups.clear()
+      await Promise.all(cleanups.map(cleanup => Promise.resolve(cleanup())))
+    }
   }
 
   private async closeRecords(records: SessionRecord[], reason: string): Promise<void> {

+ 1 - 1
packages/pty/pty/src/types.ts

@@ -127,7 +127,7 @@ export interface PtySessionSnapshot {
 
 /** Backend-owned live session retained by {@link PtyService}. */
 export interface PtyBackendSession {
-  /** Initial bounded terminal output returned from `pty_spawn`. */
+  /** Initial bounded terminal output returned from `terminal_open`. */
   readonly motd: string
   /** Top-level process id when one exists. */
   readonly pid?: number

+ 21 - 0
packages/pty/pty/tests/service.spec.ts

@@ -343,4 +343,25 @@ describe('PtyService ownership and lifecycle', () => {
     await disposePtyService(ctx)
     await expect(service.spawn(owner, { type: 'stub' })).rejects.toMatchObject({ code: 'SERVICE_DISPOSING' })
   })
+
+  it('clears registries and runs owner cleanups even when a session close fails', async () => {
+    const ctx = await harness()
+    const service = ctx.pty
+    const b = backend()
+    service.registerBackend(b.provider)
+    const owner = stubAgent(ctx, 'owner')
+    ctx.agents.register(owner)
+    await service.spawn(owner, { type: 'stub' })
+    b.sessions[0]!.rejectClose = true
+    const internal = service as unknown as {
+      disposeAll(): Promise<void>
+      backends: Map<string, unknown>
+      ownerCleanups: Map<Agent, unknown>
+    }
+    // Teardown surfaces the close failure, but its finally still clears the
+    // backend and owner-cleanup registries instead of orphaning them.
+    await expect(internal.disposeAll()).rejects.toThrow('failed to close 1 PTY session')
+    expect(internal.backends.size).toBe(0)
+    expect(internal.ownerCleanups.size).toBe(0)
+  })
 })

+ 4 - 4
packages/pty/tool-pty/README.md

@@ -1,8 +1,8 @@
 # @deepseek-ai/dsh-tool-pty
 
-Six model-facing tools over `ctx.pty`: `pty_spawn`, `pty_send`, `pty_read`, `pty_signal`, `pty_kill`, and `pty_list`. Every operation requires the exact initiating `Agent`, so a model cannot address another agent's terminal even if it learns the id.
+Six model-facing tools over `ctx.pty`: `terminal_open`, `terminal_send`, `terminal_read`, `terminal_signal`, `terminal_close`, and `terminal_list`. Every operation requires the exact initiating `Agent`, so a model cannot address another agent's terminal even if it learns the id.
 
-`pty_send(run_in_background: true)` reuses `ctx.tasks`; task preflight occurs before any terminal write, completion is collected with `task_output`, and `task_kill` requests `Ctrl-C`. Foreground sends use terminal ACP cards; lifecycle, history, signal, and list calls use generic cards.
+`terminal_send(run_in_background: true)` reuses `ctx.tasks`; task preflight occurs before any terminal write, completion is collected with `task_output`, and `task_kill` requests `Ctrl-C`. Foreground sends use terminal ACP cards; lifecycle, history, signal, and list calls use generic cards.
 
 ## Model Experience
 
@@ -12,10 +12,10 @@ Six model-facing tools over `ctx.pty`: `pty_spawn`, `pty_send`, `pty_read`, `pty
 
 The plugin contributes this fixed guidance section:
 
-##### PTY guidance
+##### Terminal guidance
 
 ```markdown
-Use PTY only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every PTY session id and kill sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.
+Use a terminal session only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every terminal session id and close sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.
 ```
 
 #### Token effect

+ 34 - 34
packages/pty/tool-pty/src/index.ts

@@ -1,5 +1,5 @@
 /**
- * Six model-facing persistent PTY tools. Owner identity comes from the exact
+ * Six model-facing persistent terminal tools. Owner identity comes from the exact
  * tool execution Agent; generic `ctx.tasks` owns background ids and collection.
  * @module @deepseek-ai/dsh-tool-pty
  */
@@ -51,7 +51,7 @@ interface SignalArgs extends SessionArgs {
 }
 
 function requireAgent(agent: Agent | undefined): Agent {
-  if (agent === undefined) throw new Error('PTY tools require an initiating agent')
+  if (agent === undefined) throw new Error('terminal tools require an initiating agent')
   return agent
 }
 
@@ -78,19 +78,19 @@ function sendDetail(result: PtySendResult): string {
     : `session exited: ${result.sessionStatus.exitCode ?? result.sessionStatus.signal ?? 'unknown'}`
 }
 
-/** Register all PTY tools and the minimal usage guidance. */
+/** Register all terminal tools and the minimal usage guidance. */
 export function apply(ctx: Context): void {
   ctx.systemPrompt.section({
     name: 'tool:pty',
     order: 106,
-    text: 'Use PTY only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every PTY session id and kill sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.',
+    text: 'Use a terminal session only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every terminal session id and close sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.',
   })
 
   ctx.tools.register(defineTool({
-    name: 'pty_spawn',
-    description: 'Create a persistent, owner-isolated PTY session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.',
+    name: 'terminal_open',
+    description: 'Create a persistent, owner-isolated terminal session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.',
     parameters: {
-      type: { type: 'string', required: true, description: 'Registered PTY backend type, usually "shell".' },
+      type: { type: 'string', required: true, description: 'Registered terminal backend type, usually "shell".' },
       name: { type: 'string', description: 'Optional owner-local display name such as "main" or "gdb".' },
       cwd: { type: 'string', description: 'Initial working directory. Defaults to the deployment workspace root.' },
     },
@@ -105,15 +105,15 @@ export function apply(ctx: Context): void {
     },
     presentCall: (args) => {
       const parsed = args
-      return { card: 'generic', title: `Start PTY ${parsed.name ?? parsed.type}`, kind: 'execute' }
+      return { card: 'generic', title: `Open terminal ${parsed.name ?? parsed.type}`, kind: 'execute' }
     },
   }))
 
   ctx.tools.register(defineTool({
-    name: 'pty_send',
-    description: 'Send text to a persistent PTY. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.',
+    name: 'terminal_send',
+    description: 'Send text to a persistent terminal. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.',
     parameters: {
-      sessionId: { type: 'string', required: true, description: 'PTY session id returned by pty_spawn or pty_list.' },
+      sessionId: { type: 'string', required: true, description: 'Terminal session id returned by terminal_open or terminal_list.' },
       text: { type: 'string', required: true, description: 'UTF-8 text to write to the terminal.' },
       submit: { type: 'boolean', description: 'Submit Enter after text (default true). Set false for control characters or incomplete REPL input.' },
       run_in_background: { type: 'boolean', description: 'Return a task id immediately; collect with task_output or stop with task_kill.' },
@@ -124,8 +124,8 @@ export function apply(ctx: Context): void {
       const request = { text: args.text, submit: args.submit ?? true }
       if (args.run_in_background === true) {
         const tasks = ctx.get('tasks')
-        if (tasks === undefined) throw new Error('background PTY sends require @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks')
-        if (exec.signal?.aborted === true) throw new Error('PTY send aborted')
+        if (tasks === undefined) throw new Error('background terminal sends require @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks')
+        if (exec.signal?.aborted === true) throw new Error('terminal send aborted')
         let cancelRequested = false
         const taskId = tasks.start({
           kind: 'pty-send',
@@ -150,15 +150,15 @@ export function apply(ctx: Context): void {
       }
       const operation = ctx.pty.startSend(owner, id, { ...request, ...exec.signal ? { signal: exec.signal } : {} })
       const result = await operation.done
-      if (exec.signal?.aborted === true) throw new Error('PTY send aborted')
+      if (exec.signal?.aborted === true) throw new Error('terminal send aborted')
       return { content: textResult(renderSend(result)), isError: false, meta: result }
     },
     presentCall(args) {
       const parsed = args as Partial<SendArgs>
       if (parsed.run_in_background === true) {
-        return { card: 'generic', title: `Send PTY ${parsed.sessionId as string} in background`, kind: 'execute', rawInput: parsed.text }
+        return { card: 'generic', title: `Send to terminal ${parsed.sessionId as string} in background`, kind: 'execute', rawInput: parsed.text }
       }
-      return { card: 'terminal', title: parsed.text || '(send input)', description: `PTY ${parsed.sessionId as string}` }
+      return { card: 'terminal', title: parsed.text || '(send input)', description: `Terminal ${parsed.sessionId as string}` }
     },
     presentResult(args, result) {
       if ((args as Partial<SendArgs>).run_in_background === true || result.isError) return undefined
@@ -168,10 +168,10 @@ export function apply(ctx: Context): void {
   }))
 
   ctx.tools.register(defineTool({
-    name: 'pty_read',
-    description: 'Read a bounded page of retained output from a persistent PTY without sending input.',
+    name: 'terminal_read',
+    description: 'Read a bounded page of retained output from a persistent terminal without sending input.',
     parameters: {
-      sessionId: { type: 'string', required: true, description: 'PTY session id.' },
+      sessionId: { type: 'string', required: true, description: 'Terminal session id.' },
       offset: { type: 'number', description: 'Newest-relative line offset (default 0).' },
       count: { type: 'number', description: 'Requested line count (default 500; backend caps apply).' },
     },
@@ -182,44 +182,44 @@ export function apply(ctx: Context): void {
       })
       return Promise.resolve(textResult(renderRead(result)))
     },
-    presentCall: args => ({ card: 'generic', title: `Read PTY ${(args).sessionId}`, kind: 'read', rawInput: args }),
+    presentCall: args => ({ card: 'generic', title: `Read terminal ${(args).sessionId}`, kind: 'read', rawInput: args }),
   }))
 
   ctx.tools.register(defineTool({
-    name: 'pty_signal',
-    description: 'Send an allowed signal to the current foreground process group of a persistent PTY.',
+    name: 'terminal_signal',
+    description: 'Send an allowed signal to the current foreground process group of a persistent terminal.',
     parameters: {
-      sessionId: { type: 'string', required: true, description: 'PTY session id.' },
-      signal: { type: 'string', required: true, enum: ['SIGINT', 'SIGTERM', 'SIGKILL', 'SIGTSTP', 'SIGHUP'], description: 'Signal to deliver. Shell-targeted SIGKILL is rejected; use pty_kill.' },
+      sessionId: { type: 'string', required: true, description: 'Terminal session id.' },
+      signal: { type: 'string', required: true, enum: ['SIGINT', 'SIGTERM', 'SIGKILL', 'SIGTSTP', 'SIGHUP'], description: 'Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.' },
     },
     async execute(args: SignalArgs, exec) {
       const result = await ctx.pty.signal(requireAgent(exec.agent), sessionId(args), args.signal)
       return textResult(`delivered ${args.signal} to foreground process group ${result.targetPgid}`)
     },
-    presentCall: args => ({ card: 'generic', title: `Signal PTY ${(args as SignalArgs).sessionId}`, kind: 'execute', rawInput: args }),
+    presentCall: args => ({ card: 'generic', title: `Signal terminal ${(args as SignalArgs).sessionId}`, kind: 'execute', rawInput: args }),
   }))
 
   ctx.tools.register(defineTool({
-    name: 'pty_kill',
-    description: 'Close one persistent PTY and wait until its captured owned process tree is gone.',
+    name: 'terminal_close',
+    description: 'Close one persistent terminal and wait until its captured owned process tree is gone.',
     parameters: {
-      sessionId: { type: 'string', required: true, description: 'PTY session id.' },
+      sessionId: { type: 'string', required: true, description: 'Terminal session id.' },
     },
     async execute(args: SessionArgs, exec) {
       const id = sessionId(args)
-      const killed = await ctx.pty.kill(requireAgent(exec.agent), id)
-      return textResult(killed ? `killed PTY session ${id}` : `PTY session ${id} was already closing`)
+      const closed = await ctx.pty.kill(requireAgent(exec.agent), id)
+      return textResult(closed ? `closed terminal session ${id}` : `terminal session ${id} was already closing`)
     },
-    presentCall: args => ({ card: 'generic', title: `Kill PTY ${(args).sessionId}`, kind: 'delete' }),
+    presentCall: args => ({ card: 'generic', title: `Close terminal ${(args).sessionId}`, kind: 'delete' }),
   }))
 
   ctx.tools.register(defineTool({
-    name: 'pty_list',
-    description: 'List persistent PTY sessions owned by the current agent.',
+    name: 'terminal_list',
+    description: 'List persistent terminal sessions owned by the current agent.',
     parameters: {},
     execute(_args: Record<string, never>, exec) {
       return Promise.resolve(textResult(renderList(ctx.pty.list(requireAgent(exec.agent)))))
     },
-    presentCall: () => ({ card: 'generic', title: 'List PTY sessions', kind: 'read' }),
+    presentCall: () => ({ card: 'generic', title: 'List terminal sessions', kind: 'read' }),
   }))
 }

+ 3 - 3
packages/pty/tool-pty/src/render.ts

@@ -1,4 +1,4 @@
-/** Model and ACP rendering for persistent PTY tool results. */
+/** Model and ACP rendering for persistent terminal tool results. */
 
 import type { PtyReadResult, PtySendRead, PtySendResult, PtySessionSnapshot, PtySpawnResult } from '@deepseek-ai/dsh-pty'
 
@@ -9,7 +9,7 @@ import type { PtyReadResult, PtySendRead, PtySendResult, PtySessionSnapshot, Pty
  */
 export function renderSpawn(result: PtySpawnResult): string {
   const label = result.name === undefined ? result.sessionId : `${result.sessionId} (${result.name})`
-  return `started PTY session ${label} [type: ${result.type}]\n${result.motd || '(no startup output)'}`
+  return `started terminal session ${label} [type: ${result.type}]\n${result.motd || '(no startup output)'}`
 }
 
 /**
@@ -50,7 +50,7 @@ export function renderRead(result: PtyReadResult): string {
  * @returns One line per session or the empty marker.
  */
 export function renderList(sessions: PtySessionSnapshot[]): string {
-  if (sessions.length === 0) return '(no PTY sessions)'
+  if (sessions.length === 0) return '(no terminal sessions)'
   return sessions.map((session) => {
     const name = session.name === undefined ? '' : ` (${session.name})`
     const pid = session.pid === undefined ? '' : ` pid=${session.pid}`

+ 5 - 5
packages/pty/tool-pty/tests/loader-composition.spec.ts

@@ -52,7 +52,7 @@ function resultText(result: { content: { type: string; text?: string }[] }): str
 
 const suite = process.platform === 'linux' || process.platform === 'darwin' ? describe : describe.skip
 
-suite('PTY real Loader composition through cordis.yml', () => {
+suite('terminal real Loader composition through cordis.yml', () => {
   it('boots cordis.yml and preserves shell state across real tool calls', async () => {
     root = await mkdtemp(join(tmpdir(), 'dsh-pty-loader-'))
     const configPath = join(root, 'cordis.yml')
@@ -103,15 +103,15 @@ suite('PTY real Loader composition through cordis.yml', () => {
 
     const owner = agent(context)
     const spawn = await context.tools.execute({
-      callId: CallId('spawn'), name: 'pty_spawn', arguments: { type: 'shell', name: 'main', cwd: root }, agent: owner,
+      callId: CallId('spawn'), name: 'terminal_open', arguments: { type: 'shell', name: 'main', cwd: root }, agent: owner,
     })
-    expect(resultText(spawn)).toContain('started PTY session pty-1 (main)')
+    expect(resultText(spawn)).toContain('started terminal session pty-1 (main)')
 
     await context.tools.execute({
-      callId: CallId('state'), name: 'pty_send', arguments: { sessionId: 'pty-1', text: 'export KEEP=loader; cd /' }, agent: owner,
+      callId: CallId('state'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'export KEEP=loader; cd /' }, agent: owner,
     })
     const read = await context.tools.execute({
-      callId: CallId('read'), name: 'pty_send', arguments: { sessionId: 'pty-1', text: 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"' }, agent: owner,
+      callId: CallId('read'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"' }, agent: owner,
     })
     expect(resultText(read)).toContain('cwd=/ keep=loader')
     expect(context.pty.list(owner)).toHaveLength(1)

+ 2 - 2
packages/pty/tool-pty/tests/render.spec.ts

@@ -5,7 +5,7 @@ import { renderList, renderRead, renderSend, renderSendRead, renderSpawn } from
 describe('tool-pty rendering', () => {
   it('renders spawn with and without names or MOTD', () => {
     expect(renderSpawn({ sessionId: PtySessionId('pty-1'), type: 'shell', status: { kind: 'running' }, motd: '' }))
-      .toBe('started PTY session pty-1 [type: shell]\n(no startup output)')
+      .toBe('started terminal session pty-1 [type: shell]\n(no startup output)')
     expect(renderSpawn({ sessionId: PtySessionId('pty-2'), name: 'main', type: 'shell', pid: 2, status: { kind: 'running' }, motd: 'ready' }))
       .toContain('pty-2 (main)')
   })
@@ -28,7 +28,7 @@ describe('tool-pty rendering', () => {
   it('renders history and every list status shape', () => {
     expect(renderRead({ text: '', totalLines: 0, lineBegin: 0, lineEnd: 0, truncated: true }))
       .toBe('(no retained output)\n[lines: 0-0 of 0]\n[output truncated]')
-    expect(renderList([])).toBe('(no PTY sessions)')
+    expect(renderList([])).toBe('(no terminal sessions)')
     expect(renderList([
       { sessionId: PtySessionId('pty-1'), type: 'shell', status: { kind: 'running' } },
       { sessionId: PtySessionId('pty-2'), name: 'done', type: 'shell', pid: 9, status: { kind: 'exited', exitCode: 2, signal: null } },

+ 40 - 40
packages/pty/tool-pty/tests/tools.spec.ts

@@ -119,41 +119,41 @@ function text(result: { content: { type: string; text?: string }[] }): string {
 describe('tool-pty foreground surface', () => {
   it('registers exactly six schemas and drives the full owner-scoped lifecycle', async () => {
     const { ctx, agent } = await setup(false)
-    expect(['pty_spawn', 'pty_send', 'pty_read', 'pty_signal', 'pty_kill', 'pty_list'].every(name => ctx.tools.get(name) !== undefined)).toBe(true)
-
-    const spawned = await call(ctx, 'pty_spawn', { type: 'stub', name: 'main' }, agent)
-    expect(text(spawned)).toContain('started PTY session pty-1 (main)')
-    expect(text(await call(ctx, 'pty_list', {}, agent))).toContain('pty-1 (main) [stub] running pid=42')
-    expect(text(await call(ctx, 'pty_read', { sessionId: 'pty-1' }, agent))).toContain('history\n[lines: 0-1 of 1]')
-    expect(text(await call(ctx, 'pty_signal', { sessionId: 'pty-1', signal: 'SIGINT' }, agent))).toBe('delivered SIGINT to foreground process group 10')
-    const sent = await call(ctx, 'pty_send', { sessionId: 'pty-1', text: 'echo hi' }, agent)
+    expect(['terminal_open', 'terminal_send', 'terminal_read', 'terminal_signal', 'terminal_close', 'terminal_list'].every(name => ctx.tools.get(name) !== undefined)).toBe(true)
+
+    const spawned = await call(ctx, 'terminal_open', { type: 'stub', name: 'main' }, agent)
+    expect(text(spawned)).toContain('started terminal session pty-1 (main)')
+    expect(text(await call(ctx, 'terminal_list', {}, agent))).toContain('pty-1 (main) [stub] running pid=42')
+    expect(text(await call(ctx, 'terminal_read', { sessionId: 'pty-1' }, agent))).toContain('history\n[lines: 0-1 of 1]')
+    expect(text(await call(ctx, 'terminal_signal', { sessionId: 'pty-1', signal: 'SIGINT' }, agent))).toBe('delivered SIGINT to foreground process group 10')
+    const sent = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'echo hi' }, agent)
     expect(text(sent)).toContain('command output\n[wait: stdin_read]\n[session: running]')
-    expect(text(await call(ctx, 'pty_kill', { sessionId: 'pty-1' }, agent))).toBe('killed PTY session pty-1')
-    expect(text(await call(ctx, 'pty_list', {}, agent))).toBe('(no PTY sessions)')
+    expect(text(await call(ctx, 'terminal_close', { sessionId: 'pty-1' }, agent))).toBe('closed terminal session pty-1')
+    expect(text(await call(ctx, 'terminal_list', {}, agent))).toBe('(no terminal sessions)')
   })
 
   it('fails without an initiating agent and rejects background before writing', async () => {
     const { ctx, agent, stub } = await setup(false)
-    expect((await call(ctx, 'pty_spawn', { type: 'stub' })).isError).toBe(true)
-    await call(ctx, 'pty_spawn', { type: 'stub' }, agent)
-    const result = await call(ctx, 'pty_send', { sessionId: 'pty-1', text: 'sleep 1', run_in_background: true }, agent)
+    expect((await call(ctx, 'terminal_open', { type: 'stub' })).isError).toBe(true)
+    await call(ctx, 'terminal_open', { type: 'stub' }, agent)
+    const result = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'sleep 1', run_in_background: true }, agent)
     expect(result.isError).toBe(true)
     expect(stub.sessions[0]?.operation).toBeUndefined()
   })
 
   it('validates required values and forwards optional spawn/read arguments', async () => {
     const { ctx, agent } = await setup(false)
-    expect((await call(ctx, 'pty_spawn', { type: '' }, agent)).isError).toBe(true)
-    expect((await call(ctx, 'pty_send', { sessionId: '', text: 'x' }, agent)).isError).toBe(true)
-    expect((await call(ctx, 'pty_send', { sessionId: 1, text: 'x' }, agent)).isError).toBe(true)
-    expect((await call(ctx, 'pty_send', { sessionId: 'pty-1', text: 1 }, agent)).isError).toBe(true)
-    await call(ctx, 'pty_spawn', { type: 'stub', name: 'named', cwd: '/tmp' }, agent)
-    expect(text(await call(ctx, 'pty_read', { sessionId: 'pty-1', offset: 2, count: 3 }, agent))).toContain('history')
+    expect((await call(ctx, 'terminal_open', { type: '' }, agent)).isError).toBe(true)
+    expect((await call(ctx, 'terminal_send', { sessionId: '', text: 'x' }, agent)).isError).toBe(true)
+    expect((await call(ctx, 'terminal_send', { sessionId: 1, text: 'x' }, agent)).isError).toBe(true)
+    expect((await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 1 }, agent)).isError).toBe(true)
+    await call(ctx, 'terminal_open', { type: 'stub', name: 'named', cwd: '/tmp' }, agent)
+    expect(text(await call(ctx, 'terminal_read', { sessionId: 'pty-1', offset: 2, count: 3 }, agent))).toContain('history')
   })
 
   it('declares terminal presentation only for foreground sends', async () => {
     const { ctx } = await setup(false)
-    const definition = ctx.tools.get('pty_send')
+    const definition = ctx.tools.get('terminal_send')
     expect(definition?.presentCall?.({ sessionId: 'pty-1', text: 'python3' })).toMatchObject({ card: 'terminal', title: 'python3' })
     expect(definition?.presentCall?.({ sessionId: 'pty-1', text: 'make', run_in_background: true })).toMatchObject({ card: 'generic' })
     expect(definition?.presentCall?.({ sessionId: 'pty-1', text: '' })).toMatchObject({ card: 'terminal', title: '(send input)' })
@@ -164,20 +164,20 @@ describe('tool-pty foreground surface', () => {
     expect(definition?.presentResult?.({ sessionId: 'pty-1', text: 'x' }, { content: [undefined as never], isError: false })).toBeUndefined()
     expect(definition?.presentResult?.({ sessionId: 'pty-1', text: 'x' }, { content: [{ type: 'text', text: 'ok' }], isError: false })).toEqual({ card: 'terminal', output: 'ok' })
 
-    expect(ctx.tools.get('pty_spawn')?.presentCall?.({ type: 'stub' })).toMatchObject({ card: 'generic', title: 'Start PTY stub' })
-    expect(ctx.tools.get('pty_spawn')?.presentCall?.({ type: 'stub', name: 'main' })).toMatchObject({ card: 'generic', title: 'Start PTY main' })
-    expect(ctx.tools.get('pty_read')?.presentCall?.({ sessionId: 'pty-1' })).toMatchObject({ card: 'generic', title: 'Read PTY pty-1' })
-    expect(ctx.tools.get('pty_signal')?.presentCall?.({ sessionId: 'pty-1', signal: 'SIGINT' })).toMatchObject({ card: 'generic', title: 'Signal PTY pty-1' })
-    expect(ctx.tools.get('pty_kill')?.presentCall?.({ sessionId: 'pty-1' })).toMatchObject({ card: 'generic', title: 'Kill PTY pty-1' })
-    expect(ctx.tools.get('pty_list')?.presentCall?.({})).toMatchObject({ card: 'generic', title: 'List PTY sessions' })
+    expect(ctx.tools.get('terminal_open')?.presentCall?.({ type: 'stub' })).toMatchObject({ card: 'generic', title: 'Open terminal stub' })
+    expect(ctx.tools.get('terminal_open')?.presentCall?.({ type: 'stub', name: 'main' })).toMatchObject({ card: 'generic', title: 'Open terminal main' })
+    expect(ctx.tools.get('terminal_read')?.presentCall?.({ sessionId: 'pty-1' })).toMatchObject({ card: 'generic', title: 'Read terminal pty-1' })
+    expect(ctx.tools.get('terminal_signal')?.presentCall?.({ sessionId: 'pty-1', signal: 'SIGINT' })).toMatchObject({ card: 'generic', title: 'Signal terminal pty-1' })
+    expect(ctx.tools.get('terminal_close')?.presentCall?.({ sessionId: 'pty-1' })).toMatchObject({ card: 'generic', title: 'Close terminal pty-1' })
+    expect(ctx.tools.get('terminal_list')?.presentCall?.({})).toMatchObject({ card: 'generic', title: 'List terminal sessions' })
   })
 })
 
 describe('tool-pty task integration', () => {
   it('registers a generic task and exposes incremental output', async () => {
     const { ctx, agent } = await setup(true)
-    await call(ctx, 'pty_spawn', { type: 'stub' }, agent)
-    expect(text(await call(ctx, 'pty_send', { sessionId: 'pty-1', text: 'build', run_in_background: true }, agent))).toBe('started background task pty-send-1')
+    await call(ctx, 'terminal_open', { type: 'stub' }, agent)
+    expect(text(await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'build', run_in_background: true }, agent))).toBe('started background task pty-send-1')
     const output = await call(ctx, 'task_output', { task_id: 'pty-send-1', wait: true }, agent)
     expect(text(output)).toContain('live output')
     expect(text(output)).toContain('[status: completed, wait: stdin_read]')
@@ -185,30 +185,30 @@ describe('tool-pty task integration', () => {
 
   it('rejects pre-aborted background calls, maps task cancellation, and contains operation failure', async () => {
     const { ctx, agent, stub } = await setup(true)
-    await call(ctx, 'pty_spawn', { type: 'stub' }, agent)
+    await call(ctx, 'terminal_open', { type: 'stub' }, agent)
     const controller = new AbortController()
     controller.abort()
-    expect((await callWithSignal(ctx, 'pty_send', { sessionId: 'pty-1', text: 'x', run_in_background: true }, agent, controller.signal)).isError).toBe(true)
+    expect((await callWithSignal(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'x', run_in_background: true }, agent, controller.signal)).isError).toBe(true)
 
     stub.sessions[0]!.autoSettle = false
-    expect(text(await call(ctx, 'pty_send', { sessionId: 'pty-1', text: '', run_in_background: true }, agent))).toContain('pty-send-1')
+    expect(text(await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: '', run_in_background: true }, agent))).toContain('pty-send-1')
     expect(text(await call(ctx, 'task_kill', { task_id: 'pty-send-1' }, agent))).toContain('requested cancellation')
     await new Promise(resolve => setTimeout(resolve, 0))
     expect(text(await call(ctx, 'task_output', { task_id: 'pty-send-1' }, agent))).toContain('[status: killed')
 
     stub.sessions[0]!.rejectOperation = true
     stub.sessions[0]!.autoSettle = false
-    expect(text(await call(ctx, 'pty_send', { sessionId: 'pty-1', text: 'bad', run_in_background: true }, agent))).toContain('pty-send-2')
+    expect(text(await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'bad', run_in_background: true }, agent))).toContain('pty-send-2')
     await new Promise(resolve => setTimeout(resolve, 0))
     expect(text(await call(ctx, 'task_output', { task_id: 'pty-send-2' }, agent))).toContain('[status: failed')
   })
 
-  it('reports foreground cancellation after the PTY operation settles', async () => {
+  it('reports foreground cancellation after the terminal operation settles', async () => {
     const { ctx, agent, stub } = await setup(false)
-    await call(ctx, 'pty_spawn', { type: 'stub' }, agent)
+    await call(ctx, 'terminal_open', { type: 'stub' }, agent)
     stub.sessions[0]!.autoSettle = false
     const controller = new AbortController()
-    const pending = callWithSignal(ctx, 'pty_send', { sessionId: 'pty-1', text: 'sleep' }, agent, controller.signal)
+    const pending = callWithSignal(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'sleep' }, agent, controller.signal)
     await Promise.resolve()
     controller.abort()
     stub.sessions[0]!.operation?.cancel()
@@ -217,20 +217,20 @@ describe('tool-pty task integration', () => {
 
   it('renders the already-closing kill result', async () => {
     const { ctx, agent, stub } = await setup(false)
-    await call(ctx, 'pty_spawn', { type: 'stub' }, agent)
+    await call(ctx, 'terminal_open', { type: 'stub' }, agent)
     stub.sessions[0]!.closeGate = Promise.withResolvers<undefined>()
     const first = ctx.pty.kill(agent, PtySessionId('pty-1'))
-    const second = call(ctx, 'pty_kill', { sessionId: 'pty-1' }, agent)
+    const second = call(ctx, 'terminal_close', { sessionId: 'pty-1' }, agent)
     stub.sessions[0]!.closeGate?.resolve(undefined)
     await first
-    expect(text(await second)).toBe('PTY session pty-1 was already closing')
+    expect(text(await second)).toBe('terminal session pty-1 was already closing')
   })
 
   it('renders an exited session detail for background completion', async () => {
     const { ctx, agent, stub } = await setup(true)
-    await call(ctx, 'pty_spawn', { type: 'stub' }, agent)
+    await call(ctx, 'terminal_open', { type: 'stub' }, agent)
     stub.sessions[0]!.statusValue = { kind: 'exited', exitCode: null, signal: null }
-    await call(ctx, 'pty_send', { sessionId: 'pty-1', text: 'exit', run_in_background: true }, agent)
+    await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'exit', run_in_background: true }, agent)
     const output = await call(ctx, 'task_output', { task_id: 'pty-send-1', wait: true }, agent)
     expect(text(output)).toContain('session exited: unknown')
   })

+ 1 - 1
scripts/gen-tool-catalog.ts

@@ -232,7 +232,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
       await ctx.plugin(ToolPty)
     },
     note:
-      'The six PTY tools are opt-in and complement one-shot bash/filesystem tools. `pty_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema.',
+      'The six terminal tools are opt-in and complement one-shot bash/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema.',
   },
   {
     pkg: '@deepseek-ai/dsh-tool-skill',