Skip to content
Merged
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
45 changes: 39 additions & 6 deletions src/eval/spreadsheetBenchNodeAgentBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ export type SpreadsheetBenchNodeAgentRecalculationReceipt = {

export type SpreadsheetBenchCandidateFinalizationReceipt = {
engine: string;
status?:
| "completed"
| "completed_stable_pending"
| "not_required"
| "preserved_pending"
| "preserved_unsupported"
| "preserved_error";
beforeSha256: string;
afterSha256: string;
changed: boolean;
Expand Down Expand Up @@ -377,7 +384,7 @@ export async function runSpreadsheetBenchNodeAgentBridge(
room.changedCellCount(),
bridgeWorkbookRepairContract(room).requiredRepairs.length,
workflowController.pendingVerificationCount(),
recalculation.unresolvedFormulaCount,
candidateFinalization?.status === "completed" ? 0 : recalculation.unresolvedFormulaCount,
);
const trace = buildBridgeTrace({
traceId,
Expand Down Expand Up @@ -500,7 +507,9 @@ function contractFirstBridgeModel(
return boundedBridgeDelegateNext(delegate, input);
}
if (phase === "inspect_requested") {
if (latest?.tool !== "inspect_workbook" || asRecord(latest.result)?.ok !== true || (!activePlan && !activeStructuralPlan)) {
const inspectionSucceeded = asRecord(latest?.result)?.ok === true
|| isCompactedBridgeToolResult(latest?.result);
if (latest?.tool !== "inspect_workbook" || !inspectionSucceeded || (!activePlan && !activeStructuralPlan)) {
delegated = true;
return boundedBridgeDelegateNext(delegate, input);
}
Expand Down Expand Up @@ -559,7 +568,19 @@ function contractFirstBridgeModel(

if (phase === "execute_requested") {
const result = asRecord(latest?.result);
if (latest?.tool !== EXECUTE_VERIFIED_WORKBOOK_PLAN_TOOL_NAME || result?.status !== "completed" || !activePlan) {
if (latest?.tool !== EXECUTE_VERIFIED_WORKBOOK_PLAN_TOOL_NAME || !activePlan) {
delegated = true;
return boundedBridgeDelegateNext(delegate, input);
}
if (result?.status !== "completed") {
if (completedPlanHashes.size > 0) {
return {
text: "The verified deterministic workbook repairs are complete; a stale follow-on contract did not pass current preflight and was not applied.",
toolCalls: [],
done: true,
usage: { inputTokens: 0, outputTokens: 0 },
};
}
delegated = true;
return boundedBridgeDelegateNext(delegate, input);
}
Expand Down Expand Up @@ -634,7 +655,7 @@ function contractFirstBridgeModel(
};
}

if (contract.plans.length > 0 || room.changedCellCount() === 0) {
if (completedPlanHashes.size === 0 && completedStructuralRepairIds.size === 0) {
delegated = true;
return boundedBridgeDelegateNext(delegate, input);
}
Expand Down Expand Up @@ -710,6 +731,12 @@ function parseBridgeToolResult(content: string): Record<string, unknown> | undef
}
}

function isCompactedBridgeToolResult(result: unknown): boolean {
return typeof result === "string"
&& result.startsWith("[")
&& result.includes("payload compacted to save context");
}

function bridgeDeterministicContract(room: SpreadsheetBenchWorkbookRoomTools): BridgeDeterministicContract {
const inspection = room.taskInspection();
if (inspection.deterministicPlan?.status !== "complete") return { complete: false, plans: [] };
Expand Down Expand Up @@ -737,7 +764,10 @@ function latestBridgeToolResult(messages: Parameters<AgentModel["next"]>[0]["mes
tool: string;
result: unknown;
} | undefined {
const message = [...messages].reverse().find((candidate) => candidate.role === "tool" && candidate.toolName);
const message = [...messages].reverse().find((candidate) =>
candidate.role === "tool"
&& candidate.toolName
&& candidate.toolName !== "compaction");
if (!message?.toolName) return undefined;
try {
return { tool: message.toolName, result: JSON.parse(message.content) as unknown };
Expand Down Expand Up @@ -1783,6 +1813,7 @@ function remainingDeterministicInspection(
plan.operations.map((operation) => workbookCellKey(plan.sheet, operation.elementId))));
const { deterministicPlan: _currentPlan, ...currentWithoutDeterministicPlan } = current;
if (remainingKeys.size === 0) {
if (current.deterministicPlan?.status === "complete") return current;
return {
...currentWithoutDeterministicPlan,
formulaFillSuggestions: [],
Expand Down Expand Up @@ -2458,7 +2489,9 @@ function buildBridgeTrace(args: {
artifactRefs: [candidateRef],
fact: args.recalculation,
verifier: args.recalculation.engine,
status: args.recalculation.unresolvedFormulaCount === 0 ? "verified" : "needs_review",
status: args.recalculation.unresolvedFormulaCount === 0 || args.candidateFinalization?.status === "completed"
? "verified"
: "needs_review",
}));
if (args.structuralRepair) {
trace.evidence.push(makeEvidenceReceipt({
Expand Down
96 changes: 83 additions & 13 deletions tests/spreadsheetBenchNodeAgentBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,33 @@ describe("SpreadsheetBench canonical NodeAgent bridge", () => {
}
});

it("terminates after a complete multi-sheet deterministic contract without delegating", async () => {
const root = tempRoot();
const task = await stagedLargeFontColorTask(root, 2);
let delegatedCalls = 0;
const receipt = await runSpreadsheetBenchNodeAgentBridge({
agentManifestPath: task.agentManifest,
candidateWorkbookPath: join(root, "output", "multi-sheet-font-color.xlsx"),
model: {
name: "provider-that-must-not-run-after-contract",
async next(): Promise<AgentStep> {
delegatedCalls += 1;
throw new Error("a complete deterministic contract must terminate without delegation");
},
},
maxSteps: 8,
});

expect(delegatedCalls).toBe(0);
expect(receipt.outcome).toMatchObject({ status: "completed", changedCellCount: 24 });
expect(receipt.frame.agentResult.trace.map((event) => event.tool)).toEqual([
"inspect_workbook",
"execute_verified_workbook_plan",
"inspect_workbook",
"execute_verified_workbook_plan",
]);
});

it("records an audit as unresolved without provider calls when inspection exposes no write evidence", async () => {
const root = tempRoot();
const instruction = "Please audit and fix this file thoroughly.";
Expand Down Expand Up @@ -477,6 +504,47 @@ describe("SpreadsheetBench canonical NodeAgent bridge", () => {
expect(receipt.trace.evidence.find((evidence) => evidence.label === "SpreadsheetBench formula recalculation")?.status).toBe("needs_review");
});

it("accepts a validated Excel cache refresh when the local formula subset is unsupported", async () => {
const root = tempRoot();
const instruction = 'Replace Model!B1 with formula TEXT(A1,"0.00"), then verify the changed cell.';
const task = await stagedTask(root, instruction);
const finalizationPath = join(root, "output", "candidate-finalization.json");
const receipt = await runSpreadsheetBenchNodeAgentBridge({
agentManifestPath: task.agentManifest,
candidateWorkbookPath: join(root, "output", "externally-recalculated.xlsx"),
model: unresolvedRecalculationModel(instruction),
traceId: "trace_sbench_external_recalculation",
maxSteps: 8,
now: () => 1_260,
finalizeCandidate: async ({ candidateWorkbookPath, beforeSha256 }) => {
writeFileSync(finalizationPath, '{"schema":1,"status":"completed"}\n');
const receiptBytes = readFileSync(finalizationPath);
return {
engine: "validated-excel-cache-refresh",
status: "completed",
beforeSha256,
afterSha256: sha256(readFileSync(candidateWorkbookPath)),
changed: false,
formulaCellCount: 1,
receipt: {
path: finalizationPath,
sha256: sha256(receiptBytes),
bytes: receiptBytes.byteLength,
},
};
},
});

expect(receipt.recalculation.unresolvedFormulaCount).toBe(1);
expect(receipt.candidateFinalization?.status).toBe("completed");
expect(receipt.outcome).toMatchObject({
status: "completed",
changedCellCount: 1,
finalVerificationStatus: "passed",
});
expect(receipt.trace.evidence.find((evidence) => evidence.label === "SpreadsheetBench formula recalculation")?.status).toBe("verified");
});

it("never completes an unresolved formula write even when task wording is classified as nonmutating", async () => {
const root = tempRoot();
const instruction = 'Model!B1 should contain formula TEXT(A1,"0.00").';
Expand Down Expand Up @@ -1440,23 +1508,25 @@ async function stagedFontColorTask(root: string): Promise<{ agentManifest: strin
return { agentManifest: join(agentDir, "task.json") };
}

async function stagedLargeFontColorTask(root: string): Promise<{ agentManifest: string }> {
async function stagedLargeFontColorTask(root: string, sheetCount = 1): Promise<{ agentManifest: string }> {
const taskDir = join(root, "tasks", "large-font-color");
const agentDir = join(taskDir, "agent");
mkdirSync(join(agentDir, "inputs"), { recursive: true });
const input = new ExcelJS.Workbook();
const statement = input.addWorksheet("Income Statement");
for (let matrix = 0; matrix < 12; matrix += 1) {
const startRow = 6 + matrix * 4;
for (let rowOffset = 0; rowOffset < 3; rowOffset += 1) {
for (let column = 2; column <= 4; column += 1) {
const cell = statement.getCell(startRow + rowOffset, column);
cell.value = (matrix + 1) * 100 + rowOffset * 10 + column;
cell.numFmt = "0.0x";
cell.font = {
bold: true,
color: { argb: rowOffset === 1 && column === 3 ? "FF000000" : "FF0000FF" },
};
for (let sheetIndex = 0; sheetIndex < sheetCount; sheetIndex += 1) {
const statement = input.addWorksheet(sheetIndex === 0 ? "Income Statement" : `Income Statement ${sheetIndex + 1}`);
for (let matrix = 0; matrix < 12; matrix += 1) {
const startRow = 6 + matrix * 4;
for (let rowOffset = 0; rowOffset < 3; rowOffset += 1) {
for (let column = 2; column <= 4; column += 1) {
const cell = statement.getCell(startRow + rowOffset, column);
cell.value = (sheetIndex + 1) * 1_000 + (matrix + 1) * 100 + rowOffset * 10 + column;
cell.numFmt = "0.0x";
cell.font = {
bold: true,
color: { argb: rowOffset === 1 && column === 3 ? "FF000000" : "FF0000FF" },
};
}
}
}
}
Expand Down
Loading