From 7e91596f0210b19c3f67f28ca75dd048de6b504f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 27 Sep 2026 13:26:16 +0200 Subject: [PATCH] perf(coach): send the table rules only when there is a table The recheck and follow-up rules now ride the tool-mode prompt only once the conversation holds a table, or from the round after the turn produced its first one. The clarifying-question rules are shorter and English for every locale, like the other instructions to the model; the German copy of them and of the recheck rule is gone. A turn without tables sends about 1 300 characters less on every round. The golden transcripts change only in the system prompt's hash and length (4 509 to 3 187 characters; 4 728 to 3 280 for the German turn). --- .../golden-transcripts.test.ts.snap | 54 +++++++++---------- .../__tests__/loop-result-tables.test.ts | 39 ++++++++++++++ src/lib/ai/coach/tools/loop.ts | 13 ++++- .../turn/addenda/__tests__/clarify.test.ts | 8 ++- .../turn/addenda/__tests__/method.test.ts | 14 +++++ src/lib/ai/coach/turn/addenda/clarify.ts | 33 ++++-------- src/lib/ai/coach/turn/addenda/index.ts | 16 ++++-- src/lib/ai/coach/turn/addenda/method.ts | 16 +++--- src/lib/ai/coach/turn/model.ts | 44 ++++++++------- 9 files changed, 155 insertions(+), 82 deletions(-) diff --git a/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap b/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap index 629b82207..66e4f6e98 100644 --- a/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap +++ b/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap @@ -50,7 +50,7 @@ exports[`coach chat golden transcripts > 01a tool mode, two rounds, present data "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -251,7 +251,7 @@ exports[`coach chat golden transcripts > 01b tool mode, present data, one figure "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -464,7 +464,7 @@ exports[`coach chat golden transcripts > 02 tool mode, every tool missed, fabric "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -1162,7 +1162,7 @@ exports[`coach chat golden transcripts > 07a all providers failed (plain) 1`] = "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -1251,7 +1251,7 @@ exports[`coach chat golden transcripts > 07b all providers failed (every hop rat "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -1340,7 +1340,7 @@ exports[`coach chat golden transcripts > 07c all providers failed (primary crede "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -1429,7 +1429,7 @@ exports[`coach chat golden transcripts > 08 an unwrapped tagged provider error 1 "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -1519,7 +1519,7 @@ exports[`coach chat golden transcripts > 09 an aborted request leaves a cancelle "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -1615,7 +1615,7 @@ exports[`coach chat golden transcripts > 10 outbound block replaces the reply an "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -1946,7 +1946,7 @@ exports[`coach chat golden transcripts > 11a KEYVALUES valid 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -2151,7 +2151,7 @@ exports[`coach chat golden transcripts > 11b KEYVALUES partial 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -2354,7 +2354,7 @@ exports[`coach chat golden transcripts > 11c KEYVALUES malformed (no closing mar "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -2541,7 +2541,7 @@ exports[`coach chat golden transcripts > 11d sentinel-only reply is an empty rep "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -2630,7 +2630,7 @@ exports[`coach chat golden transcripts > 11e empty provider reply 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -2710,7 +2710,7 @@ exports[`coach chat golden transcripts > 12a SUGGEST-REMINDER surfaced 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -2935,7 +2935,7 @@ exports[`coach chat golden transcripts > 12b SUGGEST-REMINDER suppressed 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -3124,7 +3124,7 @@ exports[`coach chat golden transcripts > 13 SUGGEST-ACTION 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -3353,7 +3353,7 @@ exports[`coach chat golden transcripts > 14a REMEMBER valid 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -3562,7 +3562,7 @@ exports[`coach chat golden transcripts > 14b REMEMBER malformed 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -3766,7 +3766,7 @@ exports[`coach chat golden transcripts > 15 an unknown learn link is scrubbed, a "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -4298,7 +4298,7 @@ exports[`coach chat golden transcripts > 17a a workout-scoped first turn 1`] = ` "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -4508,7 +4508,7 @@ exports[`coach chat golden transcripts > 17b a workout-scoped first turn with th "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -4697,7 +4697,7 @@ exports[`coach chat golden transcripts > 18 a persistence throw after the provid "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -4790,7 +4790,7 @@ exports[`coach chat golden transcripts > 19 an existing conversation with prior "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -5003,7 +5003,7 @@ exports[`coach chat golden transcripts > 20 a guided-question turn with a scope "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 30000, "tools": 2, @@ -5172,7 +5172,7 @@ exports[`coach chat golden transcripts > 21 the German locale with a stored self "window": "last30days", }, "signal": true, - "system": "e297a082792b:4728", + "system": "c61eecc94d49:3280", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, @@ -5377,7 +5377,7 @@ exports[`coach chat golden transcripts > 22 tool mode with live steps: found, em "window": "last30days", }, "signal": true, - "system": "ca96f6408388:4509", + "system": "4a4d00f5a344:3187", "temperature": 0.4, "timeoutMs": 180000, "tools": 2, diff --git a/src/lib/ai/coach/results/__tests__/loop-result-tables.test.ts b/src/lib/ai/coach/results/__tests__/loop-result-tables.test.ts index 09c05328e..3b4275694 100644 --- a/src/lib/ai/coach/results/__tests__/loop-result-tables.test.ts +++ b/src/lib/ai/coach/results/__tests__/loop-result-tables.test.ts @@ -96,4 +96,43 @@ describe("runCoachToolLoop — result tables", () => { expect(toolTurn?.content).not.toContain("987654"); expect(JSON.stringify(out.toolResults)).not.toContain("987654"); }); + + it("adds the table rules to the rounds after the first table, not before", async () => { + executeCoachTool + .mockResolvedValueOnce({ present: true, data: { n: 1 } }) + .mockResolvedValueOnce({ + present: true, + resultRef: "r1", + data: { periods: 1 }, + table: TABLE, + }); + const call = (id: string, name: string) => ({ + id, + name, + arguments: '{"metric":"pulse"}', + }); + runRawCompletionWithFallback + .mockResolvedValueOnce(round("", [call("a", "get_metric_series")])) + .mockResolvedValueOnce(round("", [call("b", "get_metric_table")])) + .mockResolvedValueOnce(round("answer")); + + await runCoachToolLoop({ + userId: "u1", + providers: [], + system: "sys", + systemOnceTableShown: "sys+tables", + messages: [{ role: "user", content: "q" }], + tools: COACH_TOOL_DEFS, + turn: { + conversationId: "c1", + locale: "en", + priorResults: [], + refs: createResultRefAllocator(), + }, + }); + const systems = runRawCompletionWithFallback.mock.calls.map( + ([arg]) => (arg as { params: { system: string } }).params.system, + ); + expect(systems).toEqual(["sys", "sys", "sys+tables"]); + }); }); diff --git a/src/lib/ai/coach/tools/loop.ts b/src/lib/ai/coach/tools/loop.ts index 7fe45adcf..dcf4f4900 100644 --- a/src/lib/ai/coach/tools/loop.ts +++ b/src/lib/ai/coach/tools/loop.ts @@ -106,6 +106,12 @@ export async function runCoachToolLoop(args: { userId: string; providers: ProviderChainResolved[]; system: string; + /** + * v1.39.4 — the system prompt for the rounds after a call produced a + * table, when it differs (the table rules ride it). Absent: `system` + * throughout. + */ + systemOnceTableShown?: string; /** The conversation messages (history + the new user turn). */ messages: AiMessage[]; tools: AiToolDef[]; @@ -156,7 +162,6 @@ export async function runCoachToolLoop(args: { const { userId, providers, - system, tools, temperature, maxTokens, @@ -171,6 +176,7 @@ export async function runCoachToolLoop(args: { } = args; const messages: AiMessage[] = [...args.messages]; + let system = args.system; let totalTokens = 0; let cachedTokens = 0; let rounds = 0; @@ -261,7 +267,10 @@ export async function runCoachToolLoop(args: { // does. It reads the compact summary in `data`, and the verifier // grades the prose against that same summary. notify(() => onCallSettled?.(call, settled, index)); - const { table: _table, ...toolResult } = settled; + const { table, ...toolResult } = settled; + if (table && args.systemOnceTableShown) { + system = args.systemOnceTableShown; + } const validArgs = parseCoachToolArgs(call.name, call.arguments); toolTrace.push({ name: call.name, diff --git a/src/lib/ai/coach/turn/addenda/__tests__/clarify.test.ts b/src/lib/ai/coach/turn/addenda/__tests__/clarify.test.ts index e3f22a377..585e8cd59 100644 --- a/src/lib/ai/coach/turn/addenda/__tests__/clarify.test.ts +++ b/src/lib/ai/coach/turn/addenda/__tests__/clarify.test.ts @@ -19,8 +19,14 @@ describe("clarifyAddendum", () => { } }); - it("uses English for the other locales", () => { + it("uses English for every locale, like the other addenda", () => { expect(clarifyAddendum("fr")).toBe(clarifyAddendum("en")); + expect(clarifyAddendum("de")).toBe(clarifyAddendum("en")); + }); + + it("stays short: it rides every tool-mode round", () => { + // About 250 tokens at four characters a token. + expect(clarifyAddendum("en").length).toBeLessThan(1_000); }); it("teaches a block the parser accepts", () => { diff --git a/src/lib/ai/coach/turn/addenda/__tests__/method.test.ts b/src/lib/ai/coach/turn/addenda/__tests__/method.test.ts index 3d8861f9a..2aa31da22 100644 --- a/src/lib/ai/coach/turn/addenda/__tests__/method.test.ts +++ b/src/lib/ai/coach/turn/addenda/__tests__/method.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { methodAddendum } from "../method"; +import { buildDialogAddenda } from ".."; describe("methodAddendum", () => { it.each(["en", "de"] as const)("carries the recheck rule (%s)", (locale) => { @@ -20,3 +21,16 @@ describe("methodAddendum", () => { expect(methodAddendum("pl")).toBe(methodAddendum("en")); }); }); + +describe("buildDialogAddenda", () => { + it("leaves the recheck and follow-up rules out until there is a table", () => { + const without = buildDialogAddenda("en", { tableRules: false }); + const withTables = buildDialogAddenda("en", { tableRules: true }); + expect(without).toContain("RESULT TABLES"); + expect(without).toContain("CLARIFYING QUESTIONS"); + expect(without).not.toContain("RECHECKING A FIGURE"); + expect(without).not.toContain("FOLLOW-UP SUGGESTIONS"); + expect(withTables).toContain("RECHECKING A FIGURE"); + expect(withTables).toContain("FOLLOW-UP SUGGESTIONS"); + }); +}); diff --git a/src/lib/ai/coach/turn/addenda/clarify.ts b/src/lib/ai/coach/turn/addenda/clarify.ts index 0dddc12cf..597b47fcf 100644 --- a/src/lib/ai/coach/turn/addenda/clarify.ts +++ b/src/lib/ai/coach/turn/addenda/clarify.ts @@ -4,35 +4,24 @@ * diagnoses. The server enforces the rest (`clarify.ts`): metric choices the * record does not hold are dropped, a second question in a row gets no card, * and a screened question loses its choices. + * + * English whatever the reply language, like the rest of the tool-mode + * prompt: these are instructions to the model, never shown to the person. */ import type { Locale } from "@/lib/i18n/config"; -const EN = `CLARIFYING QUESTIONS - -Ask a clarifying question only when the metric or the window stays genuinely ambiguous after the defaults. Example: "how is my pulse?" while the DATA INVENTORY marks pulse, resting heart rate and walking heart rate all present. Otherwise answer, using the conversation's metric and the default window. -- Never substitute a metric. If only one candidate is present, answer about that one without asking. +const CLARIFY_ADDENDUM = `CLARIFYING QUESTIONS +Ask only when the metric or window stays ambiguous after the defaults (e.g. "how is my pulse?" while the DATA INVENTORY marks pulse, resting_hr and walking_hr present); otherwise answer with the conversation's metric and the default window. +- Never substitute a metric; with one candidate present, answer about it. - Never ask about doses, medication changes or diagnoses. -- At most one question, and never right after your own clarifying question: then answer with what you have. -- When you ask, the question IS your whole reply: one short sentence, no figures, no tool calls, no ---KEYVALUES--- block. End it with: +- At most one question, never right after your own: then answer with what you have. +- The question is your whole reply: one short sentence, no figures, no tool calls, no ---KEYVALUES--- block. End it with: ---CLARIFY--- kind: metric choices: pulse, resting_hr, walking_hr ---END--- -kind is metric, window or context. For metric, list 2 to 4 metric keys the DATA INVENTORY marks present. For window, list 2 to 4 of: last7days, last30days, last90days, lastYear, allTime. For context (something only the person can tell you), leave out the choices line.`; +kind: metric (2 to 4 keys the DATA INVENTORY marks present), window (2 to 4 of last7days, last30days, last90days, lastYear, allTime) or context (only the person can tell you; no choices line).`; -const DE = `RÜCKFRAGEN - -Stelle eine Rückfrage nur, wenn Metrik oder Zeitraum nach den Standardwerten wirklich mehrdeutig bleiben. Beispiel: „Wie ist mein Puls?", während das DATA INVENTORY Puls, Ruhepuls und Gehpuls alle als present führt. Sonst antworte, mit der Metrik der Unterhaltung und dem Standardzeitraum. -- Ersetze nie eine Metrik durch eine andere. Ist nur eine passende vorhanden, antworte zu dieser, ohne zu fragen. -- Frage nie nach Dosierungen, Medikamentenänderungen oder Diagnosen. -- Höchstens eine Frage, und nie direkt nach deiner eigenen Rückfrage: dann antworte mit dem, was vorliegt. -- Wenn du fragst, IST die Frage deine ganze Antwort: ein kurzer Satz, keine Zahlen, keine Tool-Aufrufe, kein ---KEYVALUES---Block. Schließe sie ab mit: ----CLARIFY--- -kind: metric -choices: pulse, resting_hr, walking_hr ----END--- -kind ist metric, window oder context. Bei metric nenne 2 bis 4 Metrik-Schlüssel, die das DATA INVENTORY als present führt. Bei window nenne 2 bis 4 aus: last7days, last30days, last90days, lastYear, allTime. Bei context (etwas, das nur die Person wissen kann) lass die choices-Zeile weg.`; - -export function clarifyAddendum(locale: Locale): string { - return locale === "de" ? DE : EN; +export function clarifyAddendum(_locale: Locale): string { + return CLARIFY_ADDENDUM; } diff --git a/src/lib/ai/coach/turn/addenda/index.ts b/src/lib/ai/coach/turn/addenda/index.ts index bb1983e54..20f70cf04 100644 --- a/src/lib/ai/coach/turn/addenda/index.ts +++ b/src/lib/ai/coach/turn/addenda/index.ts @@ -11,12 +11,22 @@ import { followUpsAddendum } from "./followups"; import { methodAddendum } from "./method"; import { resultsAddendum } from "./results"; -export function buildDialogAddenda(locale: Locale): string { +/** + * The addenda for a turn. `tableRules` adds the recheck and follow-up rules, + * which only matter once there is a table to recheck or follow up on: the + * turn sends them when the conversation already holds one, and from the + * round after its own first table otherwise. + */ +export function buildDialogAddenda( + locale: Locale, + opts: { tableRules: boolean } = { tableRules: true }, +): string { return [ resultsAddendum(locale), clarifyAddendum(locale), - methodAddendum(locale), - followUpsAddendum(locale), + ...(opts.tableRules + ? [methodAddendum(locale), followUpsAddendum(locale)] + : []), ] .filter((block) => block.length > 0) .join("\n\n"); diff --git a/src/lib/ai/coach/turn/addenda/method.ts b/src/lib/ai/coach/turn/addenda/method.ts index ac3e29eaf..8ddac0848 100644 --- a/src/lib/ai/coach/turn/addenda/method.ts +++ b/src/lib/ai/coach/turn/addenda/method.ts @@ -4,17 +4,15 @@ * model neither writes nor sees it, so the rule here is only about what to * do when a figure is challenged: name where it came from, read it again, * and correct the answer when the second read differs. + * + * English whatever the reply language, like the rest of the tool-mode + * prompt: these are instructions to the model, never shown to the person. */ import type { Locale } from "@/lib/i18n/config"; -const EN = `RECHECKING A FIGURE +const METHOD_ADDENDUM = `RECHECKING A FIGURE +If the person questions a figure ("is that right?", "that seems high"): name the source and the window you used, fetch it again with the same tool and arguments (or call show_result for a table this conversation already holds), and compare. If the figures differ, correct your answer and say what changed; if they match, say so plainly. Never defend a figure you have not just re-read, and never adjust one to fit what the person expected.`; -If the person questions a figure ("is that right?", "that seems high"): name the source and the window you used, fetch it again with the same tool and arguments (or call show_result for a table this conversation already holds), and compare. If the figures differ, correct your answer and say what changed. If they match, say so plainly. Never defend a figure you have not just re-read, and never adjust one to fit what the person expected.`; - -const DE = `ZAHL ÜBERPRÜFEN - -Wenn die Person eine Zahl anzweifelt („stimmt das?", „das kommt mir hoch vor"): nenne die Quelle und den Zeitraum, die du verwendet hast, rufe sie mit demselben Tool und denselben Argumenten erneut ab (oder nutze show_result für eine Tabelle, die diese Unterhaltung schon enthält) und vergleiche. Weichen die Zahlen ab, korrigiere deine Antwort und sage, was sich geändert hat. Stimmen sie überein, sage das schlicht. Verteidige keine Zahl, die du nicht gerade neu gelesen hast, und passe keine an das an, was die Person erwartet hat.`; - -export function methodAddendum(locale: Locale): string { - return locale === "de" ? DE : EN; +export function methodAddendum(_locale: Locale): string { + return METHOD_ADDENDUM; } diff --git a/src/lib/ai/coach/turn/model.ts b/src/lib/ai/coach/turn/model.ts index ef46b63bb..c2d72a0f3 100644 --- a/src/lib/ai/coach/turn/model.ts +++ b/src/lib/ai/coach/turn/model.ts @@ -218,29 +218,36 @@ export async function runTurnModel(args: { // build reuses the snapshot we already computed (60s LRU), so the tools // that fire this turn share its reads. const inventory = await buildCoachDataInventory(userId, effectiveScope); - // Earlier tables whose metric the person has since excluded (or that - // fall outside this conversation's scope) are neither named for the - // model nor reachable through show_result. + // Earlier tables whose metric the person has since excluded are + // neither named for the model nor reachable through show_result. The + // scope is the one every tool of the turn reads under. const priorResults = await admittedPriorResults({ userId, prefs: ctx.coachPrefs, - scope: effectiveScope, + scope: inventory.probeScope, prior: args.priorResults ?? [], }); - const toolRequest = buildCoachToolRequest({ - systemPrompt: ctx.systemPrompt, - toolModeAddendum: appendBlocks(buildToolModeAddendum(locale), [ - buildDialogAddenda(locale), - ...args.turnHints, - ]), - focusHint: renderFocusHint(effectiveScope?.sources), - workoutEvidence, - dataInventory: renderDataInventory(inventory), - priorResults: renderPriorResultRefs(priorResults), - guidedBlock: turnContext.guidedBlock, - transcript: turnContext.transcript, - languageName: LANGUAGE_NAMES[locale], - }); + const requestWith = (tableRules: boolean) => + buildCoachToolRequest({ + systemPrompt: ctx.systemPrompt, + toolModeAddendum: appendBlocks(buildToolModeAddendum(locale), [ + buildDialogAddenda(locale, { tableRules }), + ...args.turnHints, + ]), + focusHint: renderFocusHint(effectiveScope?.sources), + workoutEvidence, + dataInventory: renderDataInventory(inventory), + priorResults: renderPriorResultRefs(priorResults), + guidedBlock: turnContext.guidedBlock, + transcript: turnContext.transcript, + languageName: LANGUAGE_NAMES[locale], + }); + // The recheck and follow-up rules are about tables: they ride the + // prompt once the conversation holds one, or from the round after this + // turn produced its first. + const withTables = requestWith(true); + const toolRequest = + priorResults.length > 0 ? withTables : requestWith(false); // v1.39.4 — every call this turn settled, by its turn-wide index, for // the result tables. Only this turn's own calls ever reach them. const settled: SettledToolCall[] = []; @@ -248,6 +255,7 @@ export async function runTurnModel(args: { userId, providers: chain, system: toolRequest.system, + systemOnceTableShown: withTables.system, messages: toolRequest.messages, tools: COACH_TOOL_DEFS, temperature: AI_BUDGETS.coach.temperature,