Archived
refactor: source thinking levels from pi and make the gauge dynamic
Depend on @earendil-works/pi-agent-core so the ThinkingLevel union has a single source of truth (re-exported via shared/thinkingLevels). Wire/data fields use string and the parser is lenient, so an unknown level from a newer pi runtime is still listed, selectable, and rendered gracefully instead of throwing. The composer gauge now derives its bar count from the levels available for the current model and fills by rank. Adds compile-time drift guards (satisfies + Exclude check) and unit tests so a changed pi level set fails fast in development.
This commit is contained in:
@@ -183,7 +183,10 @@ export interface SessionModel {
|
||||
reasoning?: unknown;
|
||||
}
|
||||
|
||||
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
// Domain type is owned by pi and re-exported from the shared thinking-levels
|
||||
// module. Wire/data fields below intentionally use `string` so an unknown level
|
||||
// from a newer pi runtime parses and renders gracefully instead of failing.
|
||||
export type { ThinkingLevel } from "./thinkingLevels.js";
|
||||
|
||||
export type AuthType = "oauth" | "api_key";
|
||||
export type AuthStatusSource = "stored" | "runtime" | "environment" | "fallback" | "models_json_key" | "models_json_command";
|
||||
@@ -222,7 +225,7 @@ export interface ModelSelectionResponse {
|
||||
}
|
||||
|
||||
export interface ThinkingLevelsResponse {
|
||||
levels: ThinkingLevel[];
|
||||
levels: string[];
|
||||
}
|
||||
|
||||
export interface SessionStatus {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { KNOWN_THINKING_LEVELS, isKnownThinkingLevel, thinkingGauge, thinkingLevelLabel } from "./thinkingLevels";
|
||||
|
||||
// Compile-time drift guard: if pi ADDS a thinking level we do not know about,
|
||||
// `Extra` becomes that level and this assignment fails to type-check. Combined
|
||||
// with the `satisfies readonly ThinkingLevel[]` clause in thinkingLevels.ts
|
||||
// (which catches removals/renames), this pins KNOWN_THINKING_LEVELS to pi's
|
||||
// union exactly. When this breaks, update KNOWN_THINKING_LEVELS and give the new
|
||||
// level a label/description where thinking levels are presented.
|
||||
type Extra = Exclude<ThinkingLevel, (typeof KNOWN_THINKING_LEVELS)[number]>;
|
||||
const _noUnknownLevels: Extra extends never ? true : never = true;
|
||||
void _noUnknownLevels;
|
||||
|
||||
describe("thinkingLevels", () => {
|
||||
it("recognizes all known levels and rejects others", () => {
|
||||
for (const level of KNOWN_THINKING_LEVELS) expect(isKnownThinkingLevel(level)).toBe(true);
|
||||
expect(isKnownThinkingLevel("ultra")).toBe(false);
|
||||
expect(isKnownThinkingLevel("")).toBe(false);
|
||||
});
|
||||
|
||||
it("labels levels, defaulting empty/undefined to off", () => {
|
||||
expect(thinkingLevelLabel(undefined)).toBe("off");
|
||||
expect(thinkingLevelLabel("")).toBe("off");
|
||||
expect(thinkingLevelLabel("high")).toBe("high");
|
||||
expect(thinkingLevelLabel("brand-new-level")).toBe("brand-new-level");
|
||||
});
|
||||
|
||||
describe("thinkingGauge", () => {
|
||||
const known = KNOWN_THINKING_LEVELS;
|
||||
|
||||
it("derives bar count from the available set (excluding the off level)", () => {
|
||||
// 6 known levels => 5 bars.
|
||||
expect(thinkingGauge("off", known).total).toBe(5);
|
||||
expect(thinkingGauge("off", ["off", "low", "high"]).total).toBe(2);
|
||||
});
|
||||
|
||||
it("treats the first level as no thinking (0 filled)", () => {
|
||||
expect(thinkingGauge("off", known)).toEqual({ total: 5, filled: 0 });
|
||||
expect(thinkingGauge(undefined, known)).toEqual({ total: 5, filled: 0 });
|
||||
});
|
||||
|
||||
it("fills up to the current level's rank", () => {
|
||||
expect(thinkingGauge("minimal", known).filled).toBe(1);
|
||||
expect(thinkingGauge("low", known).filled).toBe(2);
|
||||
expect(thinkingGauge("medium", known).filled).toBe(3);
|
||||
expect(thinkingGauge("high", known).filled).toBe(4);
|
||||
expect(thinkingGauge("xhigh", known).filled).toBe(5);
|
||||
});
|
||||
|
||||
it("adapts to a runtime-provided set of a different size", () => {
|
||||
const available = ["off", "low", "high"];
|
||||
expect(thinkingGauge("off", available)).toEqual({ total: 2, filled: 0 });
|
||||
expect(thinkingGauge("low", available)).toEqual({ total: 2, filled: 1 });
|
||||
expect(thinkingGauge("high", available)).toEqual({ total: 2, filled: 2 });
|
||||
});
|
||||
|
||||
it("falls back to the known set when no usable available set is given", () => {
|
||||
expect(thinkingGauge("high", [])).toEqual({ total: 5, filled: 4 });
|
||||
expect(thinkingGauge("high", ["only-one"])).toEqual({ total: 5, filled: 4 });
|
||||
});
|
||||
|
||||
it("fills 0 for an unknown current level instead of throwing", () => {
|
||||
expect(thinkingGauge("brand-new-level", known).filled).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
|
||||
// pi owns the set of thinking levels. We re-export pi's type so the domain has a
|
||||
// single source of truth, while the HTTP/wire contract (apiTypes.ts) keeps using
|
||||
// `string` so an unknown level reported by a newer pi runtime degrades gracefully
|
||||
// instead of failing to parse.
|
||||
export type { ThinkingLevel };
|
||||
|
||||
/**
|
||||
* Known levels in increasing intensity, derived from pi's `ThinkingLevel` union.
|
||||
* The `satisfies` clause makes this fail to compile if pi removes or renames a
|
||||
* level; thinkingLevels.test.ts adds a compile-time check for additions too. When
|
||||
* either breaks, update this list and give the new level a label/description
|
||||
* where thinking levels are presented.
|
||||
*/
|
||||
export const KNOWN_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const satisfies readonly ThinkingLevel[];
|
||||
|
||||
export function isKnownThinkingLevel(value: string): value is ThinkingLevel {
|
||||
return KNOWN_THINKING_LEVELS.some((level) => level === value);
|
||||
}
|
||||
|
||||
export function thinkingLevelLabel(level: string | undefined): string {
|
||||
return level === undefined || level === "" ? "off" : level;
|
||||
}
|
||||
|
||||
export interface ThinkingGauge {
|
||||
/** Number of bars to render (the non-"off" levels). */
|
||||
total: number;
|
||||
/** Number of filled bars for the current level. */
|
||||
filled: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe a thinking-level gauge from the available set rather than a hardcoded
|
||||
* table, so it stays correct even if pi changes the available levels at runtime.
|
||||
*
|
||||
* Convention: the first available level is treated as "no thinking". The gauge
|
||||
* therefore renders one bar per remaining level, and fills up to the current
|
||||
* level's rank. An unknown current level fills 0 bars instead of throwing.
|
||||
*/
|
||||
export function thinkingGauge(level: string | undefined, available: readonly string[]): ThinkingGauge {
|
||||
const pool = available.length >= 2 ? available : KNOWN_THINKING_LEVELS;
|
||||
const total = pool.length - 1;
|
||||
const normalized = thinkingLevelLabel(level);
|
||||
const index = pool.indexOf(normalized);
|
||||
const filled = index <= 0 ? 0 : Math.min(index, total);
|
||||
return { total, filled };
|
||||
}
|
||||
Reference in New Issue
Block a user