description: "面向组合作者与能力消费方的子进程服务(ctx.subprocess)说明:启动、观察并终止受管子进程与终端会话。"
English | 中文
ctx.subprocess 可解析可执行文件、启动显式指定的子进程或真实终端会话、流式读取或有界收集输出,并终止完整的受管进程范围。每个组合配置一个 subprocess 实现,并根据命令运行位置选择本地或远程执行。每次请求都指定 argv、工作目录、stdio、环境覆盖、终止宽限期与取消信号,不会添加 shell 解释或隐藏的执行默认值。子进程环境会先移除环境中的凭据与 DSH_* 值,再应用显式覆盖;时限、拆卸策略与面向模型的渲染由调用方负责,收集的输出在进程退出后仍可读取。
在需要运行子进程的组合中挂载一个 subprocess 提供方,并从拥有该命令的能力调用 ctx.subprocess。常用路径是显式的:解析可执行文件、用完全明确的请求 spawn、读取你要的输出,并在工作完成时终止受管范围。
每个组合由唯一一个提供方注册 ctx.subprocess;把它与经由它 spawn 的消费方放在一起加载——bash 执行器、LSP 主机、PTY shell 后端或进程外 subagent 后端。加载第二个提供方会快速失败(每个上下文只有一个服务,这是 Cordis 的标准行为)。
- name: '@deepseek-ai/dsh-subprocess-local'
- name: '@deepseek-ai/dsh-bash-local'
请求完全明确:程序与参数、工作目录、每条流一种 stdio 处置方式、终止宽限期、可选的中止信号与可选的环境覆盖。目标与受管范围标识保留在提供方内部。done 以直接命令的退出事实(exitCode 与 signal)resolve,并在 spawn 或提供方失败时 reject;收集输出在退出后仍可读取。
const executable = await ctx.subprocess.resolveExecutable('bash')
const handle = ctx.subprocess.spawn({
argv: [executable, '-c', 'echo hello'],
cwd: '/workspace',
stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: 'inherit' },
graceMs: 5000,
})
const { exitCode, signal } = await handle.done
const output = handle.collected.stdout?.readFrom(0)
'pipe' 把原始流交给你做自己的协议分帧——LSP 主机用 JSON-RPC,ACP(Agent Client Protocol)后端用 ndjson。'inherit' 让子进程直接写父进程自己的流,用于直通诊断输出。spill 上限后,完整流还可以从 spill 文件中恢复。读取基于偏移量且从不消费:后台读取与最终批量读取可以共享同一条流,而不会抢走彼此的字节。
设置 stdio.control: 'pipe' 后,handle.control 会返回独立的原始 Duplex。Node 子进程通过 @deepseek-ai/dsh-subprocess/control 的 openInheritedControlChannel() 打开 fd 7;该辅助函数会消费提供方拥有的 DSH_SUBPROCESS_CONTROL=pipe 标记。调用方不能通过 env 提供该标记。控制字节不会进入 stdout/stderr 收集器。消费方负责分帧、校验、背压和关闭自身端点;提供方销毁时会在进程拆卸后销毁仍存在的端点。省略请求则返回 control: undefined。该通道仅适用于普通进程,不授予绕过工具审批的权限。
终止与等待使用同一个由提供方管理的范围。terminate() 会启动提供方记录的流程,具有幂等性,并在该范围为空后成为空操作;请求的中止信号会启动同一流程。waitForExit() 观察同一范围,只在提供方证明它完全停稳后 resolve,因此直接命令结束不会掩盖仍存活的后代。所选 owner 无法再证明完全停稳时,它会 reject。提供方记录其 native owner 与较弱 fallback;时限、拆卸阶梯与原因分类归调用方所有。
对于交互式程序,spawnTerminal 分配真实 PTY:写入文本、读取 UTF-8 输出、检查当前前台进程组并向其发送信号,以及等待一次 terminate(),让提供方仍可观察到的每个会话成员完全停稳。就绪状态、scrollback 与提示符策略仍归 PTY 消费方所有。
终端请求可显式启用 shellActivity。inspectActivity() 结合支持的 shell 生命周期信号与自有任务观察,返回 idle、busy 或 unknown,以及句柄内的 revision。不支持或不完整的观察不能推出空闲;输入会使已有提示符证据失效。启用后,根 shell 退出时继续持有剩余工作,不把该退出视为终止后代进程的许可。保活和清理期限由消费者决定。
子进程永远不会隐式继承 harness 的环境秘密:形似凭据的名称与环境中的 DSH_* 事实都会被清除,调用方显式的 env 在该清除之后合并。有意转发的凭据或当前的 DSH_* 部署事实仍会到达子进程;显式的 undefined 墓碑值则移除一个普通的环境项。
无法解析可执行文件时,服务会明确报出稳定的错误。从未启动成功的 spawn 会让 done reject;从未运行过的进程没有任何缓冲输出。提供方无法证明所选范围为空时,waitForExit() 也会 reject;提供方 fallback 可能无法拥有逃离其进程组或已观察会话的后代。当传输拥有自己的 spawn(SDK 客户端、MCP)时,请绕开本服务并直接导入 scrubbedParentEnv,让环境策略保持单一来源。
当包级约定不够用时阅读以下页面。它们从穷尽式类型参考逐步进入各提供方,以及 seam 背后的决策证据。
DSH_* 环境。终端消费者通过 terminalEnvironment() 读取 provider 平台和首选 shell,通过 resolveExecutable() 验证候选。确定未找到可执行文件时抛出 SubprocessExecutableNotFoundError,传输故障仍单独报告。spawnTerminal 要求 terminalType 和初始尺寸,返回的 handle 通过 resize(cols, rows) 调整尺寸,不重新分配进程。
通过消费方 seam(例如 bash 执行器家族)间接影响,它们负责进程输出与生命周期的全部面向模型渲染。
不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
这些限制说明该 seam 何时不合适,或何时把工作留给消费方。它们是当前包约束,不是对比或任务积压。
scrubbedParentEnv,使环境策略保持单一来源。