Skip to content

Commit 41ec8d3

Browse files
authored
feat(benchmark): support multi agent jobs (#145)
## Description Support multi-agent benchmark jobs Removed --model flag, agent must be specified like --agent agent:model ## Type of Change <!-- Mark the relevant option with an 'x' --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [ ] Test updates ## Related Issues <!-- Link to related issues using #issue-number --> Closes # ## Changes Made <!-- Describe the changes in detail --> ## Testing <!-- Describe how you tested your changes --> - [ ] I have tested locally - [ ] I have added/updated tests - [ ] All existing tests pass ## Checklist - [ ] My code follows the code style of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation accordingly - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published ## Screenshots (if applicable) <!-- Add screenshots to help explain your changes --> ## Additional Notes <!-- Any additional information that reviewers should know -->
1 parent 3abea8a commit 41ec8d3

3 files changed

Lines changed: 87 additions & 42 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ rli mcp install # Install Runloop MCP server configurat
184184
### Benchmark-job Commands (alias: `bmj`)
185185

186186
```bash
187-
rli benchmark-job run # Run a benchmark job with an agent
187+
rli benchmark-job run # Run a benchmark job with one or more ...
188188
rli benchmark-job status <id> # Get benchmark job status and results
189189
```
190190

src/commands/benchmark-job/run.ts

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ const SUPPORTED_AGENTS = {
4343
type SupportedAgent = keyof typeof SUPPORTED_AGENTS;
4444

4545
interface RunOptions {
46-
agent: string;
47-
model: string;
46+
agent?: string[];
4847
benchmark?: string;
4948
scenarios?: string[];
5049
jobName?: string;
@@ -57,6 +56,50 @@ interface RunOptions {
5756
output?: string;
5857
}
5958

59+
interface ParsedAgent {
60+
name: SupportedAgent;
61+
model: string;
62+
}
63+
64+
// Parse agent strings in "agent:model" format
65+
function parseAgentStrings(agentStrings: string[] | undefined): ParsedAgent[] {
66+
if (!agentStrings || agentStrings.length === 0) {
67+
throw new Error(
68+
"At least one --agent is required. Format: --agent agent:model (e.g., --agent claude-code:claude-sonnet-4)",
69+
);
70+
}
71+
72+
const agents: ParsedAgent[] = [];
73+
74+
for (const agentStr of agentStrings) {
75+
const colonIndex = agentStr.indexOf(":");
76+
if (colonIndex === -1) {
77+
throw new Error(
78+
`Invalid agent format: "${agentStr}". Use format: agent:model (e.g., claude-code:claude-sonnet-4)`,
79+
);
80+
}
81+
82+
const agentName = agentStr.substring(0, colonIndex);
83+
const model = agentStr.substring(colonIndex + 1);
84+
85+
if (!model) {
86+
throw new Error(
87+
`No model specified for agent "${agentName}". Use format: --agent ${agentName}:model-name`,
88+
);
89+
}
90+
91+
// Validate agent
92+
validateAgent(agentName);
93+
94+
agents.push({
95+
name: agentName as SupportedAgent,
96+
model,
97+
});
98+
}
99+
100+
return agents;
101+
}
102+
60103
// Parse environment variables from KEY=value format
61104
function parseEnvVars(envVars: string[]): Record<string, string> {
62105
const result: Record<string, string> = {};
@@ -209,9 +252,8 @@ async function resolveBenchmarkId(benchmarkIdOrName: string): Promise<string> {
209252

210253
export async function runBenchmarkJob(options: RunOptions) {
211254
try {
212-
// Validate agent
213-
validateAgent(options.agent);
214-
const agent = options.agent as SupportedAgent;
255+
// Parse agent strings (format: agent:model)
256+
const parsedAgents = parseAgentStrings(options.agent);
215257

216258
// Parse provided env vars and secrets
217259
const providedEnvVars = options.envVars
@@ -221,30 +263,37 @@ export async function runBenchmarkJob(options: RunOptions) {
221263
? parseSecrets(options.secrets)
222264
: {};
223265

224-
// Ensure agent secrets exist (auto-create from env vars if needed)
225-
// Maps ENV_VAR -> BMJ_ENV_VAR (e.g., ANTHROPIC_API_KEY -> BMJ_ANTHROPIC_API_KEY)
226-
const agentSecrets = await ensureAgentSecrets(agent);
266+
// Get unique agent names for secret setup
267+
const uniqueAgentNames = [...new Set(parsedAgents.map((a) => a.name))];
227268

228-
// Validate that at least one secret is available (only if requiresAny is true)
229-
const agentConfig = SUPPORTED_AGENTS[agent];
230-
if (agentConfig.requiresAny) {
231-
const hasAny = agentConfig.automaticEnvVars.some(
232-
(varName) => agentSecrets[varName],
233-
);
234-
if (!hasAny) {
235-
throw new Error(
236-
`Agent ${agent} requires at least one of: ${agentConfig.automaticEnvVars.join(", ")}. ` +
237-
`Create secrets (${agentConfig.automaticEnvVars.map((v) => `${SECRET_PREFIX}${v}`).join(", ")}) ` +
238-
`or set environment variables.`,
269+
// Ensure secrets exist for all unique agents
270+
// Collect all secrets across agents
271+
const allAgentSecrets: Record<string, string> = {};
272+
for (const agentName of uniqueAgentNames) {
273+
const agentSecrets = await ensureAgentSecrets(agentName);
274+
275+
// Validate that at least one secret is available (only if requiresAny is true)
276+
const agentConfig = SUPPORTED_AGENTS[agentName];
277+
if (agentConfig.requiresAny) {
278+
const hasAny = agentConfig.automaticEnvVars.some(
279+
(varName) => agentSecrets[varName],
239280
);
281+
if (!hasAny) {
282+
throw new Error(
283+
`Agent ${agentName} requires at least one of: ${agentConfig.automaticEnvVars.join(", ")}. ` +
284+
`Create secrets (${agentConfig.automaticEnvVars.map((v) => `${SECRET_PREFIX}${v}`).join(", ")}) ` +
285+
`or set environment variables.`,
286+
);
287+
}
240288
}
289+
290+
// Merge secrets (later agents can use same secrets)
291+
Object.assign(allAgentSecrets, agentSecrets);
241292
}
242-
// If requiresAny is false, we just use whatever secrets were auto-populated
243-
// User may be configuring credentials via other means (e.g., --secrets flag)
244293

245294
// Combine agent secrets with user-provided secrets
246295
const secrets = {
247-
...agentSecrets,
296+
...allAgentSecrets,
248297
...providedSecrets,
249298
};
250299

@@ -274,25 +323,22 @@ export async function runBenchmarkJob(options: RunOptions) {
274323
quiet: false,
275324
};
276325

326+
// Build agent configs for all parsed agents
327+
const agentConfigs = parsedAgents.map((agent) => ({
328+
name: agent.name,
329+
modelName: agent.model,
330+
timeoutSeconds: options.timeout ? parseInt(options.timeout, 10) : 1800,
331+
environmentVariables:
332+
Object.keys(providedEnvVars).length > 0 ? providedEnvVars : undefined,
333+
secrets,
334+
}));
335+
277336
// Create the benchmark job
278337
const job = await createBenchmarkJob({
279338
name: options.jobName,
280339
benchmarkId,
281340
scenarioIds: options.scenarios,
282-
agentConfigs: [
283-
{
284-
name: agent,
285-
modelName: options.model,
286-
timeoutSeconds: options.timeout
287-
? parseInt(options.timeout, 10)
288-
: 1800,
289-
environmentVariables:
290-
Object.keys(providedEnvVars).length > 0
291-
? providedEnvVars
292-
: undefined,
293-
secrets,
294-
},
295-
],
341+
agentConfigs,
296342
orchestratorConfig,
297343
});
298344

src/utils/commands.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,12 +1020,11 @@ export function createProgram(): Command {
10201020

10211021
benchmarkJob
10221022
.command("run")
1023-
.description("Run a benchmark job with an agent")
1024-
.requiredOption(
1025-
"--agent <agent>",
1026-
"Agent to use (claude-code, codex, opencode, goose, gemini-cli)",
1023+
.description("Run a benchmark job with one or more agents")
1024+
.option(
1025+
"--agent <agents...>",
1026+
"Agent(s) to use. Format: agent:model (e.g., claude-code:claude-sonnet-4). Can specify multiple.",
10271027
)
1028-
.requiredOption("--model <model>", "Model name for the agent")
10291028
.option("--benchmark <id-or-name>", "Benchmark ID or name to run")
10301029
.option(
10311030
"--scenarios <ids...>",

0 commit comments

Comments
 (0)