8000 feat: add stdinContent parameter to shell commands by bhouston · Pull Request #332 · drivecore/mycoder · GitHub
[go: up one dir, main page]

Skip to content

feat: add stdinContent parameter to shell commands #332

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

Merged
merged 8 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
chore: lint and format
  • Loading branch information
bhouston committed Mar 20, 2025
commit 77ae98afd6ac8d4f98482b0209fe77bb57a6c514
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ MyCoder supports sending corrections to the main agent while it's running. This
### Usage

1. Start MyCoder with the `--interactive` flag:

```bash
mycoder --interactive "Implement a React component"
```
Expand Down
10 changes: 6 additions & 4 deletions packages/agent/src/core/toolAgent/toolAgentCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,20 @@ export const toolAgent = async (
}
}
}

// Check for messages from user (for main agent only)
// Import this at the top of the file
try {
// Dynamic import to avoid circular dependencies
const { userMessages } = await import('../../tools/interaction/userMessage.js');

const { userMessages } = await import(
'../../tools/interaction/userMessage.js'
);

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}`);
Expand Down
39 changes: 3 additions & 36 deletions packages/agent/src/tools/agent/__tests__/logCapture.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

import { Logger, LogLevel, LoggerListener } from '../../../utils/logger.js';
import { Logger } from '../../../utils/logger.js';
import { agentMessageTool } from '../agentMessage.js';
import { agentStartTool } from '../agentStart.js';
import { AgentTracker, AgentState } from '../AgentTracker.js';
import { AgentTracker } from '../AgentTracker.js';

// Mock the toolAgent function
vi.mock('../../../core/toolAgent/toolAgentCore.js', () => ({
Expand All @@ -12,37 +12,10 @@
.mockResolvedValue({ result: 'Test result', interactions: 1 }),
}));

// Create a real implementation of the log capture function
const createLogCaptureListener = (agentState: AgentState): LoggerListener => {
return (logger, logLevel, lines) => {
// Only capture log, warn, and error levels (not debug or info)
if (
logLevel === LogLevel.log ||
logLevel === LogLevel.warn ||
logLevel === LogLevel.error
) {
// Only capture logs from the agent and its immediate tools (not deeper than that)
if (logger.nesting <= 1) {
const logPrefix =
logLevel === LogLevel.warn
? '[WARN] '
: logLevel === LogLevel.error
? '[ERROR] '
: '';

// Add each line to the capturedLogs array
lines.forEach((line) => {
agentState.capturedLogs.push(`${logPrefix}${line}`);
});
}
}
};
};

describe('Log Capture in AgentTracker', () => {
let agentTracker: AgentTracker;
let logger: Logger;
let context: any;

Check warning on line 18 in packages/agent/src/tools/agent/__tests__/logCapture.test.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type

beforeEach(() => {
// Create a fresh AgentTracker and Logger for each test
Expand Down Expand Up @@ -78,20 +51,14 @@

if (!agentState) return; // TypeScript guard

// Create a tool logger that is a child of the agent logger
const toolLogger = new Logger({
name: 'tool-logger',
parent: context.logger,
});

// For testing purposes, manually add logs to the agent state
// In a real scenario, these would be added by the log listener
agentState.capturedLogs = [
'This log message should be captured',
'[WARN] This warning message should be captured',
'[ERROR] This error message should be captured',
'This tool log message should be captured',
'[WARN] This tool warning message should be captured'
'[WARN] This tool warning message should be captured',
];

// Check that the right messages were captured
Expand Down
6 changes: 4 additions & 2 deletions packages/agent/src/tools/agent/agentMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ export const agentMessageTool: Tool<Parameters, ReturnType> = {
if (output !== 'No output yet' || agentState.capturedLogs.length > 0) {
const logContent = agentState.capturedLogs.join('\n');
output = `${output}\n\n--- Agent Log Messages ---\n${logContent}`;

// Log that we're returning captured logs
logger.debug(`Returning ${agentState.capturedLogs.length} captured log messages for agent ${instanceId}`);
logger.debug(
`Returning ${agentState.capturedLogs.length} captured log messages for agent ${instanceId}`,
);
}
// Clear the captured logs after retrieving them
agentState.capturedLogs = [];
Expand Down
15 changes: 9 additions & 6 deletions packages/agent/src/tools/agent/agentStart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import chalk from 'chalk';

import {
getDefaultSystemPrompt,
Expand Down Expand Up @@ -28,7 +28,7 @@ const getRandomAgentColor = () => {
chalk.blueBright,
chalk.greenBright,
chalk.cyanBright,
chalk.magentaBright
chalk.magentaBright,
];
return colors[Math.floor(Math.random() * colors.length)];
};
Expand Down Expand Up @@ -159,7 +159,8 @@ export const agentStartTool: Tool<Parameters, ReturnType> = {

// Add each line to the capturedLogs array with logger name for context
lines.forEach((line) => {
const loggerPrefix = logger.name !== 'agent' ? `[${logger.name}] ` : '';
const loggerPrefix =
logger.name !== 'agent' ? `[${logger.name}] ` : '';
agentState.capturedLogs.push(`${logPrefix}${loggerPrefix}${line}`);
});
}
Expand All @@ -168,14 +169,14 @@ export const agentStartTool: Tool<Parameters, ReturnType> = {

// Add the listener to the context logger
context.logger.listeners.push(logCaptureListener);

// Create a new logger specifically for the sub-agent if needed
// This is wrapped in a try-catch to maintain backward compatibility with tests
let subAgentLogger = context.logger;
try {
// Generate a random color for this agent
const agentColor = getRandomAgentColor();

subAgentLogger = new Logger({
name: 'agent',
parent: context.logger,
Expand All @@ -185,7 +186,9 @@ export const agentStartTool: Tool<Parameters, ReturnType> = {
subAgentLogger.listeners.push(logCaptureListener);
} catch {
// If Logger instantiation fails (e.g., in tests), fall back to using the context logger
context.logger.debug('Failed to create sub-agent logger, using context logger instead');
context.logger.debug(
'Failed to create sub-agent logger, using context logger instead',
);
}

// Register agent state with the tracker
Expand Down
116 changes: 82 additions & 34 deletions packages/agent/src/tools/agent/logCapture.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { expect, test, describe } from 'vitest';

import { ToolContext } from '../../core/types.js';
import { LogLevel, Logger } from '../../utils/logger.js';

import { AgentState } from './AgentTracker.js';
import { ToolContext } from '../../core/types.js';

// Helper function to directly invoke a listener with a log message
function emitLog(
logger: Logger,
level: LogLevel,
message: string
) {
function emitLog(logger: Logger, level: LogLevel, message: string) {
const lines = [message];
// Directly call all listeners on this logger
logger.listeners.forEach(listener => {
logger.listeners.forEach((listener) => {
listener(logger, level, lines);
});
}
Expand All @@ -38,10 +35,17 @@ describe('Log capture functionality', () => {
const mainLogger = new Logger({ name: 'main' });
const agentLogger = new Logger({ name: 'agent', parent: mainLogger });
const toolLogger = new Logger({ name: 'tool', parent: agentLogger });
const deepToolLogger = new Logger({ name: 'deep-tool', parent: toolLogger });
const deepToolLogger = new Logger({
name: 'deep-tool',
parent: toolLogger,
});

// Create the log capture listener
const logCaptureListener = (logger: Logger, logLevel: LogLevel, lines: string[]) => {
const logCaptureListener = (
logger: Logger,
logLevel: LogLevel,
lines: string[],
) => {
// Only capture log, warn, and error levels (not debug or info)
if (
logLevel === LogLevel.log ||
Expand All @@ -55,7 +59,7 @@ describe('Log capture functionality', () => {
} else if (logger.parent === agentLogger) {
isAgentOrImmediateTool = true;
}

if (isAgentOrImmediateTool) {
const logPrefix =
logLevel === LogLevel.warn
Expand All @@ -66,7 +70,8 @@ describe('Log capture functionality', () => {

// Add each line to the capturedLogs array with logger name for context
lines.forEach((line) => {
const loggerPrefix = logger.name !== 'agent' ? `[${logger.name}] ` : '';
const loggerPrefix =
logger.name !== 'agent' ? `[${logger.name}] ` : '';
agentState.capturedLogs.push(`${logPrefix}${loggerPrefix}${line}`);
});
}
Expand Down Expand Up @@ -97,20 +102,44 @@ describe('Log capture functionality', () => {
// Verify that only the expected messages were captured
// We should have 6 messages: 3 from agent (log, warn, error) and 3 from tools (log, warn, error)
expect(agentState.capturedLogs.length).toBe(6);

// Agent messages at log, warn, and error levels should be captured
expect(agentState.capturedLogs.some(log => log === 'Agent log message')).toBe(true);
expect(agentState.capturedLogs.some(log => log === '[WARN] Agent warning message')).toBe(true);
expect(agentState.capturedLogs.some(log => log === '[ERROR] Agent error message')).toBe(true);

expect(
agentState.capturedLogs.some((log) => log === 'Agent log message'),
).toBe(true);
expect(
agentState.capturedLogs.some(
(log) => log === '[WARN] Agent warning message',
),
).toBe(true);
expect(
agentState.capturedLogs.some(
(log) => log === '[ERROR] Agent error message',
),
).toBe(true);

// Tool messages at log, warn, and error levels should be captured
expect(agentState.capturedLogs.some(log => log === '[tool] Tool log message')).toBe(true);
expect(agentState.capturedLogs.some(log => log === '[WARN] [tool] Tool warning message')).toBe(true);
expect(agentState.capturedLogs.some(log => log === '[ERROR] [tool] Tool error message')).toBe(true);

expect(
agentState.capturedLogs.some((log) => log === '[tool] Tool log message'),
).toBe(true);
expect(
agentState.capturedLogs.some(
(log) => log === '[WARN] [tool] Tool warning message',
),
).toBe(true);
expect(
agentState.capturedLogs.some(
(log) => log === '[ERROR] [tool] Tool error message',
),
).toBe(true);

// Debug and info messages should not be captured
expect(agentState.capturedLogs.some(log => log.includes('debug'))).toBe(false);
expect(agentState.capturedLogs.some(log => log.includes('info'))).toBe(false);
expect(agentState.capturedLogs.some((log) => log.includes('debug'))).toBe(
false,
);
expect(agentState.capturedLogs.some((log) => log.includes('info'))).toBe(
false,
);
});

test('should handle nested loggers correctly', () => {
Expand All @@ -133,18 +162,26 @@ describe('Log capture functionality', () => {
const mainLogger = new Logger({ name: 'main' });
const agentLogger = new Logger({ name: 'agent', parent: mainLogger });
const toolLogger = new Logger({ name: 'tool', parent: agentLogger });
const deepToolLogger = new Logger({ name: 'deep-tool', parent: toolLogger });
const deepToolLogger = new Logger({
name: 'deep-tool',
parent: toolLogger,
});

// Create the log capture listener that filters based on nesting level
const logCaptureListener = (logger: Logger, logLevel: LogLevel, lines: string[]) => {
const logCaptureListener = (
logger: Logger,
logLevel: LogLevel,
lines: string[],
) => {
// Only capture log, warn, and error levels
if (
logLevel === LogLevel.log ||
logLevel === LogLevel.warn ||
logLevel === LogLevel.error
) {
// Check nesting level - only capture from agent and immediate tools
if (logger.nesting <= 2) { // agent has nesting=1, immediate tools have nesting=2
if (logger.nesting <= 2) {
// agent has nesting=1, immediate tools have nesting=2
const logPrefix =
logLevel === LogLevel.warn
? '[WARN] '
Expand All @@ -153,7 +190,8 @@ describe('Log capture functionality', () => {
: '';

lines.forEach((line) => {
const loggerPrefix = logger.name !== 'agent' ? `[${logger.name}] ` : '';
const loggerPrefix =
logger.name !== 'agent' ? `[${logger.name}] ` : '';
agentState.capturedLogs.push(`${logPrefix}${loggerPrefix}${line}`);
});
}
Expand All @@ -164,15 +202,25 @@ describe('Log capture functionality', () => {
mainLogger.listeners.push(logCaptureListener);

// Log at different nesting levels
emitLog(mainLogger, LogLevel.log, 'Main logger message'); // nesting = 0
emitLog(agentLogger, LogLevel.log, 'Agent logger message'); // nesting = 1
emitLog(toolLogger, LogLevel.log, 'Tool logger message'); // nesting = 2
emitLog(deepToolLogger, LogLevel.log, 'Deep tool message'); // nesting = 3
emitLog(mainLogger, LogLevel.log, 'Main logger message'); // nesting = 0
emitLog(agentLogger, LogLevel.log, 'Agent logger message'); // nesting = 1
emitLog(toolLogger, LogLevel.log, 'Tool logger message'); // nesting = 2
emitLog(deepToolLogger, LogLevel.log, 'Deep tool message'); // nesting = 3

// We should capture from agent (nesting=1) and tool (nesting=2) but not deeper
expect(agentState.capturedLogs.length).toBe(3);
expect(agentState.capturedLogs.some(log => log.includes('Agent logger message'))).toBe(true);
expect(agentState.capturedLogs.some(log => log.includes('Tool logger message'))).toBe(true);
expect(agentState.capturedLogs.some(log => log.includes('Deep tool message'))).toBe(false);
expect(
agentState.capturedLogs.some((log) =>
log.includes('Agent logger message'),
),
).toBe(true);
expect(
agentState.capturedLogs.some((log) =>
log.includes('Tool logger message'),
),
).toBe(true);
expect(
agentState.capturedLogs.some((log) => log.includes('Deep tool message')),
).toBe(false);
});
});
});
2 changes: 1 addition & 1 deletion packages/agent/src/tools/getTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { agentMessageTool } from './agent/agentMessage.js';
import { agentStartTool } from './agent/agentStart.js';
import { listAgentsTool } from './agent/listAgents.js';
import { fetchTool } from './fetch/fetch.js';
import { userPromptTool } from './interaction/userPrompt.js';
import { userMessageTool } from './interaction/userMessage.js';
import { userPromptTool } from './interaction/userPrompt.js';
import { createMcpTool } from './mcp.js';
import { listSessionsTool } from './session/listSessions.js';
import { sessionMessageTool } from './session/sessionMessage.js';
Expand Down
Loading
Loading
0