pi-switchback 0.0.0-stage → 2026.10.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Turning a routing decision into the request pi dispatches.
3
+ *
4
+ * `packages/core/src/routing.ts` decides *where* a request should go and
5
+ * `packages/core/src/dispatch.ts` settles the reasoning level; this module expresses that
6
+ * as pi's `ModelRoute`: it resolves the chosen id back to a pi `Model`, clamps the
7
+ * level to what that model actually implements, and attaches the router state.
8
+ *
9
+ * The decision types themselves live in `packages/core/src/decision.ts`: they are the one
10
+ * thing the core and every host adapter must agree on, and a core module cannot
11
+ * import them from a pi module.
12
+ */
13
+
14
+ import { clampThinkingLevel } from "@earendil-works/pi-ai";
15
+ import type { ModelRoute } from "@earendil-works/pi-coding-agent";
16
+ import { lookupModel } from "switchback-core/availability.ts";
17
+ import type { Decision } from "switchback-core/decision.ts";
18
+ import type { ThinkingLevelId } from "switchback-core/ports.ts";
19
+ import type { SwitchbackState } from "switchback-core/types.ts";
20
+ import type { PiModelLookup } from "./thinking-support.ts";
21
+
22
+ /**
23
+ * The slice of pi's model registry this module reads: the catalog lookup owned
24
+ * by `./thinking-support.ts`. Narrower than pi's whole registry, but `find` still
25
+ * hands back pi's own `Model`, which is what `ModelRoute` and pi's
26
+ * `clampThinkingLevel` take.
27
+ */
28
+ type PiRegistry = PiModelLookup;
29
+
30
+ /**
31
+ * Resolve a `ModelRoute` from a `Decision`.
32
+ *
33
+ * This function NEVER throws for "config-invalid" decisions or for decisions on
34
+ * greyed entries. It only throws when the picked effective model was lost between
35
+ * `decide()` and `buildRoute()` (e.g. registry state changed mid-call) - in that
36
+ * case the caller's availability resolution was stale and the caller should retry.
37
+ */
38
+ export function buildRoute(
39
+ registry: PiRegistry,
40
+ decision: Decision,
41
+ thinkingLevel: ThinkingLevelId,
42
+ nextState: SwitchbackState | undefined,
43
+ ): ModelRoute<SwitchbackState> {
44
+ if (decision.kind === "config-invalid") {
45
+ throw new ConfigInvalidError(decision);
46
+ }
47
+ if (decision.kind === "exhausted") {
48
+ throw new Error(`switchback: ${decision.reason}`);
49
+ }
50
+ const model = lookupModel(registry, decision.modelId);
51
+ if (!model)
52
+ throw new Error(
53
+ `switchback: model "${decision.modelId}" not in catalog (lost between decide() and buildRoute())`,
54
+ );
55
+ // Clamp to the routed model. A virtual level the physical model does not implement
56
+ // (zai has no "medium" and cannot disable thinking at all) is otherwise sent as the
57
+ // provider default, which is what makes "I changed reasoning effort and nothing
58
+ // happened" look like a bug. Pi clamps too; doing it here keeps the returned route
59
+ // honest for every caller.
60
+ const route: ModelRoute<SwitchbackState> = { model, thinkingLevel: clampThinkingLevel(model, thinkingLevel) };
61
+ if (nextState !== undefined) route.state = nextState;
62
+ return route;
63
+ }
64
+
65
+ export class ConfigInvalidError extends Error {
66
+ readonly decision: Extract<Decision, { kind: "config-invalid" }>;
67
+ constructor(decision: Extract<Decision, { kind: "config-invalid" }>) {
68
+ super(
69
+ `switchback: config invalid (${decision.reason}); effective=[${decision.effective.join(", ")}] greyed=[${decision.greyed.map((g) => `${g.id}:${g.reason}`).join(", ")}]`,
70
+ );
71
+ this.name = "ConfigInvalidError";
72
+ this.decision = decision;
73
+ }
74
+ }