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:
@@ -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' }),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
|
||||
Reference in New Issue
Block a user