Skip to content
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
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ Run bundle layout:
Use `evaluate()` when your application owns the run:

```typescript
import { evaluate } from '@agentv/sdk';
import { evaluate } from 'agentv';

const { results, summary } = await evaluate({
experiment: 'with-skills',
Expand All @@ -242,14 +242,25 @@ console.log(`${summary.passed}/${summary.total} passed`);
Use `*.eval.ts` when you want AgentV to run a TypeScript eval config:

```typescript
import type { EvalConfig } from '@agentv/sdk';
import type { EvalConfig } from 'agentv';

const config: EvalConfig = {
description: 'Code generation quality',
tags: { experiment: 'with-skills' },
target: {
extends: 'copilot-sdk',
model: 'claude-sonnet-4.6',
providers: [
{
id: 'copilot-sdk',
label: 'copilot',
config: { model: 'claude-sonnet-4.6' },
},
{
id: 'openai:gpt-5-mini',
label: 'grader',
},
],
defaults: {
provider: 'copilot',
grader: 'grader',
},
repeat: 3,
threshold: 0.8,
Expand Down
31 changes: 28 additions & 3 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,42 @@
"bin": {
"agentv": "./dist/cli.js"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./sdk": {
"types": "./dist/sdk.d.ts",
"import": "./dist/sdk.js"
},
"./provider": {
"types": "./dist/provider.d.ts",
"import": "./dist/provider.js"
},
"./contracts": {
"types": "./dist/contracts.d.ts",
"import": "./dist/contracts.js"
},
"./config": {
"types": "./dist/config.d.ts",
"import": "./dist/config.js"
},
"./package.json": "./package.json"
},
"files": ["dist", "README.md"],
"scripts": {
"dev": "bun src/cli.ts",
"build": "(cd ../../packages/sdk && bun run build) && tsup && bun run copy-readme",
"build": "tsup && bun run copy-readme",
"copy-readme": "bun -e \"import { cpSync } from 'fs'; cpSync('../../README.md', 'README.md')\"",
"prepublishOnly": "node -e \"if(process.env.ALLOW_PUBLISH!=='1'){console.error('ERROR: Use bun run publish:next, then bun run promote:latest');process.exit(1)}\"",
"typecheck": "(cd ../../packages/sdk && bun run build) && tsc --noEmit",
"typecheck": "tsc --noEmit",
"lint": "biome check .",
"format": "biome format --write .",
"fix": "biome check --write .",
"test": "(cd ../../packages/sdk && bun run build) && bun test",
"test": "bun test",
"test:watch": "bun test --watch"
},
"dependencies": {
Expand Down
200 changes: 200 additions & 0 deletions apps/cli/src/cli-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import path from 'node:path';
import {
loadConfig,
loadEnvPathFiles,
runBeforeSessionHook,
runEnvFromEntries,
} from '@agentv/core';
import { binary, run, subcommands } from 'cmd-ts';
import { findRepoRoot } from './commands/eval/shared.js';

import packageJson from '../package.json' with { type: 'json' };
import { compareCommand } from './commands/compare/index.js';
import { convertCommand } from './commands/convert/index.js';
import { createCommand } from './commands/create/index.js';
import { doctorCommand } from './commands/doctor/index.js';
import { evalCommand } from './commands/eval/index.js';
import { gradeCommand } from './commands/grade/index.js';
import { importCommand } from './commands/import/index.js';
import { initCmdTsCommand } from './commands/init/index.js';
import { inspectCommand } from './commands/inspect/index.js';
import { pipelineCommand } from './commands/pipeline/index.js';
import { prepareCommand } from './commands/prepare/index.js';
import { resultsCommand } from './commands/results/index.js';
import { resultsServeCommand } from './commands/results/serve.js';
import { runsCommand } from './commands/runs/index.js';
import { selfCommand } from './commands/self/index.js';
import { skillsCommand } from './commands/skills/index.js';
import { transpileCommand } from './commands/transpile/index.js';
import { trendCommand } from './commands/trend/index.js';
import { trimCommand } from './commands/trim/index.js';
import { validateCommand } from './commands/validate/index.js';
import { getUpdateNotice } from './update-check.js';

export const app = subcommands({
name: 'agentv',
description: 'AgentV CLI',
version: packageJson.version,
cmds: {
dashboard: resultsServeCommand,
eval: evalCommand,
grade: gradeCommand,
import: importCommand,
compare: compareCommand,
convert: convertCommand,
create: createCommand,
doctor: doctorCommand,
init: initCmdTsCommand,
pipeline: pipelineCommand,
prepare: prepareCommand,
results: resultsCommand,
runs: runsCommand,
self: selfCommand,
skills: skillsCommand,
serve: resultsServeCommand,
inspect: inspectCommand,
trend: trendCommand,
transpile: transpileCommand,
trim: trimCommand,
validate: validateCommand,
},
});

/**
* Known eval subcommand names — used to decide whether to inject the
* implicit `run` subcommand for backward-compatible `agentv eval <paths>`.
*/
const EVAL_SUBCOMMANDS = new Set(['run', 'assert', 'aggregate', 'bundle', 'vitest']);
const VITEST_VERIFIER_RE = /(?:^|[/\\])(?:EVAL|[^/\\]+[.-](?:test|spec))\.[cm]?[jt]sx?$/i;

/**
* Top-level CLI command names (excluding `eval` itself).
* Used to ensure `eval` is the top-level subcommand, not nested.
*/
const TOP_LEVEL_COMMANDS = new Set([
'import',
'inspect',
'compare',
'convert',
'create',
'dashboard',
'doctor',
'grade',
'init',
'pipeline',
'prepare',
'results',
'runs',
'self',
'skills',
'serve',
'studio',
'trend',
'transpile',
'trim',
'validate',
]);

export function usesDeprecatedStudioAlias(argv: string[]): boolean {
return argv[2] === 'studio';
}

export function shouldRunBeforeSessionHook(argv: string[]): boolean {
return !(argv[2] === 'eval' && argv[3] === 'vitest');
}

export function inferEvalSubcommand(arg: string | undefined): 'run' | 'vitest' {
return arg && VITEST_VERIFIER_RE.test(arg) ? 'vitest' : 'run';
}

/**
* Preprocess argv for convenience aliases:
* - `--eval-id` → `--test-id`
* - `agentv eval <non-subcommand>` → `agentv eval run <non-subcommand>`
* (backward compat: `eval` used to be a direct command, now it's a group)
*/
export function preprocessArgv(argv: string[]): string[] {
const result = [...argv];

if (result[2] === 'studio') {
result[2] = 'dashboard';
}

// Rewrite --eval-id → --test-id (convenience alias)
for (let i = 0; i < result.length; i++) {
if (result[i] === '--eval-id') {
result[i] = '--test-id';
} else if (result[i].startsWith('--eval-id=')) {
result[i] = `--test-id=${result[i].slice('--eval-id='.length)}`;
}
}

// Implicit eval subcommand: `agentv eval [<arg>]` injects the inferred command
// when the first arg after `eval` is absent or is not a known eval subcommand.
// Backward-compat: `eval` used to be a direct command; now it is a subcommands group.
// Bare `agentv eval` falls through to the run handler so its TTY check can launch
// the interactive wizard. Vitest-looking verifier files use the protocol adapter
// directly so deterministic workspace graders can stay short in eval YAML.
// Only applies when `eval` is the top-level subcommand.
// Exception: `--help` / `-h` should show the eval group help, not run's help.
const evalIdx = result.indexOf('eval');
if (evalIdx !== -1) {
// Ensure no top-level command appears before `eval` in the argv —
// if one does, `eval` is a nested subcommand.
const isTopLevel = !result.slice(0, evalIdx).some((arg) => TOP_LEVEL_COMMANDS.has(arg));
if (isTopLevel) {
const nextArg = result[evalIdx + 1];
const isHelp = nextArg === '--help' || nextArg === '-h';
const isKnownSubcommand = nextArg !== undefined && EVAL_SUBCOMMANDS.has(nextArg);
if (!isHelp && !isKnownSubcommand) {
result.splice(evalIdx + 1, 0, inferEvalSubcommand(nextArg));
}
}
}

return result;
}

export async function runCli(argv: string[] = process.argv): Promise<void> {
// Kick off update check: reads from local cache (fast), spawns a detached
// child to refresh if stale. The notice is printed on process exit so it
// appears after command output, even if the command calls process.exit().
let updateNotice: string | null = null;
process.on('exit', () => {
if (updateNotice) process.stderr.write(`\n${updateNotice}\n`);
});
getUpdateNotice(packageJson.version).then((n) => {
updateNotice = n;
});

const processedArgv = preprocessArgv(argv);
if (usesDeprecatedStudioAlias(argv)) {
process.stderr.write(
'Warning: `agentv studio` is deprecated and will be removed in a future release. Use `agentv dashboard` instead.\n',
);
}

if (shouldRunBeforeSessionHook(processedArgv)) {
// Load env_path/env_from and run the before_session hook once at startup,
// before any command executes. Uses cwd as the search root for
// .agentv/config.yaml so validate/eval commands see the injected vars.
const cwd = process.cwd();
const repoRoot = await findRepoRoot(cwd);
const sessionConfig = await loadConfig(path.join(cwd, '_'), repoRoot);
const configDir = sessionConfig?.configDir ?? repoRoot;

if (sessionConfig?.env_path) {
await loadEnvPathFiles(sessionConfig.env_path, configDir);
}
if (sessionConfig?.env_from) {
await runEnvFromEntries(sessionConfig.env_from, { cwd: configDir });
}

const beforeSessionCommand = sessionConfig?.hooks?.before_session;
if (beforeSessionCommand) {
runBeforeSessionHook(beforeSessionCommand);
}
}

await run(binary(app), processedArgv);
}
2 changes: 1 addition & 1 deletion apps/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { killAllTrackedChildren } from '@agentv/core';

import { runCli } from './index.js';
import { runCli } from './cli-app.js';

// Forward SIGINT/SIGTERM to spawned provider subprocesses before exiting.
// Without this, Dashboard's `child.kill('SIGTERM')` against the CLI orphans
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/commands/create/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { command, option, optional, positional, string } from 'cmd-ts';

const ASSERTION_TEMPLATES: Record<string, string> = {
default: `#!/usr/bin/env bun
import { defineAssertion } from '@agentv/sdk';
import { defineAssertion } from 'agentv';

export default defineAssertion(({ output }) => {
// TODO: Implement your assertion logic
Expand All @@ -18,7 +18,7 @@ export default defineAssertion(({ output }) => {
});
`,
score: `#!/usr/bin/env bun
import { defineAssertion } from '@agentv/sdk';
import { defineAssertion } from 'agentv';

export default defineAssertion(({ output }) => {
// TODO: Implement your scoring logic (0.0 to 1.0)
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/eval/commands/vitest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { command, flag, number, option, optional, restPositionals, string } from 'cmd-ts';

import { runScriptGrader, runVitestWorkspaceGrader } from '@agentv/sdk';
import { runScriptGrader, runVitestWorkspaceGrader } from '@agentv/core/script-grader';

function parseCommand(value: string | undefined): readonly string[] | undefined {
const trimmed = value?.trim();
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
defineConfig,
type AgentVTsConfig as AgentVConfig,
} from '@agentv/core';
14 changes: 14 additions & 0 deletions apps/cli/src/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export * from './provider.js';
export * from './config.js';
export type {
AssertEntry,
ConversationTurnInput,
EvalAssertionInput,
EvalRunArtifacts,
EvalRunResult,
EvalSummary,
EvalTestInput,
EvaluationResultWire,
TraceSummaryWire,
TraceWire,
} from '@agentv/core';
Loading
Loading