Skip to content

feat(evals): add pi bench harness (integrations/pi-sdk) - #2808

Draft
miguelg719 wants to merge 4 commits into
harness/wave-mastrafrom
harness/wave-pi
Draft

feat(evals): add pi bench harness (integrations/pi-sdk)#2808
miguelg719 wants to merge 4 commits into
harness/wave-mastrafrom
harness/wave-pi

Conversation

@miguelg719

@miguelg719 miguelg719 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

What

Adds --harness pi to evals, backed by a new private @browserbasehq/stagehand-integrations-pi-sdk package.

  • packages/integrations/pi-sdkrunPiSession over the programmatic @earendil-works/pi-coding-agent SDK (createAgentSession, hermetic tool allowlist so bash/edit/write are never exposed), in-process MCP bridge for facade tools, streamed AgentSessionEvents → tool-call/result/usage, abort forwarding, redaction, status/stopReason normalization.
  • packages/evals/framework/piToolAdapter.tsvia:"mcp" mounts (stagehand_facade first, playwright_mcp, chrome_devtools_mcp); via:"handles" run-tool via the in-process bridge.
  • packages/evals/framework/harnesses/piAdapter.ts — events → NormalizedToolCall[].
  • Registered via defineExternalHarness.

Testing

  • Full unit gate green (evals + pi-sdk 12 + all other suites).
  • Connected smoke (evals run b:webvoyager --harness pi --tool stagehand_facade -l 1 -t 1 -e browserbase): 10 Stagehand tool calls via the facade, final answer produced.

Stacked on the mastra PR. Implemented with Codex (gpt-5.6) under supervision; review findings (redaction, stderr buffering, stop reasons, run-tool aborts) fixed in-branch.


Summary by cubic

Adds a new --harness pi bench harness backed by a private @browserbasehq/stagehand-integrations-pi-sdk, enabling evals to run the PI coding agent with Stagehand tools while keeping tool exposure hermetic and logs redacted. Behavior change: PI stop reasons “aborted” and “length” now map to sdk_error; harness guidance strings derive from the registry dynamically.

  • Registers pi harness with default model openai/gpt-5.4-mini; supports stagehand_facade (default), playwright_mcp, chrome_devtools_mcp, stagehand_code, playwright_code, cdp_code.
  • New @browserbasehq/stagehand-integrations-pi-sdk:
    • In-process PI session runner (turn budget, abort forwarding), token/cost usage extraction, and transcript building.
    • MCP bridge: maps stdio MCP servers to PI tools (mcp__server__tool), line-buffers stderr and redacts secrets; flushes trailing partial lines.
    • Custom-tool allowlist: hosts the harness run-tool in-process and respects AbortSignal/timeouts.
  • Evals adapters:
    • piRunner: plugs into the external harness skeleton (marker contract), adds pi_turns metric.
    • piAdapter: converts PI events into verifier trajectories (reasoning, images, probe evidence).
    • piToolAdapter: mounts MCP or in-process handles; records step observations; cleans runtimes and temp dirs; avoids unhandled rejections on late failures.
  • Safety and CI: redacts provider errorMessage fields and tool args/results; transcripts/logs never leak secrets. CI/Turbo build and tests wired for the new package.

Rollout

  • Run: evals run <task> --harness pi --tool stagehand_facade (or another supported surface).
  • Optional env:
    • EVAL_PI_MAX_TURNS (or AGENT_EVAL_MAX_STEPS) to cap turns; EVAL_PI_THINKING for thinking level.
    • EVAL_PI_RUN_TOOL_TIMEOUT_MS, EVAL_AGENT_MOUNT_CLEANUP_TIMEOUT_MS, EVAL_CAPTURE_EVIDENCE_TIMEOUT_MS.
  • MCP usage: provide mcpServers in the mount; tools appear as mcp__server__tool and emit redacted, line-buffered stderr logs.
  • No migration required; guidance strings already reflect the updated harness list.

Written for commit 48370f3. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 48370f3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@miguelg719
miguelg719 marked this pull request as draft August 24, 2026 18:19

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 24 files

Confidence score: 3/5

  • packages/evals/framework/harnesses/piAdapter.ts can discard structured MCP details when text is also returned, leaving verifiers with incomplete trajectory data; prefer non-empty details before joined.
  • packages/evals/framework/piToolAdapter.ts can throw when a handle snippet returns a function or Symbol, because JSON.stringify produces undefined; fall back to String(value) before clipping.
  • packages/integrations/pi-sdk/package.json requires Node 22.19 while the repository advertises Node 22.18, which may block installation in engine-strict environments; align the SDK requirement or supported Node range.
  • The tests in packages/evals/tests/framework/piToolAdapter.test.ts and packages/integrations/pi-sdk/tests/session.test.ts leave tool ordering, timeout diagnostics, and abort forwarding under-verified, while packages/evals/framework/benchHarness.ts is not reflected in the evals README; strengthen the assertions and document the pi harness.
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/benchHarness.ts">

<violation number="1" location="packages/evals/framework/benchHarness.ts:299">
P3: The new registry entry makes `pi` a supported harness, but the checked-in evals README still advertises only `stagehand`, `claude_code`, and `codex`. Update the harness option documentation to include `pi` so users can discover the new CLI mode.</violation>
</file>

<file name="packages/evals/tests/framework/piToolAdapter.test.ts">

<violation number="1" location="packages/evals/tests/framework/piToolAdapter.test.ts:41">
P2: This test does not actually verify the supported-surface order: it checks only `stagehand_facade` and that `browse_cli` is absent. Assert the complete array so removing or reordering a pi tool surface cannot silently pass.</violation>

<violation number="2" location="packages/evals/tests/framework/piToolAdapter.test.ts:172">
P2: This timeout test does not protect the diagnostic contract because it accepts any message containing `timed out`. Assert `timed out after 20ms` so regressions that hide the configured limit are caught.</violation>
</file>

<file name="packages/evals/framework/piToolAdapter.ts">

<violation number="1" location="packages/evals/framework/piToolAdapter.ts:352">
P2: When a handle snippet returns a function or `Symbol`, `JSON.stringify` returns `undefined`, so `clip(text, 500)` throws instead of returning a tool result. Fall back to `String(value)` when JSON serialization is undefined.</violation>
</file>

<file name="packages/integrations/pi-sdk/tests/session.test.ts">

<violation number="1" location="packages/integrations/pi-sdk/tests/session.test.ts:263">
P2: This test does not verify abort forwarding during an active prompt: it passes even if the `abort` listener or `forwardAbort` is removed. Add an in-flight prompt that aborts the caller signal, then assert the session aborts and the forwarder is detached.

(Based on your team's feedback about adding focused tests for new behavior and lifecycle edge cases.)</violation>
</file>

<file name="packages/integrations/pi-sdk/package.json">

<violation number="1" location="packages/integrations/pi-sdk/package.json:35">
P2: When the harness runs on the repository's advertised Node 22.18 minimum, its required Pi SDK declares Node 22.19 as the minimum, producing an engine mismatch and potentially failing installation in engine-strict environments. Raise this package's engine minimum to `>=22.19.0` or pin a Pi SDK release that supports 22.18.</violation>
</file>

<file name="packages/evals/framework/harnesses/piAdapter.ts">

<violation number="1" location="packages/evals/framework/harnesses/piAdapter.ts:147">
P2: When a bridged MCP tool returns text plus structured `details`, this line discards the structured result and leaves the trajectory with only a text modality. Prefer non-empty `details` before `joined` so verifier consumers retain the JSON result shape.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Runner as Evals Runner
    participant PiR as piRunner.ts
    participant PiTA as piToolAdapter.ts
    participant SDK as pi-sdk (Integration)
    participant Agent as PI Agent SDK
    participant Target as Tool Target (MCP/Handles)

    Note over Runner,Target: NEW: Evaluation Flow with pi Harness

    Runner->>PiR: runPiAgent(plan, model)
    
    PiR->>PiTA: NEW: preparePiToolAdapter()
    PiTA->>PiTA: Resolve Tool Surface & Profile
    PiTA->>PiTA: Start AgentToolRuntime
    PiTA->>PiTA: NEW: buildPiMountConfig()
    alt via: "mcp"
        PiTA-->>PiR: MCP Server Specs
    else via: "handles"
        PiTA-->>PiR: Custom Tool Definitions (in-process)
    end

    PiR->>SDK: NEW: runPiSession(prompt, tools, mcpServers)
    
    opt Has MCP Servers
        SDK->>Target: NEW: connectPiMcpServers()
        Target-->>SDK: Tool list (prefixed mcp__)
    end

    SDK->>Agent: createSession(customTools)
    Agent-->>SDK: AgentSession object

    loop Agent Step (Turn budget: EVAL_PI_MAX_TURNS)
        Agent->>SDK: Emit AgentSessionEvent
        SDK->>SDK: NEW: Redact secrets (sk-*, bb_*)
        SDK->>SDK: NEW: Line-buffer stderr logs
        
        alt Tool Call: MCP
            Agent->>SDK: tool_execution_start (mcp__*)
            SDK->>Target: Call stdio MCP tool
            Target-->>SDK: result
        else Tool Call: Handles
            Agent->>SDK: tool_execution_start (run_browser_code)
            SDK->>PiTA: NEW: executeCodeExposureSnippet()
            PiTA->>Target: Execute snippet against Browser Handles
            Target-->>PiTA: result
            PiTA-->>SDK: result text
        end

        opt After observed tool call
            PiTA->>PiTA: NEW: recordObservation() (screenshots/URL)
        end
        
        SDK-->>Agent: tool_execution_end
    end

    Agent-->>SDK: turn_end / stopReason
    SDK-->>PiR: PiSessionResult (Events, Usage, FinalAnswer)

    PiR->>PiR: NEW: parsePiResult() (look for EVAL_RESULT)
    PiR->>PiR: CHANGED: Map stopReason (aborted/length -> sdk_error)

    PiR->>PiR: NEW: piAdapter.fromHarnessResult()
    Note right of PiR: Normalizes PI Events + Usage + ProbeEvidence<br/>into Trajectory format

    PiR->>PiTA: NEW: cleanup()
    PiTA->>PiTA: Stop runtime & rm temp dirs

    PiR-->>Runner: TaskResult (Trajectory + Metrics)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment on lines +41 to +42
expect(PI_TOOL_SURFACES[0]).toBe("stagehand_facade");
expect(PI_TOOL_SURFACES).not.toContain("browse_cli");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This test does not actually verify the supported-surface order: it checks only stagehand_facade and that browse_cli is absent. Assert the complete array so removing or reordering a pi tool surface cannot silently pass.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/tests/framework/piToolAdapter.test.ts, line 41:

<comment>This test does not actually verify the supported-surface order: it checks only `stagehand_facade` and that `browse_cli` is absent. Assert the complete array so removing or reordering a pi tool surface cannot silently pass.</comment>

<file context>
@@ -0,0 +1,277 @@
+
+describe("pi tool adapter", () => {
+  it("publishes the supported surfaces in default order", () => {
+    expect(PI_TOOL_SURFACES[0]).toBe("stagehand_facade");
+    expect(PI_TOOL_SURFACES).not.toContain("browse_cli");
+  });
</file context>
Suggested change
expect(PI_TOOL_SURFACES[0]).toBe("stagehand_facade");
expect(PI_TOOL_SURFACES).not.toContain("browse_cli");
expect(PI_TOOL_SURFACES).toEqual([
"stagehand_facade",
"playwright_mcp",
"chrome_devtools_mcp",
"stagehand_code",
"playwright_code",
"cdp_code",
]);

undefined,
{} as never,
),
).rejects.toThrow(/timed out/);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This timeout test does not protect the diagnostic contract because it accepts any message containing timed out. Assert timed out after 20ms so regressions that hide the configured limit are caught.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/tests/framework/piToolAdapter.test.ts, line 172:

<comment>This timeout test does not protect the diagnostic contract because it accepts any message containing `timed out`. Assert `timed out after 20ms` so regressions that hide the configured limit are caught.</comment>

<file context>
@@ -0,0 +1,277 @@
+          undefined,
+          {} as never,
+        ),
+      ).rejects.toThrow(/timed out/);
+    } finally {
+      if (previous === undefined) delete process.env.EVAL_PI_RUN_TOOL_TIMEOUT_MS;
</file context>

if (value === undefined) return "undefined";
if (typeof value === "string") return value;
try {
return JSON.stringify(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a handle snippet returns a function or Symbol, JSON.stringify returns undefined, so clip(text, 500) throws instead of returning a tool result. Fall back to String(value) when JSON serialization is undefined.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/framework/piToolAdapter.ts, line 352:

<comment>When a handle snippet returns a function or `Symbol`, `JSON.stringify` returns `undefined`, so `clip(text, 500)` throws instead of returning a tool result. Fall back to `String(value)` when JSON serialization is undefined.</comment>

<file context>
@@ -0,0 +1,364 @@
+  if (value === undefined) return "undefined";
+  if (typeof value === "string") return value;
+  try {
+    return JSON.stringify(value);
+  } catch {
+    return String(value);
</file context>

expect(fake.disposeCount).toBe(0);
});

it("forwards an already-aborted signal", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This test does not verify abort forwarding during an active prompt: it passes even if the abort listener or forwardAbort is removed. Add an in-flight prompt that aborts the caller signal, then assert the session aborts and the forwarder is detached.

(Based on your team's feedback about adding focused tests for new behavior and lifecycle edge cases.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integrations/pi-sdk/tests/session.test.ts, line 263:

<comment>This test does not verify abort forwarding during an active prompt: it passes even if the `abort` listener or `forwardAbort` is removed. Add an in-flight prompt that aborts the caller signal, then assert the session aborts and the forwarder is detached.

(Based on your team's feedback about adding focused tests for new behavior and lifecycle edge cases.) </comment>

<file context>
@@ -0,0 +1,326 @@
+    expect(fake.disposeCount).toBe(0);
+  });
+
+  it("forwards an already-aborted signal", async () => {
+    const fake = scriptedSdk([]);
+    const controller = new AbortController();
</file context>

"vitest": "catalog:"
},
"engines": {
"node": ">=22.18.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the harness runs on the repository's advertised Node 22.18 minimum, its required Pi SDK declares Node 22.19 as the minimum, producing an engine mismatch and potentially failing installation in engine-strict environments. Raise this package's engine minimum to >=22.19.0 or pin a Pi SDK release that supports 22.18.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integrations/pi-sdk/package.json, line 35:

<comment>When the harness runs on the repository's advertised Node 22.18 minimum, its required Pi SDK declares Node 22.19 as the minimum, producing an engine mismatch and potentially failing installation in engine-strict environments. Raise this package's engine minimum to `>=22.19.0` or pin a Pi SDK release that supports 22.18.</comment>

<file context>
@@ -0,0 +1,37 @@
+    "vitest": "catalog:"
+  },
+  "engines": {
+    "node": ">=22.18.0"
+  }
+}
</file context>
Suggested change
"node": ">=22.18.0"
"node": ">=22.19.0"

}
const joined = text.join("\n");
return {
result: joined || (value.details !== undefined ? value.details : value),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a bridged MCP tool returns text plus structured details, this line discards the structured result and leaves the trajectory with only a text modality. Prefer non-empty details before joined so verifier consumers retain the JSON result shape.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/framework/harnesses/piAdapter.ts, line 147:

<comment>When a bridged MCP tool returns text plus structured `details`, this line discards the structured result and leaves the trajectory with only a text modality. Prefer non-empty `details` before `joined` so verifier consumers retain the JSON result shape.</comment>

<file context>
@@ -0,0 +1,155 @@
+  }
+  const joined = text.join("\n");
+  return {
+    result: joined || (value.details !== undefined ? value.details : value),
+    text: joined,
+    images,
</file context>
Suggested change
result: joined || (value.details !== undefined ? value.details : value),
result:
value.details !== undefined &&
(!isRecord(value.details) || Object.keys(value.details).length > 0)
? value.details
: joined || value,

["claude_code", claudeCodeHarness],
["codex", codexHarness],
["mastra", mastraHarness],
["pi", piHarness],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new registry entry makes pi a supported harness, but the checked-in evals README still advertises only stagehand, claude_code, and codex. Update the harness option documentation to include pi so users can discover the new CLI mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/framework/benchHarness.ts, line 299:

<comment>The new registry entry makes `pi` a supported harness, but the checked-in evals README still advertises only `stagehand`, `claude_code`, and `codex`. Update the harness option documentation to include `pi` so users can discover the new CLI mode.</comment>

<file context>
@@ -281,11 +283,20 @@ export const mastraHarness = defineExternalHarness({
   ["claude_code", claudeCodeHarness],
   ["codex", codexHarness],
   ["mastra", mastraHarness],
+  ["pi", piHarness],
 ]);
 
</file context>

@miguelg719
miguelg719 force-pushed the harness/wave-mastra branch from 7964829 to c0a2370 Compare August 24, 2026 18:34
Adds @browserbasehq/stagehand-integrations-pi-sdk (in-process pi-coding-agent
session layer: hermetic ResourceLoader, custom-tool allowlist, turn budget,
abort forwarding, token/cost extraction, stdio MCP -> pi tool bridge) and
wires a "pi" bench harness with defineExternalHarness + runExternalHarnessTask.

- piToolAdapter: stagehand_facade (default), playwright_mcp, chrome_devtools_mcp,
  stagehand_code, playwright_code, cdp_code; mcp mounts bridged in-process,
  handles mounts hosted as the harness run tool inside pi
- piAdapter: pi AgentSession events -> verifier Trajectory
- piRunner: marker result contract, harness_* metrics via the skeleton, pi_turns
- registry: "pi" registered after codex; EVAL_PI_MODELS derives automatically
- tests: pi-sdk session/mcp, adapter, tool adapter, runner, registry; existing
  hardcoded harness-list assertions made registry-derived
…ering, stop reasons, and run-tool aborts

- pi-sdk session: sanitize every event summary (message + detail) so
  logPiEvent/buildPiTranscript never leak tool args/results or provider
  errorMessage fields; treat pi stopReason 'aborted' (without an external
  signal) and 'length' as sdk_error instead of completed
- pi-sdk mcp: line-buffer MCP stderr via attachPiMcpStderrLogger, sanitize
  complete lines (CRLF-aware), flush the trailing partial line once on end/close
- evals piToolAdapter: honor the AbortSignal pi hands the run tool (reject
  before start when already aborted, race abort alongside the timeout), and
  attach a logging catch to abandoned snippets so late rejections never
  surface as unhandled
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant