|
|
@@ -6,12 +6,11 @@
|
|
|
|
|
|
import { Context } from 'cordis'
|
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
|
-import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
|
import { PtySessionId } from '@deepseek-ai/dsh-pty'
|
|
|
import type { PtySendResult, PtySessionId as PtySessionIdType, PtySignal } from '@deepseek-ai/dsh-pty'
|
|
|
import type {} from '@deepseek-ai/dsh-tasks'
|
|
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
-import type { ToolExecutionResult, ToolResult } from '@deepseek-ai/dsh-tools'
|
|
|
+import type { ToolResult } from '@deepseek-ai/dsh-tools'
|
|
|
import { renderList, renderRead, renderSend, renderSendRead, renderSpawn } from './render.ts'
|
|
|
|
|
|
declare module '@deepseek-ai/dsh-tasks' {
|
|
|
@@ -50,6 +49,41 @@ interface SignalArgs extends SessionArgs {
|
|
|
signal: PtySignal
|
|
|
}
|
|
|
|
|
|
+const SESSION_STATUS_SCHEMA = {
|
|
|
+ oneOf: [
|
|
|
+ {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: {
|
|
|
+ kind: { type: 'string', required: true, const: 'running' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: {
|
|
|
+ kind: { type: 'string', required: true, const: 'exited' },
|
|
|
+ exitCode: { required: true, oneOf: [{ type: 'integer' }, { type: 'null' }] },
|
|
|
+ signal: { required: true, oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+} as const
|
|
|
+
|
|
|
+const SESSION_SNAPSHOT_PROPERTIES = {
|
|
|
+ sessionId: { type: 'string', required: true },
|
|
|
+ name: { type: 'string' },
|
|
|
+ type: { type: 'string', required: true },
|
|
|
+ pid: { type: 'integer' },
|
|
|
+ status: { ...SESSION_STATUS_SCHEMA, required: true },
|
|
|
+} as const
|
|
|
+
|
|
|
+const SESSION_SNAPSHOT_SCHEMA = {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: SESSION_SNAPSHOT_PROPERTIES,
|
|
|
+} as const
|
|
|
+
|
|
|
function requireAgent(agent: Agent | undefined): Agent {
|
|
|
if (agent === undefined) throw new Error('terminal tools require an initiating agent')
|
|
|
return agent
|
|
|
@@ -62,10 +96,6 @@ function sessionId(args: SessionArgs): PtySessionIdType {
|
|
|
return PtySessionId(args.sessionId)
|
|
|
}
|
|
|
|
|
|
-function textResult(text: string): ContentBlock[] {
|
|
|
- return [{ type: 'text', text }]
|
|
|
-}
|
|
|
-
|
|
|
function rawResultText(result: ToolResult): string | undefined {
|
|
|
if (result.content.length !== 1) return undefined
|
|
|
const block = result.content[0]
|
|
|
@@ -94,6 +124,17 @@ export function apply(ctx: Context): void {
|
|
|
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.' },
|
|
|
},
|
|
|
+ output: {
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: {
|
|
|
+ ...SESSION_SNAPSHOT_PROPERTIES,
|
|
|
+ motd: { type: 'string', required: true },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ render: (_args, value) => [{ type: 'text', text: renderSpawn(value) }],
|
|
|
+ },
|
|
|
async execute(args: SpawnArgs, exec) {
|
|
|
if (args.type.length === 0) throw new Error('type must be a non-empty string')
|
|
|
const result = await ctx.pty.spawn(requireAgent(exec.agent), {
|
|
|
@@ -101,7 +142,7 @@ export function apply(ctx: Context): void {
|
|
|
...args.name !== undefined ? { name: args.name } : {},
|
|
|
...args.cwd !== undefined ? { cwd: args.cwd } : {},
|
|
|
}, exec.signal)
|
|
|
- return textResult(renderSpawn(result))
|
|
|
+ return result
|
|
|
},
|
|
|
presentCall: (args) => {
|
|
|
const parsed = args
|
|
|
@@ -118,7 +159,50 @@ export function apply(ctx: Context): void {
|
|
|
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.' },
|
|
|
},
|
|
|
- async execute(args: SendArgs, exec): Promise<ToolExecutionResult> {
|
|
|
+ output: {
|
|
|
+ schema: {
|
|
|
+ oneOf: [
|
|
|
+ {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: {
|
|
|
+ kind: { type: 'string', required: true, const: 'background' },
|
|
|
+ taskId: { type: 'string', required: true },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: {
|
|
|
+ kind: { type: 'string', required: true, const: 'foreground' },
|
|
|
+ viewport: { type: 'string', required: true },
|
|
|
+ waitReason: {
|
|
|
+ type: 'string',
|
|
|
+ required: true,
|
|
|
+ enum: ['stdin_read', 'inferred_idle', 'timeout', 'session_exit'],
|
|
|
+ },
|
|
|
+ sessionStatus: { ...SESSION_STATUS_SCHEMA, required: true },
|
|
|
+ truncated: { type: 'boolean', required: true },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ render: (_args, value) => [{
|
|
|
+ type: 'text',
|
|
|
+ text: value.kind === 'background'
|
|
|
+ ? `started background task ${value.taskId}`
|
|
|
+ : renderSend(value),
|
|
|
+ }],
|
|
|
+ presentationMeta: (_args, value) => value.kind === 'foreground'
|
|
|
+ ? {
|
|
|
+ viewport: value.viewport,
|
|
|
+ waitReason: value.waitReason,
|
|
|
+ sessionStatus: value.sessionStatus,
|
|
|
+ truncated: value.truncated,
|
|
|
+ }
|
|
|
+ : null,
|
|
|
+ },
|
|
|
+ async execute(args: SendArgs, exec) {
|
|
|
const owner = requireAgent(exec.agent)
|
|
|
const id = sessionId(args)
|
|
|
const request = { text: args.text, submit: args.submit ?? true }
|
|
|
@@ -145,12 +229,12 @@ export function apply(ctx: Context): void {
|
|
|
}
|
|
|
},
|
|
|
})
|
|
|
- return { content: textResult(`started background task ${taskId}`), isError: false }
|
|
|
+ return { kind: 'background' as const, taskId }
|
|
|
}
|
|
|
const operation = ctx.pty.startSend(owner, id, { ...request, signal: exec.signal })
|
|
|
const result = await operation.done
|
|
|
if (exec.signal.aborted) throw new Error('terminal send aborted')
|
|
|
- return { content: textResult(renderSend(result)), isError: false, meta: result }
|
|
|
+ return { kind: 'foreground' as const, ...result }
|
|
|
},
|
|
|
presentCall(args) {
|
|
|
const parsed = args as Partial<SendArgs>
|
|
|
@@ -174,12 +258,26 @@ export function apply(ctx: Context): void {
|
|
|
offset: { type: 'number', description: 'Newest-relative line offset (default 0).' },
|
|
|
count: { type: 'number', description: 'Requested line count (default 500; backend caps apply).' },
|
|
|
},
|
|
|
+ output: {
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: {
|
|
|
+ text: { type: 'string', required: true },
|
|
|
+ totalLines: { type: 'integer', required: true },
|
|
|
+ lineBegin: { type: 'integer', required: true },
|
|
|
+ lineEnd: { type: 'integer', required: true },
|
|
|
+ truncated: { type: 'boolean', required: true },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ render: (_args, value) => [{ type: 'text', text: renderRead(value) }],
|
|
|
+ },
|
|
|
execute(args: ReadArgs, exec) {
|
|
|
const result = ctx.pty.read(requireAgent(exec.agent), sessionId(args), {
|
|
|
...args.offset !== undefined ? { offset: args.offset } : {},
|
|
|
...args.count !== undefined ? { count: args.count } : {},
|
|
|
})
|
|
|
- return Promise.resolve(textResult(renderRead(result)))
|
|
|
+ return Promise.resolve(result)
|
|
|
},
|
|
|
presentCall: args => ({ card: 'generic', title: `Read terminal ${(args).sessionId}`, kind: 'read', rawInput: args }),
|
|
|
}))
|
|
|
@@ -191,9 +289,19 @@ export function apply(ctx: Context): void {
|
|
|
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.' },
|
|
|
},
|
|
|
+ output: {
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: {
|
|
|
+ delivered: { type: 'boolean', required: true, const: true },
|
|
|
+ targetPgid: { type: 'integer', required: true },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ render: (args, value) => [{ type: 'text', text: `delivered ${args.signal} to foreground process group ${value.targetPgid}` }],
|
|
|
+ },
|
|
|
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}`)
|
|
|
+ return ctx.pty.signal(requireAgent(exec.agent), sessionId(args), args.signal)
|
|
|
},
|
|
|
presentCall: args => ({ card: 'generic', title: `Signal terminal ${args.sessionId}`, kind: 'execute', rawInput: args }),
|
|
|
}))
|
|
|
@@ -204,10 +312,26 @@ export function apply(ctx: Context): void {
|
|
|
parameters: {
|
|
|
sessionId: { type: 'string', required: true, description: 'Terminal session id.' },
|
|
|
},
|
|
|
+ output: {
|
|
|
+ schema: {
|
|
|
+ type: 'object',
|
|
|
+ additionalProperties: false,
|
|
|
+ properties: {
|
|
|
+ sessionId: { type: 'string', required: true },
|
|
|
+ outcome: { type: 'string', required: true, enum: ['closed', 'already-closing'] },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ render: (_args, value) => [{
|
|
|
+ type: 'text',
|
|
|
+ text: value.outcome === 'closed'
|
|
|
+ ? `closed terminal session ${value.sessionId}`
|
|
|
+ : `terminal session ${value.sessionId} was already closing`,
|
|
|
+ }],
|
|
|
+ },
|
|
|
async execute(args: SessionArgs, exec) {
|
|
|
const id = sessionId(args)
|
|
|
const closed = await ctx.pty.kill(requireAgent(exec.agent), id)
|
|
|
- return textResult(closed ? `closed terminal session ${id}` : `terminal session ${id} was already closing`)
|
|
|
+ return { sessionId: id, outcome: closed ? 'closed' as const : 'already-closing' as const }
|
|
|
},
|
|
|
presentCall: args => ({ card: 'generic', title: `Close terminal ${(args).sessionId}`, kind: 'delete' }),
|
|
|
}))
|
|
|
@@ -216,8 +340,12 @@ export function apply(ctx: Context): void {
|
|
|
name: 'terminal_list',
|
|
|
description: 'List persistent terminal sessions owned by the current agent.',
|
|
|
parameters: {},
|
|
|
+ output: {
|
|
|
+ schema: { type: 'array', items: SESSION_SNAPSHOT_SCHEMA },
|
|
|
+ render: (_args, value) => [{ type: 'text', text: renderList(value) }],
|
|
|
+ },
|
|
|
execute(_args: Record<string, never>, exec) {
|
|
|
- return Promise.resolve(textResult(renderList(ctx.pty.list(requireAgent(exec.agent)))))
|
|
|
+ return Promise.resolve(ctx.pty.list(requireAgent(exec.agent)))
|
|
|
},
|
|
|
presentCall: () => ({ card: 'generic', title: 'List terminal sessions', kind: 'read' }),
|
|
|
}))
|