Skip to content

Commit de2861f

Browse files
committed
feat: Add interactive correction feature to CLI mode
This commit adds the ability to send corrections to the main agent while it's running. Key features: - Press Ctrl+M during agent execution to enter correction mode - Type a correction message and send it to the agent - Agent receives and incorporates the message into its context - Similar to how parent agents can send messages to sub-agents Closes #326
1 parent 226fa98 commit de2861f

File tree

11 files changed

+284
-5
lines changed

11 files changed

+284
-5
lines changed

README.md

Lines changed: 32 additions & 0 deletions
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:

issue_content.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## Add Interactive Correction Feature to CLI Mode
2+
3+
### Description
4+
Add a feature to the CLI mode that allows users to send corrections to the main agent while it's running, similar to how sub-agents can receive messages via the `agentMessage` tool. This would enable users to provide additional context, corrections, or guidance to the main agent without restarting the entire process.
5+
6+
### Requirements
7+
- Implement a key command that pauses the output and triggers a user prompt
8+
- Allow the user to type a correction message
9+
- Send the correction to the main agent using a mechanism similar to `agentMessage`
10+
- Resume normal operation after the correction is sent
11+
- Ensure the correction is integrated into the agent's context
12+
13+
### Implementation Considerations
14+
- Reuse the existing `agentMessage` functionality
15+
- Add a new tool for the main agent to receive messages from the user
16+
- Modify the CLI to capture key commands during execution
17+
- Handle the pausing and resuming of output during message entry
18+
- Ensure the correction is properly formatted and sent to the agent
19+
20+
### Why this is valuable
21+
This feature will make the tool more interactive and efficient, allowing users to steer the agent in the right direction without restarting when they notice the agent is going off track or needs additional information.

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

Lines changed: 24 additions & 0 deletions
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.log(`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/index.ts

Lines changed: 2 additions & 0 deletions
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/agentStart.ts

Lines changed: 2 additions & 2 deletions
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

Lines changed: 3 additions & 1 deletion
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
Lines changed: 63 additions & 0 deletions
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+
};
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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(chunk: Buffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
28+
if (!this.paused) {
29+
this.originalStdout.write(chunk, encoding);
30+
}
31+
callback();
32+
}
33+
}
34+
35+
// Initialize interactive input mode
36+
export const initInteractiveInput = () => {
37+
// Save original stdout
38+
const originalStdout = process.stdout;
39+
40+
// Create interceptor
41+
const interceptor = new OutputInterceptor(originalStdout);
42+
43+
// Replace stdout with our interceptor
44+
// @ts-expect-error - This is a hack to replace stdout
45+
process.stdout = interceptor;
46+
47+
// Create readline interface for listening to key presses
48+
const rl = readline.createInterface({
49+
input: process.stdin,
50+
output: interceptor,
51+
terminal: true,
52+
});
53+
54+
// Close the interface to avoid keeping the process alive
55+
rl.close();
56+
57+
// Listen for keypress events
58+
readline.emitKeypressEvents(process.stdin);
59+
if (process.stdin.isTTY) {
60+
process.stdin.setRawMode(true);
61+
}
62+
63+
process.stdin.on('keypress', async (str, key) => {
64+
// Check for Ctrl+C to exit
65+
if (key.ctrl && key.name === 'c') {
66+
process.exit(0);
67+
}
68+
69+
// Check for Ctrl+M to enter message mode
70+
if (key.ctrl && key.name === 'm') {
71+
// Pause output
72+
interceptor.pause();
73+
74+
// Create a readline interface for input
75+
const inputRl = createInterface({
76+
input: process.stdin,
77+
output: originalStdout,
78+
});
79+
80+
try {
81+
// Reset cursor position and clear line
82+
originalStdout.write('\r\n');
83+
originalStdout.write(chalk.green('Enter correction or additional context (Ctrl+C to cancel):\n') + '> ');
84+
85+
// Get user input
86+
const userInput = await inputRl.question('');
87+
88+
// Add message to queue if not empty
89+
if (userInput.trim()) {
90+
userMessages.push(userInput);
91+
originalStdout.write(chalk.green('\nMessage sent to agent. Resuming output...\n\n'));
92+
} else {
93+
originalStdout.write(chalk.yellow('\nEmpty message not sent. Resuming output...\n\n'));
94+
}
95+
} catch (error) {
96+
originalStdout.write(chalk.red(`\nError sending message: ${error}\n\n`));
97+
} finally {
98+
// Close input readline interface
99+
inputRl.close();
100+
101+
// Resume output
102+
interceptor.resume();
103+
}
104+
}
105+
});
106+
107+
// Return a cleanup function
108+
return () => {
109+
// Restore original stdout
110+
// @ts-expect-error - This is a hack to restore stdout
111+
process.stdout = originalStdout;
112+
113+
// Disable raw mode
114+
if (process.stdin.isTTY) {
115+
process.stdin.setRawMode(false);
116+
}
117+
};
118+
};

packages/cli/src/commands/$default.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
consoleOutputLogger,
2121
} from 'mycoder-agent';
2222
import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js';
23+
import { initInteractiveInput } from 'mycoder-agent/dist/utils/interactiveInput.js';
2324

2425
import { SharedOptions } from '../options.js';
2526
import { captureException } from '../sentry/index.js';
@@ -106,6 +107,9 @@ export async function executePrompt(
106107
// Use command line option if provided, otherwise use config value
107108
tokenTracker.tokenCache = config.tokenCache;
108109

110+
// Initialize interactive input if enabled
111+
let cleanupInteractiveInput: (() => void) | undefined;
112+
109113
try {
110114
// Early API key check based on model provider
111115
const providerSettings =
@@ -164,6 +168,12 @@ export async function executePrompt(
164168
);
165169
process.exit(0);
166170
});
171+
172+
// Initialize interactive input if enabled
173+
if (config.interactive) {
174+
logger.info(chalk.green('Interactive correction mode enabled. Press Ctrl+M to send a correction to the agent.'));
175+
cleanupInteractiveInput = initInteractiveInput();
176+
}
167177

168178
// Create a config for the agent
169179
const agentConfig: AgentConfig = {
@@ -206,7 +216,11 @@ export async function executePrompt(
206216
// Capture the error with Sentry
207217
captureException(error);
208218
} finally {
209-
// No cleanup needed here as it's handled by the cleanup utility
219+
// Clean up interactive input if it was initialized
220+
if (cleanupInteractiveInput) {
221+
cleanupInteractiveInput();
222+
}
223+
// Other cleanup is handled by the cleanup utility
210224
}
211225

212226
logger.log(

packages/cli/src/options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const sharedOptions = {
5151
interactive: {
5252
type: 'boolean',
5353
alias: 'i',
54-
description: 'Run in interactive mode, asking for prompts',
54+
description: 'Run in interactive mode, asking for prompts and enabling corrections during execution (use Ctrl+M to send corrections)',
5555
default: false,
5656
} as const,
5757
file: {

0 commit comments

Comments
 (0)