Skip to content

Commit fc958e3

Browse files
committed
feat: audit context isolation with concern field and toolContext suppression
- Add optional concern field to buildGroupAuditIntent type and rendering - Wire concern through executeGroupedPingPong items.map - Suppress toolContext in audit intent and cycleHistory buildOutputs - getFlowSummaryText gains {toolContext?: boolean} option (default true)
1 parent 1ccb457 commit fc958e3

4 files changed

Lines changed: 175 additions & 26 deletions

File tree

src/flow/audit-formatters.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function formatPriorBuildOutputs(entries: CycleHistoryEntry[]): string {
117117
}
118118

119119
export function buildGroupAuditIntent(
120-
builds: Array<{ aim: string; intent: string; acceptance?: string; output: string }>,
120+
builds: Array<{ aim: string; intent: string; acceptance?: string; concern?: string; output: string }>,
121121
cycleHistory?: CycleHistoryEntry[],
122122
): string {
123123
const sections = builds.map((b, i) => {
@@ -131,6 +131,9 @@ export function buildGroupAuditIntent(
131131
if (b.acceptance) {
132132
section.push(`## Acceptance Criteria`, b.acceptance, ``);
133133
}
134+
if (b.concern) {
135+
section.push(`## Concerns`, b.concern, ``);
136+
}
134137
if (b.intent) {
135138
section.push(`## Build Intent`, b.intent, ``);
136139
}

src/flow/executor.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ async function executeGroupedPingPong(
243243
aim: item.aim,
244244
intent: item.intent,
245245
acceptance: item.acceptance,
246-
output: getFlowSummaryText(buildResults[i]),
246+
concern: item.concern,
247+
output: getFlowSummaryText(buildResults[i], { toolContext: false }),
247248
})),
248249
cycleHistory,
249250
);
@@ -296,7 +297,7 @@ async function executeGroupedPingPong(
296297

297298
cycleHistory.push({
298299
cycle,
299-
buildOutputs: buildResults.map((r) => getFlowSummaryText(r)),
300+
buildOutputs: buildResults.map((r) => getFlowSummaryText(r, { toolContext: false })),
300301
verdict: anyReworkNeeded ? "rework" : "pass",
301302
feedback: anyReworkNeeded ? topLevelFeedback : undefined,
302303
buildFeedbacks: [...auditFeedbacks],

src/snapshot/runner-events.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,11 @@ function matchToolCallsWithResults(messages: Message[], maxPairs: number): ToolP
807807
/** Max tool result output chars to include per tool call in the summary. */
808808
const TOOL_RESULT_MAX_CHARS = 2000;
809809

810-
export function getFlowSummaryText(result?: FlowResult | null): string {
810+
export function getFlowSummaryText(
811+
result?: FlowResult | null,
812+
options?: { toolContext?: boolean },
813+
): string {
814+
const includeToolContext = options?.toolContext !== false;
811815
const finalText = getFlowFinalText(result?.messages ?? []);
812816
const isError =
813817
(typeof result?.exitCode === "number" && result.exitCode > 0) ||
@@ -827,24 +831,27 @@ export function getFlowSummaryText(result?: FlowResult | null): string {
827831
}
828832

829833
// Extract tool call/result pairs for context
830-
const toolPairs = matchToolCallsWithResults(result?.messages ?? [], 10);
831-
const toolSummaryParts: string[] = [];
832-
833-
for (const pair of toolPairs) {
834-
const callLabel = formatToolCallShort({ name: pair.name, args: pair.args });
835-
if (pair.output.trim()) {
836-
const truncated = pair.output.length > TOOL_RESULT_MAX_CHARS
837-
? pair.output.slice(0, TOOL_RESULT_MAX_CHARS) + "\n... (truncated)"
838-
: pair.output;
839-
toolSummaryParts.push(`${callLabel}:\n${truncated}`);
840-
} else {
841-
toolSummaryParts.push(`${callLabel}: (no output)`);
834+
let toolContext = "";
835+
if (includeToolContext) {
836+
const toolPairs = matchToolCallsWithResults(result?.messages ?? [], 10);
837+
const toolSummaryParts: string[] = [];
838+
839+
for (const pair of toolPairs) {
840+
const callLabel = formatToolCallShort({ name: pair.name, args: pair.args });
841+
if (pair.output.trim()) {
842+
const truncated = pair.output.length > TOOL_RESULT_MAX_CHARS
843+
? pair.output.slice(0, TOOL_RESULT_MAX_CHARS) + "\n... (truncated)"
844+
: pair.output;
845+
toolSummaryParts.push(`${callLabel}:\n${truncated}`);
846+
} else {
847+
toolSummaryParts.push(`${callLabel}: (no output)`);
848+
}
842849
}
843-
}
844850

845-
const toolContext = toolSummaryParts.length > 0
846-
? "\n\n[Tool Results]\n" + toolSummaryParts.join("\n---\n")
847-
: "";
851+
toolContext = toolSummaryParts.length > 0
852+
? "\n\n[Tool Results]\n" + toolSummaryParts.join("\n---\n")
853+
: "";
854+
}
848855

849856
// Append ping-pong cycle metadata if present
850857
const singleResult = result as (FlowResult & Partial<SingleResult>) | null | undefined;
@@ -876,17 +883,19 @@ export function getFlowSummaryText(result?: FlowResult | null): string {
876883

877884
// No final text
878885
if (isError) {
879-
// Surface partial tool calls (excluding read) for failed/aborted flows
880-
const toolCalls = extractNonReadToolCalls(result?.messages ?? []);
881-
if (toolCalls.length > 0) {
882-
const formatted = toolCalls.map(formatToolCallShort).join(", ");
883-
return `${errorBase}\nPartial work: ${formatted}${toolContext}`;
886+
if (includeToolContext) {
887+
// Surface partial tool calls (excluding read) for failed/aborted flows
888+
const toolCalls = extractNonReadToolCalls(result?.messages ?? []);
889+
if (toolCalls.length > 0) {
890+
const formatted = toolCalls.map(formatToolCallShort).join(", ");
891+
return `${errorBase}\nPartial work: ${formatted}${toolContext}`;
892+
}
884893
}
885894
return errorBase;
886895
}
887896

888897
// Success but no final text — show tool results if any
889-
if (toolContext) {
898+
if (includeToolContext && toolContext) {
890899
return toolContext.trim() + pingPongNote;
891900
}
892901

tests/runner-events.test.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,3 +1406,139 @@ describe("formatToolCallShort — empty path", () => {
14061406
expect(summary).not.toContain("batch read ?");
14071407
});
14081408
});
1409+
1410+
// ---------------------------------------------------------------------------
1411+
// getFlowSummaryText — toolContext: false option
1412+
// ---------------------------------------------------------------------------
1413+
1414+
describe("getFlowSummaryText — toolContext: false", () => {
1415+
it("skips [Tool Results] when final text exists", () => {
1416+
const result = {
1417+
exitCode: 0,
1418+
messages: [
1419+
{
1420+
role: "assistant",
1421+
content: [
1422+
{ type: "toolCall", name: "bash", toolCallId: "tc1", arguments: { command: "echo hello" } },
1423+
],
1424+
},
1425+
{
1426+
role: "toolResult",
1427+
toolCallId: "tc1",
1428+
content: [{ type: "text", text: "hello" }],
1429+
},
1430+
{ role: "assistant", content: [{ type: "text", text: "All done." }] },
1431+
],
1432+
};
1433+
const summary = getFlowSummaryText(result, { toolContext: false });
1434+
expect(summary).toBe("All done.");
1435+
expect(summary).not.toContain("[Tool Results]");
1436+
});
1437+
1438+
it("skips Partial work on error", () => {
1439+
const result = {
1440+
messages: [
1441+
{
1442+
role: "assistant",
1443+
content: [
1444+
{ type: "toolCall", name: "edit", toolCallId: "tc1", arguments: { path: "foo.ts", oldText: "a", newText: "b" } },
1445+
],
1446+
},
1447+
],
1448+
exitCode: 1,
1449+
stopReason: "error",
1450+
errorMessage: "Build failed",
1451+
};
1452+
const summary = getFlowSummaryText(result, { toolContext: false });
1453+
expect(summary).toBe("Build failed");
1454+
expect(summary).not.toContain("Partial work");
1455+
});
1456+
1457+
it("returns (no output) for success without final text", () => {
1458+
const result = {
1459+
exitCode: 0,
1460+
messages: [
1461+
{
1462+
role: "assistant",
1463+
content: [
1464+
{ type: "toolCall", name: "bash", toolCallId: "tc1", arguments: { command: "echo hello" } },
1465+
],
1466+
},
1467+
{
1468+
role: "toolResult",
1469+
toolCallId: "tc1",
1470+
content: [{ type: "text", text: "hello" }],
1471+
},
1472+
],
1473+
};
1474+
const summary = getFlowSummaryText(result, { toolContext: false });
1475+
expect(summary).toBe("(no output)");
1476+
});
1477+
1478+
it("returns (no output) for null/undefined", () => {
1479+
expect(getFlowSummaryText(null, { toolContext: false })).toBe("(no output)");
1480+
expect(getFlowSummaryText(undefined, { toolContext: false })).toBe("(no output)");
1481+
});
1482+
1483+
it("preserves default behavior when options is undefined", () => {
1484+
const result = {
1485+
exitCode: 0,
1486+
messages: [
1487+
{
1488+
role: "assistant",
1489+
content: [
1490+
{ type: "toolCall", name: "bash", toolCallId: "tc1", arguments: { command: "echo hello" } },
1491+
],
1492+
},
1493+
{
1494+
role: "toolResult",
1495+
toolCallId: "tc1",
1496+
content: [{ type: "text", text: "hello" }],
1497+
},
1498+
{ role: "assistant", content: [{ type: "text", text: "All done." }] },
1499+
],
1500+
};
1501+
const summary = getFlowSummaryText(result);
1502+
expect(summary).toContain("All done.");
1503+
expect(summary).toContain("[Tool Results]");
1504+
expect(summary).toContain("bash echo hello:");
1505+
});
1506+
1507+
it("preserves default behavior when toolContext is true", () => {
1508+
const result = {
1509+
exitCode: 0,
1510+
messages: [
1511+
{
1512+
role: "assistant",
1513+
content: [
1514+
{ type: "toolCall", name: "bash", toolCallId: "tc1", arguments: { command: "echo hello" } },
1515+
],
1516+
},
1517+
{
1518+
role: "toolResult",
1519+
toolCallId: "tc1",
1520+
content: [{ type: "text", text: "hello" }],
1521+
},
1522+
{ role: "assistant", content: [{ type: "text", text: "All done." }] },
1523+
],
1524+
};
1525+
const summary = getFlowSummaryText(result, { toolContext: true });
1526+
expect(summary).toContain("All done.");
1527+
expect(summary).toContain("[Tool Results]");
1528+
expect(summary).toContain("bash echo hello:");
1529+
});
1530+
1531+
it("preserves pingPongNote even with toolContext: false", () => {
1532+
const result = {
1533+
exitCode: 0,
1534+
messages: [],
1535+
pingPongMeta: {
1536+
cycles: 2,
1537+
finalVerdict: "pass",
1538+
verdicts: [{ cycle: 0, verdict: "pass" }],
1539+
},
1540+
};
1541+
const summary = getFlowSummaryText(result, { toolContext: false });
1542+
expect(summary).toContain("[Audit Loop: 2 cycle(s), final verdict: pass]");
1543+
});
1544+
});

0 commit comments

Comments
 (0)