Skip to content

Commit 2fb01a7

Browse files
committed
fix(hub): read check verdicts from the tool result's detail
1 parent 8de4424 commit 2fb01a7

5 files changed

Lines changed: 205 additions & 18 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type } from "arktype";
2+
3+
// A check's verdict is read once from its whole output and kept in the result's detail, which
4+
// history trimming leaves alone: a trimmed result loses the "check passed" line the model read.
5+
const CheckDetail = type({ checkPassed: "boolean", "filed?": "boolean" });
6+
export type CheckDetail = typeof CheckDetail.infer;
7+
8+
/** The detail a check tool records, or undefined for any other result. */
9+
export function readCheckDetail(detail: unknown): CheckDetail | undefined {
10+
const parsed = CheckDetail(detail);
11+
return parsed instanceof type.errors ? undefined : parsed;
12+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { expect, test } from "bun:test";
2+
import { appCheckDetail } from "./sidecar-tools.ts";
3+
4+
const PASSED =
5+
"check passed: built, started, and opened / at 1280 and 390 px with no errors";
6+
7+
const shell = (command: string) => ({
8+
id: "call",
9+
name: "run_shell",
10+
arguments: { command },
11+
});
12+
const output = (content: string) => ({ callId: "call", content });
13+
14+
test("a check-app run records its verdict, and only a real check-app run records one", () => {
15+
expect(appCheckDetail(shell("check-app"), output(PASSED))).toEqual({
16+
checkPassed: true,
17+
});
18+
expect(appCheckDetail(shell("cd app && check-app"), output(PASSED))).toEqual({
19+
checkPassed: true,
20+
});
21+
expect(
22+
appCheckDetail(shell("check-app"), output("check failed: 1 problem(s)")),
23+
).toEqual({ checkPassed: false });
24+
expect(
25+
appCheckDetail(shell("check-app"), {
26+
callId: "call",
27+
content: PASSED,
28+
isError: true,
29+
}),
30+
).toEqual({ checkPassed: false });
31+
expect(
32+
appCheckDetail(
33+
shell("check-app && bun test"),
34+
output(`exit code 1\n${PASSED}`),
35+
),
36+
).toEqual({ checkPassed: false });
37+
expect(
38+
appCheckDetail(shell("grep -rn check-app ."), output(PASSED)),
39+
).toBeUndefined();
40+
expect(
41+
appCheckDetail(shell("cat bin/check-app"), output(PASSED)),
42+
).toBeUndefined();
43+
expect(
44+
appCheckDetail(
45+
{ id: "call", name: "read_file", arguments: { path: "check-app" } },
46+
output(PASSED),
47+
),
48+
).toBeUndefined();
49+
});

‎apps/hub/src/builds/sidecar-tools.ts‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
noteCommandProgress,
2121
} from "./posix-sandbox.ts";
2222
import { browserTools } from "./browser.ts";
23+
import type { CheckDetail } from "./check-detail.ts";
2324
import type { BrowserCatalog } from "./browser-catalog.ts";
2425
import {
2526
CHECK_APP,
@@ -149,12 +150,25 @@ const succeeded = (result: ToolResult) =>
149150
const VERSIONED = new Set([...WRITES, "run_shell", PACKAGE_INSTALL_TOOL]);
150151
// Bounds teardown: the wait for the last version, then the revoke.
151152
const VERSION_CLOSE_MS = 15_000;
153+
// check-app run as a command, not mentioned in one such as grep -rn check-app.
154+
const RUNS_CHECK_APP = /(?:^|[;&|(]\s*)check-app(?=$|[\s;&|)])/m;
155+
const ranAppCheck = (call: ToolCall) =>
156+
call.name === "run_shell" &&
157+
RUNS_CHECK_APP.test(String(call.arguments["command"]));
152158
// An app-mode check-app pass; a mockup check in the same workspace passes without building anything.
153159
const passedAppCheck = (call: ToolCall, result: ToolResult) =>
154-
call.name === "run_shell" &&
155-
/check-app/.test(String(call.arguments["command"])) &&
160+
ranAppCheck(call) &&
156161
typeof result.content === "string" &&
157162
/check passed: built, started,/.test(result.content);
163+
/** A check-app run's verdict; check-app && npm test passes only when both do. */
164+
export function appCheckDetail(
165+
call: ToolCall,
166+
result: ToolResult,
167+
): CheckDetail | undefined {
168+
if (!ranAppCheck(call)) return undefined;
169+
const passed = /^check passed:/m.test(String(result.content));
170+
return { checkPassed: succeeded(result) && passed };
171+
}
158172

159173
// A cold install; under the executor's own ceiling.
160174
const SETUP_TIMEOUT_MS = 115_000;
@@ -552,16 +566,23 @@ export function createSidecarCodingTools(policy: {
552566
await open(signal);
553567
if (!designCheck) throw new Error("design_check_missing");
554568
await syncDesignScreensOnDisk(await realpath(env.toolCwd));
555-
const { checklist } = await designCheck.current(signal);
556-
return { callId: call.id, content: checklist };
569+
const { checklist, passed, artifact } =
570+
await designCheck.current(signal);
571+
const detail: CheckDetail = {
572+
checkPassed: passed,
573+
filed: artifact !== undefined,
574+
};
575+
return { callId: call.id, content: checklist, detail };
557576
}
558577
if (!offered(call.name))
559578
return {
560579
callId: call.id,
561580
content: `unknown_tool: ${call.name}`,
562581
isError: true,
563582
};
564-
const result = await native.run(call, signal);
583+
const ran = await native.run(call, signal);
584+
const detail = appCheckDetail(call, ran);
585+
const result = detail ? { ...ran, detail } : ran;
565586
if (repository && VERSIONED.has(call.name))
566587
repository.schedule(
567588
passedAppCheck(call, result) ? CHECK_PASSED : EDIT,

‎apps/hub/src/workflows/director.test.ts‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
createDesignCheck,
2424
} from "../builds/design-check.ts";
2525
import { REPETITION_ABORT } from "../inference/repetition.ts";
26+
import { trimHistory } from "./compaction.ts";
2627
import { compactingDirector } from "./director.ts";
2728

2829
const director = (replyOnFailure: boolean) =>
@@ -280,6 +281,7 @@ test("designer director holds replies until the latest edits pass check_design a
280281
text: 'check passed: served .builder/designs\nFiled design_option: {"artifactId":"design-1","kind":"design_option","version":1,"sha256":"a"}',
281282
},
282283
],
284+
detail: { checkPassed: true, filed: true },
283285
},
284286
],
285287
timestamp: 4,
@@ -295,6 +297,115 @@ test("designer director holds replies until the latest edits pass check_design a
295297
expect(accepted.some((action) => action.type === "reply")).toBe(true);
296298
});
297299

300+
test("coder director takes a check-app verdict from the result's detail after trimming drops its text", async () => {
301+
const subject = compactingDirector("@corbits-builder/coder-test").factory(
302+
{ contextWindow: 200_000, compactAt: 0.8, checkBeforeDone: true },
303+
{} as never,
304+
{ systemPrompt: "", toolDefinitions: [], compactorNames: [] },
305+
) as ReactorDirector;
306+
const replyTurn = {
307+
role: "assistant" as const,
308+
model: "gpt-5",
309+
content: [{ type: "text" as const, text: "Done." }],
310+
timestamp: 5,
311+
};
312+
const checked = (
313+
output: string,
314+
checkPassed: boolean,
315+
): ConversationTurn[] => [
316+
{
317+
role: "user",
318+
content: [{ type: "text", text: "Build the app." }],
319+
timestamp: 0,
320+
},
321+
{
322+
role: "assistant",
323+
content: [
324+
{
325+
type: "tool_call",
326+
id: "call-write",
327+
name: "write_file",
328+
arguments: { path: "src/App.tsx", content: "export {}" },
329+
},
330+
],
331+
timestamp: 1,
332+
},
333+
{
334+
role: "user",
335+
content: [
336+
{
337+
type: "tool_result",
338+
callId: "call-write",
339+
content: [{ type: "text", text: "wrote src/App.tsx" }],
340+
},
341+
],
342+
timestamp: 2,
343+
},
344+
{
345+
role: "assistant",
346+
content: [
347+
{
348+
type: "tool_call",
349+
id: "call-check",
350+
name: "run_shell",
351+
arguments: { command: "check-app" },
352+
},
353+
],
354+
timestamp: 3,
355+
},
356+
{
357+
role: "user",
358+
content: [
359+
{
360+
type: "tool_result",
361+
callId: "call-check",
362+
content: [{ type: "text", text: output }],
363+
detail: { checkPassed },
364+
},
365+
],
366+
timestamp: 4,
367+
},
368+
];
369+
const decideTrimmed = async (output: string, checkPassed: boolean) => {
370+
const { output: turns } = await trimHistory(1).apply(
371+
checked(output, checkPassed),
372+
{ state, trigger: "context" },
373+
);
374+
const result = turns
375+
.flatMap((turn) => turn.content)
376+
.find(
377+
(block) =>
378+
block.type === "tool_result" && block.callId === "call-check",
379+
);
380+
expect(JSON.stringify(result)).not.toContain(output.slice(0, 20));
381+
const actions = await subject.decide(
382+
{
383+
type: "inference.done",
384+
turn: replyTurn,
385+
usage: state.tokenUsage,
386+
source: { sourceId: "openai", provider: "openai", model: "gpt-5" },
387+
},
388+
{ ...state, turns: [...turns, replyTurn] },
389+
createCapabilities(),
390+
);
391+
return (Array.isArray(actions) ? actions : [actions]) as ReactorAction[];
392+
};
393+
const checklist = "\n✓ every page opened with no errors".repeat(40);
394+
395+
const passed = await decideTrimmed(
396+
`check passed: built, started, and opened / at 1280 and 390 px with no errors${checklist}`,
397+
true,
398+
);
399+
expect(passed.some((action) => action.type === "reply")).toBe(true);
400+
401+
const failed = await decideTrimmed(
402+
`check failed: 1 problem(s) across 1 page(s)${checklist}`,
403+
false,
404+
);
405+
expect(failed.some((action) => action.type === "reply")).toBe(false);
406+
expect(failed.some((action) => action.type === "compact")).toBe(true);
407+
});
408+
298409
test("check_design validates DESIGN.md and .dark in styles.css and auto-files the design option on pass", async () => {
299410
const starter = designStarterFiles();
300411
const modernSleek = designThemes()["modern-sleek"]!;

‎apps/hub/src/workflows/director.ts‎

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
RetryPolicy,
1111
} from "@intx/types/runtime";
1212
import { REPETITION_ABORT } from "../inference/repetition.ts";
13-
import { CHECK_DESIGN_TOOL } from "../builds/design-check.ts";
13+
import { readCheckDetail, type CheckDetail } from "../builds/check-detail.ts";
1414
import { COMPACTOR, DROP_DEGENERATE } from "./compaction.ts";
1515

1616
// About 1 coder turn in 100 skipped reasoning and repeated one sentence ("Ok to write. Let me
@@ -144,11 +144,11 @@ const SHELL_WRITE =
144144
const WRITE_TOOLS = new Set(["write_file", "edit_file"]);
145145
// The current task's calls, newest first: everything after the last mail the agent was handed.
146146
function taskCalls(turns: readonly ConversationTurn[]) {
147-
const results = new Map<string, { text: string; isError: boolean }>();
147+
const results = new Map<string, { detail: unknown; isError: boolean }>();
148148
const calls: {
149149
name: string;
150150
command: unknown;
151-
result: string | undefined;
151+
check: CheckDetail | undefined;
152152
isError: boolean;
153153
}[] = [];
154154
for (let index = turns.length - 1; index >= 0; index--) {
@@ -161,15 +161,15 @@ function taskCalls(turns: readonly ConversationTurn[]) {
161161
for (const block of [...turn.content].reverse()) {
162162
if (block.type === "tool_result")
163163
results.set(block.callId, {
164-
text: textOf(block.content),
164+
detail: block.detail,
165165
isError: block.isError === true,
166166
});
167167
if (block.type === "tool_call") {
168168
const result = results.get(block.id);
169169
calls.push({
170170
name: block.name,
171171
command: (block.arguments as Record<string, unknown>)["command"],
172-
result: result?.text,
172+
check: readCheckDetail(result?.detail),
173173
isError: result?.isError ?? true,
174174
});
175175
}
@@ -185,18 +185,12 @@ const ranShell = (call: TaskCall, pattern: RegExp) =>
185185
typeof call.command === "string" &&
186186
pattern.test(call.command);
187187

188-
const says = (call: TaskCall, pattern: RegExp) =>
189-
call.result !== undefined && pattern.test(call.result);
190-
191188
// Run 36's coder passed check-app, kept editing, broke the app and would have replied done; a reply
192189
// counts as done only when no file changed after the last passing check. check_design is the
193190
// designer's check-app; file_design_option files only a design it passed as it stands.
194191
function unchecked(turns: readonly ConversationTurn[]): boolean {
195192
for (const call of taskCalls(turns)) {
196-
if (ranShell(call, /check-app/))
197-
return call.isError || !says(call, /check passed/);
198-
if (call.name === CHECK_DESIGN_TOOL)
199-
return call.isError || !says(call, /check passed/);
193+
if (call.check) return call.isError || !call.check.checkPassed;
200194
if (call.name === "file_design_option") return call.isError;
201195
if (WRITE_TOOLS.has(call.name) || ranShell(call, SHELL_WRITE)) return true;
202196
}
@@ -208,7 +202,7 @@ const filed = (call: TaskCall) =>
208202
(call.name === "write_artifact" ||
209203
call.name === "file_package" ||
210204
call.name === "file_design_option" ||
211-
(call.name === CHECK_DESIGN_TOOL && says(call, /Filed design_option:/)));
205+
call.check?.filed === true);
212206

213207
// A writer on 2026-10-30 replied with the whole approaches document as text and never filed it;
214208
// the coordinator waited on a writer that had already answered. A designer's reply also waits for a

0 commit comments

Comments
 (0)