Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/strong-owls-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"eve": patch
---

fix(evals): coalesce null output for Braintrust reporter and guard against log() throws (#1405)

A no-turn eval (e.g. schedule-dispatch + DB assertions) legitimately produces `result.output === null` per the eval API's own derivation. The Braintrust SDK rejects null/undefined output (`"output must be specified"`), and the throw escaped `onEvalComplete`, killing the entire `eve eval` run — remaining evals never executed and no artifacts were written.

Two fixes:
1. Coalesce `output: result.result.output ?? ""` so null is sent as an empty string.
2. Wrap `experiment.log()` in try/catch so any reporter throw is logged but does not abort the run.
56 changes: 56 additions & 0 deletions packages/eve/src/evals/runner/reporters/braintrust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,60 @@ describe("Braintrust", () => {
}),
);
});

it("coalesces null output to empty string for no-turn evals (#1405)", async () => {
const reporter = Braintrust(makeConfig());
await reporter.onRunStart([makeEval()], makeTarget());

reporter.onEvalComplete(
makeEvalResult({
result: {
// A no-turn eval (schedule-dispatch + DB assertions) produces
// output === null per the eval API's own derivation.
output: null,
finalMessage: null,
status: "completed",
events: [],
derived: {
toolCalls: [],
toolCallCount: 0,
subagentCalls: [],
subagentCallCount: 0,
inputRequests: [],
parked: false,
messageCount: 0,
reasoningBlockCount: 0,
},
sessionId: "session-456",
},
verdict: "passed",
}),
);

// Braintrust's SDK rejects null output ("output must be specified").
// The reporter coalesces to "" so the run doesn't crash.
expect(braintrustMocks.log).toHaveBeenCalledWith(
expect.objectContaining({
output: "",
}),
);
});

it("survives a log() throw without aborting the run (#1405)", async () => {
const reporter = Braintrust(makeConfig());
await reporter.onRunStart([makeEval()], makeTarget());

// Simulate Braintrust SDK throwing on log — the reporter must catch
// so the remaining evals still execute.
braintrustMocks.log.mockImplementationOnce(() => {
throw new Error("output must be specified");
});

// Should NOT throw
expect(() => reporter.onEvalComplete(makeEvalResult())).not.toThrow();

// A second call (the next eval) should still reach log
reporter.onEvalComplete(makeEvalResult());
expect(braintrustMocks.log).toHaveBeenCalledTimes(2);
});
});
31 changes: 21 additions & 10 deletions packages/eve/src/evals/runner/reporters/braintrust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,16 +172,27 @@ class BraintrustReporter implements EvalReporter {
reasoningBlockCount: result.result.derived.reasoningBlockCount,
};

this.#experiment.log({
id: result.id,
input: evaluation?.description ?? "",
output: result.result.output,
error: result.error ?? undefined,
scores,
metadata,
metrics,
tags: evaluation?.tags ? [...evaluation.tags] : undefined,
});
try {
this.#experiment.log({
id: result.id,
input: evaluation?.description ?? "",
// Braintrust's SDK rejects null/undefined output ("output must be
// specified"). A no-turn eval (e.g. schedule-dispatch + DB assertions)
// legitimately produces output === null per the eval API's own
// derivation. Coalesce to an empty string so the reporter doesn't
// crash the entire eval run. See #1405.
output: result.result.output ?? "",
error: result.error ?? undefined,
scores,
metadata,
metrics,
tags: evaluation?.tags ? [...evaluation.tags] : undefined,
});
} catch (error) {
// A reporter throw in onEvalComplete must not abort the run and
// drop the remaining evals + artifacts. Log and continue. See #1405.
console.error(`Braintrust reporter: failed to log eval "${result.id}":`, error);
}
}

async onRunComplete(_summary: EveEvalRunSummary): Promise<void> {
Expand Down
Loading