mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(tools): canonicalize terminal outputs
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-20-canonical-tool-output-contract.md: c05be75d6a207d9d026b9a75cf50d841292929fe
|
||||
2026-07-20-canonical-tool-output-contract.zh.md: cf22921c6fd0ec74fbbfd2cfbddd1c4a3379b4f2
|
||||
2026-07-20-canonical-tool-output-contract.md: 4099568de5dcc21a89b7873d4a6d4c7e9c62f8e4
|
||||
2026-07-20-canonical-tool-output-contract.zh.md: 01b50ef7493ea6548cd238f55e445a702e4d78b3
|
||||
|
||||
@@ -48,6 +48,7 @@ The first-party tools preserve their existing Native text while returning domain
|
||||
| `web_search` / `web_fetch` | The normalized `WebSearchResult` / `WebFetchResult` |
|
||||
| `lsp` | `{ kind: "locations", locations, resolvedWorkspaceRoot }` or `{ kind: "hover", hover }` |
|
||||
| `bash` | `{ kind: "background", taskId }` or `{ kind: "foreground" } & BashRunResult` |
|
||||
| `terminal_open` / `terminal_list` / `terminal_send` / `terminal_read` / `terminal_signal` / `terminal_close` | Public session snapshots, bounded read/send DTOs, signal/close outcomes, or a background task handle |
|
||||
| `task_output` / `task_list` / `task_kill` | Public task snapshots without owner or notification bookkeeping |
|
||||
| `subagent` | Background task handle or `{ kind: "foreground", runId, output: JsonValue[] }` |
|
||||
| `workflow` / `ralph` | `{ runId, agentsStarted, result: JsonValue }` |
|
||||
|
||||
@@ -48,6 +48,7 @@ type ToolExecutionResult =
|
||||
| `web_search` / `web_fetch` | 归一化后的 `WebSearchResult` / `WebFetchResult` |
|
||||
| `lsp` | `{ kind: "locations", locations, resolvedWorkspaceRoot }` 或 `{ kind: "hover", hover }` |
|
||||
| `bash` | `{ kind: "background", taskId }` 或 `{ kind: "foreground" } & BashRunResult` |
|
||||
| `terminal_open` / `terminal_list` / `terminal_send` / `terminal_read` / `terminal_signal` / `terminal_close` | 公开会话快照、有界的读取/发送 DTO、信号/关闭操作结果,或后台任务句柄 |
|
||||
| `task_output` / `task_list` / `task_kill` | 不含所有者或通知账务字段的公开任务快照 |
|
||||
| `subagent` | 后台任务句柄或 `{ kind: "foreground", runId, output: JsonValue[] }` |
|
||||
| `workflow` / `ralph` | `{ runId, agentsStarted, result: JsonValue }` |
|
||||
|
||||
@@ -46,6 +46,8 @@ Prefix-stable while tool visibility and definitions are unchanged.
|
||||
|
||||
Spawn returns the id and bounded MOTD. Send/read return bounded terminal text plus readiness/history markers. Background mode returns a generic task id. Results remain in session history until compaction; incremental task reads do not repeat consumed output.
|
||||
|
||||
Programmatic callers receive typed session snapshots, bounded send/read DTOs, signal and close outcomes, or `{ kind: "background", taskId }`; Native rendering preserves the text above.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Data-dependent and bounded by the backend; each returned result remains in history until compaction.
|
||||
|
||||
@@ -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' }),
|
||||
}))
|
||||
|
||||
@@ -1,13 +1,55 @@
|
||||
/** Model and ACP rendering for persistent terminal tool results. */
|
||||
|
||||
import type { PtyReadResult, PtySendRead, PtySendResult, PtySessionSnapshot, PtySpawnResult } from '@deepseek-ai/dsh-pty'
|
||||
interface RenderedSessionStatusRunning {
|
||||
kind: 'running'
|
||||
}
|
||||
|
||||
interface RenderedSessionStatusExited {
|
||||
kind: 'exited'
|
||||
exitCode: number | null
|
||||
signal: string | null
|
||||
}
|
||||
|
||||
type RenderedSessionStatus = RenderedSessionStatusRunning | RenderedSessionStatusExited
|
||||
|
||||
interface RenderedSessionSnapshot {
|
||||
sessionId: string
|
||||
name?: string
|
||||
type: string
|
||||
pid?: number
|
||||
status: RenderedSessionStatus
|
||||
}
|
||||
|
||||
interface RenderedSpawnResult extends RenderedSessionSnapshot {
|
||||
motd: string
|
||||
}
|
||||
|
||||
interface RenderedSendResult {
|
||||
viewport: string
|
||||
waitReason: 'stdin_read' | 'inferred_idle' | 'timeout' | 'session_exit'
|
||||
sessionStatus: RenderedSessionStatus
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
interface RenderedSendRead {
|
||||
delta: string
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
interface RenderedReadResult {
|
||||
text: string
|
||||
totalLines: number
|
||||
lineBegin: number
|
||||
lineEnd: number
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one created session and its bounded MOTD.
|
||||
* @param result - published spawn result.
|
||||
* @returns Model-facing session acknowledgement.
|
||||
*/
|
||||
export function renderSpawn(result: PtySpawnResult): string {
|
||||
export function renderSpawn(result: RenderedSpawnResult): string {
|
||||
const label = result.name === undefined ? result.sessionId : `${result.sessionId} (${result.name})`
|
||||
return `started terminal session ${label} [type: ${result.type}]\n${result.motd || '(no startup output)'}`
|
||||
}
|
||||
@@ -17,7 +59,7 @@ export function renderSpawn(result: PtySpawnResult): string {
|
||||
* @param result - settled send outcome.
|
||||
* @returns Terminal output plus wait/session markers.
|
||||
*/
|
||||
export function renderSend(result: PtySendResult): string {
|
||||
export function renderSend(result: RenderedSendResult): string {
|
||||
const output = result.viewport || '(no new output)'
|
||||
const status = result.sessionStatus.kind === 'running'
|
||||
? 'running'
|
||||
@@ -30,7 +72,7 @@ export function renderSend(result: PtySendResult): string {
|
||||
* @param read - consuming operation delta.
|
||||
* @returns Delta plus truncation marker when needed.
|
||||
*/
|
||||
export function renderSendRead(read: PtySendRead): string {
|
||||
export function renderSendRead(read: RenderedSendRead): string {
|
||||
return `${read.delta}${read.truncated ? `${read.delta.endsWith('\n') || read.delta.length === 0 ? '' : '\n'}[output truncated]` : ''}`
|
||||
}
|
||||
|
||||
@@ -39,7 +81,7 @@ export function renderSendRead(read: PtySendRead): string {
|
||||
* @param result - retained scrollback page.
|
||||
* @returns Page text plus pagination and truncation markers.
|
||||
*/
|
||||
export function renderRead(result: PtyReadResult): string {
|
||||
export function renderRead(result: RenderedReadResult): string {
|
||||
const output = result.text || '(no retained output)'
|
||||
return `${output}\n[lines: ${result.lineBegin}-${result.lineEnd} of ${result.totalLines}]${result.truncated ? '\n[output truncated]' : ''}`
|
||||
}
|
||||
@@ -49,7 +91,7 @@ export function renderRead(result: PtyReadResult): string {
|
||||
* @param sessions - fresh owner-scoped snapshots.
|
||||
* @returns One line per session or the empty marker.
|
||||
*/
|
||||
export function renderList(sessions: PtySessionSnapshot[]): string {
|
||||
export function renderList(sessions: readonly RenderedSessionSnapshot[]): string {
|
||||
if (sessions.length === 0) return '(no terminal sessions)'
|
||||
return sessions.map((session) => {
|
||||
const name = session.name === undefined ? '' : ` (${session.name})`
|
||||
|
||||
@@ -124,13 +124,50 @@ describe('tool-pty foreground surface', () => {
|
||||
|
||||
const spawned = await call(ctx, 'terminal_open', { type: 'stub', name: 'main' }, agent)
|
||||
expect(text(spawned)).toContain('started terminal session pty-1 (main)')
|
||||
expect(text(await call(ctx, 'terminal_list', {}, agent))).toContain('pty-1 (main) [stub] running pid=42')
|
||||
expect(text(await call(ctx, 'terminal_read', { sessionId: 'pty-1' }, agent))).toContain('history\n[lines: 0-1 of 1]')
|
||||
expect(text(await call(ctx, 'terminal_signal', { sessionId: 'pty-1', signal: 'SIGINT' }, agent))).toBe('delivered SIGINT to foreground process group 10')
|
||||
expect(spawned).toMatchObject({
|
||||
isError: false,
|
||||
value: {
|
||||
sessionId: 'pty-1',
|
||||
name: 'main',
|
||||
type: 'stub',
|
||||
pid: 42,
|
||||
status: { kind: 'running' },
|
||||
motd: 'stub prompt',
|
||||
},
|
||||
})
|
||||
const listed = await call(ctx, 'terminal_list', {}, agent)
|
||||
expect(text(listed)).toContain('pty-1 (main) [stub] running pid=42')
|
||||
expect(listed).toMatchObject({ isError: false, value: [{ sessionId: 'pty-1', name: 'main', type: 'stub', pid: 42, status: { kind: 'running' } }] })
|
||||
const read = await call(ctx, 'terminal_read', { sessionId: 'pty-1' }, agent)
|
||||
expect(text(read)).toContain('history\n[lines: 0-1 of 1]')
|
||||
expect(read).toMatchObject({ isError: false, value: { text: 'history', totalLines: 1, lineBegin: 0, lineEnd: 1, truncated: false } })
|
||||
const signalled = await call(ctx, 'terminal_signal', { sessionId: 'pty-1', signal: 'SIGINT' }, agent)
|
||||
expect(text(signalled)).toBe('delivered SIGINT to foreground process group 10')
|
||||
expect(signalled).toMatchObject({ isError: false, value: { delivered: true, targetPgid: 10 } })
|
||||
const sent = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'echo hi' }, agent)
|
||||
expect(text(sent)).toContain('command output\n[wait: stdin_read]\n[session: running]')
|
||||
expect(text(await call(ctx, 'terminal_close', { sessionId: 'pty-1' }, agent))).toBe('closed terminal session pty-1')
|
||||
expect(text(await call(ctx, 'terminal_list', {}, agent))).toBe('(no terminal sessions)')
|
||||
expect(sent).toMatchObject({
|
||||
isError: false,
|
||||
value: {
|
||||
kind: 'foreground',
|
||||
viewport: 'command output',
|
||||
waitReason: 'stdin_read',
|
||||
sessionStatus: { kind: 'running' },
|
||||
truncated: false,
|
||||
},
|
||||
meta: {
|
||||
viewport: 'command output',
|
||||
waitReason: 'stdin_read',
|
||||
sessionStatus: { kind: 'running' },
|
||||
truncated: false,
|
||||
},
|
||||
})
|
||||
const closed = await call(ctx, 'terminal_close', { sessionId: 'pty-1' }, agent)
|
||||
expect(text(closed)).toBe('closed terminal session pty-1')
|
||||
expect(closed).toMatchObject({ isError: false, value: { sessionId: 'pty-1', outcome: 'closed' } })
|
||||
const empty = await call(ctx, 'terminal_list', {}, agent)
|
||||
expect(text(empty)).toBe('(no terminal sessions)')
|
||||
expect(empty).toMatchObject({ isError: false, value: [] })
|
||||
})
|
||||
|
||||
it('fails without an initiating agent and rejects background before writing', async () => {
|
||||
@@ -178,7 +215,9 @@ describe('tool-pty task integration', () => {
|
||||
it('registers a generic task and exposes incremental output', async () => {
|
||||
const { ctx, agent } = await setup(true)
|
||||
await call(ctx, 'terminal_open', { type: 'stub' }, agent)
|
||||
expect(text(await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'build', run_in_background: true }, agent))).toBe('started background task pty-send-1')
|
||||
const started = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'build', run_in_background: true }, agent)
|
||||
expect(text(started)).toBe('started background task pty-send-1')
|
||||
expect(started).toMatchObject({ isError: false, value: { kind: 'background', taskId: 'pty-send-1' } })
|
||||
const output = await call(ctx, 'task_output', { task_id: 'pty-send-1', wait: true }, agent)
|
||||
expect(text(output)).toContain('live output')
|
||||
expect(text(output)).toContain('[status: completed, wait: stdin_read]')
|
||||
@@ -224,7 +263,9 @@ describe('tool-pty task integration', () => {
|
||||
const second = call(ctx, 'terminal_close', { sessionId: 'pty-1' }, agent)
|
||||
stub.sessions[0]!.closeGate?.resolve(undefined)
|
||||
await first
|
||||
expect(text(await second)).toBe('terminal session pty-1 was already closing')
|
||||
const result = await second
|
||||
expect(text(result)).toBe('terminal session pty-1 was already closing')
|
||||
expect(result).toMatchObject({ isError: false, value: { sessionId: 'pty-1', outcome: 'already-closing' } })
|
||||
})
|
||||
|
||||
it('renders an exited session detail for background completion', async () => {
|
||||
|
||||
Reference in New Issue
Block a user