fix(terminal): retain background job ids under small result caps
This commit is contained in:
1 parent
54c13688a8
commit
6ce8654ff6
2 files changed
+38
-15
No files matched your search
@@ -31,7 +31,7 @@ export const inject = ['terminals', 'tools', 'systemPrompt', 'workingDirectory']
|
||||
|
||||
/** Default cap for one complete model-facing terminal result. */
|
||||
export const DEFAULT_MAX_RESULT_BYTES = 256 * 1024
|
||||
/** Smallest cap that preserves every counter-backed PTY and job id in its creation acknowledgement. */
|
||||
/** Smallest cap that preserves registry-issued PTY and job ids in creation acknowledgements. */
|
||||
export const MIN_MAX_RESULT_BYTES = 64
|
||||
|
||||
/** Model-facing terminal tool configuration. */
|
||||
@@ -145,6 +145,13 @@ function sendDetail(result: TerminalSendResult): string {
|
||||
: `session exited: ${result.sessionStatus.exitCode ?? result.sessionStatus.signal ?? 'unknown'}`
|
||||
}
|
||||
|
||||
function renderBackgroundJobId(jobId: string, maxBytes: number): string {
|
||||
const acknowledgement = `started background job ${jobId}`
|
||||
return Buffer.byteLength(acknowledgement) <= maxBytes
|
||||
? acknowledgement
|
||||
: `started job ${jobId}`
|
||||
}
|
||||
|
||||
/** Register all terminal tools and the minimal usage guidance. */
|
||||
export function apply(ctx: Context, config: Config = {}): void {
|
||||
const enableRunInBackground = config.enableRunInBackground ?? true
|
||||
@@ -236,7 +243,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
render: (_args, value) => [{
|
||||
type: 'text',
|
||||
text: value.kind === 'background'
|
||||
? `started background job ${value.jobId}`
|
||||
? renderBackgroundJobId(value.jobId, maxResultBytes)
|
||||
: renderSend(value, maxResultBytes),
|
||||
}],
|
||||
presentationMeta: (_args, value) => value.kind === 'foreground'
|
||||
|
||||
@@ -140,6 +140,16 @@ function text(result: { content: { type: string; text?: string }[] }): string {
|
||||
return result.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
||||
}
|
||||
|
||||
function backgroundJobId(result: Awaited<ReturnType<typeof call>>): string {
|
||||
if (!('value' in result)) throw new Error('missing background job result')
|
||||
const value = result.value
|
||||
if (typeof value !== 'object' || value === null || !('jobId' in value) || typeof value.jobId !== 'string') {
|
||||
throw new Error('missing background job id')
|
||||
}
|
||||
expect(value.jobId).toMatch(/^pty-send-[0-9a-f-]{36}$/)
|
||||
return value.jobId
|
||||
}
|
||||
|
||||
describe('tool-terminal foreground API', () => {
|
||||
it('registers exactly six schemas and drives the full owner-scoped lifecycle', async () => {
|
||||
const { ctx, agent } = await setup(false)
|
||||
@@ -345,7 +355,7 @@ describe('tool-terminal foreground API', () => {
|
||||
const background = await call(ctx, 'terminal_send', {
|
||||
sessionId: 'pty-1', text: 'work', run_in_background: true,
|
||||
}, agent)
|
||||
expect(text(background)).toContain('pty-send-1')
|
||||
expect(text(background)).toBe(`started job ${backgroundJobId(background)}`)
|
||||
expect(Buffer.byteLength(text(background))).toBeLessThanOrEqual(64)
|
||||
})
|
||||
|
||||
@@ -427,9 +437,10 @@ describe('tool-terminal task integration', () => {
|
||||
const { ctx, agent } = await setup(true)
|
||||
await call(ctx, 'terminal_open', { type: 'stub' }, agent)
|
||||
const started = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'build', run_in_background: true }, agent)
|
||||
expect(text(started)).toBe('started background job pty-send-1')
|
||||
expect(started).toMatchObject({ isError: false, value: { kind: 'background', jobId: 'pty-send-1' } })
|
||||
const output = await call(ctx, 'job_output', { job_id: 'pty-send-1', wait: true }, agent)
|
||||
const jobId = backgroundJobId(started)
|
||||
expect(text(started)).toBe(`started background job ${jobId}`)
|
||||
expect(started).toMatchObject({ isError: false, value: { kind: 'background', jobId } })
|
||||
const output = await call(ctx, 'job_output', { job_id: jobId, wait: true }, agent)
|
||||
expect(text(output)).toContain('live output')
|
||||
expect(text(output)).toContain('[status: completed, wait: stdin_read]')
|
||||
})
|
||||
@@ -443,8 +454,8 @@ describe('tool-terminal task integration', () => {
|
||||
|
||||
stub.sessions[0]!.delta = '界'.repeat(100)
|
||||
stub.sessions[0]!.deltaTruncated = true
|
||||
await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'background', run_in_background: true }, agent)
|
||||
const background = await call(ctx, 'job_output', { job_id: 'pty-send-1', wait: true }, agent)
|
||||
const started = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'background', run_in_background: true }, agent)
|
||||
const background = await call(ctx, 'job_output', { job_id: backgroundJobId(started), wait: true }, agent)
|
||||
expect(Buffer.byteLength(text(background))).toBeLessThanOrEqual(64)
|
||||
expect(text(background)).toContain('[status: completed')
|
||||
expect(text(background).match(/\[output truncated\]/g)).toHaveLength(1)
|
||||
@@ -459,16 +470,21 @@ describe('tool-terminal task integration', () => {
|
||||
expect((await callWithSignal(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'x', run_in_background: true }, agent, controller.signal)).isError).toBe(true)
|
||||
|
||||
stub.sessions[0]!.autoSettle = false
|
||||
expect(text(await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: '', run_in_background: true }, agent))).toContain('pty-send-1')
|
||||
expect(text(await call(ctx, 'job_kill', { job_id: 'pty-send-1' }, agent))).toContain('requested cancellation')
|
||||
const started = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: '', run_in_background: true }, agent)
|
||||
const jobId = backgroundJobId(started)
|
||||
expect(text(started)).toContain(jobId)
|
||||
expect(text(await call(ctx, 'job_kill', { job_id: jobId }, agent))).toContain('requested cancellation')
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
expect(text(await call(ctx, 'job_output', { job_id: 'pty-send-1' }, agent))).toContain('[status: killed')
|
||||
expect(text(await call(ctx, 'job_output', { job_id: jobId }, agent))).toContain('[status: killed')
|
||||
|
||||
stub.sessions[0]!.rejectOperation = true
|
||||
stub.sessions[0]!.autoSettle = false
|
||||
expect(text(await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'bad', run_in_background: true }, agent))).toContain('pty-send-2')
|
||||
const failed = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'bad', run_in_background: true }, agent)
|
||||
const failedJobId = backgroundJobId(failed)
|
||||
expect(failedJobId).not.toBe(jobId)
|
||||
expect(text(failed)).toContain(failedJobId)
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
expect(text(await call(ctx, 'job_output', { job_id: 'pty-send-2' }, agent))).toContain('[status: failed')
|
||||
expect(text(await call(ctx, 'job_output', { job_id: failedJobId }, agent))).toContain('[status: failed')
|
||||
})
|
||||
|
||||
it('reports foreground cancellation after the terminal operation settles', async () => {
|
||||
@@ -500,8 +516,8 @@ describe('tool-terminal task integration', () => {
|
||||
const { ctx, agent, stub } = await setup(true)
|
||||
await call(ctx, 'terminal_open', { type: 'stub' }, agent)
|
||||
stub.sessions[0]!.statusValue = { kind: 'exited', exitCode: null, signal: null }
|
||||
await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'exit', run_in_background: true }, agent)
|
||||
const output = await call(ctx, 'job_output', { job_id: 'pty-send-1', wait: true }, agent)
|
||||
const started = await call(ctx, 'terminal_send', { sessionId: 'pty-1', text: 'exit', run_in_background: true }, agent)
|
||||
const output = await call(ctx, 'job_output', { job_id: backgroundJobId(started), wait: true }, agent)
|
||||
expect(text(output)).toContain('session exited: unknown')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user