Skip to content
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
2 changes: 1 addition & 1 deletion src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export const runCommand = new Command('run')
runWithOpenAI(agentDir, manifest, { workspace: options.workspace });
break;
case 'crewai':
runWithCrewAI(agentDir, manifest, { workspace: options.workspace });
runWithCrewAI(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'openclaw':
runWithOpenClaw(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
Expand Down
140 changes: 140 additions & 0 deletions src/runners/crewai.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { loadAgentManifest } from '../utils/loader.js';
import { runWithCrewAI } from './crewai.js';

function makeAgentDir(): string {
const dir = mkdtempSync(join(tmpdir(), 'opengap-crewai-test-'));
writeFileSync(
join(dir, 'agent.yaml'),
"spec_version: '0.1.0'\nname: crew-test\nversion: '0.1.0'\ndescription: A helpful test agent\n",
'utf-8',
);
return dir;
}

test('runs CrewAI through its Python SDK and passes the user prompt safely', () => {
const agentDir = makeAgentDir();
const manifest = loadAgentManifest(agentDir);
const previousExitCode = process.exitCode;
let pythonCommand = '';
let scriptPath = '';
let configPath = '';

const fakeSpawn = (command: string, args: readonly string[]) => {
pythonCommand = command;
scriptPath = args[0];
configPath = args[1];

const script = readFileSync(scriptPath, 'utf-8');
assert.match(script, /from crewai import Agent, Crew, Task/);
assert.match(script, /crew\.kickoff\(inputs=\{"request": config\["request"\]\}\)/);
assert.deepEqual(JSON.parse(readFileSync(configPath, 'utf-8')), {
agents: {
'crew-test': {
role: 'A helpful test agent',
goal: 'A helpful test agent',
backstory: '',
verbose: true,
allow_delegation: false,
},
},
primary_agent: 'crew-test',
request: 'Answer "safely"\nKeep {braces}',
});

return { status: 0, error: undefined };
};

try {
runWithCrewAI(agentDir, manifest, {
prompt: ' Answer "safely"\nKeep {braces} ',
workspace: agentDir,
}, fakeSpawn);

assert.equal(pythonCommand, process.platform === 'win32' ? 'python' : 'python3');
assert.ok(scriptPath.endsWith('.py'));
assert.ok(configPath.endsWith('.json'));
assert.equal(process.exitCode, 0);
assert.equal(existsSync(scriptPath), false);
assert.equal(existsSync(configPath), false);
} finally {
process.exitCode = previousExitCode;
rmSync(agentDir, { recursive: true, force: true });
}
});

test('uses the agent description when no prompt is provided', () => {
const agentDir = makeAgentDir();
const manifest = loadAgentManifest(agentDir);
const previousExitCode = process.exitCode;
let request = '';

const fakeSpawn = (command: string, args: readonly string[]) => {
request = JSON.parse(readFileSync(args[1], 'utf-8')).request;
return { status: 0, error: undefined };
};

try {
runWithCrewAI(agentDir, manifest, {}, fakeSpawn);

assert.equal(request, manifest.description);
assert.equal(process.exitCode, 0);
} finally {
process.exitCode = previousExitCode;
rmSync(agentDir, { recursive: true, force: true });
}
});

test('returns failure status and cleans temporary files when Python cannot complete', () => {
const agentDir = makeAgentDir();
const manifest = loadAgentManifest(agentDir);
const previousExitCode = process.exitCode;
let scriptPath = '';
let configPath = '';

const fakeSpawn = (command: string, args: readonly string[]) => {
scriptPath = args[0];
configPath = args[1];
return { status: 7, error: undefined };
};

try {
runWithCrewAI(agentDir, manifest, {}, fakeSpawn);

assert.equal(process.exitCode, 7);
assert.equal(existsSync(scriptPath), false);
assert.equal(existsSync(configPath), false);
} finally {
process.exitCode = previousExitCode;
rmSync(agentDir, { recursive: true, force: true });
}
});

test('sets a failure exit code and cleans temporary files when Python cannot start', () => {
const agentDir = makeAgentDir();
const manifest = loadAgentManifest(agentDir);
const previousExitCode = process.exitCode;
let scriptPath = '';
let configPath = '';

const fakeSpawn = (command: string, args: readonly string[]) => {
scriptPath = args[0];
configPath = args[1];
return { status: null, error: Object.assign(new Error('Python unavailable'), { code: 'ENOENT' }) };
};

try {
runWithCrewAI(agentDir, manifest, {}, fakeSpawn);

assert.equal(process.exitCode, 1);
assert.equal(existsSync(scriptPath), false);
assert.equal(existsSync(configPath), false);
} finally {
process.exitCode = previousExitCode;
rmSync(agentDir, { recursive: true, force: true });
}
});
72 changes: 64 additions & 8 deletions src/runners/crewai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,97 @@ import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import yaml from 'js-yaml';
import { exportToCrewAI } from '../adapters/crewai.js';
import { AgentManifest } from '../utils/loader.js';
import { error, info } from '../utils/format.js';

export interface CrewAIRunOptions {
prompt?: string;
workspace?: string;
}

export function runWithCrewAI(agentDir: string, _manifest: AgentManifest, options: CrewAIRunOptions = {}): void {
const config = exportToCrewAI(agentDir);
const tmpFile = join(tmpdir(), `gitagent-${randomBytes(4).toString('hex')}.yaml`);
interface CrewAISpawnResult {
status: number | null;
error?: Error;
}

interface CrewAISpawnOptions {
stdio: 'inherit';
cwd: string;
env: NodeJS.ProcessEnv;
}

type CrewAISpawn = (
command: string,
args: readonly string[],
options: CrewAISpawnOptions,
) => CrewAISpawnResult;

const crewAIScript = `import json
import sys

from crewai import Agent, Crew, Task

writeFileSync(tmpFile, config, 'utf-8');
with open(sys.argv[1], encoding="utf-8") as config_file:
config = json.load(config_file)

agents = {name: Agent(**settings) for name, settings in config["agents"].items()}
task = Task(
description="Complete the following user request: {request}",
expected_output="A clear, complete response to the user's request.",
agent=agents[config["primary_agent"]],
)
crew = Crew(agents=list(agents.values()), tasks=[task], verbose=True)
result = crew.kickoff(inputs={"request": config["request"]})
print(result)
`;

interface CrewAIConfig {
agents: Record<string, Record<string, unknown>>;
}

export function runWithCrewAI(
agentDir: string,
manifest: AgentManifest,
options: CrewAIRunOptions = {},
spawn: CrewAISpawn = spawnSync,
): void {
const config = yaml.load(exportToCrewAI(agentDir)) as CrewAIConfig;
const request = options.prompt?.trim() || manifest.description;
const configPath = join(tmpdir(), `opengap-crewai-${randomBytes(8).toString('hex')}.json`);
const scriptPath = join(tmpdir(), `opengap-crewai-${randomBytes(8).toString('hex')}.py`);

const runCwd = resolve(options.workspace ?? agentDir);

info(`Running CrewAI agent from "${agentDir}"...`);
info(`Working directory: ${runCwd}`);

try {
const result = spawnSync('crewai', ['kickoff', '--config', tmpFile], {
writeFileSync(configPath, JSON.stringify({
agents: config.agents,
primary_agent: manifest.name,
request,
}), 'utf-8');
writeFileSync(scriptPath, crewAIScript, 'utf-8');

const python = process.platform === 'win32' ? 'python' : 'python3';
const result = spawn(python, [scriptPath, configPath], {
stdio: 'inherit',
cwd: runCwd,
env: { ...process.env },
});

if (result.error) {
error(`Failed to run CrewAI: ${result.error.message}`);
info('Make sure the crewai CLI is installed: pip install crewai');
info('Make sure the crewai Python package is installed in the active environment: pip install crewai');
process.exitCode = 1;
return;
}

process.exitCode = result.status ?? 0;
process.exitCode = result.status ?? 1;
} finally {
try { unlinkSync(tmpFile); } catch { /* ignore */ }
try { unlinkSync(configPath); } catch { /* ignore */ }
try { unlinkSync(scriptPath); } catch { /* ignore */ }
}
}
2 changes: 1 addition & 1 deletion src/runners/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export async function runWithGit(
runWithOpenAI(agentDir, manifest, { workspace: options.workspace });
break;
case 'crewai':
runWithCrewAI(agentDir, manifest, { workspace: options.workspace });
runWithCrewAI(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'openclaw':
runWithOpenClaw(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
Expand Down