fix(pty): keep bounded results actionable
This commit is contained in:
@@ -9,9 +9,9 @@ Six model-facing tools over `ctx.pty`: `terminal_open`, `terminal_send`, `termin
|
||||
| key | default | meaning |
|
||||
|---|---:|---|
|
||||
| `enableRunInBackground` | `true` | expose and accept `run_in_background`; false omits the schema field and rejects a forced undeclared argument |
|
||||
| `maxResultBytes` | `262144` | UTF-8 cap for each complete terminal result or PTY task output after wait, session, pagination, truncation, and task-status metadata |
|
||||
| `maxResultBytes` | `262144` | UTF-8 cap (minimum `64`) for each complete terminal result or PTY task output after wait, session, pagination, truncation, and task-status metadata |
|
||||
|
||||
Both values are validated at load. When a result exceeds `maxResultBytes`, rendering reserves space for control metadata and a truncation marker when they fit; cuts preserve UTF-8 boundaries.
|
||||
Both values are validated at load. The minimum result cap keeps every registry-issued session or task id visible in its creation acknowledgement. When a result exceeds `maxResultBytes`, rendering reserves space for control metadata and a truncation marker when they fit; cuts preserve UTF-8 boundaries.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -53,7 +53,7 @@ Prefix-stable while tool visibility and definitions are unchanged.
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Spawn returns the id and bounded MOTD. Send/read return bounded terminal text plus readiness/history markers. Background mode returns a generic task id. Every complete result is capped by `maxResultBytes`, including generic task status text. Results remain in session history until compaction; incremental task reads do not repeat consumed output.
|
||||
Spawn returns the id and bounded MOTD. Send/read return bounded terminal text plus readiness/history markers. Background mode returns a generic task id. Every complete result is capped by `maxResultBytes`, including normalized error text and generic task status text. Results remain in session history until compaction; incremental task reads do not repeat consumed output.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -28,6 +28,17 @@ export const inject = ['pty', 'tools', 'systemPrompt']
|
||||
|
||||
/** 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 task id in its creation acknowledgement. */
|
||||
export const MIN_MAX_RESULT_BYTES = 64
|
||||
|
||||
const TOOL_NAMES = new Set([
|
||||
'terminal_open',
|
||||
'terminal_send',
|
||||
'terminal_read',
|
||||
'terminal_signal',
|
||||
'terminal_close',
|
||||
'terminal_list',
|
||||
])
|
||||
|
||||
/** Model-facing terminal tool configuration. */
|
||||
export interface Config {
|
||||
@@ -40,7 +51,7 @@ export interface Config {
|
||||
/** Schemastery configuration for the terminal tool consumer. */
|
||||
export const Config: z<Config> = z.object({
|
||||
enableRunInBackground: z.boolean().default(true),
|
||||
maxResultBytes: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RESULT_BYTES),
|
||||
maxResultBytes: z.number().step(1).min(MIN_MAX_RESULT_BYTES).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RESULT_BYTES),
|
||||
})
|
||||
|
||||
interface SpawnArgs {
|
||||
@@ -100,9 +111,15 @@ function sendDetail(result: PtySendResult): string {
|
||||
export function apply(ctx: Context, config: Config = {}): void {
|
||||
const enableRunInBackground = config.enableRunInBackground ?? true
|
||||
const maxResultBytes = config.maxResultBytes ?? DEFAULT_MAX_RESULT_BYTES
|
||||
if (!Number.isSafeInteger(maxResultBytes) || maxResultBytes <= 0) {
|
||||
throw new Error('tool-pty: maxResultBytes must be a positive safe integer')
|
||||
if (!Number.isSafeInteger(maxResultBytes) || maxResultBytes < MIN_MAX_RESULT_BYTES) {
|
||||
throw new Error(`tool-pty: maxResultBytes must be a safe integer of at least ${MIN_MAX_RESULT_BYTES}`)
|
||||
}
|
||||
ctx.on('tools/execute', async (exec, next): Promise<ToolExecutionResult> => {
|
||||
const result = await next()
|
||||
if (!TOOL_NAMES.has(exec.name)) return result
|
||||
const raw = rawResultText(result)
|
||||
return raw === undefined ? result : { ...result, content: textResult(raw, maxResultBytes) }
|
||||
})
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:pty',
|
||||
order: 106,
|
||||
|
||||
@@ -197,6 +197,32 @@ describe('tool-pty foreground surface', () => {
|
||||
|
||||
const invalid = await setupBase(false)
|
||||
expect(() => { ToolPty.apply(invalid.ctx, { maxResultBytes: 0 }) }).toThrow('maxResultBytes')
|
||||
expect(() => { ToolPty.apply(invalid.ctx, { maxResultBytes: 63 }) }).toThrow('at least 64')
|
||||
})
|
||||
|
||||
it('bounds normalized errors and preserves allocated ids at the minimum result cap', async () => {
|
||||
const { ctx, agent } = await setup(true, { maxResultBytes: 64 })
|
||||
const failed = await call(ctx, 'terminal_open', { type: 'x'.repeat(1_000) }, agent)
|
||||
expect(failed.isError).toBe(true)
|
||||
expect(Buffer.byteLength(text(failed))).toBeLessThanOrEqual(64)
|
||||
expect(text(failed)).toContain('[output truncated]')
|
||||
|
||||
const opened = await call(ctx, 'terminal_open', { type: 'stub', name: 'n'.repeat(1_000) }, agent)
|
||||
expect(text(opened)).toContain('pty-1')
|
||||
expect(Buffer.byteLength(text(opened))).toBeLessThanOrEqual(64)
|
||||
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(Buffer.byteLength(text(background))).toBeLessThanOrEqual(64)
|
||||
})
|
||||
|
||||
it('leaves a structured around-dispatch replacement unchanged', async () => {
|
||||
const { ctx, agent } = await setup(false, { maxResultBytes: 64 })
|
||||
ctx.on('tools/execute', async (exec, next) => exec.name === 'terminal_list'
|
||||
? { content: [], isError: false }
|
||||
: next())
|
||||
expect((await call(ctx, 'terminal_list', {}, agent)).content).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user