@vercel/agent-eval 1.1.1 → 1.3.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 (83) hide show
  1. package/dist/cli.js +318 -102
  2. package/dist/cli.js.map +1 -1
  3. package/dist/lib/agents/claude-code/agent.d.ts +30 -0
  4. package/dist/lib/agents/claude-code/agent.d.ts.map +1 -0
  5. package/dist/lib/agents/claude-code/agent.js +81 -0
  6. package/dist/lib/agents/claude-code/agent.js.map +1 -0
  7. package/dist/lib/agents/claude-code/run.mjs +217 -0
  8. package/dist/lib/agents/codex/agent.d.ts +61 -0
  9. package/dist/lib/agents/codex/agent.d.ts.map +1 -0
  10. package/dist/lib/agents/codex/agent.js +222 -0
  11. package/dist/lib/agents/codex/agent.js.map +1 -0
  12. package/dist/lib/agents/codex/run.mjs +365 -0
  13. package/dist/lib/agents/cursor/agent.d.ts +32 -0
  14. package/dist/lib/agents/cursor/agent.d.ts.map +1 -0
  15. package/dist/lib/agents/cursor/agent.js +87 -0
  16. package/dist/lib/agents/cursor/agent.js.map +1 -0
  17. package/dist/lib/agents/cursor/run.mjs +183 -0
  18. package/dist/lib/agents/eval-helper.mjs +226 -0
  19. package/dist/lib/agents/gemini/agent.d.ts +27 -0
  20. package/dist/lib/agents/gemini/agent.d.ts.map +1 -0
  21. package/dist/lib/agents/gemini/agent.js +70 -0
  22. package/dist/lib/agents/gemini/agent.js.map +1 -0
  23. package/dist/lib/agents/gemini/run.mjs +180 -0
  24. package/dist/lib/agents/index.js +5 -5
  25. package/dist/lib/agents/index.js.map +1 -1
  26. package/dist/lib/agents/opencode/agent.d.ts +68 -0
  27. package/dist/lib/agents/opencode/agent.d.ts.map +1 -0
  28. package/dist/lib/agents/opencode/agent.js +166 -0
  29. package/dist/lib/agents/opencode/agent.js.map +1 -0
  30. package/dist/lib/agents/opencode/run.mjs +308 -0
  31. package/dist/lib/agents/plugin/contract.d.ts +157 -0
  32. package/dist/lib/agents/plugin/contract.d.ts.map +1 -0
  33. package/dist/lib/agents/plugin/contract.js +22 -0
  34. package/dist/lib/agents/plugin/contract.js.map +1 -0
  35. package/dist/lib/agents/plugin/orchestrator.d.ts +59 -0
  36. package/dist/lib/agents/plugin/orchestrator.d.ts.map +1 -0
  37. package/dist/lib/agents/plugin/orchestrator.js +346 -0
  38. package/dist/lib/agents/plugin/orchestrator.js.map +1 -0
  39. package/dist/lib/agents/shared.d.ts +17 -1
  40. package/dist/lib/agents/shared.d.ts.map +1 -1
  41. package/dist/lib/agents/shared.js +42 -3
  42. package/dist/lib/agents/shared.js.map +1 -1
  43. package/dist/lib/agents/types.d.ts +8 -1
  44. package/dist/lib/agents/types.d.ts.map +1 -1
  45. package/dist/lib/config.d.ts.map +1 -1
  46. package/dist/lib/config.js +19 -0
  47. package/dist/lib/config.js.map +1 -1
  48. package/dist/lib/fingerprint.d.ts +39 -1
  49. package/dist/lib/fingerprint.d.ts.map +1 -1
  50. package/dist/lib/fingerprint.js +62 -7
  51. package/dist/lib/fingerprint.js.map +1 -1
  52. package/dist/lib/results.d.ts +5 -1
  53. package/dist/lib/results.d.ts.map +1 -1
  54. package/dist/lib/results.js +8 -2
  55. package/dist/lib/results.js.map +1 -1
  56. package/dist/lib/runner.d.ts +3 -0
  57. package/dist/lib/runner.d.ts.map +1 -1
  58. package/dist/lib/runner.js +4 -1
  59. package/dist/lib/runner.js.map +1 -1
  60. package/dist/lib/types.d.ts +20 -0
  61. package/dist/lib/types.d.ts.map +1 -1
  62. package/dist/lib/types.js.map +1 -1
  63. package/package.json +2 -2
  64. package/dist/lib/agents/claude-code.d.ts +0 -33
  65. package/dist/lib/agents/claude-code.d.ts.map +0 -1
  66. package/dist/lib/agents/claude-code.js +0 -310
  67. package/dist/lib/agents/claude-code.js.map +0 -1
  68. package/dist/lib/agents/codex.d.ts +0 -23
  69. package/dist/lib/agents/codex.d.ts.map +0 -1
  70. package/dist/lib/agents/codex.js +0 -364
  71. package/dist/lib/agents/codex.js.map +0 -1
  72. package/dist/lib/agents/cursor.d.ts +0 -10
  73. package/dist/lib/agents/cursor.d.ts.map +0 -1
  74. package/dist/lib/agents/cursor.js +0 -207
  75. package/dist/lib/agents/cursor.js.map +0 -1
  76. package/dist/lib/agents/gemini.d.ts +0 -10
  77. package/dist/lib/agents/gemini.d.ts.map +0 -1
  78. package/dist/lib/agents/gemini.js +0 -208
  79. package/dist/lib/agents/gemini.js.map +0 -1
  80. package/dist/lib/agents/opencode.d.ts +0 -46
  81. package/dist/lib/agents/opencode.d.ts.map +0 -1
  82. package/dist/lib/agents/opencode.js +0 -365
  83. package/dist/lib/agents/opencode.js.map +0 -1
@@ -0,0 +1,166 @@
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 { fileURLToPath } from 'node:url';
21
+ import { AI_GATEWAY } from '../shared.js';
22
+ import { runWithDefinition } from '../plugin/orchestrator.js';
23
+ /**
24
+ * Generate OpenCode config file content.
25
+ * Configures the Vercel AI Gateway provider, plus any additional providers.
26
+ *
27
+ * STAYS host-side + exported (the unit test imports it). The content is written
28
+ * verbatim into `opencode.json` in the sandbox by the orchestrator. Behavior is
29
+ * identical to the old adapter, including writing the resolved apiKey into the
30
+ * config (falling back to the `{env:AI_GATEWAY_API_KEY}` placeholder).
31
+ */
32
+ export function generateOpenCodeConfig(extraProviders, apiKey, timeoutMs, webResearch) {
33
+ const vercelBase = {
34
+ options: {
35
+ apiKey: apiKey || '{env:AI_GATEWAY_API_KEY}',
36
+ ...(timeoutMs ? { timeout: timeoutMs } : {}),
37
+ },
38
+ };
39
+ const { vercel: vercelExtra, ...otherProviders } = extraProviders || {};
40
+ const providers = {
41
+ vercel: {
42
+ ...vercelBase,
43
+ ...vercelExtra,
44
+ options: { ...vercelBase.options, ...vercelExtra?.options },
45
+ },
46
+ ...otherProviders,
47
+ };
48
+ return JSON.stringify({
49
+ $schema: 'https://opencode.ai/config.json',
50
+ provider: providers,
51
+ permission: {
52
+ write: 'allow',
53
+ edit: 'allow',
54
+ bash: 'allow',
55
+ // OpenCode has no native web search; Exa-backed search is enabled via
56
+ // OPENCODE_ENABLE_EXA=1 alongside these tool permissions.
57
+ ...(webResearch ? { webfetch: 'allow', websearch: 'allow' } : {}),
58
+ },
59
+ }, null, 2);
60
+ }
61
+ /**
62
+ * Build the OpenCode plugin definition.
63
+ *
64
+ * Auth is single-mode: the Vercel AI Gateway. authEnv() also carries the Exa
65
+ * websearch toggle (OPENCODE_ENABLE_EXA) when webResearch is on — it rides in
66
+ * process.env alongside the gateway key, exactly as the old adapter set it on the
67
+ * CLI's spawn env. The matching tool permissions are written into opencode.json by
68
+ * generateOpenCodeConfig.
69
+ */
70
+ export function createOpenCodeDefinition() {
71
+ return {
72
+ name: 'vercel-ai-gateway/opencode',
73
+ displayName: 'OpenCode (Vercel AI Gateway)',
74
+ defaultModel: 'vercel/anthropic/claude-sonnet-4',
75
+ o11yAgentName: 'vercel-ai-gateway/opencode',
76
+ // Resolve run.mjs next to this file (works in src during dev and in dist after
77
+ // the build copies run.mjs alongside the compiled agent.js).
78
+ runnerPath: fileURLToPath(new URL('./run.mjs', import.meta.url)),
79
+ // OpenCode only supports the Vercel AI Gateway, never direct provider APIs.
80
+ getApiKeyEnvVar() {
81
+ return AI_GATEWAY.apiKeyEnvVar;
82
+ },
83
+ install(options) {
84
+ // Project deps (retried once, last-10-lines wording), then the OpenCode CLI.
85
+ // The CLI has two mutually-exclusive install paths preserved from the old
86
+ // adapter:
87
+ // - binaryUrl set → curl a custom binary into $HOME/.local/bin (e.g. a
88
+ // patched build for unreleased models). Error body is stdout+stderr.
89
+ // - otherwise → `npm install -g opencode-ai`. Error body is stderr.
90
+ const binaryUrl = options.agentOptions?.binaryUrl;
91
+ const steps = [
92
+ {
93
+ kind: 'command',
94
+ cmd: 'npm',
95
+ args: ['install'],
96
+ retryOnce: true,
97
+ errorPrefix: 'npm install failed',
98
+ errorBody: 'last10',
99
+ },
100
+ ];
101
+ if (binaryUrl) {
102
+ // Download a custom binary (e.g. a patched build for unreleased models).
103
+ // Invoked as the old adapter did: `bash -c "<line>"` via a 'command' step,
104
+ // so the curl line is byte-identical. On failure the orchestrator renders
105
+ // `OpenCode CLI install failed: <stderr>`; the old adapter rendered
106
+ // `OpenCode CLI install failed: <stdout> <stderr>` — for `curl -fsSL`
107
+ // stdout is empty on failure, so the only difference is a dropped leading
108
+ // space (see behaviorNotes / contractAdditions for the exact-parity option).
109
+ steps.push({
110
+ kind: 'command',
111
+ cmd: 'bash',
112
+ args: [
113
+ '-c',
114
+ `mkdir -p $HOME/.local/bin && curl -fsSL "${binaryUrl}" -o $HOME/.local/bin/opencode && chmod +x $HOME/.local/bin/opencode`,
115
+ ],
116
+ errorPrefix: 'OpenCode CLI install failed',
117
+ errorBody: 'stderr',
118
+ });
119
+ }
120
+ else {
121
+ steps.push({
122
+ kind: 'command',
123
+ cmd: 'npm',
124
+ args: ['install', '-g', 'opencode-ai'],
125
+ errorPrefix: 'OpenCode CLI install failed',
126
+ errorBody: 'stderr',
127
+ });
128
+ }
129
+ return steps;
130
+ },
131
+ // OpenCode is configured via a project-local opencode.json (writeFiles target,
132
+ // relative to cwd — not a `~` path, so no viaShell heredoc needed).
133
+ configFiles(options) {
134
+ const extraProviders = options.agentOptions?.extraProviders;
135
+ const content = generateOpenCodeConfig(extraProviders, options.apiKey, options.timeout, options.webResearch);
136
+ return [{ path: 'opencode.json', content }];
137
+ },
138
+ authEnv(options) {
139
+ return {
140
+ [AI_GATEWAY.apiKeyEnvVar]: options.apiKey,
141
+ // Exa-backed websearch is gated behind this env var in the OpenCode CLI;
142
+ // the matching tool permissions are written into opencode.json by
143
+ // generateOpenCodeConfig.
144
+ ...(options.webResearch ? { OPENCODE_ENABLE_EXA: '1' } : {}),
145
+ };
146
+ },
147
+ };
148
+ }
149
+ /**
150
+ * Create the OpenCode Agent. Thin wrapper over the generic orchestrator so the
151
+ * Agent interface (and thus registry.ts / index.ts / runner.ts) is unchanged.
152
+ */
153
+ export function createOpenCodeAgent() {
154
+ const definition = createOpenCodeDefinition();
155
+ return {
156
+ name: definition.name,
157
+ displayName: definition.displayName,
158
+ getApiKeyEnvVar: definition.getApiKeyEnvVar,
159
+ getDefaultModel() {
160
+ return definition.defaultModel;
161
+ },
162
+ run: (fixturePath, options) => runWithDefinition(definition, fixturePath, options),
163
+ definition,
164
+ };
165
+ }
166
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../../src/lib/agents/opencode/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAqB9D;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CACpC,cAAuD,EACvD,MAAe,EACf,SAAkB,EAClB,WAAqB;IAErB,MAAM,UAAU,GAA4B;QAC1C,OAAO,EAAE;YACP,MAAM,EAAE,MAAM,IAAI,0BAA0B;YAC5C,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC7C;KACF,CAAC;IACF,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,cAAc,EAAE,GAAG,cAAc,IAAI,EAAE,CAAC;IAExE,MAAM,SAAS,GAA4B;QACzC,MAAM,EAAE;YACN,GAAG,UAAU;YACb,GAAG,WAAW;YACd,OAAO,EAAE,EAAE,GAAI,UAAU,CAAC,OAAmC,EAAE,GAAG,WAAW,EAAE,OAAO,EAAE;SACzF;QACD,GAAG,cAAc;KAClB,CAAC;IAEF,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,OAAO,EAAE,iCAAiC;QAC1C,QAAQ,EAAE,SAAS;QACnB,UAAU,EAAE;YACV,KAAK,EAAE,OAAO;YACd,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,OAAO;YACb,sEAAsE;YACtE,0DAA0D;YAC1D,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE;KACF,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;QACL,IAAI,EAAE,4BAA4B;QAClC,WAAW,EAAE,8BAA8B;QAC3C,YAAY,EAAE,kCAAkC;QAChD,aAAa,EAAE,4BAA4B;QAC3C,+EAA+E;QAC/E,6DAA6D;QAC7D,UAAU,EAAE,aAAa,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEhE,4EAA4E;QAC5E,eAAe;YACb,OAAO,UAAU,CAAC,YAAY,CAAC;QACjC,CAAC;QAED,OAAO,CAAC,OAAwB;YAC9B,6EAA6E;YAC7E,0EAA0E;YAC1E,WAAW;YACX,yEAAyE;YACzE,yEAAyE;YACzE,0EAA0E;YAC1E,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,SAA+B,CAAC;YAExE,MAAM,KAAK,GAAkB;gBAC3B;oBACE,IAAI,EAAE,SAAS;oBACf,GAAG,EAAE,KAAK;oBACV,IAAI,EAAE,CAAC,SAAS,CAAC;oBACjB,SAAS,EAAE,IAAI;oBACf,WAAW,EAAE,oBAAoB;oBACjC,SAAS,EAAE,QAAQ;iBACpB;aACF,CAAC;YAEF,IAAI,SAAS,EAAE,CAAC;gBACd,yEAAyE;gBACzE,2EAA2E;gBAC3E,0EAA0E;gBAC1E,oEAAoE;gBACpE,sEAAsE;gBACtE,0EAA0E;gBAC1E,6EAA6E;gBAC7E,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI,EAAE,SAAS;oBACf,GAAG,EAAE,MAAM;oBACX,IAAI,EAAE;wBACJ,IAAI;wBACJ,4CAA4C,SAAS,sEAAsE;qBAC5H;oBACD,WAAW,EAAE,6BAA6B;oBAC1C,SAAS,EAAE,QAAQ;iBACpB,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI,EAAE,SAAS;oBACf,GAAG,EAAE,KAAK;oBACV,IAAI,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,aAAa,CAAC;oBACtC,WAAW,EAAE,6BAA6B;oBAC1C,SAAS,EAAE,QAAQ;iBACpB,CAAC,CAAC;YACL,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC;QAED,+EAA+E;QAC/E,oEAAoE;QACpE,WAAW,CAAC,OAAwB;YAClC,MAAM,cAAc,GAAG,OAAO,CAAC,YAAY,EAAE,cAEhC,CAAC;YACd,MAAM,OAAO,GAAG,sBAAsB,CACpC,cAAc,EACd,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,WAAW,CACpB,CAAC;YACF,OAAO,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,OAAO,CAAC,OAAwB;YAC9B,OAAO;gBACL,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM;gBACzC,yEAAyE;gBACzE,kEAAkE;gBAClE,0BAA0B;gBAC1B,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,mBAAmB,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC7D,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,UAAU,GAAG,wBAAwB,EAAE,CAAC;IAC9C,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;QAClF,UAAU;KACX,CAAC;AACJ,CAAC"}
@@ -0,0 +1,308 @@
1
+ /**
2
+ * OpenCode 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 `opencode` CLI — it cannot import anything
8
+ * from 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 (see opencode.test.ts).
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 OpenCode CLI argument list.
27
+ *
28
+ * Preserved verbatim from the old adapter:
29
+ * - `run <prompt> --format json` is always emitted (`--format json` gives the
30
+ * structured JSON event stream we parse for the transcript).
31
+ * - `--model <model>` is appended when a model override is present.
32
+ * - else, when modelPolicy === 'native-default', `--print-logs --log-level INFO`
33
+ * is appended so older OpenCode versions print the CLI-selected model (the
34
+ * log-scrape path) without a follow-up export call.
35
+ *
36
+ * @param {{prompt:string, model?:string, modelPolicy?:string}} input
37
+ * @returns {string[]}
38
+ */
39
+ export function buildOpenCodeCliArgs(input) {
40
+ const cliArgs = ['run', input.prompt, '--format', 'json'];
41
+ if (input.model) {
42
+ cliArgs.push('--model', input.model);
43
+ } else if (input.modelPolicy === 'native-default') {
44
+ cliArgs.push('--print-logs', '--log-level', 'INFO');
45
+ }
46
+ return cliArgs;
47
+ }
48
+
49
+ /**
50
+ * Extract transcript from OpenCode JSON output.
51
+ * When run with --format json, OpenCode outputs JSON events to stdout.
52
+ * Preserved verbatim from the old adapter.
53
+ *
54
+ * @param {string|undefined|null} output
55
+ * @returns {string|undefined}
56
+ */
57
+ export function extractTranscriptFromOutput(output) {
58
+ if (!output || !output.trim()) {
59
+ return undefined;
60
+ }
61
+
62
+ // The --format json output contains JSON events, one per line.
63
+ // Filter to only include lines that look like JSON objects.
64
+ const lines = output.split('\n').filter((line) => {
65
+ const trimmed = line.trim();
66
+ return trimmed.startsWith('{') && trimmed.endsWith('}');
67
+ });
68
+
69
+ if (lines.length === 0) {
70
+ return undefined;
71
+ }
72
+
73
+ return lines.join('\n');
74
+ }
75
+
76
+ /**
77
+ * Scrape the OpenCode printed logs for the primary build model.
78
+ * Works on OpenCode <= 1.16.x, whose INFO log lines carry providerID/modelID.
79
+ * Preserved verbatim from the old adapter.
80
+ *
81
+ * @param {string} output
82
+ * @returns {string|undefined}
83
+ */
84
+ export function extractObservedModelFromOpenCodeOutput(output) {
85
+ let observedModel;
86
+
87
+ for (const line of output.split('\n')) {
88
+ if (!line.includes('service=llm') || !line.includes('small=false') || !line.includes('agent=build')) {
89
+ continue;
90
+ }
91
+
92
+ const providerMatch = line.match(/providerID=([^\s]+)/);
93
+ const modelMatch = line.match(/modelID=([^\s]+)/);
94
+ const providerID = providerMatch?.[1];
95
+ const modelID = modelMatch?.[1];
96
+ if (providerID && modelID) {
97
+ observedModel = `${providerID}/${modelID}`;
98
+ }
99
+ }
100
+
101
+ return observedModel;
102
+ }
103
+
104
+ /**
105
+ * Extract the session id from the `--format json` event stream.
106
+ * Every emitted event carries a `sessionID` field.
107
+ * Preserved verbatim from the old adapter.
108
+ *
109
+ * @param {string|undefined} transcript
110
+ * @returns {string|undefined}
111
+ */
112
+ export function extractSessionIdFromTranscript(transcript) {
113
+ if (!transcript) {
114
+ return undefined;
115
+ }
116
+
117
+ for (const line of transcript.split('\n')) {
118
+ try {
119
+ const event = JSON.parse(line);
120
+ if (typeof event.sessionID === 'string' && event.sessionID) {
121
+ return event.sessionID;
122
+ }
123
+ } catch {
124
+ // Skip non-JSON lines
125
+ }
126
+ }
127
+
128
+ return undefined;
129
+ }
130
+
131
+ /**
132
+ * Extract the observed model from `opencode export <sessionID>` output.
133
+ * Assistant messages carry `providerID` and `modelID` in their info.
134
+ * Preserved verbatim from the old adapter.
135
+ *
136
+ * @param {string} exportOutput
137
+ * @returns {string|undefined}
138
+ */
139
+ export function extractObservedModelFromSessionExport(exportOutput) {
140
+ const start = exportOutput.indexOf('{');
141
+ if (start === -1) {
142
+ return undefined;
143
+ }
144
+
145
+ let parsed;
146
+ try {
147
+ parsed = JSON.parse(exportOutput.slice(start));
148
+ } catch {
149
+ return undefined;
150
+ }
151
+
152
+ const messages = parsed.messages;
153
+ if (!Array.isArray(messages)) {
154
+ return undefined;
155
+ }
156
+
157
+ for (const message of messages) {
158
+ const info = message.info;
159
+ if (info?.role !== 'assistant') {
160
+ continue;
161
+ }
162
+ if (
163
+ typeof info.providerID === 'string' && info.providerID &&
164
+ typeof info.modelID === 'string' && info.modelID
165
+ ) {
166
+ return `${info.providerID}/${info.modelID}`;
167
+ }
168
+ }
169
+
170
+ return undefined;
171
+ }
172
+
173
+ /**
174
+ * Run OpenCode over the workspace at `input.cwd` and return a RunnerResult.
175
+ *
176
+ * Auth (AI_GATEWAY_API_KEY, and OPENCODE_ENABLE_EXA when web research is on)
177
+ * arrives via process.env — the orchestrator sets it on the `node run.mjs`
178
+ * invocation, and we pass process.env straight through to the CLI. The runner
179
+ * never handles secrets itself.
180
+ *
181
+ * @param {import('../plugin/contract.js').AgentRunInput} input
182
+ * @returns {Promise<import('../plugin/contract.js').RunnerResult>}
183
+ */
184
+ export async function runAgent(input) {
185
+ const args = buildOpenCodeCliArgs(input);
186
+
187
+ // spawnSync (not a shell string): the prompt is a plain argv element, so there
188
+ // is no shell quoting/escaping to get wrong. Blocking is fine — the runner has
189
+ // nothing else to do while the agent works. The sandbox-level timeout bounds it.
190
+ const res = spawnSync('opencode', args, {
191
+ cwd: input.cwd,
192
+ env: process.env,
193
+ encoding: 'utf8',
194
+ maxBuffer: 64 * 1024 * 1024,
195
+ });
196
+
197
+ const stdout = res.stdout || '';
198
+ const stderr = res.stderr || '';
199
+ // Preserve the old concatenation order: stdout THEN stderr.
200
+ const output = stdout + stderr;
201
+ // spawnSync sets status=null + error when the binary can't be spawned at all.
202
+ const agentExitCode = res.status == null ? -1 : res.status;
203
+
204
+ // Capture the transcript from the combined output (old: over stdout+stderr).
205
+ const transcript = extractTranscriptFromOutput(output) ?? null;
206
+
207
+ // Resolve the model OpenCode actually used. The `--format json` events never
208
+ // include it, so first try scraping the printed logs (works on OpenCode <=
209
+ // 1.16.x), then fall back to `opencode export <sessionID>`, whose assistant
210
+ // messages carry providerID/modelID (OpenCode 1.17.0 changed the log format,
211
+ // which broke the scrape). Observation must never fail the run.
212
+ let observedModel = extractObservedModelFromOpenCodeOutput(output);
213
+ if (!observedModel) {
214
+ const sessionId = extractSessionIdFromTranscript(transcript ?? undefined);
215
+ if (sessionId) {
216
+ try {
217
+ const exportRes = spawnSync('opencode', ['export', sessionId], {
218
+ cwd: input.cwd,
219
+ env: process.env,
220
+ encoding: 'utf8',
221
+ maxBuffer: 64 * 1024 * 1024,
222
+ });
223
+ if ((exportRes.status == null ? -1 : exportRes.status) === 0) {
224
+ observedModel = extractObservedModelFromSessionExport(exportRes.stdout || '');
225
+ }
226
+ } catch {
227
+ // Leave observedModel undefined.
228
+ }
229
+ }
230
+ }
231
+ const observedModelOrNull = observedModel ?? null;
232
+
233
+ if (res.error || agentExitCode !== 0) {
234
+ // Mirror the old error string: last 5 lines of output, else a coded fallback.
235
+ const errorLines = output.trim().split('\n').slice(-5).join('\n');
236
+ const fallback = res.error
237
+ ? `Failed to run opencode: ${res.error.message}`
238
+ : `OpenCode CLI exited with code ${agentExitCode}`;
239
+ return {
240
+ ok: false,
241
+ output,
242
+ transcript,
243
+ observedModel: observedModelOrNull,
244
+ error: errorLines || fallback,
245
+ agentExitCode,
246
+ };
247
+ }
248
+
249
+ return {
250
+ ok: true,
251
+ output,
252
+ transcript,
253
+ observedModel: observedModelOrNull,
254
+ error: null,
255
+ agentExitCode,
256
+ };
257
+ }
258
+
259
+ /* ─────────────────────────── runnable (CLI) entry ─────────────────────────── */
260
+
261
+ // True when this file is executed directly (`node run.mjs ...`), false when imported.
262
+ const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
263
+
264
+ if (isMain) {
265
+ // argv[2] is the AgentRunInput JSON (never contains secrets).
266
+ const input = JSON.parse(process.argv[2]);
267
+
268
+ // Always produce a RunnerResult, even if the runner itself throws, so the host
269
+ // always has a result file to read (node exit code stays 0 except on a truly
270
+ // unrecoverable crash before we can write).
271
+ let result;
272
+ try {
273
+ result = await runAgent(input);
274
+ } catch (e) {
275
+ result = {
276
+ ok: false,
277
+ output: '',
278
+ transcript: null,
279
+ observedModel: null,
280
+ error: e && e.message ? e.message : String(e),
281
+ agentExitCode: -1,
282
+ };
283
+ }
284
+
285
+ // Source of truth: the result file the host reads back via sandbox.readFile.
286
+ try {
287
+ mkdirSync(dirname(input.resultPath), { recursive: true });
288
+ writeFileSync(input.resultPath, JSON.stringify(result));
289
+ } catch {
290
+ // If the file can't be written, the host falls back to the marker line below.
291
+ }
292
+
293
+ // Fallback channel: a compact status line (no transcript — it can be huge).
294
+ process.stdout.write(
295
+ '__AGENT_RESULT__ ' +
296
+ JSON.stringify({
297
+ ok: result.ok,
298
+ observedModel: result.observedModel,
299
+ error: result.error,
300
+ agentExitCode: result.agentExitCode,
301
+ }) +
302
+ '\n'
303
+ );
304
+
305
+ // Exit 0: "the runner ran". Agent success/failure is conveyed via result.ok, not
306
+ // the node exit code (the host distinguishes the two).
307
+ process.exit(0);
308
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Agent plugin contract.
3
+ *
4
+ * An agent is described in two halves:
5
+ *
6
+ * 1. an {@link AgentDefinition} — HOST-side data + tiny pure functions that say
7
+ * *how to set up and authenticate* the agent (install commands, config files,
8
+ * auth env). It performs NO sandbox lifecycle and NO CLI invocation itself.
9
+ *
10
+ * 2. a `run.mjs` — a SANDBOX-side script (one per agent) that actually invokes
11
+ * the CLI and captures its transcript, returning a {@link RunnerResult}.
12
+ *
13
+ * The generic {@link runWithDefinition} orchestrator (orchestrator.ts) consumes a
14
+ * definition, drives the shared sandbox lifecycle, ships the agent's `run.mjs` into
15
+ * the sandbox, invokes it, and assembles the final `AgentRunResult`.
16
+ *
17
+ * This split exists so the invocation logic lives *in the sandbox* (where a future
18
+ * in-sandbox judge can `import { runAgent }` and reuse it) rather than only on the
19
+ * host. See the migration notes in the PR for the full rationale.
20
+ */
21
+ import type { ModelTier } from '../../types.js';
22
+ import type { AgentRunOptions } from '../types.js';
23
+ /**
24
+ * One install action the orchestrator runs in the sandbox before invocation.
25
+ *
26
+ * Each step carries its own failure policy so the per-agent install error
27
+ * messages are preserved verbatim (e.g. claude throws `Claude Code install
28
+ * failed: <stderr>`, the project `npm install` throws `npm install failed
29
+ * (exit code N):\n<last 10 lines>` and is retried once).
30
+ */
31
+ export interface InstallStep {
32
+ /** 'command' → sandbox.runCommand(cmd, args); 'shell' → sandbox.runShell(script). */
33
+ kind: 'command' | 'shell';
34
+ /** For kind:'command'. */
35
+ cmd?: string;
36
+ args?: string[];
37
+ /** For kind:'shell'. */
38
+ script?: string;
39
+ /** Retry once on a non-zero exit before failing (the project `npm install`). */
40
+ retryOnce?: boolean;
41
+ /** Prefix of the thrown Error on final failure, e.g. 'Claude Code install failed'. */
42
+ errorPrefix: string;
43
+ /**
44
+ * How the failure body is rendered in the thrown Error, to match today's wording:
45
+ * - 'last10': `${errorPrefix} (exit code N):\n<last 10 lines of stdout+stderr>`
46
+ * - 'stderr': `${errorPrefix}: <stderr>`
47
+ */
48
+ errorBody: 'last10' | 'stderr';
49
+ }
50
+ /**
51
+ * One config file an agent writes into the sandbox before invocation.
52
+ * Either a writeFiles target (relative to cwd) or a shell snippet — the latter is
53
+ * the escape hatch for absolute `~` paths that writeFiles cannot target (codex's
54
+ * `~/.codex/default.config.toml`).
55
+ */
56
+ export interface ConfigFile {
57
+ /** writeFiles target path (relative to cwd). Mutually exclusive with viaShell. */
58
+ path?: string;
59
+ content?: string;
60
+ /** Exact shell to run instead, e.g. `mkdir -p ~/.codex && cat > ~/.codex/... << 'EOF' ...`. */
61
+ viaShell?: string;
62
+ }
63
+ /**
64
+ * Host-side plugin description for one agent. Pure data + tiny pure functions —
65
+ * no sandbox calls, no CLI invocation.
66
+ */
67
+ export interface AgentDefinition {
68
+ /** Registry name, unchanged: 'claude-code' | 'vercel-ai-gateway/claude-code' | ... */
69
+ name: string;
70
+ /** Human-readable name (logs/UI), unchanged from the old adapter. */
71
+ displayName: string;
72
+ /** Default model tier when none is provided (old getDefaultModel()). */
73
+ defaultModel: ModelTier;
74
+ /** Parser name handed to parseTranscript() on the host ('claude-code'|'codex'|...). */
75
+ o11yAgentName: string;
76
+ /** Absolute path to this agent's run.mjs (resolved from the definition's import.meta.url). */
77
+ runnerPath: string;
78
+ /**
79
+ * Which host env var the apiKey is read from. Mirrors the old getApiKeyEnvVar()
80
+ * EXACTLY, including claude-code's 3-way runtime branch (gateway →
81
+ * CLAUDE_CODE_OAUTH_TOKEN → ANTHROPIC_API_KEY). Stays a function because it reads
82
+ * process.env at call time.
83
+ */
84
+ getApiKeyEnvVar(): string;
85
+ /** Ordered install steps (project deps + the agent CLI), run by the orchestrator. */
86
+ install(options: AgentRunOptions): InstallStep[];
87
+ /** Config files written before invocation (codex TOML, opencode.json). [] for most. */
88
+ configFiles(options: AgentRunOptions): ConfigFile[];
89
+ /**
90
+ * The agent-specific auth/env handed to the run.mjs process (merged with the
91
+ * neutral-workspace env by the orchestrator). This is the ONLY place provider
92
+ * auth branching lives. Receives the resolved apiKey via options.apiKey.
93
+ */
94
+ authEnv(options: AgentRunOptions): Record<string, string>;
95
+ /**
96
+ * OPTIONAL host-computed values threaded into the runner as `input.extra`.
97
+ *
98
+ * Use this when run.mjs needs a value that MUST be derived on the host (e.g.
99
+ * codex's parseModelString output — the resolved `--model`, reasoning effort,
100
+ * and verbosity — which must match the TOML config written by configFiles()).
101
+ * Keeping the derivation host-side avoids re-implementing host-only parsing in
102
+ * the zero-dependency runner. Must contain NO secrets (it rides in the argv
103
+ * JSON). Omit (or return undefined) for agents that don't need it.
104
+ */
105
+ runnerExtra?(options: AgentRunOptions): Record<string, unknown>;
106
+ }
107
+ /**
108
+ * What the host passes INTO run.mjs (serialized as argv[2] JSON).
109
+ *
110
+ * SECURITY: the apiKey is intentionally NOT here — it travels only via process.env
111
+ * (and, for codex, the CLI's stdin login). Never put secrets in argv (they show up
112
+ * in process listings).
113
+ */
114
+ export interface AgentRunInput {
115
+ /** The prompt to run, verbatim (any host-side editPrompt was already applied upstream). */
116
+ prompt: string;
117
+ /** Model override, or undefined for the CLI's native default. */
118
+ model?: string;
119
+ /** 'agent-default' | 'native-default' (opencode toggles --print-logs on native). */
120
+ modelPolicy?: string;
121
+ /** Web-research toggle (claude --allowedTools, opencode permissions/env). */
122
+ webResearch?: boolean;
123
+ /** Passthrough of agentOptions (e.g. claude effort). Secrets must NOT be in here. */
124
+ agentOptions?: Record<string, unknown>;
125
+ /** Absolute sandbox cwd AFTER neutral-workspace relocation (transcript paths use it). */
126
+ cwd: string;
127
+ /** Where run.mjs writes its RunnerResult JSON (host reads it back). */
128
+ resultPath: string;
129
+ /**
130
+ * OPTIONAL host-computed values from {@link AgentDefinition.runnerExtra} — see
131
+ * there for what belongs here and the no-secrets constraint.
132
+ */
133
+ extra?: Record<string, unknown>;
134
+ }
135
+ /**
136
+ * Host↔sandbox result contract. run.mjs writes this JSON to `input.resultPath`
137
+ * AND prints one `__AGENT_RESULT__ <json>` status line to stdout (a fallback the
138
+ * host uses only if the result file can't be read).
139
+ *
140
+ * The `ok` (agent CLI succeeded) vs the node exit code (the runner itself crashed)
141
+ * are deliberately separate signals — see the orchestrator's READ RESULT step.
142
+ */
143
+ export interface RunnerResult {
144
+ /** True iff the agent CLI exited 0. */
145
+ ok: boolean;
146
+ /** Combined stdout + stderr of the agent CLI (old `agentOutput`). */
147
+ output: string;
148
+ /** Raw transcript string the runner located (JSONL/JSON), or null. */
149
+ transcript: string | null;
150
+ /** Observed model extracted by the runner (claude/codex/opencode), or null. */
151
+ observedModel: string | null;
152
+ /** Error message when !ok (old "last 5 lines" / "<Agent> exited with code N"). */
153
+ error: string | null;
154
+ /** The agent CLI's exit code (diagnostics; -1 if it could not be spawned). */
155
+ agentExitCode: number;
156
+ }
157
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../../src/lib/agents/plugin/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,qFAAqF;IACrF,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,0BAA0B;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sFAAsF;IACtF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,YAAY,EAAE,SAAS,CAAC;IACxB,uFAAuF;IACvF,aAAa,EAAE,MAAM,CAAC;IACtB,8FAA8F;IAC9F,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;;OAKG;IACH,eAAe,IAAI,MAAM,CAAC;IAE1B,qFAAqF;IACrF,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,WAAW,EAAE,CAAC;IAEjD,uFAAuF;IACvF,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,UAAU,EAAE,CAAC;IAEpD;;;;OAIG;IACH,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE1D;;;;;;;;;OASG;IACH,WAAW,CAAC,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjE;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,2FAA2F;IAC3F,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,EAAE,EAAE,OAAO,CAAC;IACZ,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,+EAA+E;IAC/E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kFAAkF;IAClF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;CACvB"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Agent plugin contract.
3
+ *
4
+ * An agent is described in two halves:
5
+ *
6
+ * 1. an {@link AgentDefinition} — HOST-side data + tiny pure functions that say
7
+ * *how to set up and authenticate* the agent (install commands, config files,
8
+ * auth env). It performs NO sandbox lifecycle and NO CLI invocation itself.
9
+ *
10
+ * 2. a `run.mjs` — a SANDBOX-side script (one per agent) that actually invokes
11
+ * the CLI and captures its transcript, returning a {@link RunnerResult}.
12
+ *
13
+ * The generic {@link runWithDefinition} orchestrator (orchestrator.ts) consumes a
14
+ * definition, drives the shared sandbox lifecycle, ships the agent's `run.mjs` into
15
+ * the sandbox, invokes it, and assembles the final `AgentRunResult`.
16
+ *
17
+ * This split exists so the invocation logic lives *in the sandbox* (where a future
18
+ * in-sandbox judge can `import { runAgent }` and reuse it) rather than only on the
19
+ * host. See the migration notes in the PR for the full rationale.
20
+ */
21
+ export {};
22
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../../src/lib/agents/plugin/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG"}