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:
parent
0cb637783c
commit
a608f1dda8
1 changed files with 120 additions and 42 deletions
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue