Skip to content

Commit 643ea39

Browse files
committed
fix(dashboard-agent): read the report's untrustworthy reason under its current name
`curateReport` still read `facts.staleReason`, renamed to `untrustworthyReason` in dc3b502 and split into telemetry_stale / telemetry_absent / flow_unmeasured. The read had been undefined since, so the agent got "untrustworthy" with no why, and the prompt still told it every such case was stale telemetry. `facts` is `z.record(z.unknown())`, so nothing typechecked the key. The new test goes presenter -> reports route JSON -> curateReport without naming a facts key on the way in, and asserts curation carries every key the presenter emits.
1 parent ad2698b commit 643ea39

5 files changed

Lines changed: 94 additions & 12 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { curateReport } from "@internal/dashboard-agent/tool-curation";
2+
import { describe, expect, it } from "vitest";
3+
import { interpret, type HealthInput } from "~/presenters/v3/reports/health/health";
4+
import { reportResponse } from "~/presenters/v3/reports/reportsApi.server";
5+
6+
/**
7+
* The real chain the agent's `get_report` runs: the health presenter builds the view model, the
8+
* reports route serializes it as `format=json`, and the agent curates that body. Nothing here
9+
* names a `facts` key on the way in, so a producer/consumer rename fails instead of passing.
10+
*/
11+
async function curatedFacts(input: HealthInput): Promise<Record<string, unknown>> {
12+
const body = await reportResponse(interpret(input), "json").json();
13+
return curateReport(body).facts as Record<string, unknown>;
14+
}
15+
16+
const HEALTHY: HealthInput = {
17+
scope: "prod",
18+
period: "last 1h",
19+
baselineLabel: "vs your 7d normal",
20+
generatedAt: "2026-07-20T12:00:00.000Z",
21+
windowMinutes: 60,
22+
flowSource: "queue_metrics_v1",
23+
pending: { now: 84, normal: 120, series: [110, 96, 88, 90, 84], estimated: false },
24+
startLatency: { p95Ms: 6000, normalP95Ms: 7000, series: [6500, 6200, 6000, 5900, 6000] },
25+
throughput: {
26+
finishedPerMin: 1000,
27+
completedPerMin: 1000,
28+
triggeredPerMin: 1000,
29+
normalTriggeredPerMin: 1000,
30+
},
31+
failures: { rate: 0.009, normalRate: 0.011, series: [0.01, 0.009, 0.009] },
32+
duration: { p95Ms: 1100, normalP95Ms: 1180 },
33+
liveness: { telemetryAgeMs: 2000 },
34+
flowEvidence: {
35+
runningSeries: [40, 45, 50, 48, 44],
36+
envLimit: 100,
37+
throttledShare: 0,
38+
worstQueue: null,
39+
dlqDelta: 0,
40+
},
41+
};
42+
43+
describe("the agent's curated report keeps the reason its numbers can't be trusted", () => {
44+
it("carries telemetry_stale", async () => {
45+
const facts = await curatedFacts({ ...HEALTHY, liveness: { telemetryAgeMs: 30 * 60_000 } });
46+
expect(facts.trustworthy).toBe(false);
47+
expect(facts.untrustworthyReason).toBe("telemetry_stale");
48+
});
49+
50+
it("carries telemetry_absent", async () => {
51+
const facts = await curatedFacts({ ...HEALTHY, liveness: { telemetryAgeMs: null } });
52+
expect(facts.trustworthy).toBe(false);
53+
expect(facts.untrustworthyReason).toBe("telemetry_absent");
54+
});
55+
56+
it("carries flow_unmeasured", async () => {
57+
const facts = await curatedFacts({
58+
...HEALTHY,
59+
pending: { now: 0, series: [], estimated: true, availability: "unknown" },
60+
});
61+
expect(facts.trustworthy).toBe(false);
62+
expect(facts.untrustworthyReason).toBe("flow_unmeasured");
63+
});
64+
65+
it("states no reason on a trustworthy report", async () => {
66+
const facts = await curatedFacts(HEALTHY);
67+
expect(facts.trustworthy).toBe(true);
68+
expect(facts.untrustworthyReason).toBeUndefined();
69+
});
70+
71+
it("carries every fact key the presenter emits, so the next rename fails here", async () => {
72+
const emitted = interpret(HEALTHY).facts;
73+
const curated = await curatedFacts(HEALTHY);
74+
// `telemetry` is deliberately dropped: `trustworthy` + the reason already say what the agent acts on.
75+
const dropped = new Set(["telemetry"]);
76+
for (const key of Object.keys(emitted)) {
77+
if (dropped.has(key)) continue;
78+
expect(Object.keys(curated)).toContain(key);
79+
}
80+
});
81+
});

internal-packages/dashboard-agent/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"type": "module",
88
"exports": {
99
".": "./src/index.ts",
10+
"./tool-curation": "./src/tool-curation.ts",
1011
"./tool-schemas": "./src/tool-schemas.ts",
1112
"./prompt-prefix": "./src/prompt-prefix.ts"
1213
},

internal-packages/dashboard-agent/src/__snapshots__/prompt-prefix.test.ts.snap

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,34 @@ exports[`the prefix stays inside its budget > matches the committed measurement
44
{
55
"assistant": {
66
"prompt": {
7-
"chars": 19266,
8-
"estimatedTokens": 4817,
7+
"chars": 19285,
8+
"estimatedTokens": 4821,
99
},
1010
"tools": {
1111
"chars": 38555,
1212
"count": 20,
1313
"estimatedTokens": 9639,
1414
},
1515
"total": {
16-
"chars": 57822,
17-
"estimatedTokens": 14456,
18-
"fingerprint": "f6751f8f",
16+
"chars": 57841,
17+
"estimatedTokens": 14460,
18+
"fingerprint": "52bee164",
1919
},
2020
},
2121
"code": {
2222
"prompt": {
23-
"chars": 22021,
24-
"estimatedTokens": 5505,
23+
"chars": 22040,
24+
"estimatedTokens": 5510,
2525
},
2626
"tools": {
2727
"chars": 41564,
2828
"count": 24,
2929
"estimatedTokens": 10391,
3030
},
3131
"total": {
32-
"chars": 63586,
33-
"estimatedTokens": 15897,
34-
"fingerprint": "6432d8b0",
32+
"chars": 63605,
33+
"estimatedTokens": 15901,
34+
"fingerprint": "037bcfc3",
3535
},
3636
},
3737
}

internal-packages/dashboard-agent/src/tool-curation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export function curateReport(data: unknown) {
189189
})),
190190
facts: {
191191
trustworthy: facts.trustworthy,
192-
staleReason: facts.staleReason,
192+
untrustworthyReason: facts.untrustworthyReason,
193193
flowSource: facts.flowSource,
194194
pendingEstimated: facts.pendingEstimated,
195195
throughput: facts.throughput,

internal-packages/dashboard-agent/src/tool-schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ Knowing where the user is, and taking them places:
458458
459459
Is anything wrong?:
460460
- For "is anything wrong", "how is prod doing", "is everything healthy", start with get_report. It grades flow, execution, and liveness together, which is a better first answer than any single query.
461-
- If the report's facts.trustworthy is false, the underlying telemetry is stale: say the data can't be trusted right now and what would confirm it. Do NOT diagnose a cause or recommend an action off untrusted numbers.
461+
- If the report's facts.trustworthy is false, say why from facts.untrustworthyReason (telemetry_stale, telemetry_absent or flow_unmeasured) and what would confirm it. Do NOT diagnose a cause or recommend an action off untrusted numbers.
462462
- When the report points at flow (runs not starting), follow up with get_queue on the queue it names to see depth, wait time, and throttling. When it points at execution, follow up with list_errors / get_run_trace.
463463
- When something started failing at a particular time, check list_deploys for a deploy in that window, and correlate_version on a failing run to see the exact commit and pull request it ran.
464464

0 commit comments

Comments
 (0)