Skip to content

Commit 26f1675

Browse files
authored
Merge pull request #329 from drivecore/fix/cli-logging-output
Fix: Restore visibility of tool execution output
2 parents 86a4f30 + 8d19c41 commit 26f1675

File tree

14 files changed

+284
-14
lines changed

14 files changed

+284
-14
lines changed

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ mycoder "Implement a React component that displays a list of items"
3535
# Run with a prompt from a file
3636
mycoder -f prompt.txt
3737

38+
# Enable interactive corrections during execution (press Ctrl+M to send corrections)
39+
mycoder --interactive "Implement a React component that displays a list of items"
40+
3841
# Disable user prompts for fully automated sessions
3942
mycoder --userPrompt false "Generate a basic Express.js server"
4043

@@ -119,6 +122,35 @@ export default {
119122

120123
CLI arguments will override settings in your configuration file.
121124

125+
## Interactive Corrections
126+
127+
MyCoder supports sending corrections to the main agent while it's running. This is useful when you notice the agent is going off track or needs additional information.
128+
129+
### Usage
130+
131+
1. Start MyCoder with the `--interactive` flag:
132+
```bash
133+
mycoder --interactive "Implement a React component"
134+
```
135+
136+
2. While the agent is running, press `Ctrl+M` to enter correction mode
137+
3. Type your correction or additional context
138+
4. Press Enter to send the correction to the agent
139+
140+
The agent will receive your message and incorporate it into its decision-making process, similar to how parent agents can send messages to sub-agents.
141+
142+
### Configuration
143+
144+
You can enable interactive corrections in your configuration file:
145+
146+
```js
147+
// mycoder.config.js
148+
export default {
149+
// ... other options
150+
interactive: true,
151+
};
152+
```
153+
122154
### GitHub Comment Commands
123155

124156
MyCoder can be triggered directly from GitHub issue comments using the flexible `/mycoder` command:

packages/agent/src/core/executeToolCall.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ export const executeToolCall = async (
7373
if (tool.logParameters) {
7474
tool.logParameters(validatedJson, toolContext);
7575
} else {
76-
logger.log('Parameters:');
76+
logger.info('Parameters:');
7777
Object.entries(validatedJson).forEach(([name, value]) => {
78-
logger.log(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`);
78+
logger.info(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`);
7979
});
8080
}
8181

@@ -103,12 +103,12 @@ export const executeToolCall = async (
103103
if (tool.logReturns) {
104104
tool.logReturns(output, toolContext);
105105
} else {
106-
logger.log('Results:');
106+
logger.info('Results:');
107107
if (typeof output === 'string') {
108-
logger.log(` - ${output}`);
108+
logger.info(` - ${output}`);
109109
} else if (typeof output === 'object') {
110110
Object.entries(output).forEach(([name, value]) => {
111-
logger.log(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`);
111+
logger.info(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`);
112112
});
113113
}
114114
}

packages/agent/src/core/toolAgent/toolAgentCore.ts

+24
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,30 @@ export const toolAgent = async (
8888
}
8989
}
9090
}
91+
92+
// Check for messages from user (for main agent only)
93+
// Import this at the top of the file
94+
try {
95+
// Dynamic import to avoid circular dependencies
96+
const { userMessages } = await import('../../tools/interaction/userMessage.js');
97+
98+
if (userMessages && userMessages.length > 0) {
99+
// Get all user messages and clear the queue
100+
const pendingUserMessages = [...userMessages];
101+
userMessages.length = 0;
102+
103+
// Add each message to the conversation
104+
for (const message of pendingUserMessages) {
105+
logger.info(`Message from user: ${message}`);
106+
messages.push({
107+
role: 'user',
108+
content: `[Correction from user]: ${message}`,
109+
});
110+
}
111+
}
112+
} catch (error) {
113+
logger.debug('Error checking for user messages:', error);
114+
}
91115

92116
// Convert tools to function definitions
93117
const functionDefinitions = tools.map((tool) => ({

packages/agent/src/core/toolAgent/toolExecutor.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export async function executeTools(
3737

3838
const { logger } = context;
3939

40-
logger.debug(`Executing ${toolCalls.length} tool calls`);
40+
logger.info(`Executing ${toolCalls.length} tool calls`);
4141

4242
const toolResults = await Promise.all(
4343
toolCalls.map(async (call) => {

packages/agent/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export * from './tools/agent/AgentTracker.js';
2525
// Tools - Interaction
2626
export * from './tools/agent/agentExecute.js';
2727
export * from './tools/interaction/userPrompt.js';
28+
export * from './tools/interaction/userMessage.js';
2829

2930
// Core
3031
export * from './core/executeToolCall.js';
@@ -49,3 +50,4 @@ export * from './utils/logger.js';
4950
export * from './utils/mockLogger.js';
5051
export * from './utils/stringifyLimited.js';
5152
export * from './utils/userPrompt.js';
53+
export * from './utils/interactiveInput.js';

packages/agent/src/tools/agent/agentDone.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ export const agentDoneTool: Tool<Parameters, ReturnType> = {
2727
execute: ({ result }) => Promise.resolve({ result }),
2828
logParameters: () => {},
2929
logReturns: (output, { logger }) => {
30-
logger.log(`Completed: ${output}`);
30+
logger.log(`Completed: ${output.result}`);
3131
},
3232
};

packages/agent/src/tools/agent/agentStart.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '../../core/toolAgent/config.js';
88
import { toolAgent } from '../../core/toolAgent/toolAgentCore.js';
99
import { Tool, ToolContext } from '../../core/types.js';
10-
import { LogLevel, LoggerListener } from '../../utils/logger.js';
10+
import { LogLevel, Logger, LoggerListener } from '../../utils/logger.js';
1111
import { getTools } from '../getTools.js';
1212

1313
import { AgentStatus, AgentState } from './AgentTracker.js';
@@ -161,7 +161,7 @@ export const agentStartTool: Tool<Parameters, ReturnType> = {
161161
});
162162
// Add the listener to the sub-agent logger as well
163163
subAgentLogger.listeners.push(logCaptureListener);
164-
} catch (e) {
164+
} catch {
165165
// If Logger instantiation fails (e.g., in tests), fall back to using the context logger
166166
context.logger.debug('Failed to create sub-agent logger, using context logger instead');
167167
}

packages/agent/src/tools/getTools.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { agentStartTool } from './agent/agentStart.js';
88
import { listAgentsTool } from './agent/listAgents.js';
99
import { fetchTool } from './fetch/fetch.js';
1010
import { userPromptTool } from './interaction/userPrompt.js';
11+
import { userMessageTool } from './interaction/userMessage.js';
1112
import { createMcpTool } from './mcp.js';
1213
import { listSessionsTool } from './session/listSessions.js';
1314
import { sessionMessageTool } from './session/sessionMessage.js';
@@ -52,9 +53,10 @@ export function getTools(options?: GetToolsOptions): Tool[] {
5253
waitTool as unknown as Tool,
5354
];
5455

55-
// Only include userPrompt tool if enabled
56+
// Only include user interaction tools if enabled
5657
if (userPrompt) {
5758
tools.push(userPromptTool as unknown as Tool);
59+
tools.push(userMessageTool as unknown as Tool);
5860
}
5961

6062
// Add MCP tool if we have any servers configured
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { z } from 'zod';
2+
import { zodToJsonSchema } from 'zod-to-json-schema';
3+
4+
import { Tool } from '../../core/types.js';
5+
6+
// Track the messages sent to the main agent
7+
export const userMessages: string[] = [];
8+
9+
const parameterSchema = z.object({
10+
message: z
11+
.string()
12+
.describe('The message or correction to send to the main agent'),
13+
description: z
14+
.string()
15+
.describe('The reason for this message (max 80 chars)'),
16+
});
17+
18+
const returnSchema = z.object({
19+
received: z
20+
.boolean()
21+
.describe('Whether the message was received by the main agent'),
22+
messageCount: z
23+
.number()
24+
.describe('The number of messages in the queue'),
25+
});
26+
27+
type Parameters = z.infer<typeof parameterSchema>;
28+
type ReturnType = z.infer<typeof returnSchema>;
29+
30+
export const userMessageTool: Tool<Parameters, ReturnType> = {
31+
name: 'userMessage',
32+
description: 'Sends a message or correction from the user to the main agent',
33+
logPrefix: '✉️',
34+
parameters: parameterSchema,
35+
parametersJsonSchema: zodToJsonSchema(parameterSchema),
36+
returns: returnSchema,
37+
returnsJsonSchema: zodToJsonSchema(returnSchema),
38+
execute: async ({ message }, { logger }) => {
39+
logger.debug(`Received message from user: ${message}`);
40+
41+
// Add the message to the queue
42+
userMessages.push(message);
43+
44+
logger.debug(`Added message to queue. Total messages: ${userMessages.length}`);
45+
46+
return {
47+
received: true,
48+
messageCount: userMessages.length,
49+
};
50+
},
51+
logParameters: (input, { logger }) => {
52+
logger.log(`User message received: ${input.description}`);
53+
},
54+
logReturns: (output, { logger }) => {
55+
if (output.received) {
56+
logger.log(
57+
`Message added to queue. Queue now has ${output.messageCount} message(s).`,
58+
);
59+
} else {
60+
logger.error('Failed to add message to queue.');
61+
}
62+
},
63+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import * as readline from 'readline';
2+
import { createInterface } from 'readline/promises';
3+
import { Writable } from 'stream';
4+
5+
import chalk from 'chalk';
6+
7+
import { userMessages } from '../tools/interaction/userMessage.js';
8+
9+
// Custom output stream to intercept console output
10+
class OutputInterceptor extends Writable {
11+
private originalStdout: NodeJS.WriteStream;
12+
private paused: boolean = false;
13+
14+
constructor(originalStdout: NodeJS.WriteStream) {
15+
super();
16+
this.originalStdout = originalStdout;
17+
}
18+
19+
pause() {
20+
this.paused = true;
21+
}
22+
23+
resume() {
24+
this.paused = false;
25+
}
26+
27+
_write(
28+
chunk: Buffer | string,
29+
encoding: BufferEncoding,
30+
callback: (error?: Error | null) => void,
31+
): void {
32+
if (!this.paused) {
33+
this.originalStdout.write(chunk, encoding);
34+
}
35+
callback();
36+
}
37+
}
38+
39+
// Initialize interactive input mode
40+
export const initInteractiveInput = () => {
41+
// Save original stdout
42+
const originalStdout = process.stdout;
43+
44+
// Create interceptor
45+
const interceptor = new OutputInterceptor(originalStdout);
46+
47+
// We no longer try to replace process.stdout as it's not allowed in newer Node.js versions
48+
// Instead, we'll just use the interceptor for readline
49+
50+
// Create readline interface for listening to key presses
51+
const rl = readline.createInterface({
52+
input: process.stdin,
53+
output: interceptor,
54+
terminal: true,
55+
});
56+
57+
// Close the interface to avoid keeping the process alive
58+
rl.close();
59+
60+
// Listen for keypress events
61+
readline.emitKeypressEvents(process.stdin);
62+
if (process.stdin.isTTY) {
63+
process.stdin.setRawMode(true);
64+
}
65+
66+
process.stdin.on('keypress', async (str, key) => {
67+
// Check for Ctrl+C to exit
68+
if (key.ctrl && key.name === 'c') {
69+
process.exit(0);
70+
}
71+
72+
// Check for Ctrl+M to enter message mode
73+
if (key.ctrl && key.name === 'm') {
74+
// Pause output
75+
interceptor.pause();
76+
77+
// Create a readline interface for input
78+
const inputRl = createInterface({
79+
input: process.stdin,
80+
output: originalStdout,
81+
});
82+
83+
try {
84+
// Reset cursor position and clear line
85+
originalStdout.write('\r\n');
86+
originalStdout.write(
87+
chalk.green(
88+
'Enter correction or additional context (Ctrl+C to cancel):\n',
89+
) + '> ',
90+
);
91+
92+
// Get user input
93+
const userInput = await inputRl.question('');
94+
95+
// Add message to queue if not empty
96+
if (userInput.trim()) {
97+
userMessages.push(userInput);
98+
originalStdout.write(
99+
chalk.green('\nMessage sent to agent. Resuming output...\n\n'),
100+
);
101+
} else {
102+
originalStdout.write(
103+
chalk.yellow('\nEmpty message not sent. Resuming output...\n\n'),
104+
);
105+
}
106+
} catch (error) {
107+
originalStdout.write(
108+
chalk.red(`\nError sending message: ${error}\n\n`),
109+
);
110+
} finally {
111+
// Close input readline interface
112+
inputRl.close();
113+
114+
// Resume output
115+
interceptor.resume();
116+
}
117+
}
118+
});
119+
120+
// Return a cleanup function
121+
return () => {
122+
// We no longer need to restore process.stdout
123+
124+
// Disable raw mode
125+
if (process.stdin.isTTY) {
126+
process.stdin.setRawMode(false);
127+
}
128+
};
129+
};

packages/agent/src/utils/logger.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export class Logger {
6666
}
6767

6868
private emitMessages(level: LogLevel, messages: unknown[]) {
69-
if (LogLevel.debug < this.logLevelIndex) return;
69+
// Allow all messages at the configured log level or higher
70+
if (level < this.logLevelIndex) return;
7071

7172
const lines = messages
7273
.map((message) =>

0 commit comments

Comments
 (0)