infinity-harness 2.8.9 → 2.8.10
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.
- package/CHANGELOG.md +10 -0
- package/extensions/infinity-harness/index.ts +23 -1
- package/package.json +1 -1
- package/src/core/modelRouter.ts +23 -1
- package/src/daemon/index.ts +3 -3
- package/src/daemon/worker.ts +28 -5
- package/src/ui/config.ts +25 -1
- package/src/ui/wizard.ts +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.8.10] — 2026-08-31
|
|
8
|
+
|
|
9
|
+
Thinking levels from pi, not inventoried; locals inherit correctly.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Thinking menu was hard-coded, not model-aware.** Wizard now filters the thinking-level picker to exactly what pi reports for the picked model (“pi truth”): local reasoning-off models (lfm2.5/qwen3.8/granite) only offer “(inherit)” + “off”; reasoning locals offer off…high; full providers offer off…max. “/infinity:config” thinking settings do the same per-tier. Previously all 7 were always offered, so locals could be set to xhigh.
|
|
14
|
+
- **Locals died on “inherit” even with thinking blank.** Worker forced “inherit” (“”) to “medium” before “createAgentSession”, so even blank B/C/D sent reasoning_effort → max_completion_tokens:1. Changed to leave “” as undefined so pi uses its per-model default then clamps via model.thinkingLevelMap (off for reasoning:false locals). Fixed in daemon/worker, daemon/index, and core/modelRouter clampThinkingForProvider (now model-aware with provider-string fallback).
|
|
15
|
+
- **Provider-string clamp was model-blind.** ”lm-studio → medium” broke xhigh-capable locals and still sent medium to 2b QAD models. Replaced with pi-ai getSupportedThinkingLevels semantics: reasoning:false → only off; reasoning:true + empty map → off…high (no xhigh/max); map null → filtered. Config file stays on pi’s THINKING_LEVELS definition, nothing invented.
|
|
16
|
+
|
|
7
17
|
## [2.8.9] — 2026-08-31
|
|
8
18
|
|
|
9
19
|
Locals no longer die on xhigh; DEFINE ladder re-consults.
|
|
@@ -540,6 +540,27 @@ export default function (pi: ExtensionAPI): void {
|
|
|
540
540
|
* would produce a tier that fails at the first task rather than at setup.
|
|
541
541
|
*/
|
|
542
542
|
const availableModels = (ctx: ExtensionContext): ModelChoice[] => {
|
|
543
|
+
// Helper: get pi-default thinking levels for a model (the truth — not our THINKING_LEVELS)
|
|
544
|
+
const supportedOf = (m: unknown): readonly string[] | null => {
|
|
545
|
+
try {
|
|
546
|
+
const mod = m as { reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> };
|
|
547
|
+
if (!mod) return null;
|
|
548
|
+
// pi-ai getSupportedThinkingLevels: reasoning false => ["off"]
|
|
549
|
+
if (mod.reasoning === false) return ["off"] as const;
|
|
550
|
+
if (mod.reasoning === true) {
|
|
551
|
+
const map = mod.thinkingLevelMap ?? {};
|
|
552
|
+
const all: readonly string[] = ["off","minimal","low","medium","high","xhigh","max"];
|
|
553
|
+
const supported = all.filter(l => {
|
|
554
|
+
const v = (map as Record<string, string | null | undefined>)[l];
|
|
555
|
+
if (v === null) return false;
|
|
556
|
+
if ((l === "xhigh" || l === "max") && v === undefined) return false;
|
|
557
|
+
return true;
|
|
558
|
+
});
|
|
559
|
+
return supported.length ? supported : (["off"] as const);
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
} catch { return null; }
|
|
563
|
+
};
|
|
543
564
|
try {
|
|
544
565
|
const scoped = ctx.scopedModels ?? [];
|
|
545
566
|
const models =
|
|
@@ -558,7 +579,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
558
579
|
if (m.name && m.name !== m.id) bits.push(`· ${m.name}`);
|
|
559
580
|
if (m.contextWindow) bits.push(`· ${Math.round(m.contextWindow / 1000)}k ctx`);
|
|
560
581
|
if (m.reasoning) bits.push("· reasoning");
|
|
561
|
-
|
|
582
|
+
const levels = supportedOf(m);
|
|
583
|
+
out.push({ ref, label: bits.join(" "), ...(levels ? { supportedThinkingLevels: levels } : {}) } as unknown as ModelChoice & { supportedThinkingLevels?: readonly string[] });
|
|
562
584
|
}
|
|
563
585
|
out.sort((a, b) => a.ref.localeCompare(b.ref));
|
|
564
586
|
return out;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinity-harness",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.10",
|
|
4
4
|
"description": "A pi agent extension that runs a gated build pipeline unattended \u2014 enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
package/src/core/modelRouter.ts
CHANGED
|
@@ -13,8 +13,30 @@ import type { TierMap, TierSpec, HarnessConfig, FeatureList, Phase, Difficulty }
|
|
|
13
13
|
export type ThinkingLevel = import("./types.ts").ThinkingLevel;
|
|
14
14
|
export type TierId = import("./types.ts").TierId;
|
|
15
15
|
|
|
16
|
-
export function clampThinkingForProvider(provider: string, level: string | "" | undefined): string | "" {
|
|
16
|
+
export function clampThinkingForProvider(provider: string, level: string | "" | undefined, modelForCheck?: unknown): string | "" {
|
|
17
17
|
if (!level) return (level ?? "") as string | "";
|
|
18
|
+
// If we have the actual model, use pi-default truth (getSupportedThinkingLevels semantics)
|
|
19
|
+
try {
|
|
20
|
+
const m = modelForCheck as { reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> } | null | undefined;
|
|
21
|
+
if (m && typeof m.reasoning === "boolean") {
|
|
22
|
+
if (m.reasoning === false) return level === "off" || level === "" ? (level as string | "") : "off";
|
|
23
|
+
const map = m.thinkingLevelMap ?? {};
|
|
24
|
+
const all = ["off","minimal","low","medium","high","xhigh","max"] as const;
|
|
25
|
+
const supported = all.filter(l => {
|
|
26
|
+
const v = (map as Record<string, string | null | undefined>)[l];
|
|
27
|
+
if (v === null) return false;
|
|
28
|
+
if ((l === "xhigh" || l === "max") && v === undefined) return false;
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
if (supported.includes(level as typeof all[number])) return level as string | "";
|
|
32
|
+
// clamp to nearest supported (prefer lower)
|
|
33
|
+
const idx = (all as readonly string[]).indexOf(level);
|
|
34
|
+
for (let i = idx; i >= 0; i--) if (supported.includes(all[i] as typeof all[number])) return all[i] as string | "";
|
|
35
|
+
for (let i = idx+1; i < all.length; i++) if (supported.includes(all[i] as typeof all[number])) return all[i] as string | "";
|
|
36
|
+
return supported[0] ?? "off";
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
// Fallback: old provider-string heuristic for when model not available (e.g. in config migration)
|
|
18
40
|
const pp = String(provider || "").toLowerCase();
|
|
19
41
|
const isLocal = pp === "lm-studio" || pp.startsWith("lm-studio/") || pp === "lmstudio";
|
|
20
42
|
if (!isLocal) return level as string | "";
|
package/src/daemon/index.ts
CHANGED
|
@@ -275,7 +275,7 @@ async function runLoop(targetDir: string): Promise<void> {
|
|
|
275
275
|
model: askedModel,
|
|
276
276
|
askedModel,
|
|
277
277
|
servedModel: null,
|
|
278
|
-
thinking: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "
|
|
278
|
+
thinking: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "") || undefined,
|
|
279
279
|
state: "starting",
|
|
280
280
|
doing: "starting",
|
|
281
281
|
startedAt: new Date().toISOString(),
|
|
@@ -298,7 +298,7 @@ async function runLoop(targetDir: string): Promise<void> {
|
|
|
298
298
|
const workerFactory = await import("./worker.ts");
|
|
299
299
|
workerHandle = await workerFactory.createWorker({
|
|
300
300
|
cwd: targetDir,
|
|
301
|
-
modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "
|
|
301
|
+
modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "") || undefined },
|
|
302
302
|
askedModel,
|
|
303
303
|
sessionManagerDir: `harness/sessions/${workerLabel.replace(/[^a-z0-9._-]/gi, "-")}`,
|
|
304
304
|
customTools: (await import("./isolation.ts")).harnessToolsForWorker() as unknown[],
|
|
@@ -317,7 +317,7 @@ async function runLoop(targetDir: string): Promise<void> {
|
|
|
317
317
|
if (!workerHandle) {
|
|
318
318
|
workerHandle = await workerFactory.createWorker({
|
|
319
319
|
cwd: targetDir,
|
|
320
|
-
modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "
|
|
320
|
+
modelSpec: { provider: routed.provider, id: routed.id, thinkingLevel: clampThinkingForProvider(routed.provider, ((tierSpec as { thinkingLevel?: string } | null)?.thinkingLevel as unknown as string) ?? "") || undefined },
|
|
321
321
|
askedModel,
|
|
322
322
|
sessionManagerDir: `harness/sessions/${workerLabel.replace(/[^a-z0-9._-]/gi, "-")}`,
|
|
323
323
|
customTools: (await import("./isolation.ts")).harnessToolsForWorker() as unknown[],
|
package/src/daemon/worker.ts
CHANGED
|
@@ -110,12 +110,35 @@ export async function createWorker(opts: CreateWorkerOpts): Promise<{
|
|
|
110
110
|
? SessionManager.create(opts.cwd, opts.sessionManagerDir)
|
|
111
111
|
: SessionManager.inMemory(opts.cwd);
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
// inherit means inherit — don't force medium. Only clamp when explicitly set.
|
|
114
|
+
let thinkingLevel: string | undefined = (opts.thinkingLevel ?? opts.modelSpec.thinkingLevel ?? undefined) as unknown as string | undefined;
|
|
115
|
+
// capability-aware clamp: use pi-default supported levels for the actual model, not provider string
|
|
115
116
|
try {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
const mod = model as unknown as { reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> } | null;
|
|
118
|
+
if (thinkingLevel) {
|
|
119
|
+
// inline getSupportedThinkingLevels semantics
|
|
120
|
+
if (mod && typeof mod.reasoning === "boolean" && mod.reasoning === false) {
|
|
121
|
+
if (thinkingLevel !== "off") thinkingLevel = "off";
|
|
122
|
+
} else if (mod && mod.reasoning === true) {
|
|
123
|
+
const map = mod.thinkingLevelMap ?? {};
|
|
124
|
+
const all = ["off","minimal","low","medium","high","xhigh","max"] as const;
|
|
125
|
+
const supported = all.filter(l => {
|
|
126
|
+
const v = (map as Record<string, string | null | undefined>)[l];
|
|
127
|
+
if (v === null) return false;
|
|
128
|
+
if ((l === "xhigh" || l === "max") && v === undefined) return false;
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
if (!supported.includes(thinkingLevel as typeof all[number])) {
|
|
132
|
+
const idx = (all as readonly string[]).indexOf(thinkingLevel);
|
|
133
|
+
let found: string | null = null;
|
|
134
|
+
for (let i = idx; i >= 0; i--) if (supported.includes(all[i] as typeof all[number])) { found = all[i] as string; break; }
|
|
135
|
+
if (!found) for (let i = idx+1; i < all.length; i++) if (supported.includes(all[i] as typeof all[number])) { found = all[i] as string; break; }
|
|
136
|
+
thinkingLevel = found ?? supported[0] ?? "off";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else if (thinkingLevel === undefined) {
|
|
140
|
+
// inherit (undefined) — leave undefined so createAgentSession uses pi-default for that model
|
|
141
|
+
}
|
|
119
142
|
} catch {}
|
|
120
143
|
// B: debug trace of routed model/thinking (readable in harness/daemon.log)
|
|
121
144
|
try { console.log("[worker] model=" + opts.modelSpec.provider + "/" + opts.modelSpec.id + " thinking=" + thinkingLevel + " askedModel=" + opts.askedModel); } catch {}
|
package/src/ui/config.ts
CHANGED
|
@@ -39,6 +39,8 @@ export type ModelChoice = {
|
|
|
39
39
|
ref: string;
|
|
40
40
|
/** What the user sees. */
|
|
41
41
|
label: string;
|
|
42
|
+
/** Pi-default supported thinking levels for this model, or null if unknown. */
|
|
43
|
+
supportedThinkingLevels?: readonly string[];
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export type ConfigMenuOptions = {
|
|
@@ -167,7 +169,29 @@ async function editSetting(setting: Setting, options: ConfigMenuOptions): Promis
|
|
|
167
169
|
break;
|
|
168
170
|
}
|
|
169
171
|
case "thinking": {
|
|
170
|
-
|
|
172
|
+
// capability-aware: if setting is tiers.X.thinkingLevel etc, filter to what the model actually supports
|
|
173
|
+
let choices: string[] = ["(inherit)", "off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
174
|
+
try {
|
|
175
|
+
const m = setting.path.match(/^tiers.([A-Z]).(provider|id|thinkingLevel)/);
|
|
176
|
+
// For tiers.A/B/C/D/X thinkingLevel, look up that tier's model and its supported levels
|
|
177
|
+
const tierMatch = setting.path.match(/^tiers.([A-Z]).thinkingLevel/);
|
|
178
|
+
if (tierMatch) {
|
|
179
|
+
const tier = tierMatch[1]!;
|
|
180
|
+
const all = readAll(targetDir);
|
|
181
|
+
const tierSpec = (all.config as unknown as { tiers?: Record<string, { provider?: string; id?: string }> }).tiers?.[tier];
|
|
182
|
+
if (tierSpec?.provider && tierSpec?.id) {
|
|
183
|
+
const ref = tierSpec.provider + "/" + tierSpec.id;
|
|
184
|
+
const models = await options.models();
|
|
185
|
+
const found = models.find(x => x.ref === ref) as unknown as { supportedThinkingLevels?: readonly string[] } | undefined;
|
|
186
|
+
if (found?.supportedThinkingLevels?.length) {
|
|
187
|
+
choices = ["(inherit)", ...found.supportedThinkingLevels.filter(l => l !== "off")];
|
|
188
|
+
// ensure off is last fallback if model supports only off
|
|
189
|
+
if (!found.supportedThinkingLevels.includes("off")) choices = ["(inherit)", ...found.supportedThinkingLevels];
|
|
190
|
+
else if (choices.indexOf("off") === -1) choices.splice(1,0,"off");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
171
195
|
const picked = await prompt.select(`${setting.label} — ${setting.help}`, [...choices, BACK]);
|
|
172
196
|
if (picked === undefined || picked === BACK) return false;
|
|
173
197
|
raw = picked;
|
package/src/ui/wizard.ts
CHANGED
|
@@ -153,8 +153,9 @@ async function pickModelChoice(prompt: Prompter, title: string, models: ModelCho
|
|
|
153
153
|
return model?.ref;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
async function pickThinkingLevel(prompt: Prompter, title: string): Promise<ThinkingLevel | "" | undefined> {
|
|
157
|
-
const
|
|
156
|
+
async function pickThinkingLevel(prompt: Prompter, title: string, supported?: readonly string[] | null): Promise<ThinkingLevel | "" | undefined> {
|
|
157
|
+
const levels = Array.isArray(supported) && supported.length ? supported as ThinkingLevel[] : THINKING_LEVELS;
|
|
158
|
+
const picked = await prompt.select(title, [THINK_INHERIT, ...levels]);
|
|
158
159
|
if (picked === undefined) return undefined;
|
|
159
160
|
if (picked === THINK_INHERIT) return "";
|
|
160
161
|
return picked as ThinkingLevel;
|
|
@@ -214,21 +215,25 @@ async function pickModelsStep(
|
|
|
214
215
|
const tiers = ["easy", "moderate", "difficult"] as const;
|
|
215
216
|
const byDifficulty: Record<string, string> = {};
|
|
216
217
|
const thinkingByDifficulty: Partial<Record<string, ThinkingLevel | "">> = {};
|
|
218
|
+
const supportedFor = (ref: string): readonly string[] | null => {
|
|
219
|
+
const m = models.find(x => x.ref === ref) as unknown as { supportedThinkingLevels?: readonly string[] } | undefined;
|
|
220
|
+
return (m as { supportedThinkingLevels?: readonly string[] } | undefined)?.supportedThinkingLevels ?? null;
|
|
221
|
+
};
|
|
217
222
|
for (const tier of tiers) {
|
|
218
223
|
const model = await pickModelChoice(prompt, `${tier.toUpperCase()} tier — model`, models, "");
|
|
219
224
|
if (model === undefined) return undefined;
|
|
220
225
|
byDifficulty[tier] = model;
|
|
221
|
-
const thinking = await pickThinkingLevel(prompt, `${tier.toUpperCase()} tier — thinking level
|
|
226
|
+
const thinking = await pickThinkingLevel(prompt, `${tier.toUpperCase()} tier — thinking level`, model ? supportedFor(model) : null);
|
|
222
227
|
if (thinking === undefined) return undefined;
|
|
223
228
|
thinkingByDifficulty[tier] = thinking;
|
|
224
229
|
}
|
|
225
230
|
const masterModel = await pickModelChoice(prompt, "Consulting master — model (used only when the ladder is exhausted)", models, "");
|
|
226
231
|
if (masterModel === undefined) return undefined;
|
|
227
|
-
const masterThinking = await pickThinkingLevel(prompt, "Consulting master — thinking level");
|
|
232
|
+
const masterThinking = await pickThinkingLevel(prompt, "Consulting master — thinking level", masterModel ? supportedFor(masterModel) : null);
|
|
228
233
|
if (masterThinking === undefined) return undefined;
|
|
229
234
|
const defaultModel = await pickModelChoice(prompt, "Default (tier A) — general work + undifficult tasks — idle under task handoff when every task has difficulty (" + tierScopeNote(handoff) + ")", models, "");
|
|
230
235
|
if (defaultModel === undefined) return undefined;
|
|
231
|
-
const defaultThinking = await pickThinkingLevel(prompt, "Default — thinking level fallback");
|
|
236
|
+
const defaultThinking = await pickThinkingLevel(prompt, "Default — thinking level fallback", defaultModel ? supportedFor(defaultModel) : null);
|
|
232
237
|
if (defaultThinking === undefined) return undefined;
|
|
233
238
|
return {
|
|
234
239
|
router: {
|