Skip to content

Commit eef4131

Browse files
committed
perf(webapp): keep report blocks out of the investigation winner pass
1 parent 774d6b1 commit eef4131

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function viewSpecFor(part: UIMessage["parts"][number]): { blocks: unknown[] } |
5050
return Array.isArray(p.output?.blocks) ? { blocks: p.output!.blocks! } : null;
5151
}
5252

53-
function blocksFor(part: UIMessage["parts"][number]): unknown[] | null {
53+
export function blocksFor(part: UIMessage["parts"][number]): unknown[] | null {
5454
const spec = viewSpecFor(part);
5555
if (spec) return spec.blocks;
5656
const hostBlocks = hostViewBlocks(part);
@@ -65,6 +65,15 @@ function hostViewBlocks(part: UIMessage["parts"][number]): unknown[] | null {
6565
return Array.isArray(p.data?.blocks) ? p.data!.blocks! : null;
6666
}
6767

68+
// `blocksFor` minus the report branch, which the winner pass would only throw away:
69+
// a report block is always `type: "report"`, so it can never be an investigation.
70+
// Reports are parsed by the turn that renders them, not once per streamed token.
71+
function investigationBlocksFor(part: UIMessage["parts"][number]): unknown[] | null {
72+
const spec = viewSpecFor(part);
73+
if (spec) return spec.blocks;
74+
return hostViewBlocks(part);
75+
}
76+
6877
type InvestigationRef = { id: string; revision: number };
6978

7079
function investigationRef(block: unknown): InvestigationRef | null {
@@ -81,7 +90,7 @@ export function winningInvestigationOccurrences(messages: UIMessage[]): Map<stri
8190
const best = new Map<string, { revision: number; occurrence: string }>();
8291
for (const message of messages.map(stripStepParts)) {
8392
(message.parts ?? []).forEach((part, partIndex) => {
84-
for (const block of blocksFor(part) ?? []) {
93+
for (const block of investigationBlocksFor(part) ?? []) {
8594
const ref = investigationRef(block);
8695
if (!ref) continue;
8796
const current = best.get(ref.id);

apps/webapp/app/components/dashboard-agent/investigation-winners.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { readFileSync } from "node:fs";
2+
import type { UIMessage } from "@ai-sdk/react";
23
import { describe, expect, it } from "vitest";
4+
import { blocksFor, winningInvestigationOccurrences } from "./DashboardAgentMessages";
35
import { reuseWinners, sameOccurrences } from "./investigation-winners";
46

57
const source = readFileSync(new URL("./DashboardAgentMessages.tsx", import.meta.url), "utf8");
@@ -47,3 +49,87 @@ describe("investigation winners identity", () => {
4749
expect(source).not.toMatch(/=\s*winningInvestigationOccurrences\(stripped\)/);
4850
});
4951
});
52+
53+
/**
54+
* The winner pass runs once per streamed token over the whole transcript, so it must
55+
* not touch report payloads. `output` is a counting getter because a report parse is
56+
* otherwise silent: it returns `null` on a bad payload rather than throwing.
57+
*/
58+
function countingReportPart(vm: unknown) {
59+
let reads = 0;
60+
const part = {
61+
type: "tool-get_report",
62+
state: "output-available",
63+
toolCallId: "toolcall_1",
64+
get output() {
65+
reads++;
66+
return { vm };
67+
},
68+
};
69+
return { part: part as unknown as UIMessage["parts"][number], reads: () => reads };
70+
}
71+
72+
const VALID_VM = {
73+
title: "health",
74+
scope: "prod",
75+
period: "last 1h",
76+
generatedAt: "2026-07-27T10:15:00.000Z",
77+
windowMinutes: 60,
78+
summary: { severity: "ok", statements: [] },
79+
};
80+
81+
describe("the winner pass does not parse report blocks", () => {
82+
it("leaves a report part's payload untouched", () => {
83+
const valid = countingReportPart(VALID_VM);
84+
// Would fail `reportBlockSchema`: no `generatedAt`, no `windowMinutes`.
85+
const invalid = countingReportPart({ title: "health" });
86+
87+
const messages = [
88+
{ id: "m1", role: "assistant", parts: [valid.part, invalid.part] },
89+
] as unknown as UIMessage[];
90+
91+
let winners: Map<string, string> | undefined;
92+
expect(() => (winners = winningInvestigationOccurrences(messages))).not.toThrow();
93+
94+
expect(winners!.size).toBe(0);
95+
expect(valid.reads()).toBe(0);
96+
expect(invalid.reads()).toBe(0);
97+
});
98+
99+
it("still parses the same part when the turn renders it", () => {
100+
const valid = countingReportPart(VALID_VM);
101+
const blocks = blocksFor(valid.part);
102+
103+
expect(valid.reads()).toBeGreaterThan(0);
104+
expect(blocks).toHaveLength(1);
105+
expect((blocks![0] as { type: string }).type).toBe("report");
106+
});
107+
108+
it("still finds investigation winners emitted by the view tools", () => {
109+
const messages = [
110+
{
111+
id: "m1",
112+
role: "assistant",
113+
parts: [
114+
countingReportPart(VALID_VM).part,
115+
{
116+
type: "tool-render_view",
117+
output: { blocks: [{ type: "investigation", id: "inv_1", revision: 0 }] },
118+
},
119+
],
120+
},
121+
{
122+
id: "m2",
123+
role: "assistant",
124+
parts: [
125+
{
126+
type: "data-view",
127+
data: { blocks: [{ type: "investigation", id: "inv_1", revision: 1 }] },
128+
},
129+
],
130+
},
131+
] as unknown as UIMessage[];
132+
133+
expect(winningInvestigationOccurrences(messages)).toEqual(new Map([["inv_1", "m2:0"]]));
134+
});
135+
});

0 commit comments

Comments
 (0)