[STG-2515] feat(evals): external-harness scaffolding — skill modes, bare-loop core, shared browse adapter#2337
[STG-2515] feat(evals): external-harness scaffolding — skill modes, bare-loop core, shared browse adapter#2337shrey150 wants to merge 8 commits into
Conversation
|
…re, shared browse adapter Base of the external-harness stack (design doc at packages/evals/docs/external-harnesses.md). Adds the shared machinery for four upcoming --harness adapters (vercel_ai_sdk, anthropic_sdk, openai_agents_sdk, cursor_sdk) without registering any of them yet: - Harness union + classification (BARE_LOOP_HARNESSES, EXTERNAL_HARNESSES); the new names are parseable/plannable (dry-run works), execution stays gated on EXECUTABLE_BENCH_HARNESSES until each runner PR lands - --skill-mode <none|prompt_show|injected>: per-run skill-delivery arm, orthogonal to harness (prompt_show gated on the browse release carrying #2335, loud adapter warning until then) - externalHarnessToolAdapter: browse provisioning via the same prepareBrowseCliHarnessAdapter contract codex uses + isAllowedBrowseCommand gating (single browse command per tool call, no shell metacharacters), bare one-liner system prompt mirroring the sandbox templates verbatim - bareLoopRunner core: execution-time NormalizedToolCall recording, 20k-char tool-output clip, EVAL_RESULT parsing, gradeExternalTrajectory wiring unchanged from claude_code/codex - buildAdapterBackedHarness factory in benchHarness.ts (registrations come with each runner) - 40-step bare-loop default cap + per-harness env overrides; per-harness default-model env vars Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
592f1a2 to
b03e3f8
Compare
…ime design doc Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… parser follow-up
`${prefix}Status`/`${prefix}StopReason` mixed the snake_case harness id
(vercel_ai_sdk, openai_agents_sdk) into what should be a camelCase
TaskResult field name (vercel_ai_sdkStatus). Metrics keys keep the
snake_case harness id -- that's the existing metrics-naming convention
across every harness, untouched here.
Also flags (via a TODO) that claudeCodeRunner/codexRunner each hand-fork
their own EVAL_RESULT marker+JSON parse instead of using this module's
parseEvalResultText, and that the three have already drifted slightly --
noted as a follow-up rather than folded in blind.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
6 issues found across 15 files
Confidence score: 3/5
- In
packages/evals/tui/commands/run.ts,--previewcan hide the selected skill-delivery arm (prompt_showvsinjected), so two materially different plans may look the same and reviewers could approve the wrong run configuration—surfacerunOptions.skillModeexplicitly in preview output before merging. - The new bare-loop execution path across
packages/evals/framework/bareLoopRunner.ts,packages/evals/framework/harnesses/bareLoopAdapter.ts, andpackages/evals/framework/evalResultParser.tshas little focused coverage, which raises regression risk in tool-call passthrough, result/status shaping, marker parsing, and usage/finalAnswer mapping—add targeted unit tests for these contracts to de-risk merge. packages/evals/tui/commands/help.tscurrently presents external harnesses as normal--harnessoptions even though execution is still gated, which can lead users to choose paths that fail later—label these harnesses as preview/plan-only in help text until execution support is enabled.- The new flag flow in
packages/evals/tui/commands/parse.tsis untested, so invalid-value handling and stagehand rejection could break silently while lower-level parsing tests still pass—add focusedparseRunArgs/resolveRunOptionstests before merging.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/evals/framework/harnesses/bareLoopAdapter.ts">
<violation number="1" location="packages/evals/framework/harnesses/bareLoopAdapter.ts:30">
P3: Bare-loop trajectory conversion is new executable behavior but has no focused test coverage, so regressions in tool-call passthrough, default status, finalAnswer, or usage mapping would be caught only indirectly. A small unit test for `BareLoopTrajectoryAdapter.fromHarnessResult()` covering the happy path and default status would lock the intended identity-style mapping.
(Based on your team's feedback about adding unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>
<file name="packages/evals/tui/commands/help.ts">
<violation number="1" location="packages/evals/tui/commands/help.ts:95">
P3: `evals run --help` now presents the four external harnesses as normal runnable `--harness` choices, but this PR only makes them parseable/plannable and leaves execution gated. Consider marking these entries as preview-only/non-executable so users do not start runs that later fail unexpectedly.</violation>
</file>
<file name="packages/evals/tui/commands/run.ts">
<violation number="1" location="packages/evals/tui/commands/run.ts:406">
P2: `--preview` hides the selected skill-delivery arm, so runs planned with `--skill-mode prompt_show` or `injected` can look identical in the human-readable plan. Since this new `runOptions.skillMode` field feeds both renderers, the preview header should also surface non-null skill mode.</violation>
</file>
<file name="packages/evals/tui/commands/parse.ts">
<violation number="1" location="packages/evals/tui/commands/parse.ts:279">
P3: The new TUI flag path is untested, so parsing, invalid-value handling, and the stagehand rejection could regress while `parseSkillDeliveryMode` tests still pass. A couple of focused `parseRunArgs`/`resolveRunOptions` cases would lock in the CLI behavior.
(Based on your team's feedback about adding unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>
<file name="packages/evals/framework/evalResultParser.ts">
<violation number="1" location="packages/evals/framework/evalResultParser.ts:35">
P3: This shared parser is now a core result-contract path for bare-loop adapters, but it has no focused tests for marker parsing, trailing SDK text, malformed JSON, or no-marker fallback. Adding direct unit tests would lock the new `_success` behavior before downstream harnesses depend on it.
(Based on your team's feedback about adding unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>
<file name="packages/evals/framework/bareLoopRunner.ts">
<violation number="1" location="packages/evals/framework/bareLoopRunner.ts:147">
P3: This new bare-loop core currently has no focused unit coverage, leaving prompt construction, clipping/recording, TaskResult shaping, and verifier handoff regressions easy to miss. Consider adding tests for the normal finalization path plus edge cases like invalid env limits, clipped failed tool output, and verifier failure fallback.
(Based on your team's feedback about adding unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>
Architecture diagram
sequenceDiagram
participant CLI as CLI (tui/commands)
participant Planner as Bench Planner
participant HarnessReg as Harness Registry
participant AdapterFactory as buildAdapterBackedHarness
participant ToolAdapter as ExternalHarnessToolAdapter
participant BrowseProvision as prepareBrowseCliHarnessAdapter
participant BareLoopRunner as bareLoopRunner.ts
participant EvalParser as evalResultParser.ts
participant Verifier as Verifier (gradeExternalTrajectory)
participant TrajectoryAdapter as bareLoopAdapter.ts
Note over CLI,TrajectoryAdapter: NEW: External Harness Scaffolding Flow
CLI->>CLI: Parse --skill-mode flag
CLI->>CLI: Validate skillMode only for external harnesses
CLI->>Planner: generateBenchTestcases(harness, skillMode)
Note over Planner: NEW: resolveExternalHarnessStartupProfile
alt External Harness (vercel_ai_sdk, anthropic_sdk, etc.)
Planner->>Planner: Set toolSurface = "browse_cli"
Planner->>Planner: Resolve models from EXTERNAL_HARNESS_MODEL_DEFAULTS
Planner->>Planner: Resolve startup profile
Planner->>Planner: Build row with skillMode in config
else Stagehand
Planner->>Planner: Legacy paths unchanged
end
Planner-->>CLI: Bench matrix rows (dry-run only for new harnesses)
alt Dry-run mode
CLI->>CLI: Print matrix, exit (harnesses not yet executable)
else Executable run (harness registered)
CLI->>HarnessReg: Lookup harness runner
HarnessReg->>AdapterFactory: Create BenchHarness for external harness
Note over AdapterFactory: NEW: Factory shared by all 4 external harnesses
AdapterFactory->>AdapterFactory: buildVerifierCarrierV3()
AdapterFactory->>ToolAdapter: prepareExternalHarnessAdapter(skillMode)
alt skillMode = "none"
ToolAdapter->>ToolAdapter: Use BARE_LOOP_DEFAULT_SYSTEM_PROMPT (verbatim one-liner)
else skillMode = "prompt_show"
ToolAdapter->>ToolAdapter: Append PROMPT_SHOW_SYSTEM_PROMPT_ADDENDUM
ToolAdapter->>ToolAdapter: Log warning if browse CLI lacks #2335
else skillMode = "injected"
ToolAdapter->>ToolAdapter: Read SKILL.md from disk
ToolAdapter->>ToolAdapter: Embed skill text in system prompt
end
ToolAdapter->>BrowseProvision: prepareBrowseCliHarnessAdapter()
Note over BrowseProvision: Shared with codex: temp cwd, session name, browse wrapper
BrowseProvision-->>ToolAdapter: adapter with cwd, env, browseBinPath, cleanup
alt skillMode = "prompt_show"
ToolAdapter->>ToolAdapter: Warn about unreleased browse CLI support
end
ToolAdapter-->>AdapterFactory: PreparedExternalHarnessAdapter with systemPromptAddendum
AdapterFactory->>BareLoopRunner: runner({plan, model, toolAdapter, verifier})
Loop For each tool call (up to DEFAULT_BARE_LOOP_MAX_STEPS = 40)
BareLoopRunner->>BareLoopRunner: Build system prompt + user prompt
BareLoopRunner->>BareLoopRunner: buildBareLoopUserPrompt(plan)
Note over BareLoopRunner: User prompt includes EVAL_RESULT instructions
BareLoopRunner->>BareLoopRunner: createBareLoopToolRecorder()
BareLoopRunner->>ToolAdapter: runBareBrowseCommand(args)
ToolAdapter->>ToolAdapter: tokenizeBrowseArgs(args)
ToolAdapter->>ToolAdapter: isAllowedBrowseCommand(candidate)
alt Command rejected (metacharacters)
ToolAdapter-->>BareLoopRunner: ok=false, rejected message
else Command allowed
ToolAdapter->>BrowseProvision: execFile(browseBinPath, argv)
BrowseProvision-->>ToolAdapter: stdout/stderr
ToolAdapter-->>BareLoopRunner: ok=true, clipped output (20k char limit)
end
BareLoopRunner->>BareLoopRunner: Record NormalizedToolCall (name, args, result, ok, error, reasoning)
BareLoopRunner->>BareLoopRunner: Log duration + status
End
BareLoopRunner->>EvalParser: parseEvalResultText(finalText)
Note over EvalParser: Find EVAL_RESULT: marker, parse JSON
EvalParser-->>BareLoopRunner: ParsedEvalResult (success, summary, finalAnswer)
BareLoopRunner->>BareLoopRunner: Build base TaskResult with camelCase field names
Note over BareLoopRunner: "vercel_ai_sdk" -> "vercelAiSdkStatus"
alt Verifier configured
BareLoopRunner->>TrajectoryAdapter: fromHarnessResult(toolCalls, finalAnswer, status)
Note over TrajectoryAdapter: Near-identity: bare loop already has NormalizedToolCalls
TrajectoryAdapter-->>BareLoopRunner: Trajectory object
BareLoopRunner->>Verifier: gradeExternalTrajectory(trajectory, rubric)
Note over Verifier: Same path as claude_code/codex: gemini-2.5-flash judge
Verifier-->>BareLoopRunner: Graded TaskResult (overrides _success)
end
BareLoopRunner-->>AdapterFactory: Final TaskResult
AdapterFactory->>ToolAdapter: cleanup() - stop --force, remove temp cwd
AdapterFactory->>AdapterFactory: carrierV3.close() - deregister V3 instance
AdapterFactory-->>HarnessReg: TaskResult complete
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Code comments cited originating events and experiment vocabulary (dated smoke runs, "A/B/C experiment arms", PR numbers, "unreleased at the time of writing", a stale assertSkillsShowSupport reference). Rewritten as present-tense statements of the constraint each code path enforces; "skill-delivery arm" is now "skill-delivery mode" everywhere, including the prompt_show runtime warning and the README flag table. The EVAL_RESULT parser consolidation follow-up now lives in STG-2516; the in-code TODO shrinks to a one-line ticket reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 9 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- evalResultParser: anchor EVAL_RESULT detection to line starts so the literal marker inside a JSON payload cannot hijack parsing; add focused parser tests - externalHarnessToolAdapter: keep stderr in successful browse output; skip on-disk SKILL.md install (installSkill:false) so no-skill / prompt_show runs can't read skill content from the per-run cwd - claudeCodeToolAdapter: optional installSkill flag (default true, claude_code/codex behavior unchanged) - benchHarness: deregister the verifier carrier even when adapter cleanup throws; narrow buildAdapterBackedHarness to the new AdapterBackedHarness union - benchTypes: add ADAPTER_BACKED_HARNESSES + predicate; align prompt_show docs with the actual `browse skills show` prompt; clarify that only adapter-backed harnesses consume skillMode - benchPlanner: record skillMode only for adapter-backed rows and default it to "none" so experiment metadata matches execution - bareLoopRunner: redact API keys / bearer tokens / signed query params in loop-error messages before they land in TaskResult.error; add focused recorder/finalize/prompt tests - bareLoopAdapter: add trajectory-mapping tests - parse: validate --skill-mode against isExternalHarness instead of special-casing stagehand; add flag tests - preview: surface the selected skill mode in the human-readable header - types/evals: type Testcase.metadata.skillMode as SkillDeliveryMode - README: note that harnesses without a registered runner are plan/preview-only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 15 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…preview header - redactSecrets also scrubs secret-bearing JSON/object field forms (api_key/apiKey/access_token/authorization/... : "value") that stringified SDK errors echo; tests added - ADAPTER_BACKED_HARNESSES now derives from BARE_LOOP_HARNESSES + cursor_sdk so future bare-loop additions inherit skillMode/planner handling automatically - preview: focused render tests for the skill-mode header bit (shown when planned, omitted when null) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
The JSON/object-field redaction matched values with [^"']+, which stops at either quote character — a secret containing the other quote (or an escaped quote) was only partially redacted. The value is now matched from its own opening quote through the matching close, allowing escapes. Tests cover other-quote, escaped-quote, and single-quoted values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Part 1 of 5 (base of the stack). Scaffolding + design doc for four upcoming external-harness adapters — no runner is registered here; each lands in its own stacked PR so the runners can be reviewed in isolation:
vercel_ai_sdkrunner (stacked on this)anthropic_sdkrunneropenai_agents_sdkrunnercursor_sdkrunnerExternal harnesses design + usage:
packages/evals/README.md#external-harnessesLinear: STG-2515
What's in this PR:
Harnessunion grows the four names + classification helpers (BARE_LOOP_HARNESSES,EXTERNAL_HARNESSES). The new names are parseable/plannable (dry-run works) but not executable —EXECUTABLE_BENCH_HARNESSESstays at stagehand/claude_code/codex, and each runner PR flips its own bit. Every mid-stack state is coherent.--skill-mode <none|prompt_show|injected>— per-run skill-delivery arm, orthogonal to harness (A/B/C experiment arms).prompt_showis gated on the browse release carrying [STG-2511] feat: add browse skills show and agent-facing --help header #2335 (unreleased); the adapter logs a loud warning with the installed CLI version until then. claude_code/codex provisioning is untouched.externalHarnessToolAdapter.ts— shared browse provisioning via the sameprepareBrowseCliHarnessAdaptercontract codex delegates to, plusisAllowedBrowseCommandgating (singlebrowse ...per tool call, no shell metacharacters) and the bare one-liner system prompt mirroring the sandbox templates verbatim (the bareness is the instrument).bareLoopRunner.tscore — execution-timeNormalizedToolCallrecording, 20k-char tool-output clip (EVAL_BARE_LOOP_TOOL_OUTPUT_LIMIT), shared EVAL_RESULT parsing, andgradeExternalTrajectory(feat(evals): add verifier benchmark instrumentation #2138) wiring unchanged from claude_code/codex: rubric hydration → gemini-2.5-flash judge → persisted trajectory → outcome/process folded into TaskResult.buildAdapterBackedHarnessfactory inbenchHarness.ts(registrations come with each runner PR).Why
Measure browse CLI + SKILL.md performance across the harness-richness spectrum, so skill-content improvements can be validated regardless of model intelligence:
TS-only note: real-world anthropic/openai bare loops are mostly Python; the loop shape is language-invariant and packages/evals is TS — deliberate simplification, recorded in the doc.
Comparability fixes (2026-07-09 audit)
An independent comparability audit across all five harnesses found several cross-cutting issues. The two landing in this PR's layer (shared
bareLoopRunner.ts/evalResultParser.ts):TaskResultfield names.finalizeBareLoopResultbuilt[`${prefix}Status`]/[`${prefix}StopReason`]directly off the harness id, so mixed-case ids (vercel_ai_sdk,openai_agents_sdk) produced awkward field names likevercel_ai_sdkStatus. Now camelCased:vercelAiSdkStatus,openaiAgentsSdkStatus,anthropicSdkStatus, etc. Metrics keys (${prefix}_steps,${prefix}_tool_calls, ...) are unchanged — they intentionally keep the snake_case harness id, matching the metrics-naming convention shared with claude_code/codex.claudeCodeRunner.parseClaudeCodeResultandcodexRunner.parseCodexResultare two more hand-forked copies of this module'sparseEvalResultText(predating it) that have already drifted slightly from each other and from this one (codexRunner has an extra first-line fallback on the no-marker path that claudeCodeRunner doesn't). Consolidating all three isn't a same-behavior mechanical change, so it's left as aTODOinevalResultParser.tsfor a dedicated follow-up PR instead of risked here.The other three findings (cursor_sdk isError/output-clip propagation, vercel_ai_sdk/openai_agents_sdk step-cap detection, anthropic_sdk native abort wiring) land in their owning stacked PRs — #2338, #2339, #2340, #2341.
Review-round fixes (Shrey + cubic)
Comment-style sweep (per review): code comments now state present-tense constraints only — dated smoke references, "A/B/C experiment arm" vocabulary, and PR-number release status moved out of code; "skill-delivery arm" → "skill-delivery mode" everywhere. The EVAL_RESULT parser consolidation is tracked as STG-2516.
cubic-round fixes in this layer: line-anchored EVAL_RESULT parsing (marker inside a JSON payload can't hijack
_success), stderr kept in successful browse output, no on-disk SKILL.md for external harnesses (installSkill: false; claude_code/codex unchanged), verifier-carrier deregistration survives adapter-cleanup failures,ADAPTER_BACKED_HARNESSES(= bare loops + cursor_sdk) now types the factory / gates planner skillMode metadata / backs--skill-modevalidation,Testcase.metadata.skillModetyped, preview header shows the planned skill mode, loop-error messages redact API keys/bearer tokens/signed params/secret-bearing JSON fields, plus focused tests for the parser, bare-loop core, bare-loop adapter, preview header, and--skill-modeflag path.Compose order vs. open PRs
Based on
origin/main; composes with either merge order of #2299 (this stack only imports fromclaudeCodeToolAdapter.ts, no edits) and #2334 (injectedmode reads the sameBROWSER_SKILL_SOURCEconstant, so it inherits whichever SKILL.md lands).E2E Test Matrix
The three live smokes ran on the pre-split combined branch, whose tip content is identical to this stack's tip (verified by
git diff— only intentional restructuring deltas: dynamic error-message lists, split registry tests, exported factory). Live rows live in the stacked PR that owns each runner; this base PR carries the shared rows:pnpm exec vitest run(packages/evals, this branch)Test Files 50 passed, Tests 367 passed— includes newexternalHarnessToolAdapter.test.ts(harness classification, skill-mode parsing, verbatim bare prompt, quote-aware arg tokenizing, command gating incl. metacharacter rejection, real temp-binary execution)pnpm -w exec tsc -p packages/evals/tsconfig.json --noEmit+ prettier + eslintevals run b:webtailbench --harness vercel_ai_sdk -m anthropic/claude-haiku-4-5-20251001 -l 1 --dry-run(this branch)toolSurface: browse_cli,startupProfile: tool_launch_local,browseCliVersion: 0.9.4evals run b:webtailbench --harness vercel_ai_sdk ... -t 1(this branch, non-dry)Harness "vercel_ai_sdk" is dry-run only for now. Use an executable harness (stagehand, claude_code, codex) ...pnpm exec vitest run(this branch, post-fix)Test Files 53 passed, Tests 394 passed(this layer)Test Files 57 passed, Tests 419 passedpnpm -w exec tsc --noEmit+ prettier + eslint (post-fix, this branch and stack tip)No changeset:
packages/evalsis private (not released).Stack mechanics for review
Merge top-down (#2337 first). Each stacked PR's base is the previous branch, so GitHub shows only its own delta. After a parent merges, the child needs its base retargeted to
main(GitHub usually auto-retargets on branch delete) + a rebase.🤖 Generated with Claude Code