From aed1b9f6ba489da19f2170c136861a7c80ad6e33 Mon Sep 17 00:00:00 2001 From: MyCoder AI Date: Tue, 11 Mar 2025 14:30:33 +0000 Subject: [PATCH] feat: add showStdIn and showStdout options to shellMessage and shellStart Closes #167 --- .../src/tools/system/shellMessage.test.ts | 68 +++++++++++++++++++ .../agent/src/tools/system/shellMessage.ts | 44 +++++++++++- .../agent/src/tools/system/shellStart.test.ts | 33 +++++++++ packages/agent/src/tools/system/shellStart.ts | 40 ++++++++++- 4 files changed, 180 insertions(+), 5 deletions(-) diff --git a/packages/agent/src/tools/system/shellMessage.test.ts b/packages/agent/src/tools/system/shellMessage.test.ts index 918508b..24b1061 100644 --- a/packages/agent/src/tools/system/shellMessage.test.ts +++ b/packages/agent/src/tools/system/shellMessage.test.ts @@ -214,4 +214,72 @@ describe('shellMessageTool', () => { expect(checkResult.completed).toBe(true); expect(processStates.has(instanceId)).toBe(true); }); + + it('should respect showStdIn and showStdout parameters', async () => { + // Start a process with default visibility settings + const startResult = await shellStartTool.execute( + { + command: 'cat', + description: 'Test with stdin/stdout visibility', + timeout: 50, // Force async mode + }, + toolContext, + ); + + const instanceId = getInstanceId(startResult); + + // Verify process state has default visibility settings + const processState = processStates.get(instanceId); + expect(processState?.showStdIn).toBe(false); + expect(processState?.showStdout).toBe(false); + + // Send input with explicit visibility settings + await shellMessageTool.execute( + { + instanceId, + stdin: 'test input', + description: 'Test with explicit visibility settings', + showStdIn: true, + showStdout: true, + }, + toolContext, + ); + + // Verify process state still exists + expect(processStates.has(instanceId)).toBe(true); + }); + + it('should inherit visibility settings from process state', async () => { + // Start a process with explicit visibility settings + const startResult = await shellStartTool.execute( + { + command: 'cat', + description: 'Test with inherited visibility settings', + timeout: 50, // Force async mode + showStdIn: true, + showStdout: true, + }, + toolContext, + ); + + const instanceId = getInstanceId(startResult); + + // Verify process state has the specified visibility settings + const processState = processStates.get(instanceId); + expect(processState?.showStdIn).toBe(true); + expect(processState?.showStdout).toBe(true); + + // Send input without specifying visibility settings + await shellMessageTool.execute( + { + instanceId, + stdin: 'test input', + description: 'Test with inherited visibility settings', + }, + toolContext, + ); + + // Verify process state still exists + expect(processStates.has(instanceId)).toBe(true); + }); }); diff --git a/packages/agent/src/tools/system/shellMessage.ts b/packages/agent/src/tools/system/shellMessage.ts index 4521197..77479d6 100644 --- a/packages/agent/src/tools/system/shellMessage.ts +++ b/packages/agent/src/tools/system/shellMessage.ts @@ -54,6 +54,18 @@ const parameterSchema = z.object({ description: z .string() .describe('The reason for this shell interaction (max 80 chars)'), + showStdIn: z + .boolean() + .optional() + .describe( + 'Whether to show the input in the logs (default: false or value from shellStart)', + ), + showStdout: z + .boolean() + .optional() + .describe( + 'Whether to show output in the logs (default: false or value from shellStart)', + ), }); const returnSchema = z @@ -82,7 +94,7 @@ export const shellMessageTool: Tool = { returnsJsonSchema: zodToJsonSchema(returnSchema), execute: async ( - { instanceId, stdin, signal }, + { instanceId, stdin, signal, showStdIn, showStdout }, { logger }, ): Promise => { logger.verbose( @@ -115,6 +127,14 @@ export const shellMessageTool: Tool = { if (!processState.process.stdin?.writable) { throw new Error('Process stdin is not available'); } + + // Determine whether to show stdin (prefer explicit parameter, fall back to process state) + const shouldShowStdIn = + showStdIn !== undefined ? showStdIn : processState.showStdIn; + if (shouldShowStdIn) { + logger.info(`[${instanceId}] stdin: ${stdin}`); + } + processState.process.stdin.write(`${stdin}\n`); } @@ -130,11 +150,22 @@ export const shellMessageTool: Tool = { processState.stderr = []; logger.verbose('Interaction completed successfully'); + + // Determine whether to show stdout (prefer explicit parameter, fall back to process state) + const shouldShowStdout = + showStdout !== undefined ? showStdout : processState.showStdout; + if (stdout) { logger.verbose(`stdout: ${stdout.trim()}`); + if (shouldShowStdout) { + logger.info(`[${instanceId}] stdout: ${stdout.trim()}`); + } } if (stderr) { logger.verbose(`stderr: ${stderr.trim()}`); + if (shouldShowStdout) { + logger.info(`[${instanceId}] stderr: ${stderr.trim()}`); + } } return { @@ -168,8 +199,17 @@ export const shellMessageTool: Tool = { logParameters: (input, { logger }) => { const processState = processStates.get(input.instanceId); + const showStdIn = + input.showStdIn !== undefined + ? input.showStdIn + : processState?.showStdIn || false; + const showStdout = + input.showStdout !== undefined + ? input.showStdout + : processState?.showStdout || false; + logger.info( - `Interacting with shell command "${processState ? processState.command : ''}", ${input.description}`, + `Interacting with shell command "${processState ? processState.command : ''}", ${input.description} (showStdIn: ${showStdIn}, showStdout: ${showStdout})`, ); }, logReturns: () => {}, diff --git a/packages/agent/src/tools/system/shellStart.test.ts b/packages/agent/src/tools/system/shellStart.test.ts index 7984296..1abad12 100644 --- a/packages/agent/src/tools/system/shellStart.test.ts +++ b/packages/agent/src/tools/system/shellStart.test.ts @@ -158,4 +158,37 @@ describe('shellStartTool', () => { expect(result.mode).toBe('sync'); }); + + it('should store showStdIn and showStdout settings in process state', async () => { + const result = await shellStartTool.execute( + { + command: 'echo "test"', + description: 'Test with stdout visibility', + showStdIn: true, + showStdout: true, + }, + toolContext, + ); + + expect(result.mode).toBe('sync'); + + // For async mode, check the process state directly + const asyncResult = await shellStartTool.execute( + { + command: 'sleep 1', + description: 'Test with stdin/stdout visibility in async mode', + timeout: 50, // Force async mode + showStdIn: true, + showStdout: true, + }, + toolContext, + ); + + if (asyncResult.mode === 'async') { + const processState = processStates.get(asyncResult.instanceId); + expect(processState).toBeDefined(); + expect(processState?.showStdIn).toBe(true); + expect(processState?.showStdout).toBe(true); + } + }); }); diff --git a/packages/agent/src/tools/system/shellStart.ts b/packages/agent/src/tools/system/shellStart.ts index bb98aa6..8832ff4 100644 --- a/packages/agent/src/tools/system/shellStart.ts +++ b/packages/agent/src/tools/system/shellStart.ts @@ -20,6 +20,8 @@ type ProcessState = { signaled: boolean; exitCode: number | null; }; + showStdIn: boolean; + showStdout: boolean; }; // Global map to store process state @@ -36,6 +38,14 @@ const parameterSchema = z.object({ .describe( 'Timeout in ms before switching to async mode (default: 10s, which usually is sufficient)', ), + showStdIn: z + .boolean() + .optional() + .describe('Whether to show the command input in the logs (default: false)'), + showStdout: z + .boolean() + .optional() + .describe('Whether to show command output in the logs (default: false)'), }); const returnSchema = z.union([ @@ -77,9 +87,17 @@ export const shellStartTool: Tool = { returnsJsonSchema: zodToJsonSchema(returnSchema), execute: async ( - { command, timeout = DEFAULT_TIMEOUT }, + { + command, + timeout = DEFAULT_TIMEOUT, + showStdIn = false, + showStdout = false, + }, { logger, workingDirectory }, ): Promise => { + if (showStdIn) { + logger.info(`Command input: ${command}`); + } logger.verbose(`Starting shell command: ${command}`); return new Promise((resolve) => { @@ -100,6 +118,8 @@ export const shellStartTool: Tool = { stdout: [], stderr: [], state: { completed: false, signaled: false, exitCode: null }, + showStdIn, + showStdout, }; // Initialize combined process state @@ -111,6 +131,9 @@ export const shellStartTool: Tool = { const output = data.toString(); processState.stdout.push(output); logger.verbose(`[${instanceId}] stdout: ${output.trim()}`); + if (processState.showStdout) { + logger.info(`[${instanceId}] stdout: ${output.trim()}`); + } }); if (process.stderr) @@ -118,6 +141,9 @@ export const shellStartTool: Tool = { const output = data.toString(); processState.stderr.push(output); logger.verbose(`[${instanceId}] stderr: ${output.trim()}`); + if (processState.showStdout) { + logger.info(`[${instanceId}] stderr: ${output.trim()}`); + } }); process.on('error', (error) => { @@ -186,10 +212,18 @@ export const shellStartTool: Tool = { }, logParameters: ( - { command, description, timeout = DEFAULT_TIMEOUT }, + { + command, + description, + timeout = DEFAULT_TIMEOUT, + showStdIn = false, + showStdout = false, + }, { logger }, ) => { - logger.info(`Running "${command}", ${description} (timeout: ${timeout}ms)`); + logger.info( + `Running "${command}", ${description} (timeout: ${timeout}ms, showStdIn: ${showStdIn}, showStdout: ${showStdout})`, + ); }, logReturns: (output, { logger }) => { if (output.mode === 'async') {