feat: Modell der Coder/Judge-Schleife per PI_CODER_MODEL wählbar

PI_CODER_MODEL=<provider>/<id> setzt das Modell für beide Rollen; ohne
Variable bleibt es bei llama-cpp-single/coding_model. Die Bereitschafts-
prüfung nutzt baseUrl des Modells statt fest Port 8001. Neuer Provider
strata (lokaler Strata-Server, 127.0.0.1:9090) in models.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-15 02:24:01 +02:00
commit a608f1dda8

View file

@ -1,6 +1,6 @@
// pi-coding-extension.ts
// Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten
// Ein Modell (coding_model) auf Single-Server (Port 8001); Rollen Coder/Judge via System-Prompt
// Ein Modell (Standard: coding_model auf Single-Server :8001, sonst PI_CODER_MODEL); Rollen Coder/Judge via System-Prompt
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
@ -432,13 +432,30 @@ async function tickTaskMdStatus(
);
}
// Single-Server: genau ein Modell (coding_model auf :8001) für beide Rollen.
// Single-Modell: genau ein Modell (Standard coding_model auf :8001, oder PI_CODER_MODEL) für beide Rollen.
// Der Rollenwechsel Coder/Judge erfolgt NICHT mehr über den Modellwechsel, sondern
// über den System-Prompt (siehe before_agent_start-Hook + currentRole). Diese Funktion
// stellt nur sicher, dass das eine Single-Modell aktiv ist; nach dem ersten Aufruf
// ist der Wechsel via currentModelKey gecacht und damit ein No-op.
const SINGLE_PROVIDER = "llama-cpp-single";
const SINGLE_MODEL_ID = "coding_model";
// PI_CODER_MODEL="<provider>/<id>" setzt ein anderes Modell für beide Rollen (z. B. strata/qwen3.8-flash-next-iq3_s);
// ohne die Variable bleibt es beim Single-Server auf :8001.
const DEFAULT_SINGLE_MODEL = "llama-cpp-single/coding_model";
const SINGLE_MODEL_SPEC = (process.env.PI_CODER_MODEL || DEFAULT_SINGLE_MODEL).trim();
const SINGLE_PROVIDER = SINGLE_MODEL_SPEC.slice(0, SINGLE_MODEL_SPEC.indexOf("/"));
const SINGLE_MODEL_ID = SINGLE_MODEL_SPEC.slice(SINGLE_MODEL_SPEC.indexOf("/") + 1);
const SINGLE_LABEL = `${SINGLE_PROVIDER}/${SINGLE_MODEL_ID}`;
// Endpunkt des Single-Modells laut models.json, für Bereitschaftsprüfung und Meldungen.
function singleEndpoint(ctx: ExtensionCommandContext): string {
const model = ctx.modelRegistry.find(SINGLE_PROVIDER, SINGLE_MODEL_ID);
return model?.baseUrl ? model.baseUrl.replace(/\/+$/, "") : "";
}
function notReadyMessage(ctx: ExtensionCommandContext): string {
const where = singleEndpoint(ctx) || SINGLE_LABEL;
const hint = SINGLE_MODEL_SPEC === DEFAULT_SINGLE_MODEL ? "start-coding-server.sh ausführen" : "Server starten";
return `Server nicht bereit (${where}) — ${hint}`;
}
async function ensureSingleModel(
pi: ExtensionAPI,
@ -518,17 +535,20 @@ async function sendAndWait(
async function waitUntilModelReady(
pi: ExtensionAPI,
ctx: ExtensionCommandContext,
port: number,
modelAlias: string,
maxWaitMs = 180_000
): Promise<boolean> {
const endpoint = singleEndpoint(ctx);
if (!endpoint) {
ctx.ui.notify(`Modell ${SINGLE_LABEL} nicht in models.json gefunden`, "error");
return false;
}
const deadline = Date.now() + maxWaitMs;
const body = JSON.stringify({
model: modelAlias,
model: SINGLE_MODEL_ID,
messages: [{ role: "user", content: "ping" }],
max_tokens: 1, temperature: 0.0, stream: false,
});
// Body als Datei — verhindert Shell-Injection wenn modelAlias Sonderzeichen enthält
// Body als Datei — verhindert Shell-Injection wenn die Modell-ID Sonderzeichen enthält
const tmpBody = `/tmp/pi_ready_${Date.now()}_${Math.random().toString(36).slice(2)}.json`;
await pi.exec("bash", ["-c", `printf "%s" "$1" > "${tmpBody}"`, "_", body], { cwd: ctx.cwd });
let notified = false;
@ -536,13 +556,13 @@ async function waitUntilModelReady(
while (Date.now() < deadline) {
const r = await pi.exec("bash", ["-c",
`curl -s -o /dev/null -w "%{http_code}" --max-time 5 ` +
`-X POST http://localhost:${port}/v1/chat/completions ` +
`-X POST "$1/chat/completions" ` +
`-H "Content-Type: application/json" ` +
`-d "@${tmpBody}"`
`-d "@${tmpBody}"`, "_", endpoint
], { cwd: ctx.cwd });
if (r.stdout?.trim() === "200") return true;
if (!notified) {
ctx.ui.notify(`Modell-Server (Port ${port}) lädt noch — warte bis zu 3 min…`, "info");
ctx.ui.notify(`Modell-Server (${endpoint}) lädt noch — warte bis zu 3 min…`, "info");
notified = true;
}
await new Promise(res => setTimeout(res, 3000));
@ -1128,15 +1148,15 @@ export default function (pi: ExtensionAPI) {
// ── Manuelle Kommandos ───────────────────────────────────────────────────
pi.registerCommand("coder", {
description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (:8001).",
description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) {
const task = (args || "").trim();
if (!task) {
ctx.ui.notify("Benutzung: /coder <auftrag>", "error");
return;
}
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await ensureLocalGitRepo(pi, ctx);
@ -1149,10 +1169,10 @@ export default function (pi: ExtensionAPI) {
});
pi.registerCommand("judge", {
description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (:8001).",
description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await selectRole(pi, ctx, "judge");
@ -1163,10 +1183,10 @@ export default function (pi: ExtensionAPI) {
});
pi.registerCommand("fix", {
description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (:8001).",
description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await ensureLocalGitRepo(pi, ctx);
@ -1178,10 +1198,10 @@ export default function (pi: ExtensionAPI) {
});
pi.registerCommand("shipit", {
description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (:8001).",
description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await selectRole(pi, ctx, "judge");
@ -1246,11 +1266,11 @@ export default function (pi: ExtensionAPI) {
: `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`;
ctx.ui.notify(continueMsg, "info");
// Im --continue-Modus: Single-Server prüfen (Port 8001 — Coder + Judge in einem).
ctx.ui.setStatus("optimize", "Single-Server wird geprüft (Port 8001)…");
const serverReady = await waitUntilModelReady(pi, ctx, 8001, "coding_model");
// Im --continue-Modus: Single-Modell prüfen (Coder + Judge in einem).
ctx.ui.setStatus("optimize", `Single-Modell wird geprüft (${SINGLE_LABEL})…`);
const serverReady = await waitUntilModelReady(pi, ctx);
if (!serverReady) {
finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coding-server.sh ausführen");
finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`);
return;
}
} else {
@ -1260,7 +1280,7 @@ export default function (pi: ExtensionAPI) {
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
if (!await selectRole(pi, ctx, "coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return;
}
currentActivity = "Coder implementiert…";
@ -1274,8 +1294,8 @@ export default function (pi: ExtensionAPI) {
// GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen
// echten Completion-Request bedeutet: Modell ist im VRAM und bereit.
ctx.ui.setStatus("optimize", "Single-Server wird geprüft…");
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coding-server.sh ausführen");
if (!await waitUntilModelReady(pi, ctx)) {
finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`);
return;
}
}
@ -1313,7 +1333,7 @@ export default function (pi: ExtensionAPI) {
for (let round = 1; round <= maxRounds; round++) {
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
if (!await selectRole(pi, ctx, "judge")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return;
}
@ -1378,7 +1398,7 @@ export default function (pi: ExtensionAPI) {
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
: "Kritikpunkte aus Judge-Bericht";
if (!await selectRole(pi, ctx, "coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return;
}
currentActivity = `Coder fixt: ${blockerHint}`;
@ -1471,15 +1491,15 @@ export default function (pi: ExtensionAPI) {
// ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
pi.registerCommand("patch", {
description: "Gezielte Minimaländerung ohne Refactoring, committet → Coder-Rolle (:8001).",
description: "Gezielte Minimaländerung ohne Refactoring, committet → Coder-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) {
const change = (args || "").trim();
if (!change) {
ctx.ui.notify("Benutzung: /patch <beschreibung der änderung>", "error");
return;
}
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await ensureLocalGitRepo(pi, ctx);
@ -1491,10 +1511,10 @@ export default function (pi: ExtensionAPI) {
});
pi.registerCommand("quick_check", {
description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → Judge-Rolle (:8001).",
description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → Judge-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await selectRole(pi, ctx, "judge");
@ -1562,8 +1582,8 @@ export default function (pi: ExtensionAPI) {
ctx.ui.notify("Benutzung: /plan <auftrag>", "error");
return;
}
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await writeTaskMd(pi, ctx, task);
@ -1645,8 +1665,8 @@ export default function (pi: ExtensionAPI) {
}
// Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await selectRole(pi, ctx, "coder");