Merge commit 'refs/codex-unblock/20260723/master' into worktree/pty-review-fixes
# Conflicts: # .agents/notes/implemented/feature/2026-06-30-interception-seams.md # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/core-data-structures/tools.md # docs/event-producer-consumer.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/tools/README.md # packages/core/tools/src/index.ts # packages/core/tools/src/schema.ts # packages/core/tools/tests/tools.spec.ts # packages/pty/tool-pty/README.md # packages/pty/tool-pty/src/index.ts # packages/pty/tool-pty/src/render.ts # packages/tasks/tool-tasks/README.md # packages/tasks/tool-tasks/src/index.ts
This commit is contained in:
@@ -12,7 +12,7 @@ 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 { ToolDefinition, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
import { boundTerminalText, renderList, renderRead, renderSend, renderSendRead, renderSpawn } from './render.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-tasks' {
|
||||
@@ -70,6 +70,50 @@ 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
|
||||
|
||||
const BACKGROUND_TASK_OUTPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, const: 'background' },
|
||||
taskId: { type: 'string', required: true },
|
||||
},
|
||||
} as const
|
||||
|
||||
function requireAgent(agent: Agent | undefined): Agent {
|
||||
if (agent === undefined) throw new Error('terminal tools require an initiating agent')
|
||||
return agent
|
||||
@@ -124,6 +168,17 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
cwd: { type: 'string', description: 'Initial working directory. Defaults to the deployment workspace root.' },
|
||||
},
|
||||
finalizeContent,
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
...SESSION_SNAPSHOT_PROPERTIES,
|
||||
motd: { type: 'string', required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: renderSpawn(value, maxResultBytes) }],
|
||||
},
|
||||
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), {
|
||||
@@ -131,7 +186,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
...args.name !== undefined ? { name: args.name } : {},
|
||||
...args.cwd !== undefined ? { cwd: args.cwd } : {},
|
||||
}, exec.signal)
|
||||
return textResult(renderSpawn(result, maxResultBytes), maxResultBytes)
|
||||
return result
|
||||
},
|
||||
presentCall: (args) => {
|
||||
const parsed = args
|
||||
@@ -152,7 +207,43 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
: {},
|
||||
},
|
||||
finalizeContent,
|
||||
async execute(args: SendArgs, exec): Promise<ToolExecutionResult> {
|
||||
output: {
|
||||
schema: {
|
||||
oneOf: [
|
||||
BACKGROUND_TASK_OUTPUT_SCHEMA,
|
||||
{
|
||||
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, maxResultBytes),
|
||||
}],
|
||||
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 }
|
||||
@@ -181,12 +272,12 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
}
|
||||
},
|
||||
})
|
||||
return { content: textResult(`started background task ${taskId}`, maxResultBytes), 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, maxResultBytes), maxResultBytes), isError: false, meta: result }
|
||||
return { kind: 'foreground' as const, ...result }
|
||||
},
|
||||
presentCall(args) {
|
||||
const parsed = args as Partial<SendArgs>
|
||||
@@ -211,12 +302,26 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
count: { type: 'number', description: 'Requested line count (default 500; backend caps apply).' },
|
||||
},
|
||||
finalizeContent,
|
||||
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, maxResultBytes) }],
|
||||
},
|
||||
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, maxResultBytes), maxResultBytes))
|
||||
return Promise.resolve(result)
|
||||
},
|
||||
presentCall: args => ({ card: 'generic', title: `Read terminal ${(args).sessionId}`, kind: 'read', rawInput: args }),
|
||||
}))
|
||||
@@ -229,11 +334,21 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
signal: { type: 'string', required: true, enum: ['SIGINT', 'SIGTERM', 'SIGKILL', 'SIGTSTP', 'SIGHUP'], description: 'Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.' },
|
||||
},
|
||||
finalizeContent,
|
||||
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}`, maxResultBytes)
|
||||
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}` }],
|
||||
},
|
||||
presentCall: args => ({ card: 'generic', title: `Signal terminal ${(args as SignalArgs).sessionId}`, kind: 'execute', rawInput: args }),
|
||||
async execute(args: SignalArgs, exec) {
|
||||
return ctx.pty.signal(requireAgent(exec.agent), sessionId(args), args.signal)
|
||||
},
|
||||
presentCall: args => ({ card: 'generic', title: `Signal terminal ${args.sessionId}`, kind: 'execute', rawInput: args }),
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
@@ -243,10 +358,26 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
sessionId: { type: 'string', required: true, description: 'Terminal session id.' },
|
||||
},
|
||||
finalizeContent,
|
||||
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`, maxResultBytes)
|
||||
return { sessionId: id, outcome: closed ? 'closed' as const : 'already-closing' as const }
|
||||
},
|
||||
presentCall: args => ({ card: 'generic', title: `Close terminal ${(args).sessionId}`, kind: 'delete' }),
|
||||
}))
|
||||
@@ -256,8 +387,12 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
description: 'List persistent terminal sessions owned by the current agent.',
|
||||
parameters: {},
|
||||
finalizeContent,
|
||||
output: {
|
||||
schema: { type: 'array', items: SESSION_SNAPSHOT_SCHEMA },
|
||||
render: (_args, value) => [{ type: 'text', text: renderList(value, maxResultBytes) }],
|
||||
},
|
||||
execute(_args: Record<string, never>, exec) {
|
||||
return Promise.resolve(textResult(renderList(ctx.pty.list(requireAgent(exec.agent)), maxResultBytes), maxResultBytes))
|
||||
return Promise.resolve(ctx.pty.list(requireAgent(exec.agent)))
|
||||
},
|
||||
presentCall: () => ({ card: 'generic', title: 'List terminal sessions', kind: 'read' }),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user