Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Cancel job on user request in interactive mode #367

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ MyCoder supports sending corrections to the main agent while it's running. This
mycoder --interactive "Implement a React component"
```

2. While the agent is running, press `Ctrl+M` to enter correction mode
2. While the agent is running, you can:
- Press `Ctrl+M` to enter correction mode and send additional context
- Press `Ctrl+X` to cancel the current job and provide new instructions
3. Type your correction or additional context
4. Press Enter to send the correction to the agent

Expand Down
52 changes: 46 additions & 6 deletions packages/agent/src/core/toolAgent/toolAgentCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,62 @@ export const toolAgent = async (
// Import this at the top of the file
try {
// Dynamic import to avoid circular dependencies
const { userMessages } = await import(
const { userMessages, cancelJobFlag } = await import(
'../../tools/interaction/userMessage.js'
);

// Check if job cancellation was requested
if (cancelJobFlag.value) {
cancelJobFlag.value = false; // Reset the flag
logger.info('Job cancellation requested by user');

// If there are no new instructions in userMessages, we'll add a default message
if (userMessages.length === 0) {
userMessages.push(
'[CANCEL JOB] Please stop the current task and wait for new instructions.',
);
}
}

if (userMessages && userMessages.length > 0) {
// Get all user messages and clear the queue
const pendingUserMessages = [...userMessages];
userMessages.length = 0;

// Add each message to the conversation
for (const message of pendingUserMessages) {
logger.info(`Message from user: ${message}`);
messages.push({
role: 'user',
content: `[Correction from user]: ${message}`,
});
if (message.startsWith('[CANCEL JOB]')) {
// For cancel job messages, we'll clear the conversation history and start fresh
const newInstruction = message.replace('[CANCEL JOB]', '').trim();
logger.info(
`Job cancelled by user. New instruction: ${newInstruction}`,
);

// Clear the message history except for the system message
const systemMessage = messages.find((msg) => msg.role === 'system');
messages.length = 0;

// Add back the system message if it existed
if (systemMessage) {
messages.push(systemMessage);
}

// Add a message explaining what happened
messages.push({
role: 'user',
content: `The previous task was cancelled by the user. Please stop whatever you were doing before and focus on this new task: ${newInstruction}`,
});

// Reset interactions counter to avoid hitting limits
interactions = 0;
} else {
// Regular correction
logger.info(`Message from user: ${message}`);
messages.push({
role: 'user',
content: `[Correction from user]: ${message}`,
});
}
}
}
} catch (error) {
Expand Down
3 changes: 3 additions & 0 deletions packages/agent/src/tools/interaction/userMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { Tool } from '../../core/types.js';
// Track the messages sent to the main agent
export const userMessages: string[] = [];

// Flag to indicate if the job should be cancelled
export const cancelJobFlag = { value: false };

const parameterSchema = z.object({
message: z
.string()
Expand Down
74 changes: 73 additions & 1 deletion packages/agent/src/utils/interactiveInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { Writable } from 'stream';

import chalk from 'chalk';

import { userMessages } from '../tools/interaction/userMessage.js';
import {
userMessages,
cancelJobFlag,
} from '../tools/interaction/userMessage.js';

// Custom output stream to intercept console output
class OutputInterceptor extends Writable {
Expand Down Expand Up @@ -69,6 +72,75 @@ export const initInteractiveInput = () => {
process.exit(0);
}

// Check for Ctrl+X to cancel job
if (key.ctrl && key.name === 'x') {
// Pause output
interceptor.pause();

// Create a readline interface for input
const inputRl = createInterface({
input: process.stdin,
output: originalStdout,
});

try {
// Reset cursor position and clear line
originalStdout.write('\r\n');
originalStdout.write(
chalk.yellow(
'Are you sure you want to cancel the current job? (y/n):\n',
) + '> ',
);

// Get user confirmation
const confirmation = await inputRl.question('');

if (confirmation.trim().toLowerCase() === 'y') {
// Set cancel flag to true
cancelJobFlag.value = true;

// Create a readline interface for new instructions
originalStdout.write(
chalk.green('\nJob cancelled. Enter new instructions:\n') + '> ',
);

// Get new instructions
const newInstructions = await inputRl.question('');

// Add message to queue if not empty
if (newInstructions.trim()) {
userMessages.push(`[CANCEL JOB] ${newInstructions}`);
originalStdout.write(
chalk.green(
'\nNew instructions sent. Resuming with new task...\n\n',
),
);
} else {
originalStdout.write(
chalk.yellow(
'\nNo new instructions provided. Job will still be cancelled...\n\n',
),
);
userMessages.push(
'[CANCEL JOB] Please stop the current task and wait for new instructions.',
);
}
} else {
originalStdout.write(
chalk.green('\nCancellation aborted. Resuming output...\n\n'),
);
}
} catch (error) {
originalStdout.write(chalk.red(`\nError cancelling job: ${error}\n\n`));
} finally {
// Close input readline interface
inputRl.close();

// Resume output
interceptor.resume();
}
}

// Check for Ctrl+M to enter message mode
if (key.ctrl && key.name === 'm') {
// Pause output
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/$default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export async function executePrompt(
if (config.interactive) {
logger.info(
chalk.green(
'Interactive correction mode enabled. Press Ctrl+M to send a correction to the agent.',
'Interactive mode enabled. Press Ctrl+M to send a correction to the agent, Ctrl+X to cancel job.',
),
);
cleanupInteractiveInput = initInteractiveInput();
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const sharedOptions = {
type: 'boolean',
alias: 'i',
description:
'Run in interactive mode, asking for prompts and enabling corrections during execution (use Ctrl+M to send corrections)',
'Run in interactive mode, asking for prompts and enabling corrections during execution (use Ctrl+M to send corrections, Ctrl+X to cancel job)',
default: false,
} as const,
file: {
Expand Down
Loading