LM-Studio-Matrix mit gemma-4-e4b: zwoelf Laeufe und vier Befunde
Skill 12.0.0 bis 12.2.0 und Adapter 2.0.0 bis 2.2.0. Werkzeugfreigabe: Die Shell-Rechte des OpenCode-Adapters sind jetzt eine Denylist wie beim Claude-Adapter statt einer Allowlist. Ausloeser war der erste Qwen-Lauf, dessen einzige beide Werkzeugaufrufe an einer Pipeline scheiterten. Kontrolltest belegt beide Richtungen: Get-ChildItem | Format-Table laeuft durch, rm wird verweigert, die Datei bleibt bestehen. Lokaler Betrieb: Der Preflight entlaedt alle Modelle vor jedem Lauf und laedt den Kontext auf das Modellmaximum. Gemessen waren zuvor beide Modelle gleichzeitig geladen - 168 MiB frei von 16,3 GB. Qwen 27B passt auf dieser Karte nicht und wurde durch qwen3.5-9b ersetzt, in Q4_K_M wie Gemma. Messinstrument: analyse-anforderungen.py erkennt Feldnamen in Markdown- Fettschrift und Modulpraefixe in IDs. Der erste lokale Lauf mit Artefakten wurde sonst mit null Anforderungen gezaehlt statt mit neun. Regressionsprobe an vier Claude-Laeufen unveraendert. Befunde: Der Standard-Ausgabeblock, mit dem Claude sieben Artefakte erzeugt, liefert bei gemma-4-e4b null von sechs Laeufen ein Ergebnis - das Modell loest den Pfad relativ zum Arbeitsverzeichnis auf. Ein Lauf startete alle sieben vorgesehenen Rollen und lieferte neun formkonforme Anforderungen. Ein anderer erzeugte sieben richtig benannte Dateien ohne eine einzige formkonforme Anforderung. Und eine Shell-Umleitung schrieb an der Denylist vorbei in den eingefrorenen Snapshot - gefunden vom Vorher/Nachher-Vergleich, nicht von der Regel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
654339464e
commit
28e927013b
@@ -2,7 +2,7 @@
|
||||
name: run-experiment
|
||||
description: Führt einen Versuchs-Prompt aus einer Prompt-Datei als messbaren Headless-Lauf mit Claude Code, Codex CLI oder OpenCode (TensorX oder lokales LM Studio) aus und schreibt ein Messprotokoll mit Start-/Endzeit, Modell, Tokenverbrauch und weiteren Metriken. Verwenden bei "/run-experiment <Pfad-zur-Prompt-Datei>" oder wenn der User einen Versuch/ein Experiment ausführen und tracken will.
|
||||
argument-hint: <Pfad zur Prompt-Datei> <Root-Verzeichnis>
|
||||
version: 12.0.0
|
||||
version: 12.2.0
|
||||
---
|
||||
|
||||
# RunExperiment – Versuchslauf mit Messprotokoll
|
||||
@@ -127,12 +127,12 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen"
|
||||
3. **Werkzeugadapter bestimmen und CLI-Pfad auflösen.** Die Modell-ID entscheidet eindeutig:
|
||||
`claude-*` verwendet Claude Code, OpenAI-IDs wie `gpt-*` oder `o*` verwenden Codex CLI,
|
||||
`z-ai/*`, `qwen/qwen3.8-flash-next` und `moonshotai/*` verwenden OpenCode mit
|
||||
`--provider tensorx`, `google/gemma-4-e4b` und `qwen/qwen3.8-27b` verwenden OpenCode mit
|
||||
`--provider tensorx`, `google/gemma-4-e4b` und `qwen/qwen3.5-9b` verwenden OpenCode mit
|
||||
`--provider lmstudio` gegen den lokalen LM-Studio-Server.
|
||||
Keine Modell-ID an eine CLI übergeben, die sie nicht unterstützt.
|
||||
|
||||
**Achtung Präfixkollision:** `qwen/qwen3.8-flash-next` läuft remote über TensorX,
|
||||
`qwen/qwen3.8-27b` lokal über LM Studio. Das Präfix `qwen/` allein entscheidet **nicht** –
|
||||
`qwen/qwen3.5-9b` lokal über LM Studio. Das Präfix `qwen/` allein entscheidet **nicht** –
|
||||
maßgeblich ist die vollständige Modell-ID.
|
||||
|
||||
Unter Windows liegt `claude` in der Regel **nicht im PATH**. Erst
|
||||
@@ -186,7 +186,7 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen"
|
||||
| Lokale Modell-ID (LM Studio) | Einordnung |
|
||||
|---|---|
|
||||
| `google/gemma-4-e4b` | Gemma 4 E4B, 7,5B Parameter, lokal über LM Studio |
|
||||
| `qwen/qwen3.8-27b` | Qwen 3.8 27B, lokal über LM Studio |
|
||||
| `qwen/qwen3.5-9b` | Qwen 3.5 9B, lokal über LM Studio (Q4_K_M wie Gemma) |
|
||||
|
||||
Lokale IDs nur anbieten, wenn `lms ls` das Modell als heruntergeladen ausweist. Fehlt es,
|
||||
den User auf `lms get <ID>` hinweisen und den Download **nicht** ungefragt starten – es
|
||||
@@ -1119,7 +1119,7 @@ streamt die Rohereignisse, erzwingt Berechtigungen und normalisiert das Ergebnis
|
||||
| `--provider` | Modell-IDs | Betrieb | Vorlage |
|
||||
|---|---|---|---|
|
||||
| `tensorx` (Standard) | `z-ai/*`, `qwen/qwen3.8-flash-next`, `moonshotai/*` | Remote `https://api.tensorx.ai/v1` | `opencode-tensorx.json` |
|
||||
| `lmstudio` | `google/gemma-4-e4b`, `qwen/qwen3.8-27b` | lokal `http://localhost:1234/v1` | `opencode-lmstudio.json` |
|
||||
| `lmstudio` | `google/gemma-4-e4b`, `qwen/qwen3.5-9b` | lokal `http://localhost:1234/v1` | `opencode-lmstudio.json` |
|
||||
|
||||
Vor jedem Lauf die vollständige Referenz
|
||||
[`references/opencode-adapter.md`](references/opencode-adapter.md) lesen und deren Aufruf,
|
||||
@@ -1354,6 +1354,8 @@ der Historie unten – im selben Arbeitsschritt.
|
||||
|
||||
| Version | Änderung | Grund | Verwendet in |
|
||||
|---|---|---|---|
|
||||
| **12.2.0** | **`analyse-anforderungen.py` erkennt Feldnamen in Markdown-Fettschrift und Modulpraefixe in IDs.** `**ID:** M003-StRS-01` wird wie `ID: StRS-01` gelesen; die Ebene wird aus der ID auch dann bestimmt, wenn ein Praefix vorangeht. Zwei neue Hilfsskripte: `lauf-uebersicht.py` verdichtet die `RawResult.json` einer Matrix zu einer Tabelle und zeigt mit `--details` die tatsaechlichen Schreibziele; `protokoll-geruest.py` erzeugt aus den Rohdaten eines Laufs das Protokollgeruest und fuellt nur belegbare Felder - Deutung und Gueltigkeit bleiben Handarbeit. | Der erste lokale Lauf mit Artefakten (`Iteration 7/.../v12.1.0-001c`) erzeugte vier regelkonform gefuellte Dateien, wurde vom Parser aber mit **0 Anforderungen** gezaehlt: Das Modell formatierte die Feldnamen als Markdown. Nach der Korrektur sind es **9**. Die Praefixregel behob zugleich eine falsche Auffaelligkeitsmeldung ('StRS-Block in StRS.md' als Fremdablage). Regressionsprobe an vier Claude-Laeufen: 42/82/60/73 Anforderungen vor und nach der Aenderung identisch - die Korrektur findet nur zusaetzlich, was zuvor uebersehen wurde. `lauf-uebersicht.py` entstand, weil `written_files` bei fehlgeleiteten Schreibversuchen schlicht leer bleibt und die Ursache so unsichtbar ist. MINOR: Korrektur am Messinstrument, keine Aenderung der Versuchsbedingung; rueckwirkend auf alle Laeufe anwendbar. | rueckwirkend; ab sofort |
|
||||
| **12.1.0** | **Speicherhygiene und Kontextgroesse fuer lokale Laeufe; Modellwechsel `qwen/qwen3.8-27b` -> `qwen/qwen3.5-9b`.** Der Preflight entlaedt vor jedem Lauf **alle** Modelle (`lms unload --all`) und bricht ab, wenn neben dem angeforderten ein weiteres geladen ist. Neue Optionen `--lmstudio-context` (Standard `max`: laedt das Modellmaximum statt der Mindestgroesse), `--lmstudio-parallel` (Standard 4) und `--lmstudio-gpu` (Standard `max`). `local_runtime` fuehrt zusaetzlich `parallel_slots`, `gpu_offload` und `alleiniges_modell`. Adapter-Version 2.2.0. | Messungen vom 01.09.2026 auf einer RTX 5080 Laptop GPU (16.303 MiB): Gemma und Qwen 27B waren **gleichzeitig geladen** - 15.836 MiB belegt, 168 MiB frei, Durchsatz 0,028 Mio. Tokens/h. Nach `unload --all` laeuft Gemma mit 48,3 tok/s. Das Modellmaximum kostet fast nichts: 131.072 statt 32.768 Kontext bedeutet 6.854 statt 5.162 MiB und 46,8 statt 48,3 tok/s - vierfaches Fenster fuer 1,5 tok/s. `--parallel 4` ist gegenueber 1 messtechnisch neutral (47,7 vs. 48,3 tok/s). **Der Modellwechsel ist hardwarebedingt:** `qwen3.8-27b` belegt mit 17,74 GB Gewichten mehr, als die Karte hat; ein Generierungstest brach nach 10 Minuten ohne Ergebnis ab. Die 27B-Klasse ist auch mit kleinerer Quantisierung nicht messbar, weil der KV-Cache bei brauchbarem Kontext mehrere GB zusaetzlich fordert. `qwen3.5-9b` ist die groesste Qwen-Variante, die mit vollem Fenster hineinpasst, und laeuft wie Gemma in **Q4_K_M** - damit unterscheiden sich die beiden lokalen Modelle nur in der Groesse, nicht zusaetzlich in der Quantisierung. MINOR fuer die Ladeparameter; der Modellwechsel eroeffnet ohnehin eine neue Iteration, weil `qwen3.8-27b` keinen gueltigen Messpunkt geliefert hat. | ab der LM-Studio-Matrix in Iteration 13 (V1) bzw. 6 (V2) |
|
||||
| **12.0.0** | **Die Shell-Rechte des OpenCode-Adapters werden eine Denylist statt einer Allowlist** – spiegelbildlich zu den Claude-Eintraegen: alles erlaubt ausser den ausdruecklich gesperrten schreibenden und bauenden Kommandos (`rm`, `mv`, `sed -i`, schreibende `git`-Kommandos inkl. `fetch`/`pull`/`remote`, `dotnet`, `msbuild`, `npm install`, `Remove-Item`, `Set-Content`, `Out-File` u. a.). Das Catch-all `"*": "allow"` steht zuerst, weil OpenCode die **zuletzt passende** Regel gewinnen laesst. Zusaetzlich lehnt der Adapter `--stall-timeout > 0` jetzt fuer **jeden** lokalen Provider ab, nicht nur fuer die delegierenden Modi. Adapter-Version 2.0.0, vier neue Regressionstests. | Der Claude-Adapter erlaubt ueber eine Denylist jedes nicht gesperrte Kommando, der OpenCode-Adapter nur explizit Gelistetes. Die Werkzeugfreiheit war zwischen beiden **nie aequivalent** – ein Confounder fuer jeden Werkzeugvergleich. Ausloeser war der erste Qwen-Lauf (`v11.1.0-45b1`): Dessen **einzige beide** Werkzeugaufrufe, `Get-ChildItem ... | Format-Table ...`, wurden verweigert, weil die Allowlist nur Praefixe trifft und an Pipelines scheitert. Bei Gemma war das ein Randfall (1 von 106 Aufrufen), bei Qwen legte es den Lauf still. Der zweite Ausloeser: Qwen 27B benoetigte fuer einen einzelnen Schritt mehr als 15 Minuten, sodass der Stall-Timeout auch in `solo` Modellgeschwindigkeit als Haenger wertete. **Kontrolltest am 01.09.2026 bestaetigte beide Richtungen:** `Get-ChildItem ... | Format-Table Name` lief durch, `rm opfer.txt` wurde verweigert, und die Datei blieb auf der Platte. MAJOR: Die Toolfreigabe ist eine unabhaengige Variable; Laeufe ab dieser Version sind mit allen frueheren OpenCode- und TensorX-Laeufen nicht poolbar. | ab dem naechsten OpenCode-Lauf; Iterationen 10 und 11 bleiben unter der Allowlist |
|
||||
| **11.1.0** | **Der Adapter lehnt `--stall-timeout > 0` in den Modi `builtin` und `custom` ab.** Die Laufzeit wird dort ausschliesslich ueber `--max-runtime` begrenzt. Adapter-Version 1.3.0; Referenz und Aufrufbeschreibung entsprechend ergaenzt. | Der erste `builtin`-Lauf mit LM Studio (`v11.0.0-9ad0`) wurde nach 15:48 min abgebrochen, obwohl das Limit bei 60 min lag. Ursache war nicht das Modell: OpenCode sendet **keine Ereignisse, solange ein Subagent arbeitet**. Nach 8 Ereignissen in den ersten 39 Sekunden schwieg der Strom, waehrend der gestartete `explore`-Subagent lief; der Stall-Timeout deutete das als Haenger. Jeder Lauf mit Subagenten waere so zuverlaessig zu frueh gestorben. Die Kombination wird abgelehnt statt stillschweigend korrigiert, damit die Entscheidung bewusst faellt. MINOR: Es existierte noch **kein** gueltiger OpenCode-Lauf in `builtin` oder `custom`, dessen Bedingung sich dadurch aendern koennte; die `solo`-Laeufe waren nie betroffen, weil bei ihnen der Stall-Timeout nie griff. **Zu pruefen:** ob die als Fehler protokollierten TensorX-V2-Laeufe (Modus `custom`, `--stall-timeout 600`) dieselbe Ursache haben. | ab dem naechsten OpenCode-Lauf in `builtin` oder `custom` |
|
||||
| **11.0.0** | **Read-only-Shell-Allowlist des OpenCode-Adapters erweitert.** Neben `rg` und den lesenden `git`-Kommandos sind jetzt die verbreiteten POSIX-Werkzeuge `ls`, `cat`, `head`, `tail`, `find`, `grep`, `wc`, `file`, `stat`, `tree` sowie `dir`, `type`, `Get-Item` und `Measure-Object` freigegeben; `git log` und `git show` kommen hinzu. Alles Übrige bleibt `deny`. Zwei Regressionstests sichern die Liste ab: Sie muss die lesenden Kommandos enthalten und darf kein schreibendes enthalten. Adapter-Version 1.2.0. | Der erste LM-Studio-Lauf (`v10.1.0-b00a`, Gemma/solo) erzeugte zwei Permission-Denials auf `ls src` – das Kommando fehlte auf der Allowlist. Der Agent hatte damit kein POSIX-Mittel, Verzeichnisse aufzulisten, obwohl genau das laut Werkzeugkontext zur Bedingung gehört. Die Lücke benachteiligte OpenCode-Läufe gegenüber den Claude-Läufen, die mit einer Denylist arbeiten und deshalb jedes nicht ausdrücklich gesperrte Lesekommando erlauben. **MAJOR: Die Toolfreigabe ist eine unabhängige Variable.** Läufe ab dieser Version sind mit den bisherigen OpenCode- und TensorX-Läufen nicht poolbar; der nächste Lauf eröffnet eine neue Iteration. | ab dem nächsten OpenCode-Lauf; Iteration 10 bleibt unter der alten Allowlist |
|
||||
|
||||
Binary file not shown.
@@ -16,11 +16,24 @@ RISIKO = re.compile(r'sicherheit|abrechnung|fakturier|berechtigung|recht|zugriff
|
||||
r'passwort|rolle|lizenz|steuer|zahlung|mahn', re.I)
|
||||
|
||||
|
||||
# Feldnamen in Markdown-Fettschrift, z. B. "**ID:** StRS-01" statt "ID: StRS-01".
|
||||
# Modelle formatieren die Vorgabe des Prompts haeufig als Markdown aus; ohne
|
||||
# Normalisierung bleibt eine vollstaendig korrekt gefuellte Datei unerkannt.
|
||||
# Beobachtet am 01.09.2026 im Lauf Iteration 7/.../v12.1.0-001c: vier Dateien mit
|
||||
# regelkonformen Bloecken wurden als "keine Anforderungen" gezaehlt.
|
||||
FETTES_FELD = re.compile(r'(?m)^[ \t]*\*\*([^*:\n]{1,40}):\*\*[ \t]*')
|
||||
|
||||
|
||||
def normalisiere(text):
|
||||
"""Fettgedruckte Feldnamen auf die Klartextform des Prompts zuruecknehmen."""
|
||||
return FETTES_FELD.sub(lambda m: m.group(1) + ': ', text)
|
||||
|
||||
|
||||
def bloecke(pfad):
|
||||
"""Zerlegt eine Anforderungsdatei in Bloecke ab jeder ID:-Zeile."""
|
||||
if not os.path.exists(pfad):
|
||||
return
|
||||
text = io.open(pfad, encoding='utf-8').read()
|
||||
text = normalisiere(io.open(pfad, encoding='utf-8').read())
|
||||
teile = re.split(r'(?m)^ID:', text)
|
||||
for t in teile[1:]:
|
||||
yield 'ID:' + t
|
||||
@@ -44,9 +57,13 @@ def ebene_von(block, aid, datei_ebene):
|
||||
for eb in EBENEN:
|
||||
if f.strip().upper().startswith(eb.upper()):
|
||||
return eb, False
|
||||
# IDs tragen haeufig ein Modulpraefix ("M003-StRS-01"). Die Ebene steht dann
|
||||
# nicht am Anfang, sondern als Bestandteil der ID; ein reiner Praefixtest
|
||||
# meldet sonst eine Fremdablage, die es nicht gibt.
|
||||
kennung = aid.strip().upper()
|
||||
for eb in EBENEN:
|
||||
if aid.strip().upper().startswith(eb.upper()):
|
||||
return eb, aid.strip().upper()[:4] != datei_ebene.upper()[:4]
|
||||
if re.search(r'(?:^|[^A-Z])%s' % eb.upper(), kennung):
|
||||
return eb, eb.upper() != datei_ebene.upper()
|
||||
return datei_ebene, False
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verdichtet die RawResult.json unterhalb eines Verzeichnisses zu einer Tabelle.
|
||||
|
||||
Gedacht fuer die Sichtung einer Matrix: Welche Zelle hat gemessen, welche ist
|
||||
eine Fehlmessung, und woran lag es. Schreibt nichts - reine Auswertung.
|
||||
|
||||
python lauf-uebersicht.py "<Verzeichnis>" [--details]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def schreibziele(lauf: Path) -> list[str]:
|
||||
"""Wohin der Lauf zu schreiben versuchte, samt Ergebnis.
|
||||
|
||||
Der haeufigste Grund fuer ein leeres Ergebnisverzeichnis ist ein
|
||||
Schreibversuch an den falschen Ort - der ist ohne diese Aufstellung nicht
|
||||
zu sehen, weil ``written_files`` dann schlicht leer bleibt.
|
||||
"""
|
||||
session = lauf / "_meta" / "opencode-session.json"
|
||||
if not session.is_file():
|
||||
return []
|
||||
try:
|
||||
daten = json.loads(session.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return []
|
||||
ziele = []
|
||||
for nachricht in daten.get("messages", []):
|
||||
for teil in nachricht.get("parts", []):
|
||||
if teil.get("type") != "tool" or teil.get("tool") not in ("write", "edit"):
|
||||
continue
|
||||
zustand = teil.get("state", {})
|
||||
ziele.append(
|
||||
f"{teil.get('tool')} {zustand.get('status')} -> "
|
||||
f"{zustand.get('input', {}).get('filePath')}"
|
||||
)
|
||||
return ziele
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Uebersicht ueber Versuchslaeufe")
|
||||
parser.add_argument("verzeichnis")
|
||||
parser.add_argument("--details", action="store_true", help="Schreibziele zeigen")
|
||||
args = parser.parse_args()
|
||||
|
||||
wurzel = Path(args.verzeichnis)
|
||||
zeilen = []
|
||||
for datei in sorted(wurzel.rglob("RawResult.json")):
|
||||
try:
|
||||
d = json.loads(datei.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
lauf = datei.parent
|
||||
zeilen.append(
|
||||
{
|
||||
"lauf": lauf.name,
|
||||
"modus": d.get("mode", "?"),
|
||||
"exit": d.get("exit_code"),
|
||||
"status": d.get("subtype", "?"),
|
||||
"min": round(d.get("duration_ms", 0) / 60000, 1),
|
||||
"turns": d.get("num_turns", 0),
|
||||
"tools": d.get("tool_call_count", 0),
|
||||
"sub": d.get("subagent_stats", {}).get("spawned", 0),
|
||||
"dateien": len(d.get("written_files") or []),
|
||||
# Eine 0 aus einem laufenden Subagenten ist kein Messwert.
|
||||
"tokens": (
|
||||
d["usage"]["total_tokens"]
|
||||
if d.get("usage_captured", True)
|
||||
else None
|
||||
),
|
||||
"pfad": lauf,
|
||||
}
|
||||
)
|
||||
|
||||
if not zeilen:
|
||||
print("Keine RawResult.json gefunden.")
|
||||
return 1
|
||||
|
||||
kopf = f"{'Lauf':44} {'Modus':8} {'St':3} {'Min':>6} {'Turn':>5} {'Tool':>5} {'Sub':>4} {'Dat':>4} {'Tokens':>10}"
|
||||
print(kopf)
|
||||
print("-" * len(kopf))
|
||||
for z in zeilen:
|
||||
tok = "n. erf." if z["tokens"] is None else f"{z['tokens']:,}"
|
||||
print(
|
||||
f"{z['lauf'][:44]:44} {z['modus']:8} {str(z['exit']):3} {z['min']:6.1f} "
|
||||
f"{z['turns']:5} {z['tools']:5} {z['sub']:4} {z['dateien']:4} {tok:>10}"
|
||||
)
|
||||
if args.details:
|
||||
for ziel in schreibziele(z["pfad"]):
|
||||
print(f" {ziel}")
|
||||
|
||||
gueltig = [z for z in zeilen if z["dateien"] > 0]
|
||||
print(
|
||||
f"\n{len(zeilen)} Laeufe, davon {len(gueltig)} mit Ergebnisdateien, "
|
||||
f"{len(zeilen) - len(gueltig)} Fehlmessungen."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -16,8 +16,8 @@
|
||||
"output": 32768
|
||||
}
|
||||
},
|
||||
"qwen/qwen3.8-27b": {
|
||||
"name": "Qwen 3.8 27B (lokal)",
|
||||
"qwen/qwen3.5-9b": {
|
||||
"name": "Qwen 3.5 9B (lokal)",
|
||||
"limit": {
|
||||
"context": 262144,
|
||||
"output": 32768
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Erzeugt das Messprotokoll-Geruest eines Laufs aus seinen Rohdaten.
|
||||
|
||||
Fuellt ausschliesslich Felder, die aus `RawResult.json`, `_meta` und dem
|
||||
Dateibestand **belegbar** sind. Alles Uebrige bleibt als Platzhalter stehen und
|
||||
ist von Hand zu ergaenzen - insbesondere der Abschnitt *Anmerkungen*, der die
|
||||
Deutung enthaelt und nicht generierbar ist.
|
||||
|
||||
Nicht ermittelbare Groessen werden als `nicht erfasst` ausgewiesen, niemals
|
||||
geschaetzt (Randbedingung des Skills).
|
||||
|
||||
python protokoll-geruest.py "<Laufverzeichnis>" [--ueberschreiben]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
EBENEN_DATEIEN = ("StRS.md", "SyRS.md", "SwRS.md")
|
||||
|
||||
|
||||
def sha256(pfad: Path) -> str:
|
||||
return hashlib.sha256(pfad.read_bytes()).hexdigest().upper()
|
||||
|
||||
|
||||
def lies(pfad: Path, standard: str = "nicht erfasst") -> str:
|
||||
try:
|
||||
return pfad.read_text(encoding="utf-8").strip() or standard
|
||||
except OSError:
|
||||
return standard
|
||||
|
||||
|
||||
def zahl(n) -> str:
|
||||
"""Tausenderpunkte nach deutscher Schreibweise."""
|
||||
try:
|
||||
return f"{int(n):,}".replace(",", ".")
|
||||
except (TypeError, ValueError):
|
||||
return "nicht erfasst"
|
||||
|
||||
|
||||
def dauer(ms: int) -> str:
|
||||
s = ms // 1000
|
||||
return f"{s // 3600:02d}:{(s % 3600) // 60:02d}:{s % 60:02d}"
|
||||
|
||||
|
||||
def git_kopf(repo: Path) -> str:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["git", "-C", str(repo), "rev-parse", "HEAD"],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
return r.stdout.strip() or "nicht erfasst"
|
||||
except OSError:
|
||||
return "nicht erfasst"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Protokollgeruest erzeugen")
|
||||
parser.add_argument("lauf")
|
||||
parser.add_argument("--repo", default="C:/DEV/MasterArbeit")
|
||||
parser.add_argument("--ueberschreiben", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
lauf = Path(args.lauf).resolve()
|
||||
ziel = lauf / "Protokoll.md"
|
||||
if ziel.exists() and not args.ueberschreiben:
|
||||
print(f"{ziel} existiert bereits - mit --ueberschreiben erzwingen.")
|
||||
return 1
|
||||
|
||||
roh = json.loads((lauf / "RawResult.json").read_text(encoding="utf-8"))
|
||||
meta = lauf / "_meta"
|
||||
u = roh.get("usage", {})
|
||||
lr = roh.get("local_runtime") or {}
|
||||
erfasst = roh.get("usage_captured", True)
|
||||
|
||||
def tok(feld: str) -> str:
|
||||
return zahl(u.get(feld, 0)) if erfasst else "nicht erfasst"
|
||||
|
||||
# Ebene und Iteration aus der Ablagestruktur ableiten
|
||||
teile = lauf.parts
|
||||
iteration = next((p for p in teile if p.startswith("Iteration")), "?")
|
||||
zelle = "/".join(teile[teile.index(iteration):-1]) if iteration in teile else "?"
|
||||
|
||||
# Die Prompt-Datei liegt im Versuchsordner, also oberhalb der
|
||||
# Bedingungsebenen; deren Anzahl schwankt mit der Modell-ID (ein- oder
|
||||
# zweiteilig). Deshalb aufwaerts suchen statt eine feste Tiefe annehmen.
|
||||
prompt_datei = None
|
||||
for eltern in lauf.parents[:8]:
|
||||
treffer = sorted(eltern.glob("*_Prompt.md"))
|
||||
if treffer:
|
||||
prompt_datei = treffer[-1]
|
||||
break
|
||||
anforderungen = lies(meta / "anforderungen.md", "*(nicht ausgewertet)*")
|
||||
dateien = sorted(p.name for p in (lauf / "Ergebnisse").glob("*") if p.is_file())
|
||||
vorher, nachher = lies(meta / "before.txt", ""), lies(meta / "after.txt", "")
|
||||
|
||||
text = f"""# Messprotokoll – {zelle}
|
||||
|
||||
> **GERUEST** – maschinell aus den Rohdaten erzeugt. Die Abschnitte *Anmerkungen*
|
||||
> und *Gueltigkeit* sind von Hand zu pruefen und zu ergaenzen.
|
||||
|
||||
## Lauf
|
||||
- **Prompt-Datei:** `{prompt_datei.name if prompt_datei else 'nicht erfasst'}`
|
||||
- **SHA-256 (Prompt):** `{sha256(prompt_datei) if prompt_datei else 'nicht erfasst'}`
|
||||
- **Startzeit:** {lies(meta / 'startzeit.txt')}
|
||||
- **Endzeit:** {lies(meta / 'endzeit.txt')}
|
||||
- **Dauer gesamt:** {dauer(roh.get('duration_ms', 0))} (API: nicht erfasst – OpenCode liefert keine separate API-Zeit)
|
||||
- **Root-Verzeichnis:** `{args.repo}/QuellCode/CentronERP`
|
||||
- **Codebasis-Commit:** `{git_kopf(Path(args.repo))}` (vor dem Lauf dirty: {'nein' if not vorher else 'ja – ' + vorher.replace(chr(10), '; ')})
|
||||
- **Snapshot-Zustand:** bereinigt von KI-Konfigurationen: ja; Remote entkoppelt: ja
|
||||
|
||||
## Werkzeugkonfiguration
|
||||
- **Skill-Version:** {lauf.name.split('_v')[-1].split('-')[0] if '_v' in lauf.name else 'nicht erfasst'}
|
||||
- **Werkzeugadapter:** OpenCode, `opencode-adapter.py --provider {roh.get('provider')}` (Adapter-Version {roh.get('adapter_version')})
|
||||
- **CLI-Version:** OpenCode {roh.get('opencode_version', 'nicht erfasst')}
|
||||
- **Modell (angefordert):** `{roh.get('model_requested')}`
|
||||
- **Modell (tatsaechlich):** `{roh.get('model')}`
|
||||
- **Kontrolle Modell:** {'bestanden' if roh.get('model') == roh.get('model_requested') else '**verletzt**'}
|
||||
- **Effort:** `{roh.get('effort')}` angefordert, **{'wirksam' if roh.get('effort_applied') else 'nicht wirksam'}** (`effort_applied: {str(roh.get('effort_applied')).lower()}`)
|
||||
- **Ablage:** `{zelle}/`
|
||||
- **Agentenmodus:** `{roh.get('mode')}`
|
||||
- **Kontextfenster:** {zahl(roh.get('context_window', 0))} Tokens geladen (Modellmaximum {zahl(lr.get('max_context_length', 0))})
|
||||
- **Sampling-Parameter:** nicht steuerbar
|
||||
- **Lokaler Modellbetrieb:** Runtime `{lr.get('compatibility_type', '?')}`, `lms` {lr.get('lms_version', '?')}, Architektur `{lr.get('arch', '?')}`, **Quantisierung `{lr.get('quantization', '?')}`**, {lr.get('parallel_slots', '?')} Slots, GPU-Offload `{lr.get('gpu_offload', '?')}`, alleiniges Modell: {str(lr.get('alleiniges_modell', '?')).lower()}
|
||||
- **Abbruchsicherungen:** `--stall-timeout 0`, `--max-runtime` siehe Matrixskript
|
||||
- **Subagenten:** `spawned` = {roh.get('subagent_stats', {}).get('spawned', 0)}, `completed` = {roh.get('subagent_stats', {}).get('completed', 0)}, `failed` = {roh.get('subagent_stats', {}).get('failed', 0)}
|
||||
- **Rollen:** {json.dumps(roh.get('subagent_stats', {}).get('by_type', {}), ensure_ascii=False)}
|
||||
|
||||
## Validierungsstichprobe
|
||||
- **Stand:** entfaellt
|
||||
|
||||
## Verbrauch
|
||||
|
||||
| Messgroesse | Wert |
|
||||
|---|---|
|
||||
| Input-Tokens | {tok('prompt_tokens')} |
|
||||
| Output-Tokens | {tok('completion_tokens')} |
|
||||
| Reasoning-Tokens | {tok('reasoning_tokens')} |
|
||||
| Cache-Write-/Cache-Read-Tokens | nicht erfasst – der lokale Server liefert keine |
|
||||
| Agent-Turns | {roh.get('num_turns', 0)} |
|
||||
|
||||
**Tokens gesamt: {tok('total_tokens')}.** Kosten `0` – lokaler Betrieb ({roh.get('cost_source', '')}).
|
||||
{'' if erfasst else chr(10) + '**Achtung:** ' + roh.get('usage_note', '') + chr(10)}
|
||||
## Gefundene Anforderungen
|
||||
|
||||
{anforderungen}
|
||||
|
||||
## Ergebnis
|
||||
- **Status:** `is_error: {str(roh.get('is_error')).lower()}`, `subtype: {roh.get('subtype')}`, `exit_code: {roh.get('exit_code')}`, `timed_out: {str(roh.get('timed_out')).lower()}`
|
||||
- **Session-ID:** `{roh.get('session_id')}`
|
||||
- **Werkzeugaufrufe:** {roh.get('tool_call_count', 0)} – {json.dumps(roh.get('tool_call_types', {}), ensure_ascii=False)}
|
||||
- **Kontrolle Agentenmodus:** `subagent_stats.spawned` = {roh.get('subagent_stats', {}).get('spawned', 0)}{' – korrekt fuer solo' if roh.get('mode') == 'solo' else ''}
|
||||
- **Gueltigkeit:** *(pruefen)* {'**Fehlmessung** – Ergebnisverzeichnis leer.' if not dateien else 'Ergebnisdateien vorhanden.'}
|
||||
- **Erzeugte Dateien:** {', '.join(dateien) if dateien else 'keine'}
|
||||
- **Root unveraendert:** {'ja' if vorher == nachher else '**nein – pruefen**'}
|
||||
- **Fehlermeldungen:** {json.dumps(roh.get('errors', []), ensure_ascii=False)}
|
||||
|
||||
## Anmerkungen/Auffaelligkeiten
|
||||
|
||||
*(von Hand zu ergaenzen)*
|
||||
"""
|
||||
ziel.write_text(text, encoding="utf-8")
|
||||
print(f"{ziel}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -252,8 +252,8 @@ class OpenCodeLmStudioTests(unittest.TestCase):
|
||||
ADAPTER.normalize_model("google/gemma-4-e4b", "lmstudio"),
|
||||
)
|
||||
self.assertEqual(
|
||||
("lmstudio/qwen/qwen3.8-27b", "qwen/qwen3.8-27b"),
|
||||
ADAPTER.normalize_model("lmstudio/qwen/qwen3.8-27b", "lmstudio"),
|
||||
("lmstudio/qwen/qwen3.5-9b", "qwen/qwen3.5-9b"),
|
||||
ADAPTER.normalize_model("lmstudio/qwen/qwen3.5-9b", "lmstudio"),
|
||||
)
|
||||
|
||||
def test_template_declares_both_local_models_without_key(self):
|
||||
@@ -267,7 +267,7 @@ class OpenCodeLmStudioTests(unittest.TestCase):
|
||||
"http://localhost:1234/v1", provider["options"]["baseURL"]
|
||||
)
|
||||
self.assertEqual(
|
||||
{"google/gemma-4-e4b", "qwen/qwen3.8-27b"},
|
||||
{"google/gemma-4-e4b", "qwen/qwen3.5-9b"},
|
||||
set(provider["models"]),
|
||||
)
|
||||
# Lokale Server pruefen den Key nicht; er darf nur ein Platzhalter sein.
|
||||
|
||||
Reference in New Issue
Block a user