Merge #1284: declare the labs availability metadata in get_labs

The tool module was split into one file per tool on this branch, so the
change lands in tools/get-labs.ts, and the advertised tool list snapshot
gains the two declared fields.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 12:32:41 +01:00
commit a895f16e08
3 files changed
+117 -5

No files matched your search

@@ -411,6 +411,74 @@
"grounding": {
"type": "string"
},
"searchedWindow": {
"type": "string"
},
"available": {
"type": "object",
"properties": {
"count": {
"type": "number"
},
"firstDate": {
"type": "string"
},
"lastDate": {
"type": "string"
},
"series": {
"type": "array",
"items": {
"type": "object",
"properties": {
"series": {
"type": "string"
},
"unit": {
"type": [
"string",
"null"
]
},
"count": {
"type": "number"
},
"mean": {
"type": "number"
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
},
"required": [
"series",
"unit",
"count",
"mean",
"min",
"max"
],
"additionalProperties": false
}
},
"reachableWithWindow": {
"type": [
"string",
"null"
]
}
},
"required": [
"count",
"firstDate",
"lastDate",
"reachableWithWindow"
],
"additionalProperties": false
},
"analyte": {
"type": "string"
},
+46
View File
@@ -510,6 +510,52 @@ describe("get_illness_recovery — condition body site", () => {
});
describe("get_labs", () => {
it.each([{}, { analyte: "LDL" }])(
"declares empty-read availability for ordinary and filtered reads: %j",
async (args) => {
const { z } = await import("zod/v4");
const empty = {
present: false,
reason: "out_of_window",
searchedWindow: "lastYear",
available: {
count: 2,
firstDate: "2020-01-01",
lastDate: "2020-01-02",
reachableWithWindow: null,
},
};
vi.mocked(executeCoachTool).mockResolvedValue(empty);
const def = tool("get_labs");
const result = await def.run(CTX, args);
expect(executeCoachTool).toHaveBeenCalledWith({
userId: CTX.userId,
name: "get_labs",
rawArguments: JSON.stringify(args),
});
expect(z.object(def.outputShape!).strict().parse(result)).toEqual(empty);
},
);
it("keeps output validation closed and availability typed", async () => {
const { z } = await import("zod/v4");
const schema = z.object(tool("get_labs").outputShape!).strict();
expect(z.toJSONSchema(schema).additionalProperties).toBe(false);
expect(schema.safeParse({ present: false, unexpected: true }).success).toBe(
false,
);
expect(
schema.safeParse({ present: false, searchedWindow: 7 }).success,
).toBe(false);
expect(
schema.safeParse({ present: false, available: { count: "2" } }).success,
).toBe(false);
expect(schema.parse({ present: false, reason: "no_data" })).toEqual({
present: false,
reason: "no_data",
});
});
it("forwards the optional analyte filter and returns readings with units + bands", async () => {
vi.mocked(executeCoachTool).mockResolvedValue({
present: true,
+3 -5
View File
@@ -4,16 +4,14 @@ import { resolveMarkerForRead } from "@/lib/labs/biomarker-resolve";
import { getLabHistory, LAB_HISTORY_MAX_LIMIT } from "@/lib/mcp/rich-reads";
import { decodeOffsetCursor } from "@/lib/mcp/pagination";
import type { McpAuthContext } from "../auth";
import { coachReadOutput } from "./output-shapes";
import { runCoachTool, whenModuleOn } from "./shared";
import { READ_ONLY_ANNOTATIONS, type McpToolDefinition } from "./types";
/** Output schema for `get_labs` — latest-per-biomarker OR a paginated history. */
const getLabsOutput: z.ZodRawShape = {
present: z.boolean(),
reason: z.string().optional(),
// Latest-mode payload (the Coach labs section, carried opaque).
data: z.unknown().optional(),
grounding: z.string().optional(),
// Latest-mode payload, including typed empty-read availability metadata.
...coachReadOutput,
// History-mode payload (per-analyte trajectory + pagination).
analyte: z.string().optional(),
readings: z