跳到主要内容
路径文档

交互式终端模型工作流

一句话版terminal 是六个面向模型的持久终端工具(terminal_open / terminal_send / terminal_read / terminal_signal / terminal_close / terminal_list),在 DSH 的 ctx.terminals 服务缝上提供 owner 隔离的交互式 shell/REPL——前台发送等你提示、走 ctx.jobs 的后台发送给你 job id,并用真正的进程组信号中断它。

子进程篇七节讲了"DSH 怎么分配一个 PTY、ctx.terminals 服务缝怎么路由到 terminal-bash 后端"。这一篇站到模型面:模型实际看到的六个工具长什么样、怎么编排一次完整的交互会话。

一、定位:机制层的模型呈现

packages/terminal/tool-terminal 在你装了 terminal 行之后,把那层受管终端机制映射成六个模型可见的工具。分层的边界很清楚:

包 / ctx负责什么本文覆盖
终端机制terminalctx.terminals 服务缝:铸不透明 session id、owner 栅栏、按名路由后端、dispose 时等 quiescence背景
后端terminal-bashctx.subprocess.spawnTerminal 上起持久 shell(type: shell)背景
模型工具tool-terminal(ctx.terminals 之上)六个工具 schema、owner 认证、收尾上限、系统提示词、后台接 ctx.jobs本篇正文

tool-terminal 的注入只有三个:['terminals', 'tools', 'systemPrompt']——它不碰 node-pty、不碰沙箱、不碰任务调度,只管把机制暴露成模型工作流。

二、六个工具拆解

下面按 terminal_openterminal_close 的完整生命周期介绍。所有参数/返回值都来自 packages/terminal/tool-terminal/src/index.ts 的 schema。

1. terminal_open — 建会话

terminal_open(type?, name?, cwd?) → { sessionId, name?, type, pid?, status, motd }
  • type(必填):注册的终端后端类型,通常就是 "shell"
  • name(选填):owner 本地的显示名,如 "main""gdb"
  • cwd(选填):初始工作目录,缺省为部署工作区根。
  • 返回带 motd 的会话快照:模型靠它拿"这个会话该叫什么"的提示。

源码 execute(packages/terminal/tool-terminal/src/index.ts):

const result = await ctx.terminals.spawn(requireAgent(exec.agent), {
type: args.type,
...args.name !== undefined ? { name: args.name } : {},
...args.cwd !== undefined ? { cwd: args.cwd } : {},
}, exec.signal)
return result

2. terminal_send — 写输入(前台 / 后台)

terminal_send(sessionId, text, submit?, run_in_background?) →
前台: { kind:'foreground', viewport, waitReason, sessionStatus, truncated }
后台: { kind:'background', jobId }
  • submit 缺省 true(发回车);要发控制字符或不完整的 REPL 输入就设 false
  • 前台发送默认"等"——一直等到四件事之一发生,返回的 waitReason 精确告诉你是哪一件:
waitReason含义
stdin_read后台又读到一次输入(程序还要后续输入)
inferred_idle推断静默,认为输出告一段落
timeout达到等待超时
session_exit这个 session 已退出

⚠️ 系统提示词原话:inferred_idletimeout 并不能证明前台命令已退出——静默≠结束,超时≠退出。要确认状态看 sessionStatus

  • 后台模式(run_in_background: true)不阻塞模型:预检 + 该会话独占发送预留都在返回 job id 之前完成,然后返回 { kind:'background', jobId } 接通用 ctx.jobs。收集用 job_output,停止用 job_kill——而 job_kill 对这条 pty-send job 会转发成向当前前台进程组发真实 SIGINT

源码后台分支(packages/terminal/tool-terminal/src/index.ts):

const jobId = jobs.start({
kind: 'pty-send',
label: `${id}: ${args.text || '(input)'}`,
owner,
outputLimitBytes: maxResultBytes,
run: () => {
const operation = ctx.terminals.startSend(owner, id, request)
return {
cancel: () => { cancelRequested = true; operation.cancel() },
done: operation.done.then(
result => ({ status: cancelRequested ? 'killed' : 'completed', detail: sendDetail(result) }),
(error) => ({ status: 'failed', detail: String(error) }),
),
readOutput: () => renderSendRead(operation.readOutput()),
}
},
})
return { kind: 'background', jobId }

kind: 'pty-send' 是这个插件对 @deepseek-ai/dsh-jobsJobKindMap 的一份声明:

declare module '@deepseek-ai/dsh-jobs' {
interface JobKindMap { 'pty-send': 'pty-send' }
}

前台发送用"终端调用/结果卡片"呈现,后台发送用通用执行卡片。

3. terminal_read — 有界翻页,不发输入

terminal_read(sessionId, offset?, count?) → { text, totalLines, lineBegin, lineEnd, truncated }
  • offset 缺省 0(相对最新的偏移);count 缺省 500(后端有 cap)。
  • 返回一页带分页标记的保留输出,不发任何输入:模型想"复盘前面"而不打扰正在等输入的程序时用它。
  • truncated 表示这页没装下被裁了,配合 lineBegin/lineEnd/totalLines 决定要不要往前翻。

4. terminal_signal — 发信号

terminal_signal(sessionId, signal) → { delivered:true, targetPgid }
  • signal 是枚举:SIGINT / SIGTERM / SIGKILL / SIGTSTP / SIGHUP,发到当前前台进程组(返回 targetPgid)。
  • 发向 shell 自身的 SIGKILL 会被拒绝——要整个干掉就 terminal_close,不要对 shell 进程发 SIGKILL

5. terminal_close — 关掉并等进程树消失

terminal_close(sessionId) → { sessionId, outcome:'closed'|'already-closing' }
  • 关闭一个持久会话,并等到它俘获的受管进程树完全终止才返回。
  • outcome: 'closed' 表示这次由你关掉;'already-closing' 表示它正在关闭,这次是幂等确认。

源码 execute:

const closed = await ctx.terminals.kill(requireAgent(exec.agent), id)
return { sessionId: id, outcome: closed ? 'closed' : 'already-closing' }

6. terminal_list — 盘点当前 agent 的会话

terminal_list() → [ { sessionId, name?, type, pid?, status } ... ]
  • 当前发起 agent 自己持久会话的最新快照,每个会话一行快照。

工具速查表

工具作用关键返回
terminal_open建持久会话sessionId + motd
terminal_send写输入;前台等,后台给 job idwaitReason/sessionStatus/viewport{kind:'background', jobId}
terminal_read有界翻页读保留输出,不发输入totalLines/lineBegin/lineEnd/truncated
terminal_signal给前台进程组发 SIGINT/SIGTERM/SIGKILL/SIGTSTP/SIGHUPtargetPgid
terminal_close关会话,等进程树完全终止closed/already-closing
terminal_list列当前 agent 全部会话快照数组

三、核心使用约定

owner 隔离(每步都 requireAgent)

源码里有个硬性前提:六个工具的执行都过 requireAgent(exec.agent):

function requireAgent(agent: Agent | undefined): Agent {
if (agent === undefined) throw new Error('terminal tools require an initiating agent')
return agent
}

owner 身份来自发起那次工具执行的 exact Agent。所以就算模型获知了另一个 agent 的 session id,也撬不动对方的会话——每次 spawn/send/read/signal/kill/list 都精准栅栏到同一个 owner。

系统提示词段(order: 106)

插件挂一段固定指引,提点该什么时候用终端:

ctx.systemPrompt.section({
name: 'tool:pty',
order: 106,
text: 'Use a terminal session only when work needs persistent terminal state or interactive stdin; prefer shell/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.',
})

要点(是给模型听的,也是写 prompt 时该记住的):

  • 只在需要持久终端状态或交互 stdin 时开终端;一次性有界操作,优先 shell / read / write / edit
  • 记下每个 session id,用后关掉,别漏 terminal_close
  • inferred_idletimeout 不代表前台命令已退出

配置

默认最小含义
enableRunInBackgroundtrue公开并接受 run_in_background;设 false 时 schema 直接省略该字段,抢占传入未声明参数会被拒
maxResultBytes26214464每个完整终端/任务输出结果的 UTF-8 上限;在等待、会话、分页、截断、任务状态元数据都拼进去后计算
  • 加载时校验两个值:maxResultBytes 必须是安全整数且 ≥64(MIN_MAX_RESULT_BYTES)。
  • 最小 64 字节是刻意选的:保证注册表签发的每个 session id / job id 都能完整出现在创建确认里
  • 渲染把控制元数据和截断标记([output truncated],渲染层用 UTF-8 边界切割)预留空间,再裁正文。

四、编排一次完整交互

典型的长会话流程——打开,发命令,读到静默或被程序要求继续输入,需要时信号,最后关闭:

验证/试一试(在你跑得起来 DSH 的环境里),一个真实 REPL:

# 1) 发起一个调用终端的 session
dsh run --agent-terminal-demo

# 2) 会话日志里看 terminal_* 工具事件的顺序
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"terminal_' | head

# 3) 组合树里确认 tool-terminal 行已挂载
dsh web --dump-config | grep -iE "tool-terminal|terminal-bash"

模型侧想亲历一遍:让 agent terminal_open 起一个 shell,发 python3,再发 print(2+2),对着 stdin_read 的等待继续投喂,然后观察返回的 waitReasonstdin_read 切到 inferred_idlesession_exit,最后 terminal_close 清掉。

五、和子进程篇的分工

别和 子进程与终端 混:

  • 子进程篇在机制层:ctx.terminals 服务缝 + terminal-bash 后端,以及再底下的 spawnTerminal——它们负责"怎么分配/路由/清理一个 PTY"。
  • 本篇在模型层:那六个工具叫什么、参数怎么填、返回怎么读、怎么编排一次交互会话,以及后台怎么接 ctx.jobs
  • 工具全览见 内置工具 的"终端类"。
机制层(subprocess 篇)模型层(本篇)
terminal / terminal-bashtool-terminal
交付物ctx.terminals 服务缝、后端六个工具 schema + owner 认证 + 提示词 + 收尾上限
面向谁其他插件 / 后端模型

下一步