feat(coach): resolve the turn's thinking depth from the person and the operator

The chat route hands the turn the level resolveCoachTurnReasoningLevel
returns: the stored Coach preference, absent read as medium, after the
operator's switch and cap. The turn still applies the operator-paid
ceiling once its chain is known. The controls loader no longer throws
when the request cache cannot serve it; it fails closed to off. The
admin guard pins the route to the resolver.
This commit is contained in:
Marc-André Bombeck committed 2026-10-06 19:57:04 +02:00
1 parent 85ebdad5c9
commit ae5cee331e
6 files changed
+168 -10

No files matched your search

@@ -0,0 +1,90 @@
/**
* v1.41 — the Coach turn's level as the chat route resolves it: the stored
* preference against the operator's controls, read through the real loader
* against a stubbed settings row.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
const { row } = vi.hoisted(() => ({
row: { current: null as unknown, fail: false },
}));
vi.mock("@/lib/db", () => ({
prisma: {
appSettings: {
findUnique: vi.fn(async () => {
if (row.fail) throw new Error("db down");
return row.current;
}),
},
},
}));
vi.mock("@/lib/logging/context", () => ({
annotate: vi.fn(),
getEvent: () => undefined,
}));
import {
loadReasoningControls,
resolveCoachTurnReasoningLevel,
} from "../controls";
beforeEach(() => {
row.current = null;
row.fail = false;
});
describe("loadReasoningControls", () => {
it("reads an untouched instance as on and uncapped", async () => {
expect(await loadReasoningControls()).toEqual({
enabled: true,
maxEffort: "high",
});
});
it("fails closed when the row cannot be read", async () => {
row.fail = true;
expect((await loadReasoningControls()).enabled).toBe(false);
});
it("reads an unknown stored cap as no cap", async () => {
row.current = { aiReasoningEnabled: true, aiReasoningMaxEffort: "xhigh" };
expect((await loadReasoningControls()).maxEffort).toBe("high");
});
});
describe("resolveCoachTurnReasoningLevel", () => {
it("runs a never-chosen preference at medium", async () => {
expect(await resolveCoachTurnReasoningLevel(null)).toBe("medium");
expect(await resolveCoachTurnReasoningLevel({ tone: "warm" })).toBe(
"medium",
);
});
it("reads a malformed stored level as the default", async () => {
expect(await resolveCoachTurnReasoningLevel({ reasoning: "max" })).toBe(
"medium",
);
});
it("clamps the stored level to the operator's cap", async () => {
row.current = { aiReasoningEnabled: true, aiReasoningMaxEffort: "low" };
expect(await resolveCoachTurnReasoningLevel({ reasoning: "high" })).toBe(
"low",
);
});
it("is off when the operator switched reasoning off", async () => {
row.current = { aiReasoningEnabled: false, aiReasoningMaxEffort: "high" };
expect(await resolveCoachTurnReasoningLevel({ reasoning: "high" })).toBe(
"off",
);
});
it("is off when the controls cannot be read", async () => {
row.fail = true;
expect(await resolveCoachTurnReasoningLevel({ reasoning: "high" })).toBe(
"off",
);
});
});
+40 -2
View File
@@ -18,7 +18,12 @@ import {
type CoachPrefs,
} from "@/lib/validations/coach-prefs";
import type { BackgroundReasoningJob } from "./levels";
import {
DEFAULT_REASONING_LEVEL,
isReasoningLevel,
type BackgroundReasoningJob,
type ReasoningLevel,
} from "./levels";
import {
coachReasoningCeiling,
completionReasoning,
@@ -45,7 +50,17 @@ const UNREADABLE: ReasoningAdminControls = Object.freeze({
* inside a background run, so switching reasoning off at 02:05 stops the
* nightly pass that began at 02:00 for every account after the switch.
*/
export function loadReasoningControls(): Promise<ReasoningAdminControls> {
export async function loadReasoningControls(): Promise<ReasoningAdminControls> {
// The request cache itself can fail (an event without a kind); the
// controls then fail closed like an unreadable row, never the caller.
try {
return await loadMemoisedControls();
} catch {
return UNREADABLE;
}
}
function loadMemoisedControls(): Promise<ReasoningAdminControls> {
return memoizePerRequest(
"ai-reasoning-controls",
async () => {
@@ -174,3 +189,26 @@ export async function resolveJobReasoning(
});
return completionReasoning(resolved, job);
}
/**
* The effective Coach level for a turn: the person's stored choice
* (`coachPrefsJson.reasoning`, absent read as the default) after the
* operator's switch and cap. Who pays is applied inside the turn, once its
* chain is known; the provider's ability is left to its client. Never
* throws: an unreadable preference reads as the default, unreadable
* controls as "off".
*/
export async function resolveCoachTurnReasoningLevel(
coachPrefsJson: unknown,
): Promise<ReasoningLevel> {
const stored =
coachPrefsJson && typeof coachPrefsJson === "object"
? (coachPrefsJson as { reasoning?: unknown }).reasoning
: undefined;
return resolveReasoning({
surface: "coach",
userPref: isReasoningLevel(stored) ? stored : DEFAULT_REASONING_LEVEL,
admin: await loadReasoningControls(),
costOwner: "user",
}).effort;
}