Skip to content

feat: add stdinContent parameter to shell commands #332

New issue

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

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

Already on GitHub? # to your account

Merged
merged 8 commits into from
Mar 20, 2025
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ packages/docs/.env.development.local
packages/docs/.env.test.local
packages/docs/.env.production.local
mcp.server.setup.json
coverage
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
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
"build": "pnpm -r build",
"start": "pnpm -r start",
"test": "pnpm -r test",
"test:coverage": "pnpm -r test:coverage",
"typecheck": "pnpm -r typecheck",
"lint": "eslint . --fix",
"format": "prettier . --write",
"clean": "pnpm -r clean",
"clean:all": "pnpm -r clean:all && rimraf node_modules",
"clean": "rimraf **/dist",
"clean:all": "rimraf **/dist node_modules **/node_modules",
"cloc": "pnpm exec cloc * --exclude-dir=node_modules,dist,.vinxi,.output",
"gcloud-setup": "gcloud auth application-default login && gcloud config set account \"ben@drivecore.ai\" && gcloud config set project drivecore-primary && gcloud config set run/region us-central1",
"cli": "cd packages/cli && node --no-deprecation bin/cli.js",
Expand Down Expand Up @@ -71,6 +72,8 @@
"@prisma/client",
"@prisma/engines",
"bcrypt",
"core-js",
"core-js-pure",
"esbuild",
"msw",
"prisma"
Expand Down
3 changes: 1 addition & 2 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist",
"clean:all": "rimraf node_modules dist",
"semantic-release": "pnpm exec semantic-release -e semantic-release-monorepo"
},
"keywords": [
Expand Down Expand Up @@ -62,6 +60,7 @@
"devDependencies": {
"@types/node": "^18",
"@types/uuid": "^10",
"@vitest/coverage-v8": "^3",
"rimraf": "^5",
"type-fest": "^4",
"typescript": "^5",
Expand Down
9 changes: 4 additions & 5 deletions packages/agent/src/core/toolAgent/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,10 @@ export function getDefaultSystemPrompt(toolContext: ToolContext): string {
'',
'You should use the Github CLI tool, gh, and the git cli tool, git, that you can access via shell commands.',
'',
'When creating GitHub issues, PRs, or comments, via the gh cli tool, use temporary markdown files for the content instead of inline text:',
'- Create a temporary markdown file with the content you want to include',
'- Use the file with GitHub CLI commands (e.g., `gh issue create --body-file temp.md`)',
'- Clean up the temporary file when done',
'- This approach preserves formatting, newlines, and special characters correctly',
'When creating GitHub issues, PRs, or comments via the gh cli tool, use the shellStart or shellExecute stdinContent parameter for multiline content:',
'- Use the stdinContent parameter to pass the content directly to the command',
'- For example: `shellStart({ command: "gh issue create --body-stdin", stdinContent: "Issue description here with **markdown** support", description: "Creating a new issue" })`',
'- This approach preserves formatting, newlines, and special characters correctly without requiring temporary files',
].join('\n')
: '';

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
31 changes: 28 additions & 3 deletions packages/agent/src/tools/agent/agentStart.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import chalk from 'chalk';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

Expand All @@ -15,6 +16,23 @@ import { AgentStatus, AgentState } from './AgentTracker.js';
// For backward compatibility
export const agentStates = new Map<string, AgentState>();

// Generate a random color for an agent
// Avoid colors that are too light or too similar to error/warning colors
const getRandomAgentColor = () => {
// List of bright chalk colors that are visually distinct
const colors = [
chalk.cyan,
chalk.green,
chalk.blue,
chalk.magenta,
chalk.blueBright,
chalk.greenBright,
chalk.cyanBright,
chalk.magentaBright,
];
return colors[Math.floor(Math.random() * colors.length)];
};

const parameterSchema = z.object({
description: z
.string()
Expand Down Expand Up @@ -141,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 @@ -150,20 +169,26 @@ 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,
color: agentColor, // Assign the random color to the agent
});
// Add the listener to the sub-agent logger as well
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
Loading
Loading