@vercel/agent-eval 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/lib/agents/claude-code/agent.d.ts +30 -0
  2. package/dist/lib/agents/claude-code/agent.d.ts.map +1 -0
  3. package/dist/lib/agents/claude-code/agent.js +80 -0
  4. package/dist/lib/agents/claude-code/agent.js.map +1 -0
  5. package/dist/lib/agents/claude-code/run.mjs +217 -0
  6. package/dist/lib/agents/codex/agent.d.ts +61 -0
  7. package/dist/lib/agents/codex/agent.d.ts.map +1 -0
  8. package/dist/lib/agents/codex/agent.js +221 -0
  9. package/dist/lib/agents/codex/agent.js.map +1 -0
  10. package/dist/lib/agents/codex/run.mjs +365 -0
  11. package/dist/lib/agents/cursor/agent.d.ts +32 -0
  12. package/dist/lib/agents/cursor/agent.d.ts.map +1 -0
  13. package/dist/lib/agents/cursor/agent.js +86 -0
  14. package/dist/lib/agents/cursor/agent.js.map +1 -0
  15. package/dist/lib/agents/cursor/run.mjs +183 -0
  16. package/dist/lib/agents/eval-helper.mjs +222 -0
  17. package/dist/lib/agents/gemini/agent.d.ts +27 -0
  18. package/dist/lib/agents/gemini/agent.d.ts.map +1 -0
  19. package/dist/lib/agents/gemini/agent.js +69 -0
  20. package/dist/lib/agents/gemini/agent.js.map +1 -0
  21. package/dist/lib/agents/gemini/run.mjs +180 -0
  22. package/dist/lib/agents/index.js +5 -5
  23. package/dist/lib/agents/index.js.map +1 -1
  24. package/dist/lib/agents/opencode/agent.d.ts +68 -0
  25. package/dist/lib/agents/opencode/agent.d.ts.map +1 -0
  26. package/dist/lib/agents/opencode/agent.js +165 -0
  27. package/dist/lib/agents/opencode/agent.js.map +1 -0
  28. package/dist/lib/agents/opencode/run.mjs +308 -0
  29. package/dist/lib/agents/plugin/contract.d.ts +157 -0
  30. package/dist/lib/agents/plugin/contract.d.ts.map +1 -0
  31. package/dist/lib/agents/plugin/contract.js +22 -0
  32. package/dist/lib/agents/plugin/contract.js.map +1 -0
  33. package/dist/lib/agents/plugin/orchestrator.d.ts +22 -0
  34. package/dist/lib/agents/plugin/orchestrator.d.ts.map +1 -0
  35. package/dist/lib/agents/plugin/orchestrator.js +286 -0
  36. package/dist/lib/agents/plugin/orchestrator.js.map +1 -0
  37. package/dist/lib/agents/shared.d.ts +7 -1
  38. package/dist/lib/agents/shared.d.ts.map +1 -1
  39. package/dist/lib/agents/shared.js +30 -3
  40. package/dist/lib/agents/shared.js.map +1 -1
  41. package/package.json +2 -2
  42. package/dist/lib/agents/claude-code.d.ts +0 -23
  43. package/dist/lib/agents/claude-code.d.ts.map +0 -1
  44. package/dist/lib/agents/claude-code.js +0 -300
  45. package/dist/lib/agents/claude-code.js.map +0 -1
  46. package/dist/lib/agents/codex.d.ts +0 -23
  47. package/dist/lib/agents/codex.d.ts.map +0 -1
  48. package/dist/lib/agents/codex.js +0 -364
  49. package/dist/lib/agents/codex.js.map +0 -1
  50. package/dist/lib/agents/cursor.d.ts +0 -10
  51. package/dist/lib/agents/cursor.d.ts.map +0 -1
  52. package/dist/lib/agents/cursor.js +0 -207
  53. package/dist/lib/agents/cursor.js.map +0 -1
  54. package/dist/lib/agents/gemini.d.ts +0 -10
  55. package/dist/lib/agents/gemini.d.ts.map +0 -1
  56. package/dist/lib/agents/gemini.js +0 -208
  57. package/dist/lib/agents/gemini.js.map +0 -1
  58. package/dist/lib/agents/opencode.d.ts +0 -46
  59. package/dist/lib/agents/opencode.d.ts.map +0 -1
  60. package/dist/lib/agents/opencode.js +0 -365
  61. package/dist/lib/agents/opencode.js.map +0 -1
@@ -0,0 +1,222 @@
1
+ /**
2
+ * In-sandbox eval helper — the EVAL.ts agentic-judge surface.
3
+ *
4
+ * Shipped INTO the sandbox by the orchestrator (like run.mjs) and aliased to
5
+ * `@vercel/agent-eval/eval` + registered as a vitest setup file by the generated
6
+ * vitest config. That lets EVAL.ts do:
7
+ *
8
+ * import { environment, transcript } from '@vercel/agent-eval/eval';
9
+ *
10
+ * test('quality', async () => {
11
+ * await expect(environment).toSatisfyCriterion('uses Server Components for the list');
12
+ * await expect(transcript).toSatisfyCriterion('diagnosed with DevTools, not guesswork');
13
+ * await expect(environment).toScoreAtLeast('code quality', 0.8);
14
+ * });
15
+ *
16
+ * It is AGENTIC and reuses the SAME harness: each assertion re-invokes the codegen
17
+ * agent's runner (`__agent_eval__/run.mjs`, already shipped) IN this sandbox. The
18
+ * judge explores the final state (cwd) or reads the materialized transcript file —
19
+ * no fresh sandbox, no copying evidence around, no new harness.
20
+ *
21
+ * Zero-dependency apart from `vitest` (already present in the fixture). Runs only
22
+ * in-sandbox. The path constants below mirror shared.ts (the orchestrator writes
23
+ * these files before validation).
24
+ */
25
+
26
+ import { spawnSync } from 'node:child_process';
27
+ import { readFileSync, existsSync, mkdirSync } from 'node:fs';
28
+ import { expect } from 'vitest';
29
+
30
+ // Canonical sandbox paths (kept in sync with shared.ts).
31
+ const RUNNER_PATH = '__agent_eval__/run.mjs';
32
+ const JUDGE_CONFIG_PATH = '__agent_eval__/judge-config.json';
33
+ const TRANSCRIPT_FILE = '__agent_eval__/transcript.txt';
34
+ const JUDGE_IO_DIR = '__agent_eval__/judge';
35
+
36
+ /**
37
+ * The two things you can judge — import sentinels, NOT paths. The matcher routes
38
+ * on which one you pass; the path/cwd resolution is an internal detail.
39
+ */
40
+ export const environment = { __judgeSubject: 'environment' };
41
+ export const transcript = { __judgeSubject: 'transcript' };
42
+
43
+ /** Path to the materialized transcript. Rarely needed — prefer the `transcript` subject. */
44
+ export function transcriptPath() {
45
+ return TRANSCRIPT_FILE;
46
+ }
47
+
48
+ /**
49
+ * Build the judge prompt for a subject + criterion. Framework-owned: you supply
50
+ * only the criterion; the scaffolding (skeptical stance, how to gather evidence,
51
+ * and the verdict output contract) is fixed so the verdict parses reliably.
52
+ */
53
+ export function buildJudgePrompt(subject, criterion, verdictPath, opts = {}) {
54
+ const lines = [];
55
+ lines.push(
56
+ "You are a strict, skeptical judge evaluating an AI coding agent's work. " +
57
+ 'Decide the criterion ONLY on clear evidence; when in doubt, FAIL it.'
58
+ );
59
+ lines.push('');
60
+ if (subject === 'transcript') {
61
+ lines.push(
62
+ `Read the agent's transcript at ${TRANSCRIPT_FILE} (open it with your tools) to ` +
63
+ 'see how the agent worked. Cite what the transcript actually shows; do not assume.'
64
+ );
65
+ } else {
66
+ lines.push(
67
+ 'Inspect the project in the current directory — read files, grep, run commands as ' +
68
+ 'needed — to gather concrete evidence.'
69
+ );
70
+ }
71
+ lines.push('');
72
+ lines.push(`Criterion: ${criterion}`);
73
+ lines.push('');
74
+ if (opts.numeric) {
75
+ lines.push(
76
+ 'Also give a score from 0 to 1 (1 = fully satisfies the criterion). Set pass=true ' +
77
+ 'iff the criterion is satisfied.'
78
+ );
79
+ }
80
+ lines.push(`When done, write your verdict to ${verdictPath} as JSON of exactly this shape:`);
81
+ lines.push(
82
+ '{"pass": true|false,' +
83
+ (opts.numeric ? ' "score": <0-1>,' : '') +
84
+ ' "reason": "<1-2 sentences citing concrete evidence>"}'
85
+ );
86
+ lines.push('Then print that same JSON as your final message.');
87
+ return lines.join('\n');
88
+ }
89
+
90
+ /**
91
+ * Parse a verdict from the verdict file or the agent's final output. Tolerant of
92
+ * surrounding prose / ```json fences. Returns null when nothing parseable is found.
93
+ */
94
+ export function parseJudgeVerdict(raw) {
95
+ if (!raw) return null;
96
+ const match = raw.match(/\{[\s\S]*"pass"[\s\S]*\}/);
97
+ if (!match) return null;
98
+ try {
99
+ const obj = JSON.parse(match[0]);
100
+ return {
101
+ pass: !!obj.pass,
102
+ score: typeof obj.score === 'number' ? obj.score : undefined,
103
+ reason: typeof obj.reason === 'string' ? obj.reason : '',
104
+ };
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /* ───────────────────────────── judge invocation ───────────────────────────── */
111
+
112
+ let counter = 0;
113
+ function nextId() {
114
+ counter += 1;
115
+ return `${process.pid}-${counter}`;
116
+ }
117
+
118
+ function readJudgeConfig() {
119
+ try {
120
+ return JSON.parse(readFileSync(JUDGE_CONFIG_PATH, 'utf8'));
121
+ } catch {
122
+ // No config (e.g. run outside the orchestrator) → let the agent CLI default.
123
+ return { model: null, extra: null };
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Run one agentic judgment IN this sandbox by re-invoking the codegen agent's
129
+ * runner. Blocking (spawnSync) — the test has nothing else to do while the judge
130
+ * works, and the sandbox-level timeout bounds it. Returns {pass, score?, reason}.
131
+ */
132
+ function runJudge(subject, criterion, opts = {}) {
133
+ const id = nextId();
134
+ mkdirSync(JUDGE_IO_DIR, { recursive: true });
135
+ const verdictPath = `${JUDGE_IO_DIR}/${id}-verdict.json`;
136
+ const resultPath = `${JUDGE_IO_DIR}/${id}-result.json`;
137
+ const cfg = readJudgeConfig();
138
+
139
+ // Same AgentRunInput contract run.mjs already understands. Reuse the codegen
140
+ // model + host-computed extra (e.g. codex's resolved model/effort) so the judge
141
+ // is the SAME agent. No secrets here — the key rides in process.env (inherited).
142
+ const input = {
143
+ prompt: buildJudgePrompt(subject, criterion, verdictPath, opts),
144
+ model: cfg.model ?? undefined,
145
+ cwd: process.cwd(),
146
+ resultPath,
147
+ extra: cfg.extra ?? undefined,
148
+ };
149
+
150
+ const res = spawnSync('node', [RUNNER_PATH, JSON.stringify(input)], {
151
+ cwd: process.cwd(),
152
+ env: process.env,
153
+ encoding: 'utf8',
154
+ maxBuffer: 64 * 1024 * 1024,
155
+ });
156
+
157
+ // Prefer the verdict file the judge agent wrote; then the runner result's output
158
+ // (the agent's final message); then raw stdout.
159
+ let verdict = existsSync(verdictPath) ? parseJudgeVerdict(readFileSync(verdictPath, 'utf8')) : null;
160
+ if (!verdict && existsSync(resultPath)) {
161
+ try {
162
+ const rr = JSON.parse(readFileSync(resultPath, 'utf8'));
163
+ verdict = parseJudgeVerdict(rr.output);
164
+ if (!verdict && !rr.ok) {
165
+ return { pass: false, reason: `judge agent failed: ${rr.error || 'unknown error'}` };
166
+ }
167
+ } catch {
168
+ /* fall through */
169
+ }
170
+ }
171
+ if (!verdict) verdict = parseJudgeVerdict(res.stdout || '');
172
+ if (!verdict) {
173
+ const tail = (res.stdout || res.stderr || '').slice(-300);
174
+ return { pass: false, reason: `could not parse a judge verdict (node exit ${res.status}). Tail: ${tail}` };
175
+ }
176
+ return verdict;
177
+ }
178
+
179
+ function subjectOf(received) {
180
+ return received && typeof received === 'object' ? received.__judgeSubject : undefined;
181
+ }
182
+
183
+ // Sync matchers: spawnSync blocks, so these work whether or not you `await` them.
184
+ expect.extend({
185
+ toSatisfyCriterion(received, criterion) {
186
+ const subject = subjectOf(received);
187
+ if (!subject) {
188
+ return {
189
+ pass: false,
190
+ message: () =>
191
+ 'toSatisfyCriterion expects `environment` or `transcript` from @vercel/agent-eval/eval',
192
+ };
193
+ }
194
+ const v = runJudge(subject, criterion);
195
+ return {
196
+ pass: v.pass,
197
+ message: () =>
198
+ `[judge:${subject}] ${v.pass ? 'PASS' : 'FAIL'}` +
199
+ (typeof v.score === 'number' ? ` (score ${v.score})` : '') +
200
+ ` — ${criterion}\n reason: ${v.reason}`,
201
+ };
202
+ },
203
+
204
+ toScoreAtLeast(received, criterion, threshold) {
205
+ const subject = subjectOf(received);
206
+ if (!subject) {
207
+ return {
208
+ pass: false,
209
+ message: () =>
210
+ 'toScoreAtLeast expects `environment` or `transcript` from @vercel/agent-eval/eval',
211
+ };
212
+ }
213
+ const v = runJudge(subject, criterion, { numeric: true });
214
+ const score = typeof v.score === 'number' ? v.score : v.pass ? 1 : 0;
215
+ const pass = score >= threshold;
216
+ return {
217
+ pass,
218
+ message: () =>
219
+ `[judge:${subject}] score ${score} ${pass ? '>=' : '<'} ${threshold} — ${criterion}\n reason: ${v.reason}`,
220
+ };
221
+ },
222
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Gemini CLI agent — host-side definition + the thin Agent wrapper.
3
+ *
4
+ * Mirrors the Claude Code exemplar: the definition is pure data/auth, while the
5
+ * actual CLI invocation + transcript capture live in ./run.mjs (shipped into the
6
+ * sandbox by the orchestrator). The wrapper keeps the public Agent interface
7
+ * identical so the registry / runner are untouched.
8
+ *
9
+ * Gemini uses DIRECT Google Gemini API access only (no AI Gateway variant in the
10
+ * old adapter), configured purely via env — no config files.
11
+ */
12
+ import type { Agent } from '../types.js';
13
+ import type { AgentDefinition } from '../plugin/contract.js';
14
+ /**
15
+ * Build the Gemini CLI plugin definition.
16
+ *
17
+ * Auth is a single mode, preserved exactly from the old adapter: the resolved
18
+ * apiKey is exported as GEMINI_API_KEY. getApiKeyEnvVar() and authEnv() agree on
19
+ * that env var so the host's key resolution and the sandbox env stay consistent.
20
+ */
21
+ export declare function createGeminiDefinition(): AgentDefinition;
22
+ /**
23
+ * Create the Gemini CLI Agent. Thin wrapper over the generic orchestrator so the
24
+ * Agent interface (and thus registry.ts / index.ts / runner.ts) is unchanged.
25
+ */
26
+ export declare function createGeminiAgent(): Agent;
27
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../../src/lib/agents/gemini/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAmB,MAAM,aAAa,CAAC;AAG1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAG7D;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CAkCxD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,KAAK,CAWzC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Gemini CLI agent — host-side definition + the thin Agent wrapper.
3
+ *
4
+ * Mirrors the Claude Code exemplar: the definition is pure data/auth, while the
5
+ * actual CLI invocation + transcript capture live in ./run.mjs (shipped into the
6
+ * sandbox by the orchestrator). The wrapper keeps the public Agent interface
7
+ * identical so the registry / runner are untouched.
8
+ *
9
+ * Gemini uses DIRECT Google Gemini API access only (no AI Gateway variant in the
10
+ * old adapter), configured purely via env — no config files.
11
+ */
12
+ import { fileURLToPath } from 'node:url';
13
+ import { GEMINI_DIRECT } from '../shared.js';
14
+ import { runWithDefinition } from '../plugin/orchestrator.js';
15
+ /**
16
+ * Build the Gemini CLI plugin definition.
17
+ *
18
+ * Auth is a single mode, preserved exactly from the old adapter: the resolved
19
+ * apiKey is exported as GEMINI_API_KEY. getApiKeyEnvVar() and authEnv() agree on
20
+ * that env var so the host's key resolution and the sandbox env stay consistent.
21
+ */
22
+ export function createGeminiDefinition() {
23
+ return {
24
+ name: 'gemini',
25
+ displayName: 'Gemini CLI',
26
+ defaultModel: 'gemini-3-pro-preview',
27
+ o11yAgentName: 'gemini',
28
+ // Resolve run.mjs next to this file (works in src during dev and in dist after
29
+ // the build copies run.mjs alongside the compiled agent.js).
30
+ runnerPath: fileURLToPath(new URL('./run.mjs', import.meta.url)),
31
+ getApiKeyEnvVar() {
32
+ return GEMINI_DIRECT.apiKeyEnvVar;
33
+ },
34
+ install(_options) {
35
+ // Project deps (retried once), then the Gemini CLI globally.
36
+ // Error wording matches the old adapter verbatim:
37
+ // npm install → `npm install failed (exit code N):\n<last 10 lines>`
38
+ // gemini-cli → `Gemini CLI install failed: <stderr>`
39
+ return [
40
+ { kind: 'command', cmd: 'npm', args: ['install'], retryOnce: true, errorPrefix: 'npm install failed', errorBody: 'last10' },
41
+ { kind: 'command', cmd: 'npm', args: ['install', '-g', '@google/gemini-cli'], errorPrefix: 'Gemini CLI install failed', errorBody: 'stderr' },
42
+ ];
43
+ },
44
+ // Gemini CLI is configured purely via env — no config files.
45
+ configFiles() {
46
+ return [];
47
+ },
48
+ authEnv(options) {
49
+ return { [GEMINI_DIRECT.apiKeyEnvVar]: options.apiKey };
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * Create the Gemini CLI Agent. Thin wrapper over the generic orchestrator so the
55
+ * Agent interface (and thus registry.ts / index.ts / runner.ts) is unchanged.
56
+ */
57
+ export function createGeminiAgent() {
58
+ const definition = createGeminiDefinition();
59
+ return {
60
+ name: definition.name,
61
+ displayName: definition.displayName,
62
+ getApiKeyEnvVar: definition.getApiKeyEnvVar,
63
+ getDefaultModel() {
64
+ return definition.defaultModel;
65
+ },
66
+ run: (fixturePath, options) => runWithDefinition(definition, fixturePath, options),
67
+ };
68
+ }
69
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../../src/lib/agents/gemini/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,YAAY;QACzB,YAAY,EAAE,sBAAsB;QACpC,aAAa,EAAE,QAAQ;QACvB,+EAA+E;QAC/E,6DAA6D;QAC7D,UAAU,EAAE,aAAa,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEhE,eAAe;YACb,OAAO,aAAa,CAAC,YAAY,CAAC;QACpC,CAAC;QAED,OAAO,CAAC,QAAyB;YAC/B,6DAA6D;YAC7D,kDAAkD;YAClD,yEAAyE;YACzE,0DAA0D;YAC1D,OAAO;gBACL,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,oBAAoB,EAAE,SAAS,EAAE,QAAQ,EAAE;gBAC3H,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,oBAAoB,CAAC,EAAE,WAAW,EAAE,2BAA2B,EAAE,SAAS,EAAE,QAAQ,EAAE;aAC9I,CAAC;QACJ,CAAC;QAED,6DAA6D;QAC7D,WAAW;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO,CAAC,OAAwB;YAC9B,OAAO,EAAE,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;QAC1D,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,UAAU,GAAG,sBAAsB,EAAE,CAAC;IAC5C,OAAO;QACL,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,WAAW,EAAE,UAAU,CAAC,WAAW;QACnC,eAAe,EAAE,UAAU,CAAC,eAAe;QAC3C,eAAe;YACb,OAAO,UAAU,CAAC,YAAY,CAAC;QACjC,CAAC;QACD,GAAG,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,CAAC;KACnF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Gemini CLI in-sandbox runner.
3
+ *
4
+ * This file is shipped INTO the sandbox by the orchestrator and executed there as
5
+ * `node __agent_eval__/run.mjs '<AgentRunInput JSON>'`. It is intentionally
6
+ * ZERO-DEPENDENCY (only `node:*` builtins) because the sandbox only has the
7
+ * fixture's own deps + the installed `gemini` CLI — it cannot import anything from
8
+ * the @vercel/agent-eval package.
9
+ *
10
+ * Dual mode:
11
+ * - runnable: invoked directly → reads argv, runs the agent, writes the result
12
+ * file + prints a status line, exits 0.
13
+ * - importable: `import { runAgent } from './run.mjs'` → returns a RunnerResult
14
+ * (no file write, no exit). This is what a future in-sandbox judge reuses.
15
+ *
16
+ * The pure helpers below are exported (not just `runAgent`) so they can be
17
+ * unit-tested directly — the same code the sandbox runs.
18
+ */
19
+
20
+ import { spawnSync } from 'node:child_process';
21
+ import { mkdirSync, writeFileSync } from 'node:fs';
22
+ import { dirname } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ /**
26
+ * Build the Gemini CLI argument list.
27
+ *
28
+ * Order is preserved verbatim from the old adapter:
29
+ * `--prompt <prompt>` [ `--model <model>` ] `--approval-mode yolo`
30
+ * `--output-format stream-json`
31
+ *
32
+ * `--output-format stream-json` makes Gemini emit JSONL (newline-delimited JSON)
33
+ * so we can capture a detailed event transcript (similar to Codex's --json).
34
+ * `--approval-mode yolo` auto-approves tool actions (non-interactive sandbox run).
35
+ *
36
+ * @param {{prompt:string, model?:string}} input
37
+ * @returns {string[]}
38
+ */
39
+ export function buildGeminiCliArgs(input) {
40
+ const geminiArgs = ['--prompt', input.prompt];
41
+ if (input.model) {
42
+ geminiArgs.push('--model', input.model);
43
+ }
44
+ geminiArgs.push('--approval-mode', 'yolo', '--output-format', 'stream-json');
45
+ return geminiArgs;
46
+ }
47
+
48
+ /**
49
+ * Extract transcript from Gemini stream-json output.
50
+ *
51
+ * Preserved verbatim from the old adapter. When run with
52
+ * `--output-format stream-json`, Gemini outputs JSONL (one JSON object per line);
53
+ * we keep only the lines that look like a JSON object ({ ... }) and re-join them.
54
+ *
55
+ * @param {string|undefined|null} output combined stdout+stderr
56
+ * @returns {string|undefined}
57
+ */
58
+ export function extractTranscriptFromOutput(output) {
59
+ if (!output || !output.trim()) {
60
+ return undefined;
61
+ }
62
+
63
+ // The --output-format stream-json output contains JSON events, one per line.
64
+ // Filter to only include lines that look like JSON objects.
65
+ const lines = output.split('\n').filter((line) => {
66
+ const trimmed = line.trim();
67
+ return trimmed.startsWith('{') && trimmed.endsWith('}');
68
+ });
69
+
70
+ if (lines.length === 0) {
71
+ return undefined;
72
+ }
73
+
74
+ return lines.join('\n');
75
+ }
76
+
77
+ /**
78
+ * Run Gemini CLI over the workspace at `input.cwd` and return a RunnerResult.
79
+ *
80
+ * Auth (GEMINI_API_KEY) arrives via process.env — the orchestrator sets it on the
81
+ * `node run.mjs` invocation, and we pass process.env straight through to the CLI.
82
+ * The runner never handles secrets itself.
83
+ *
84
+ * @param {import('../plugin/contract.js').AgentRunInput} input
85
+ * @returns {Promise<import('../plugin/contract.js').RunnerResult>}
86
+ */
87
+ export async function runAgent(input) {
88
+ const args = buildGeminiCliArgs(input);
89
+
90
+ // spawnSync (not a shell string): the prompt is a plain argv element, so there is
91
+ // no shell quoting/escaping to get wrong. Blocking is fine — the runner has
92
+ // nothing else to do while the agent works. The sandbox-level timeout bounds it.
93
+ const res = spawnSync('gemini', args, {
94
+ cwd: input.cwd,
95
+ env: process.env,
96
+ encoding: 'utf8',
97
+ maxBuffer: 64 * 1024 * 1024,
98
+ });
99
+
100
+ const stdout = res.stdout || '';
101
+ const stderr = res.stderr || '';
102
+ // Preserve the old concatenation order: stdout THEN stderr.
103
+ const output = stdout + stderr;
104
+ // spawnSync sets status=null + error when the binary can't be spawned at all.
105
+ const agentExitCode = res.status == null ? -1 : res.status;
106
+
107
+ // Transcript from the stream-json output. Gemini exposes no observable model,
108
+ // so observedModel is always null (matches the old adapter exactly).
109
+ const transcript = extractTranscriptFromOutput(output) ?? null;
110
+ const observedModel = null;
111
+
112
+ if (res.error || agentExitCode !== 0) {
113
+ // Mirror the old error string: last 5 lines of output, else a coded fallback.
114
+ const errorLines = output.trim().split('\n').slice(-5).join('\n');
115
+ const fallback = res.error
116
+ ? `Failed to run gemini: ${res.error.message}`
117
+ : `Gemini CLI exited with code ${agentExitCode}`;
118
+ return {
119
+ ok: false,
120
+ output,
121
+ transcript,
122
+ observedModel,
123
+ error: errorLines || fallback,
124
+ agentExitCode,
125
+ };
126
+ }
127
+
128
+ return { ok: true, output, transcript, observedModel, error: null, agentExitCode };
129
+ }
130
+
131
+ /* ─────────────────────────── runnable (CLI) entry ─────────────────────────── */
132
+
133
+ // True when this file is executed directly (`node run.mjs ...`), false when imported.
134
+ const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
135
+
136
+ if (isMain) {
137
+ // argv[2] is the AgentRunInput JSON (never contains secrets).
138
+ const input = JSON.parse(process.argv[2]);
139
+
140
+ // Always produce a RunnerResult, even if the runner itself throws, so the host
141
+ // always has a result file to read (node exit code stays 0 except on a truly
142
+ // unrecoverable crash before we can write).
143
+ let result;
144
+ try {
145
+ result = await runAgent(input);
146
+ } catch (e) {
147
+ result = {
148
+ ok: false,
149
+ output: '',
150
+ transcript: null,
151
+ observedModel: null,
152
+ error: e && e.message ? e.message : String(e),
153
+ agentExitCode: -1,
154
+ };
155
+ }
156
+
157
+ // Source of truth: the result file the host reads back via sandbox.readFile.
158
+ try {
159
+ mkdirSync(dirname(input.resultPath), { recursive: true });
160
+ writeFileSync(input.resultPath, JSON.stringify(result));
161
+ } catch {
162
+ // If the file can't be written, the host falls back to the marker line below.
163
+ }
164
+
165
+ // Fallback channel: a compact status line (no transcript — it can be huge).
166
+ process.stdout.write(
167
+ '__AGENT_RESULT__ ' +
168
+ JSON.stringify({
169
+ ok: result.ok,
170
+ observedModel: result.observedModel,
171
+ error: result.error,
172
+ agentExitCode: result.agentExitCode,
173
+ }) +
174
+ '\n'
175
+ );
176
+
177
+ // Exit 0: "the runner ran". Agent success/failure is conveyed via result.ok, not
178
+ // the node exit code (the host distinguishes the two).
179
+ process.exit(0);
180
+ }
@@ -2,11 +2,11 @@
2
2
  * Agent registry with built-in agents.
3
3
  */
4
4
  import { registerAgent, getAgent, listAgents, hasAgent } from './registry.js';
5
- import { createClaudeCodeAgent } from './claude-code.js';
6
- import { createCodexAgent } from './codex.js';
7
- import { createOpenCodeAgent } from './opencode.js';
8
- import { createGeminiAgent } from './gemini.js';
9
- import { createCursorAgent } from './cursor.js';
5
+ import { createClaudeCodeAgent } from './claude-code/agent.js';
6
+ import { createCodexAgent } from './codex/agent.js';
7
+ import { createOpenCodeAgent } from './opencode/agent.js';
8
+ import { createGeminiAgent } from './gemini/agent.js';
9
+ import { createCursorAgent } from './cursor/agent.js';
10
10
  // Register all agent variants (Vercel AI Gateway + Direct API)
11
11
  registerAgent(createClaudeCodeAgent({ useVercelAiGateway: true })); // vercel-ai-gateway/claude-code
12
12
  registerAgent(createClaudeCodeAgent({ useVercelAiGateway: false })); // claude-code
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/agents/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,+DAA+D;AAC/D,aAAa,CAAC,qBAAqB,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAG,gCAAgC;AACtG,aAAa,CAAC,qBAAqB,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAE,cAAc;AACpF,aAAa,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAQ,0BAA0B;AAChG,aAAa,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAO,QAAQ;AAC9E,aAAa,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAiC,6BAA6B;AACnG,aAAa,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAmC,SAAS;AAC/E,aAAa,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAmC,SAAS;AAE/E,+BAA+B;AAC/B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/agents/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,+DAA+D;AAC/D,aAAa,CAAC,qBAAqB,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAG,gCAAgC;AACtG,aAAa,CAAC,qBAAqB,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAE,cAAc;AACpF,aAAa,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAQ,0BAA0B;AAChG,aAAa,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAO,QAAQ;AAC9E,aAAa,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAiC,6BAA6B;AACnG,aAAa,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAmC,SAAS;AAC/E,aAAa,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAmC,SAAS;AAE/E,+BAA+B;AAC/B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * OpenCode agent — host-side definition + the thin Agent wrapper.
3
+ *
4
+ * The definition is pure data/auth/config; the actual CLI invocation + transcript
5
+ * capture live in ./run.mjs (shipped into the sandbox by the orchestrator). The
6
+ * wrapper keeps the public Agent interface identical so the registry / runner are
7
+ * untouched.
8
+ *
9
+ * OpenCode only supports the Vercel AI Gateway (no direct provider APIs), so there
10
+ * is a single variant: `vercel-ai-gateway/opencode`.
11
+ *
12
+ * `generateOpenCodeConfig` (the pure host-side config generator) STAYS in this file
13
+ * and is exported so the existing unit tests keep importing it. The transcript /
14
+ * observed-model parsing helpers (`extractObservedModelFromOpenCodeOutput`,
15
+ * `extractSessionIdFromTranscript`, `extractObservedModelFromSessionExport`) moved
16
+ * INTO ./run.mjs (so the tested logic is exactly what the sandbox runs) and the
17
+ * test now imports them from there — see the test import change in the migration
18
+ * notes.
19
+ */
20
+ import type { Agent } from '../types.js';
21
+ import type { AgentDefinition } from '../plugin/contract.js';
22
+ /**
23
+ * Additional provider configuration for models not yet available in the
24
+ * default Vercel AI Gateway (e.g., early access / unreleased models).
25
+ *
26
+ * Preserved verbatim from the old adapter — passed through agentOptions.extraProviders.
27
+ */
28
+ export interface OpenCodeProviderConfig {
29
+ npm?: string;
30
+ options?: Record<string, unknown>;
31
+ models?: Record<string, {
32
+ name?: string;
33
+ tool_call?: boolean;
34
+ reasoning?: boolean;
35
+ attachment?: boolean;
36
+ temperature?: boolean;
37
+ limit?: {
38
+ context: number;
39
+ output: number;
40
+ };
41
+ }>;
42
+ }
43
+ /**
44
+ * Generate OpenCode config file content.
45
+ * Configures the Vercel AI Gateway provider, plus any additional providers.
46
+ *
47
+ * STAYS host-side + exported (the unit test imports it). The content is written
48
+ * verbatim into `opencode.json` in the sandbox by the orchestrator. Behavior is
49
+ * identical to the old adapter, including writing the resolved apiKey into the
50
+ * config (falling back to the `{env:AI_GATEWAY_API_KEY}` placeholder).
51
+ */
52
+ export declare function generateOpenCodeConfig(extraProviders?: Record<string, OpenCodeProviderConfig>, apiKey?: string, timeoutMs?: number, webResearch?: boolean): string;
53
+ /**
54
+ * Build the OpenCode plugin definition.
55
+ *
56
+ * Auth is single-mode: the Vercel AI Gateway. authEnv() also carries the Exa
57
+ * websearch toggle (OPENCODE_ENABLE_EXA) when webResearch is on — it rides in
58
+ * process.env alongside the gateway key, exactly as the old adapter set it on the
59
+ * CLI's spawn env. The matching tool permissions are written into opencode.json by
60
+ * generateOpenCodeConfig.
61
+ */
62
+ export declare function createOpenCodeDefinition(): AgentDefinition;
63
+ /**
64
+ * Create the OpenCode Agent. Thin wrapper over the generic orchestrator so the
65
+ * Agent interface (and thus registry.ts / index.ts / runner.ts) is unchanged.
66
+ */
67
+ export declare function createOpenCodeAgent(): Agent;
68
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../../src/lib/agents/opencode/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAmB,MAAM,aAAa,CAAC;AAG1D,OAAO,KAAK,EAAE,eAAe,EAA2B,MAAM,uBAAuB,CAAC;AAGtF;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QACtB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,KAAK,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;KAC7C,CAAC,CAAC;CACJ;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,EACvD,MAAM,CAAC,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,OAAO,GACpB,MAAM,CA8BR;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI,eAAe,CA2F1D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,KAAK,CAW3C"}