Skip to content

Commit 866c7bb

Browse files
committed
examples/rlm: Show rejected executor output
Keep successful JavaScript executions visible when their returned shape cannot be scored. Sanitize invalid output into a metadata-only summary and explain the semantic capability mismatch in the comparison UI.
1 parent aa6a34b commit 866c7bb

6 files changed

Lines changed: 90 additions & 27 deletions

File tree

examples/rlm/src/App.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,11 @@ function Lane({
388388
const run = state.run;
389389
const metric = run?.metrics;
390390
const execution = latestExecution(messages);
391+
const showsSemanticMismatch =
392+
state.lane === "executor" &&
393+
run?.sample.dataset === "oolongbench/oolong-synth" &&
394+
run.sample.row === 1200 &&
395+
run.score?.score === 0;
391396
const running =
392397
state.status === "loading" ||
393398
state.status === "running" ||
@@ -548,20 +553,36 @@ function Lane({
548553
</section>
549554
) : null}
550555
{execution ? (
551-
<details className="execution">
552-
<summary>
553-
{state.lane === "rlm" ? "Generated map/reduce module" : "Generated executor module"}
554-
{" · "}
555-
{formatBytes(new TextEncoder().encode(execution.source).byteLength)}
556-
</summary>
557-
<pre>{execution.source}</pre>
556+
<>
557+
<details className="execution">
558+
<summary>
559+
{state.lane === "rlm" ? "Generated map/reduce module" : "Generated executor module"}
560+
{" · "}
561+
{formatBytes(new TextEncoder().encode(execution.source).byteLength)}
562+
</summary>
563+
<pre>{execution.source}</pre>
564+
</details>
558565
{execution.result !== undefined ? (
559-
<div className="result">
560-
<span>{state.lane === "rlm" ? "Reducer output" : "Execution output"}</span>
566+
<section className="result">
567+
<span>
568+
{state.lane === "rlm"
569+
? "JavaScript reducer output"
570+
: "JavaScript execution output"}
571+
</span>
561572
<pre>{formatValue(execution.result)}</pre>
562-
</div>
573+
</section>
563574
) : null}
564-
</details>
575+
</>
576+
) : null}
577+
{showsSemanticMismatch ? (
578+
<section className="capability-note">
579+
<span>Capability mismatch</span>
580+
<strong>JavaScript completed, but its output does not fit this task.</strong>
581+
<p>
582+
The records need semantic labels before code can count them. This lane has no model
583+
capability; Structured RLM adds bounded <code>ws:model</code> map calls.
584+
</p>
585+
</section>
565586
) : null}
566587
{state.error ? (
567588
<div className="error">

examples/rlm/src/layout.test.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ describe("RLM trace layout", () => {
1212
it("uses RLM map/reduce language instead of stale transcript and chat language", () => {
1313
expect(appSource).toContain("Structured RLM");
1414
expect(appSource).toContain("Model map");
15-
expect(appSource).toContain("Reducer output");
15+
expect(appSource).toContain("JavaScript reducer output");
1616
expect(appSource).toContain("Cancel comparison");
1717
expect(appSource).toContain("Waiting for the RLM map");
1818
expect(appSource).toContain("All three strategies are overlapping now");
1919
expect(appSource).toContain("Workers AI charges may apply");
2020
expect(appSource).not.toContain("Model response");
2121
expect(appSource).not.toContain("transcript");
2222
});
23+
24+
it("shows executor output separately from semantic capability mismatches", () => {
25+
expect(appSource).toContain("JavaScript execution output");
26+
expect(appSource).toContain("Capability mismatch");
27+
expect(appSource).toContain("JavaScript completed, but its output does not fit this task.");
28+
expect(appSource).toContain("bounded <code>ws:model</code> map calls");
29+
});
2330
});

examples/rlm/src/styles.css

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ button { cursor:pointer; }
7373
.working span { color:var(--muted); font-size:7px; }
7474
.working button { display:flex; align-items:center; gap:4px; border:0; color:var(--red); background:transparent; font-size:8px; }
7575
.working button svg { width:9px; }
76-
.sample-card,.answer,.children,.execution { border:1px solid var(--line); border-radius:9px; background:white; }
76+
.sample-card,.answer,.children,.execution,.result,.capability-note { border:1px solid var(--line); border-radius:9px; background:white; }
7777
.sample-card { display:flex; flex-direction:column; gap:6px; padding:11px; }
7878
.sample-card>span,.answer>span { color:var(--orange); font-family:var(--mono); font-size:7px; font-weight:600; text-transform:uppercase; }
7979
.sample-card>strong { font-size:10px; line-height:1.45; }
@@ -101,9 +101,14 @@ button { cursor:pointer; }
101101
.execution { overflow:hidden; }
102102
.execution summary { padding:9px 10px; font-family:var(--mono); font-size:8px; cursor:pointer; }
103103
.execution>pre { max-height:320px; margin:0; padding:11px; overflow:auto; color:#e6e6de; background:#20201e; font-family:var(--mono); font-size:7.5px; line-height:1.55; white-space:pre-wrap; }
104-
.result { border-top:1px solid var(--line); }
105-
.result span { display:block; padding:7px 10px 0; color:var(--orange); font-family:var(--mono); font-size:7px; text-transform:uppercase; }
106-
.result pre { margin:0; padding:8px 10px 10px; overflow:auto; font-family:var(--mono); font-size:8px; }
104+
.result { overflow:hidden; }
105+
.result span { display:block; padding:8px 10px; border-bottom:1px solid var(--line); color:var(--orange); font-family:var(--mono); font-size:7px; text-transform:uppercase; }
106+
.result pre { max-height:180px; margin:0; padding:10px; overflow:auto; background:var(--soft); font-family:var(--mono); font-size:8px; line-height:1.5; white-space:pre-wrap; }
107+
.capability-note { padding:11px; border-color:#f1c69d; background:#fff9f3; }
108+
.capability-note>span { color:var(--orange); font-family:var(--mono); font-size:7px; font-weight:600; text-transform:uppercase; }
109+
.capability-note>strong { display:block; margin-top:5px; font-size:9px; }
110+
.capability-note p { margin:5px 0 0; color:var(--muted); font-size:8px; line-height:1.5; }
111+
.capability-note code { color:#333; font-family:var(--mono); font-size:7.5px; }
107112
.answer { display:grid; grid-template-columns:1fr auto; gap:8px; padding:11px; }
108113
.answer>strong { color:var(--green); font-family:var(--mono); font-size:18px; }
109114
.answer>div { grid-column:1/-1; display:grid; grid-template-columns:78px minmax(0,1fr); gap:7px; align-items:start; }

examples/rlm/worker/executor-agent.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ export class ExecutorAgent extends AIChatAgent<ModelEnv, BenchmarkAgentState> {
165165
...this.state,
166166
status: hasExecutionResult ? "completed" : "failed",
167167
finalAnswer: text,
168-
error: hasExecutionResult ? null : "The model did not return an execution result.",
168+
error: hasExecutionResult
169+
? null
170+
: metrics.executionAttempts > 0
171+
? "Generated JavaScript execution failed."
172+
: "The model did not call the JavaScript executor.",
169173
finishedAt: new Date().toISOString(),
170174
run,
171175
});

examples/rlm/worker/executor-tool.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe("executor output sanitation", () => {
3030
});
3131
});
3232

33-
it("drops untyped, oversized, and error output", () => {
33+
it("summarizes rejected output without exposing its values", () => {
3434
expect(
3535
sanitizeExecutorOutput({
3636
...base,
@@ -46,9 +46,16 @@ describe("executor output sanitation", () => {
4646
exitCode: 0,
4747
stdout: "raw corpus",
4848
stderr: "",
49-
result: { evidence: "raw corpus evidence" },
49+
result: { diagnostic: "raw corpus evidence" },
5050
}),
51-
).not.toHaveProperty("result");
51+
).toMatchObject({
52+
stdout: "",
53+
result: {
54+
accepted: false,
55+
reason: "Expected a bounded scalar/list or an object containing answer.",
56+
received: "object without answer",
57+
},
58+
});
5259
expect(
5360
sanitizeExecutorOutput({
5461
...base,
@@ -57,7 +64,12 @@ describe("executor output sanitation", () => {
5764
stderr: "",
5865
result: { answer: "x".repeat(17 * 1024) },
5966
}),
60-
).not.toHaveProperty("result");
67+
).toMatchObject({
68+
result: {
69+
accepted: false,
70+
received: "object with answer",
71+
},
72+
});
6173
expect(
6274
sanitizeExecutorOutput({
6375
...base,

examples/rlm/worker/executor-tool.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const EXECUTOR_BACKEND = "oolong-javascript";
55
export const RLM_BACKEND = "oolong-rlm-javascript";
66

77
type ExecWorkspace = Parameters<typeof createExecTool>[0]["workspace"];
8-
const MAX_SAFE_ANSWER_BYTES = 16 * 1024;
8+
const MAX_SAFE_RESULT_BYTES = 16 * 1024;
99

1010
export function createExecutorTool(
1111
workspace: ExecWorkspace,
@@ -45,17 +45,16 @@ export function sanitizeExecutorOutput(output: ExecToolOutput): ExecToolOutput {
4545
const base = { command: output.command, cwd: output.cwd, backend: output.backend };
4646
if ("error" in output) return { ...base, error: "Generated execution failed." };
4747

48-
const answer = safeAnswer(output.result);
4948
return {
5049
...base,
5150
exitCode: output.exitCode,
5251
stdout: "",
5352
stderr: "",
54-
...(answer === undefined ? {} : { result: { answer } }),
53+
result: safeResult(output.result),
5554
};
5655
}
5756

58-
function safeAnswer(value: unknown): string | number | Array<string | number> | undefined {
57+
function safeResult(value: unknown): Record<string, unknown> {
5958
const answer = isRecord(value) && Object.hasOwn(value, "answer") ? value.answer : value;
6059
const valid =
6160
typeof answer === "string" ||
@@ -65,8 +64,23 @@ function safeAnswer(value: unknown): string | number | Array<string | number> |
6564
(item): item is string | number =>
6665
typeof item === "string" || (typeof item === "number" && Number.isFinite(item)),
6766
));
68-
if (!valid || encodedBytes(answer) > MAX_SAFE_ANSWER_BYTES) return undefined;
69-
return answer;
67+
68+
if (valid && encodedBytes(answer) <= MAX_SAFE_RESULT_BYTES) return { answer };
69+
70+
return {
71+
accepted: false,
72+
reason: "Expected a bounded scalar/list or an object containing answer.",
73+
received: describeResult(value),
74+
};
75+
}
76+
77+
function describeResult(value: unknown): string {
78+
if (Array.isArray(value)) return `array with ${value.length} items`;
79+
if (isRecord(value)) {
80+
return Object.hasOwn(value, "answer") ? "object with answer" : "object without answer";
81+
}
82+
if (value === null) return "null";
83+
return typeof value;
7084
}
7185

7286
function isAsyncIterable(value: unknown): value is AsyncIterable<ExecToolOutput> {

0 commit comments

Comments
 (0)