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.
This commit is contained in:
NI0317
2026-07-21 19:29:56 +08:00
parent 85ac747208
commit a7bfade7eb
28 changed files with 568 additions and 401 deletions
@@ -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
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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`
@@ -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"}}}
@@ -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"}}
@@ -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.
@@ -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).",
@@ -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"}}}
@@ -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"}}}}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}`
@@ -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
View File
@@ -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 } },
+39 -39
View File
@@ -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)
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, '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)
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
View File
@@ -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',