Add TensorX models for Versuch 2

This commit is contained in:
Christoph Schwörer
2026-08-31 16:51:35 +02:00
parent 8a22d586f1
commit ca52aa4701
4 changed files with 751 additions and 176 deletions
+129 -21
View File
@@ -1,8 +1,8 @@
--- ---
name: run-experiment name: run-experiment
description: Führt einen Versuchs-Prompt aus einer Prompt-Datei als messbaren Headless-Lauf mit Claude Code, Codex CLI oder dem Python-API-Adapter (GLM/Kimi) 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. description: Führt einen Versuchs-Prompt aus einer Prompt-Datei als messbaren Headless-Lauf mit Claude Code, Codex CLI oder dem Python-API-Adapter (GLM/Qwen/Kimi) 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> argument-hint: <Pfad zur Prompt-Datei> <Root-Verzeichnis>
version: 8.0.0 version: 9.3.0
--- ---
# RunExperiment – Versuchslauf mit Messprotokoll # RunExperiment – Versuchslauf mit Messprotokoll
@@ -101,7 +101,7 @@ Der Skill ist zweigeteilt:
- **`## Prozess`** beschreibt werkzeugneutral, **was** je Lauf zu tun und zu erheben ist. Dieser - **`## Prozess`** beschreibt werkzeugneutral, **was** je Lauf zu tun und zu erheben ist. Dieser
Teil gilt unabhängig davon, mit welchem LLM oder welcher CLI gearbeitet wird. Teil gilt unabhängig davon, mit welchem LLM oder welcher CLI gearbeitet wird.
- **`## Werkzeugadapter`** beschreibt **wie** das mit einem konkreten Werkzeug umgesetzt wird. - **`## Werkzeugadapter`** beschreibt **wie** das mit einem konkreten Werkzeug umgesetzt wird.
Ausgearbeitet sind Adapter für Claude Code, Codex CLI und den Python-API-Adapter (GLM/Kimi). Konkrete Flags und Rohfelder sind Ausgearbeitet sind Adapter für Claude Code, Codex CLI und den Python-API-Adapter (GLM/Qwen/Kimi). Konkrete Flags und Rohfelder sind
ausschließlich dem gewählten Adapterabschnitt zu entnehmen. ausschließlich dem gewählten Adapterabschnitt zu entnehmen.
**Arbeitsteilung mit der Prompt-Datei:** Der Prompt enthält ausschließlich die *Analyseanweisung* **Arbeitsteilung mit der Prompt-Datei:** Der Prompt enthält ausschließlich die *Analyseanweisung*
@@ -125,7 +125,7 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen"
2. Aus dem Metadaten-Block der Prompt-Datei (falls vorhanden) Versuch/Prompt-Version übernehmen. 2. Aus dem Metadaten-Block der Prompt-Datei (falls vorhanden) Versuch/Prompt-Version übernehmen.
3. **Werkzeugadapter bestimmen und CLI-Pfad auflösen.** Die Modell-ID entscheidet eindeutig: 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, `claude-*` verwendet Claude Code, OpenAI-IDs wie `gpt-*` oder `o*` verwenden Codex CLI,
`z-ai/*` und `moonshotai/*` verwenden den Python-API-Adapter über den TensorX-Gateway. `z-ai/*`, `qwen/*` und `moonshotai/*` verwenden den Python-API-Adapter über den TensorX-Gateway.
Keine Modell-ID an eine CLI übergeben, die sie nicht unterstützt. Keine Modell-ID an eine CLI übergeben, die sie nicht unterstützt.
Unter Windows liegt `claude` in der Regel **nicht im PATH**. Erst Unter Windows liegt `claude` in der Regel **nicht im PATH**. Erst
@@ -166,6 +166,8 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen"
| TensorX-Modell-ID | Einordnung | | TensorX-Modell-ID | Einordnung |
|---|---| |---|---|
| `z-ai/glm-5.2` | Z.AI GLM 5.2 über TensorX-Gateway | | `z-ai/glm-5.2` | Z.AI GLM 5.2 über TensorX-Gateway |
| `z-ai/glm-5.3-flash` | Z.AI GLM 5.3 Flash über TensorX-Gateway |
| `qwen/qwen3.8-flash-next` | Qwen 3.8 Flash Next über TensorX-Gateway |
| `moonshotai/kimi-k3` | Moonshot Kimi K3 – 1-Mio.-Kontext, 2,8T Parameter | | `moonshotai/kimi-k3` | Moonshot Kimi K3 – 1-Mio.-Kontext, 2,8T Parameter |
In der Frage den letzten verwendeten Stand nennen, damit der User bewusst wechseln oder In der Frage den letzten verwendeten Stand nennen, damit der User bewusst wechseln oder
@@ -223,6 +225,30 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen"
unangetastet und die Agentenkonfiguration ist eine dokumentierte, versionierte unangetastet und die Agentenkonfiguration ist eine dokumentierte, versionierte
Versuchsbedingung. Format siehe `claude --help` zu `--agents`. Versuchsbedingung. Format siehe `claude --help` zu `--agents`.
**Delegationstiefe – nur Modus `custom`, beim User erfragen.** Zwei Fassungen der
Agentendatei stehen bereit; sie unterscheiden sich ausschließlich darin, ob die Rollen selbst
delegieren dürfen:
| Fassung | Rollen dürfen delegieren | Wirkung |
|---|---|---|
| `verschachtelt` | ja | Rollen erben über `--agents` alle Werkzeuge einschließlich `Task` und starten eigene Subagenten. `max_depth` > 1. |
| `unverschachtelt` | nein | Jede Rolle führt ihren Auftrag selbst aus. Nur der Hauptagent delegiert; `max_depth` soll 1 sein. |
**Warum das eine eigene Option ist.** Im ersten V2-Lauf (`v9.1.0-0c39`) entfielen 26 von
86 Subagenten auf Starts durch Subagenten, `max_depth` = 3. Das war nicht beabsichtigt – der
`iso29148-orchestrator` ist ausdrücklich als nicht-delegierende Rolle entworfen – und es
verteuert den Lauf erheblich, weil jede zusätzliche Ebene ihren Kontext erneut liest. Es
verwischt außerdem die Zuständigkeit: Ein von einer Rolle gestarteter Subagent ist keiner
Teilaufgabe der Bindungstabelle mehr zuzuordnen.
Die Fassung ist **nicht** technisch erzwungen, sondern in jedem Rollenprompt als Abschnitt
*Keine Weiterdelegation* formuliert. Kontrolle nach dem Lauf: `subagent_stats.max_depth` und
`spawned_by_subagents`. Beide gehen ins Protokoll; bei `unverschachtelt` und
`spawned_by_subagents` > 0 ist die Bedingung verletzt und im Protokoll zu kennzeichnen.
Beide Fassungen liegen neben der Prompt-Datei und werden per SHA-256 geführt; die gewählte
Fassung ist über ihren Hash eindeutig belegt.
**Codex-Adapter ab Version 5.0.0:** ausschließlich `solo` ist freigegeben und wird doppelt **Codex-Adapter ab Version 5.0.0:** ausschließlich `solo` ist freigegeben und wird doppelt
mit `--disable multi_agent` sowie `-c agents.enabled=false` erzwungen. Bei `builtin` oder mit `--disable multi_agent` sowie `-c agents.enabled=false` erzwungen. Bei `builtin` oder
`custom` abbrechen und mitteilen, dass dieser Adaptermodus noch nicht verifiziert ist. Eine `custom` abbrechen und mitteilen, dass dieser Adaptermodus noch nicht verifiziert ist. Eine
@@ -262,7 +288,7 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen"
Sort-Object { [int]($_.Name -replace '\D','') } Sort-Object { [int]($_.Name -replace '\D','') }
$iteration = if ($iterationen) { $iterationen[-1].Name } else { 'Iteration 1' } $iteration = if ($iterationen) { $iterationen[-1].Name } else { 'Iteration 1' }
$zelle = Join-Path (Join-Path (Join-Path $iteration $modell) $modus) $effort $zelle = Join-Path (Join-Path (Join-Path $iteration $modell) $modus) $effort
$skillVer = 'v8.0.0' # entspricht version: im Frontmatter dieses Skills $skillVer = 'v9.3.0' # entspricht version: im Frontmatter dieses Skills
do { do {
$id4 = '{0:x4}' -f (Get-Random -Maximum 65536) $id4 = '{0:x4}' -f (Get-Random -Maximum 65536)
$lauf = Join-Path "<promptverzeichnis>\$zelle" "<NN>_Lauf_$(Get-Date -Format 'yyyy-MM-dd_HHmmss')_${skillVer}-$id4" $lauf = Join-Path "<promptverzeichnis>\$zelle" "<NN>_Lauf_$(Get-Date -Format 'yyyy-MM-dd_HHmmss')_${skillVer}-$id4"
@@ -344,6 +370,50 @@ Der Inhalt richtet sich nach dem Agentenmodus:
| `builtin` | zusätzlich die werkzeugeigenen Subagenten | spezialisierte Agentenrollen aus Konfigurationsdateien, externe Werkzeugserver | | `builtin` | zusätzlich die werkzeugeigenen Subagenten | spezialisierte Agentenrollen aus Konfigurationsdateien, externe Werkzeugserver |
| `custom` | zusätzlich die beigestellten Agentenrollen (namentlich nennen) | externe Werkzeugserver, sofern nicht Teil des Versuchs | | `custom` | zusätzlich die beigestellten Agentenrollen (namentlich nennen) | externe Werkzeugserver, sofern nicht Teil des Versuchs |
**Zuständigkeitsbindung – nur Modus `custom`.** Der Prompt verlangt im Abschnitt
*Arbeitsteilung*, dass eine Teilaufgabe von dem dafür vorgesehenen Bearbeiter ausgeführt wird,
nennt aber bewusst keine Rolle – sonst wäre er nicht mehr für `solo` und `builtin` gültig. Welche
Rolle wofür zuständig ist, gehört deshalb in den Werkzeugkontext. Block 1 wird bei `custom` um
folgende Tabelle ergänzt, mit den Rollennamen aus der `--agents`-Datei:
```
Für die folgenden Teilaufgaben stehen vorgesehene Bearbeiter bereit. Führe diese
Teilaufgaben durch den jeweils genannten Bearbeiter aus, nicht selbst:
| Teilaufgabe | Vorgesehener Bearbeiter |
|---|---|
| Modulinventar (Schritt 0) | modulinventar |
| Faktenerhebung zu einem Modulausschnitt (Schritte 2 bis 4) | faktenermittler |
| Formulierung der StRS-Anforderungen | strs-autor |
| Formulierung der SyRS-Anforderungen | syrs-autor |
| Formulierung der SwRS-Anforderungen samt Konsolidierungsprüfung | swrs-autor |
| Prüfung ausgewiesener Belege gegen die Codebasis | belegpruefer |
| Prüfung des Gesamtbestands an den Nahtstellen der Ausschnitte | iso29148-orchestrator |
| Konsistenzcheck des fertigen Anforderungssatzes (Abschnitt Abschluss) | konsistenzpruefer |
Zuschnitt, Anzahl der Aufträge je Bearbeiter, deren Reihenfolge und die Tiefe
entscheidest du. Gebunden ist allein, wer eine Teilaufgabe ausführt. Die Bearbeiter
lesen nur; das Anlegen der Ergebnisdateien und die Übernahme ihrer Rückmeldungen
bleiben deine Aufgabe.
```
Die Tabelle wird aus der `--agents`-Datei abgeleitet und **nicht** freihändig formuliert: Jede
Zeile nennt eine Rolle, die dort definiert ist, und jede definierte Rolle kommt vor. Kommt eine
Rolle in der Datei vor, aber nicht in der Tabelle, ist sie im Lauf faktisch nicht vorgesehen –
das ist zulässig, aber im Protokoll zu vermerken.
**Warum die Bindung keine Strategieinjektion ist.** Sie ist vor dem Lauf statisch deklariert,
wird als Teil von `_meta\combined_prompt.md` archiviert und ist über den SHA-256 der
`--agents`-Datei versioniert. Damit unterscheidet sie sich grundlegend von den in Version 9.0.0
entfernten **laufzeitabhängigen** Eingriffen des TensorX-Adapters (Subagenten-Limit,
turnabhängige Schreib-Erinnerungen), die den Lauf abhängig von seinem eigenen Verlauf umsteuerten
und Lauf 34 unpoolbar machten. Was in `custom` weiterhin **nicht** vorgegeben wird: Anzahl der
Aufrufe, Zerlegungstiefe, Reihenfolge und Turn-Anzahl.
**Die Bindung ist nicht technisch erzwungen.** Sie wirkt über den Prompt; ob der Agent sie
einhält, ist selbst eine Messgröße. Nach dem Lauf ist sie zu prüfen – siehe Protokollfeld
*Zuständigkeitsbindung*.
**Block 2 – Ausgabeverzeichnis.** Damit die Ergebnisse im Laufverzeichnis landen und nicht in **Block 2 – Ausgabeverzeichnis.** Damit die Ergebnisse im Laufverzeichnis landen und nicht in
der Codebasis: der Codebasis:
@@ -772,6 +842,15 @@ Vorlage:
- **MCP-Server / Agentendateien:** <keine – aus dem Snapshot entfernt, zusätzlich --safe-mode | - **MCP-Server / Agentendateien:** <keine – aus dem Snapshot entfernt, zusätzlich --safe-mode |
Liste der Server mit Pfad und SHA-256 der `--mcp-config`-Datei; Pfad und SHA-256 der Liste der Server mit Pfad und SHA-256 der `--mcp-config`-Datei; Pfad und SHA-256 der
`--agents`-Datei> `--agents`-Datei>
- **Delegationstiefe (nur `custom`):** <verschachtelt | unverschachtelt> gemäß gewählter
Agentendatei. Kontrolle: `subagent_stats.max_depth` und `spawned_by_subagents`. Bei
`unverschachtelt` muss `spawned_by_subagents` = 0 und `max_depth` = 1 sein; andernfalls hat
die Rollenvorgabe nicht gegriffen und der Lauf ist entsprechend zu kennzeichnen.
- **Zuständigkeitsbindung (nur `custom`):** je Rolle die Zahl der Aufrufe aus
`subagent_stats.by_type`; Rollen mit null Aufrufen namentlich nennen. Dazu der Abgleich mit
der Dokumentationspflicht aus dem `Analysebericht.md`: Welche Teilaufgabe hat der Hauptagent
entgegen der Bindung selbst ausgeführt, und hat er das dort offengelegt? Eine nicht
offengelegte Abweichung ist im Protokoll als solche zu kennzeichnen.
- **Umgebungsprüfung (nur `custom` / MCP):** <keine Hooks, Plugins, Output-Styles im - **Umgebungsprüfung (nur `custom` / MCP):** <keine Hooks, Plugins, Output-Styles im
User-Profil vorgefunden | **abweichend**: welche> User-Profil vorgefunden | **abweichend**: welche>
- **Subagenten:** <Anzahl und Typ aus `subagent_stats`, z. B. 8 × Explore, 0 fehlgeschlagen> - **Subagenten:** <Anzahl und Typ aus `subagent_stats`, z. B. 8 × Explore, 0 fehlgeschlagen>
@@ -1003,7 +1082,7 @@ die angeforderte ID; da `codex exec --json` sie im Ereignisstrom nicht wiederhol
Modellkontrolle im Protokoll `nicht prüfbar`. Temperatur und weitere Sampling-Parameter sind in Modellkontrolle im Protokoll `nicht prüfbar`. Temperatur und weitere Sampling-Parameter sind in
diesem CLI-Ablauf nicht steuerbar; Effort und Service-Tier werden dagegen explizit festgelegt. diesem CLI-Ablauf nicht steuerbar; Effort und Service-Tier werden dagegen explizit festgelegt.
### Adapter: Python API / GLM & Kimi-Modelle über TensorX ### Adapter: Python API / GLM-, Qwen- und Kimi-Modelle über TensorX
Dieser Adapter wurde für Modelle entwickelt, die über den **TensorX API-Gateway** Dieser Adapter wurde für Modelle entwickelt, die über den **TensorX API-Gateway**
(`https://api.tensorx.ai/v1`) erreichbar sind. Er nutzt das Skript `glm-kimi-adapter.py`, (`https://api.tensorx.ai/v1`) erreichbar sind. Er nutzt das Skript `glm-kimi-adapter.py`,
@@ -1015,9 +1094,11 @@ REST-API implementiert. Referenzstand bei Einführung: **Python 3.13, requests 2
| TensorX-Modell-ID | Hersteller | Effort-Parameter | | TensorX-Modell-ID | Hersteller | Effort-Parameter |
|---|---|---| |---|---|---|
| `z-ai/glm-5.2` | Z.AI (Zhipu AI) | `thinking` (`{"type":"enabled","level":"…"}`) | | `z-ai/glm-5.2` | Z.AI (Zhipu AI) | `thinking` (`{"type":"enabled","level":"…"}`) |
| `z-ai/glm-5.3-flash` | Z.AI (Zhipu AI) | `thinking` (`{"type":"enabled","level":"…"}`) |
| `qwen/qwen3.8-flash-next` | Alibaba Qwen | `thinking` (`{"type":"enabled","level":"…"}`) |
| `moonshotai/kimi-k3` | Moonshot AI | `reasoning_effort` (top-level) | | `moonshotai/kimi-k3` | Moonshot AI | `reasoning_effort` (top-level) |
Das Modell-Präfix (`z-ai/` bzw. `moonshotai/`) bestimmt, welcher Effort-Parameter an die Das Modell-Präfix (`z-ai/`, `qwen/` bzw. `moonshotai/`) bestimmt, welcher Effort-Parameter an die
API gesendet wird. Der Adapter wählt ihn automatisch anhand des Präfixes. API gesendet wird. Der Adapter wählt ihn automatisch anhand des Präfixes.
**Authentifizierung.** Der API-Key wird **automatisch aus der Cline providers.json** **Authentifizierung.** Der API-Key wird **automatisch aus der Cline providers.json**
@@ -1025,13 +1106,29 @@ gelesen (`~/.cline/data/settings/providers.json`, Provider `tensorx`). Alternati
er per `--api-key` oder Umgebungsvariable `TENSORX_API_KEY` übergeben werden. Der Key er per `--api-key` oder Umgebungsvariable `TENSORX_API_KEY` übergeben werden. Der Key
wird **nicht** in Laufartefakten gespeichert. wird **nicht** in Laufartefakten gespeichert.
**Unterstützter Agentenmodus:** `solo` (V1) und `builtin` (V1b). Der Modus wird per **Unterstützte Agentenmodi:** `solo` (V1), `builtin` (V1b) und `custom` (V2). Der Modus wird per
`--mode solo|builtin` gesteuert. Im Modus `solo` steht das `spawn_subagent`-Tool nicht `--mode solo|builtin|custom` gesteuert. Im Modus `solo` steht das `spawn_subagent`-Tool nicht
zur Verfügung. Im Modus `builtin` kann der Hauptagent Subagenten mit eigenem Kontext zur Verfügung. Im Modus `builtin` kann der Hauptagent Subagenten mit eigenem Kontext
starten (`spawn_subagent`-Tool) – diese erhalten Read-Only-Tools (kein `write_file`) und starten (`spawn_subagent`-Tool) – diese erhalten Read-Only-Tools (kein `write_file`) und
eine eigene, vom Hauptagenten unabhängige Konversation. Der Subagent-Typ (`explore` oder eine eigene, vom Hauptagenten unabhängige Konversation. Anzahl und Typen bestimmt allein
`general-purpose`) bestimmt den System-Prompt. Subagent-Token fließen vollständig in das Modell; der Adapter setzt weder ein Anzahl- noch standardmäßig ein Turn-Limit. Fordert
`usage` und `modelUsage` ein. `custom` ist nicht freigegeben und führt zum Abbruch. das Modell in einer Antwort mehrere Subagenten an, laufen sie tatsächlich parallel. Der
Subagent-Typ (`explore` oder `general-purpose`) bestimmt den System-Prompt. Subagent-Token
fließen vollständig in `usage` und `modelUsage` ein. Im Modus `custom` werden die Rollen mit
`--agents <JSON-Datei>` geladen und über dasselbe `spawn_subagent`-Werkzeug bereitgestellt.
**Lebenszeichen.** Der Adapter schreibt standardmäßig alle 60 Sekunden eine
`LIFESIGN`-Zeile nach `Stderr.log`. Sie nennt die aktiven Operationen des Hauptagenten und
jedes Subagenten, beispielsweise einen API-Turn, einen Tool-Aufruf oder das Warten auf eine
parallele Subagentengruppe. Das Intervall ist mit `--heartbeat-interval` steuerbar. Ein
Lebenszeichen während eines nicht gestreamten HTTP-Aufrufs beweist nur, dass der lokale
Adapterprozess lebt und auf TensorX wartet; es ist kein Nachweis, dass serverseitig weiterhin
Tokens erzeugt werden.
**Keine impliziten Abbrüche.** `--timeout 0`, `--max-turns 0` und
`--subagent-max-turns 0` bedeuten tatsächlich unbegrenzt. Der Adapter injiziert keine
laufzeit- oder limitbedingten Schreibaufforderungen. Damit bleibt es Teil der Messung, ob,
wie und mit wie vielen Subagenten ein Modell zum Abschluss kommt.
**Isolation.** Der Adapter ist ein eigenständiges Python-Skript, das nur die **Isolation.** Der Adapter ist ein eigenständiges Python-Skript, das nur die
Python-Standardbibliothek und `requests` benötigt. Es liest die Codebasis über die Python-Standardbibliothek und `requests` benötigt. Es liest die Codebasis über die
@@ -1043,13 +1140,13 @@ zusätzliche Pflicht.
**Effort-Steuerung.** Der Adapter mappt die Skill-Effort-Stufen auf beide APIs: **Effort-Steuerung.** Der Adapter mappt die Skill-Effort-Stufen auf beide APIs:
| Skill-Effort | GLM `thinking.level` | Kimi `reasoning_effort` | | Skill-Effort | GLM `thinking.level` | Qwen `thinking.level` | Kimi `reasoning_effort` |
|---|---|---| |---|---|---|---|
| `low` | `low` | `low` | | `low` | `low` | `low` | `low` |
| `medium` | `medium` | `medium` | | `medium` | `medium` | `medium` | `medium` |
| `high` | `high` | `high` | | `high` | `high` | `high` | `high` |
| `xhigh` | `xhigh` | `high` (höchste verfügbare Stufe) | | `xhigh` | `xhigh` | `xhigh` | `high` (höchste verfügbare Stufe) |
| `max` | `xhigh` | `high` | | `max` | `xhigh` | `xhigh` | `high` |
**Der Aufruf** (als Background-Task starten): **Der Aufruf** (als Background-Task starten):
@@ -1059,6 +1156,8 @@ $lauf = "<absoluter Pfad zum Laufverzeichnis>"
$root = "<Root-Verzeichnis der Codebasis>" $root = "<Root-Verzeichnis der Codebasis>"
$modell = "<vom User gewählte Modell-ID, z.B. z-ai/glm-5.2>" $modell = "<vom User gewählte Modell-ID, z.B. z-ai/glm-5.2>"
$effort = "<vom User gewählte Stufe>" $effort = "<vom User gewählte Stufe>"
$modus = "<solo|builtin|custom>"
$agents = "<Agenten-JSON; im Modus custom erforderlich>"
# Prompt zusammenstellen (wie bei den anderen Adaptern) # Prompt zusammenstellen (wie bei den anderen Adaptern)
$prompt = (Get-Content "<prompt-datei>" -Raw) + "`n`n<Werkzeugkontext-Block>`n`n<Ausgabe-Block>" $prompt = (Get-Content "<prompt-datei>" -Raw) + "`n`n<Werkzeugkontext-Block>`n`n<Ausgabe-Block>"
@@ -1073,7 +1172,11 @@ python "$skillDir\glm-kimi-adapter.py" `
--model $modell ` --model $modell `
--effort $effort ` --effort $effort `
--mode $modus ` --mode $modus `
--max-turns 80 ` --agents $agents `
--max-turns 0 `
--subagent-max-turns 0 `
--timeout 0 `
--heartbeat-interval 60 `
--result-dir $lauf ` --result-dir $lauf `
2> "$lauf\Stderr.log" 2> "$lauf\Stderr.log"
@@ -1095,10 +1198,11 @@ Set-Content -Path "$lauf\_meta\endzeit.txt" -Value (Get-Date -Format o)
| Agent-Turns | `num_turns` | | Agent-Turns | `num_turns` |
| Tatsächlich eingesetztes Modell | `model` (aus API-Antwort; mit `model_requested` abzugleichen) | | Tatsächlich eingesetztes Modell | `model` (aus API-Antwort; mit `model_requested` abzugleichen) |
| Tool-Aufrufe | `tool_call_count`, `tool_call_types` (nach Werkzeugname) | | Tool-Aufrufe | `tool_call_count`, `tool_call_types` (nach Werkzeugname) |
| Subagenten | `subagent_stats` (`spawned`, `completed`, `failed`, `by_type`) – nur im Modus `builtin` | | Subagenten | `subagent_stats` (`spawned`, `completed`, `failed`, `by_type`) – nur in den Modi `builtin` und `custom` |
| Subagenten-Details | `subagent_details` (je Subagent: Typ, Beschreibung, Turns, Tokens, Status) | | Subagenten-Details | `subagent_details` (je Subagent: Typ, Beschreibung, Turns, Tokens, Status) |
| Erzeugte Artefakte | `written_files` (Pfad und Größe je Datei) | | Erzeugte Artefakte | `written_files` (Pfad und Größe je Datei) |
| Abschlusstext | `result` | | Abschlusstext | `result` |
| Lebenszeichen | periodische `LIFESIGN`-Zeilen in `Stderr.log` |
**Semantik der Token-Zählung.** `usage.total_tokens` wird über alle Turns aufsummiert. **Semantik der Token-Zählung.** `usage.total_tokens` wird über alle Turns aufsummiert.
Jeder Turn umfasst dabei den vollen Kontext (Eingabe + Ausgabe); die Token-Zahl ist Jeder Turn umfasst dabei den vollen Kontext (Eingabe + Ausgabe); die Token-Zahl ist
@@ -1179,6 +1283,10 @@ der Historie unten – im selben Arbeitsschritt.
| Version | Änderung | Grund | Verwendet in | | Version | Änderung | Grund | Verwendet in |
|---|---|---|---| |---|---|---|---|
| **9.3.0** | **TensorX-Modellkatalog für Versuch 2 erweitert:** `qwen/qwen3.8-flash-next` und `z-ai/glm-5.3-flash`; Qwen erhält explizit das `thinking.level`-Effort-Mapping. Der TensorX-Aufruf dokumentiert nun `--agents`. Im Adapter stellt `custom` wie `builtin` das Werkzeug `spawn_subagent` bereit; Adapter-Version 2.1.0. | Beide IDs wurden am 31.08.2026 über `GET /v1/models` des konfigurierten TensorX-Gateways bestätigt. Ein Qwen-Smoke-Test bestätigte `thinking`, Reasoning-Tokens und Tool-Calling. Bei der Konfigurationsprüfung fiel außerdem auf, dass `custom` trotz geladener Rollen das Delegationswerkzeug ausblendete und der dokumentierte Aufruf die Agentendatei nicht übergab; damit wäre Versuch 2 über TensorX ohne spezialisierte Rollen gelaufen. MINOR für die Modellauswahl, funktionale Korrektur für V2. | ab dem ersten TensorX-Lauf in Versuch 2 |
| **9.2.0** | **Delegationstiefe als Option im Modus `custom`.** Zwei gehashte Fassungen der Agentendatei: `verschachtelt` (Rollen dürfen selbst delegieren, bisheriges Verhalten) und `unverschachtelt` (Abschnitt *Keine Weiterdelegation* in jedem Rollenprompt). Die Fassung wird wie Modell, Modus und Effort vor dem Lauf beim User erfragt. Neues Protokollfeld *Delegationstiefe* mit Kontrolle über `subagent_stats.max_depth` und `spawned_by_subagents`. | Im ersten V2-Lauf (`v9.1.0-0c39`) entfielen 26 von 86 Subagenten auf Starts durch Subagenten (`max_depth` = 3). Das war nicht beabsichtigt – der `iso29148-orchestrator` ist als nicht-delegierende Rolle entworfen – und ist ein erheblicher Kostentreiber: Jede zusätzliche Ebene liest ihren Kontext erneut, und Cache-Reads stellten 46 % der Laufkosten. Es verwischt zudem die Zuständigkeit, weil ein von einer Rolle gestarteter Subagent keiner Teilaufgabe der Bindungstabelle mehr zuzuordnen ist. MINOR: neue Option und neue Messgröße; die Bedingung bestehender Läufe ändert sich nicht, sie gelten rückwirkend als `verschachtelt`. | ab dem zweiten V2-Lauf |
| **9.1.0** | **Zuständigkeitsbindung im Modus `custom`.** Block 1 (Werkzeugkontext) wird bei `custom` um eine Tabelle `Teilaufgabe → vorgesehener Bearbeiter` ergänzt, abgeleitet aus der `--agents`-Datei. Der Prompt formuliert die Bindung abstrakt und nennt weiterhin keine Rolle, damit dieselbe Prompt-Datei für `solo` und `builtin` gültig bleibt. Neues Protokollfeld *Zuständigkeitsbindung*: Aufrufe je Rolle aus `subagent_stats.by_type`, Rollen mit null Aufrufen namentlich, Abgleich gegen die Dokumentationspflicht im `Analysebericht.md`. | V2 untersucht die Wirkung rollenspezialisierter Agentendateien. Bleibt die Nutzung freigestellt, wird die Bedingung nicht hergestellt: Im Smoke-Test vom 26.08. nutzte der Agent von acht beigestellten Rollen nur zwei (`modulinventar`, `konsistenzpruefer`) — der Lauf hätte die Rollen mitgeführt, ohne sie einzusetzen. Die Bindung ist statisch deklariert, in `_meta\combined_prompt.md` archiviert und über den SHA-256 der `--agents`-Datei versioniert; sie unterscheidet sich damit von den in 9.0.0 entfernten laufzeitabhängigen Adaptereingriffen. Anzahl der Aufrufe, Zerlegungstiefe, Reihenfolge und Turn-Anzahl bleiben unvorgegeben. MINOR, weil bislang **kein** Lauf im Modus `custom` existiert und deshalb keine Läufe unpoolbar werden; der erste V2-Lauf eröffnet ohnehin eine neue Iteration. | ab dem ersten V2-Lauf |
| **9.0.0** | **Freie und tatsächlich parallele Subagenten-Orchestrierung im Python-Adapter.** Das künstliche 10er-Limit, die limitbedingte Zwangsnachricht und die laufzeitabhängigen Schreib-Erinnerungen entfallen. Mehrere `spawn_subagent`-Aufrufe desselben Turns werden parallel ausgeführt; Haupt- und Subagenten haben standardmäßig kein Turnlimit. `--timeout 0` bedeutet nun wirklich keinen HTTP-Timeout. Ein threadsicherer Heartbeat protokolliert alle 60 Sekunden den lokalen Zustand von Haupt- und Subagenten. API-Fehler setzen unabhängig von vorhandenem Freitext `is_error: true`, werden nach `Stderr.log` geschrieben und führen zu Exitcode 1. | Das 10er-Limit und die serielle Abarbeitung veränderten gerade die zu untersuchende autonome Delegationsstrategie des Modells. Außerdem wurden fünf Kimi-Timeouts wegen fehlerhafter Statuslogik als Erfolg klassifiziert. MAJOR, weil Subagentenfreiheit, Parallelität und Abbruchsemantik die Versuchsbedingung ändern; der nächste Lauf eröffnet eine neue Iteration. | ab dem nächsten TensorX-Lauf |
| **8.0.0** | **Neuer Python-API-Adapter für GLM- und Kimi-Modelle über den TensorX-Gateway.** Das Skript `glm-kimi-adapter.py` implementiert einen minimalen Agent-Loop mit Tool-Calling (read_file, list_directory, search_files, execute_command, write_file) direkt gegen die OpenAI-kompatible TensorX-API (`https://api.tensorx.ai/v1`). Modelle: `z-ai/glm-5.2` und `moonshotai/kimi-k3`. Der API-Key wird automatisch aus der Cline providers.json gelesen. Token-Verbrauch wird pro Turn aus dem API-Response `usage`-Objekt akkumuliert, inkl. Reasoning-Tokens (`completion_tokens_details.reasoning_tokens`). Effort-Mapping anhand des Modell-Präfixes: `z-ai/*` nutzt `thinking.level`, `moonshotai/*` nutzt `reasoning_effort`. Nur Modus `solo` freigegeben. Neue Modell-Tabelle für TensorX-Modell-IDs im Vorbereitungsabschnitt. | Die Cline CLI (`npm i -g cline`) versprach einen Headless-Modus mit GLM-Support, ihr natives Binary (143 MB, Bun-kompiliert) wurde jedoch durch die Application-Control-Richtlinie der Maschine blockiert (EPERM/Zugriff verweigert). Der Python-Adapter umgeht dieses Problem und bietet zusätzliche Vorteile: exakte Token-Metriken direkt aus der API (inkl. Reasoning-Tokens, die Claude Code nur als `thinking_tokens` liefert), keine externen Binary-Abhängigkeiten, API-Key aus bestehender Cline-Konfiguration. MAJOR, da neues Werkzeug mit anderer Isolationsarchitektur und anderen Metrik-Quellen eine neue Versuchsbedingung bildet; der nächste Lauf eröffnet eine neue Iteration. | ab dem ersten GLM/Kimi-Lauf | | **8.0.0** | **Neuer Python-API-Adapter für GLM- und Kimi-Modelle über den TensorX-Gateway.** Das Skript `glm-kimi-adapter.py` implementiert einen minimalen Agent-Loop mit Tool-Calling (read_file, list_directory, search_files, execute_command, write_file) direkt gegen die OpenAI-kompatible TensorX-API (`https://api.tensorx.ai/v1`). Modelle: `z-ai/glm-5.2` und `moonshotai/kimi-k3`. Der API-Key wird automatisch aus der Cline providers.json gelesen. Token-Verbrauch wird pro Turn aus dem API-Response `usage`-Objekt akkumuliert, inkl. Reasoning-Tokens (`completion_tokens_details.reasoning_tokens`). Effort-Mapping anhand des Modell-Präfixes: `z-ai/*` nutzt `thinking.level`, `moonshotai/*` nutzt `reasoning_effort`. Nur Modus `solo` freigegeben. Neue Modell-Tabelle für TensorX-Modell-IDs im Vorbereitungsabschnitt. | Die Cline CLI (`npm i -g cline`) versprach einen Headless-Modus mit GLM-Support, ihr natives Binary (143 MB, Bun-kompiliert) wurde jedoch durch die Application-Control-Richtlinie der Maschine blockiert (EPERM/Zugriff verweigert). Der Python-Adapter umgeht dieses Problem und bietet zusätzliche Vorteile: exakte Token-Metriken direkt aus der API (inkl. Reasoning-Tokens, die Claude Code nur als `thinking_tokens` liefert), keine externen Binary-Abhängigkeiten, API-Key aus bestehender Cline-Konfiguration. MAJOR, da neues Werkzeug mit anderer Isolationsarchitektur und anderen Metrik-Quellen eine neue Versuchsbedingung bildet; der nächste Lauf eröffnet eine neue Iteration. | ab dem ersten GLM/Kimi-Lauf |
| **7.0.0** | **Isolationsmechanismus wird modusabhängig.** In den Modi `solo` und `builtin` unverändert `--safe-mode`; im Modus `custom` und bei jedem Lauf mit `--mcp-config` **kein** `--safe-mode`, stattdessen `--strict-mcp-config` plus `--disallowedTools Skill WebSearch WebFetch SlashCommand`. Neue Pflicht-Umgebungsprüfung auf Hooks, Plugins und Output-Styles im User-Profil; neue Protokollfelder für die MCP-Konfiguration und die Umgebungsprüfung. | `--safe-mode` schaltet ausweislich der CLI-Hilfe „MCP servers, custom commands and agents" ab – also genau das, was V2 und V3 untersuchen. Smoke-Test am 2026-08-26 (CLI 2.1.246, identischer Aufruf, nur `--safe-mode` variiert): mit Flag `spawned` = 0 und die Meldung, die Rollen seien „nicht in der Agent-Registry registriert"; ohne Flag `spawned` = 2 mit `{"modulinventar": 1, "konsistenzpruefer": 1}`. Ohne Ersatz hätte der erste V2-Lauf stillschweigend als V1-Lauf gemessen. Der Ersatz wurde gegengeprüft: nichts vorgeladen, keine Skills, kein Webzugriff, alle Rollen verfügbar. **MAJOR: Läufe im Modus `custom` sind hinsichtlich der Isolation nicht unmittelbar mit `solo`- und `builtin`-Läufen vergleichbar** – Plugins, Hooks und Output-Styles sind dort nicht durch das Flag, sondern nur durch die Umgebungsprüfung ausgeschlossen. | ab dem ersten V2-Lauf | | **7.0.0** | **Isolationsmechanismus wird modusabhängig.** In den Modi `solo` und `builtin` unverändert `--safe-mode`; im Modus `custom` und bei jedem Lauf mit `--mcp-config` **kein** `--safe-mode`, stattdessen `--strict-mcp-config` plus `--disallowedTools Skill WebSearch WebFetch SlashCommand`. Neue Pflicht-Umgebungsprüfung auf Hooks, Plugins und Output-Styles im User-Profil; neue Protokollfelder für die MCP-Konfiguration und die Umgebungsprüfung. | `--safe-mode` schaltet ausweislich der CLI-Hilfe „MCP servers, custom commands and agents" ab – also genau das, was V2 und V3 untersuchen. Smoke-Test am 2026-08-26 (CLI 2.1.246, identischer Aufruf, nur `--safe-mode` variiert): mit Flag `spawned` = 0 und die Meldung, die Rollen seien „nicht in der Agent-Registry registriert"; ohne Flag `spawned` = 2 mit `{"modulinventar": 1, "konsistenzpruefer": 1}`. Ohne Ersatz hätte der erste V2-Lauf stillschweigend als V1-Lauf gemessen. Der Ersatz wurde gegengeprüft: nichts vorgeladen, keine Skills, kein Webzugriff, alle Rollen verfügbar. **MAJOR: Läufe im Modus `custom` sind hinsichtlich der Isolation nicht unmittelbar mit `solo`- und `builtin`-Läufen vergleichbar** – Plugins, Hooks und Output-Styles sind dort nicht durch das Flag, sondern nur durch die Umgebungsprüfung ausgeschlossen. | ab dem ersten V2-Lauf |
+285 -127
View File
@@ -3,7 +3,7 @@
TensorX API Adapter fuer den run-experiment Skill. TensorX API Adapter fuer den run-experiment Skill.
Dieser Adapter fuehrt einen Headless-Versuchslauf mit einem OpenAI-kompatiblen Dieser Adapter fuehrt einen Headless-Versuchslauf mit einem OpenAI-kompatiblen
Modell (Z.AI GLM oder Moonshot Kimi) ueber den TensorX API-Gateway durch. Modell (Z.AI GLM, Qwen oder Moonshot Kimi) ueber den TensorX API-Gateway durch.
Er implementiert einen minimalen Agent-Loop mit Tool-Calling und erfasst Er implementiert einen minimalen Agent-Loop mit Tool-Calling und erfasst
Token-Metadaten (inkl. Reasoning-Tokens) aus jeder API-Antwort. Token-Metadaten (inkl. Reasoning-Tokens) aus jeder API-Antwort.
@@ -15,11 +15,13 @@ Verwendung:
--prompt <Pfad zur combined_prompt.md> \ --prompt <Pfad zur combined_prompt.md> \
--root <Root-Verzeichnis der Codebasis> \ --root <Root-Verzeichnis der Codebasis> \
--output <Laufverzeichnis/Ergebnisse> \ --output <Laufverzeichnis/Ergebnisse> \
--model <Modell-ID, z.B. z-ai/glm-5.2 oder moonshotai/kimi-k3> \ --model <Modell-ID, z.B. z-ai/glm-5.3-flash oder qwen/qwen3.8-flash-next> \
--effort <low|medium|high|xhigh> \ --effort <low|medium|high|xhigh> \
[--max-turns 50] \ [--max-turns 0] \
[--subagent-max-turns 0] \
[--temperature 1.0] \ [--temperature 1.0] \
[--timeout 0] [--timeout 0] \
[--heartbeat-interval 60]
Ausgaben: Ausgaben:
<Laufverzeichnis>/RawResult.json – normalisierte Messdaten <Laufverzeichnis>/RawResult.json – normalisierte Messdaten
@@ -35,8 +37,10 @@ import os
import re import re
import subprocess import subprocess
import sys import sys
import threading
import time import time
import traceback import traceback
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@@ -60,9 +64,11 @@ PROVIDERS = {
# Modell-Praefix -> Effort-Parameter-Typ # Modell-Praefix -> Effort-Parameter-Typ
# z-ai/* Modelle nutzen den 'thinking'-Parameter (level: none|low|medium|high|xhigh) # z-ai/* Modelle nutzen den 'thinking'-Parameter (level: none|low|medium|high|xhigh)
# qwen/* Modelle nutzen ebenfalls den 'thinking'-Parameter
# moonshotai/* Modelle nutzen 'reasoning_effort' (low|medium|high) # moonshotai/* Modelle nutzen 'reasoning_effort' (low|medium|high)
MODEL_EFFORT_TYPE = { MODEL_EFFORT_TYPE = {
"z-ai": "thinking", "z-ai": "thinking",
"qwen": "thinking",
"moonshotai": "reasoning_effort", "moonshotai": "reasoning_effort",
} }
@@ -75,6 +81,76 @@ EFFORT_MAP = {
"max": {"thinking": "xhigh", "reasoning_effort": "high"}, "max": {"thinking": "xhigh", "reasoning_effort": "high"},
} }
ADAPTER_VERSION = "2.1.0"
_STDERR_LOCK = threading.Lock()
def log_stderr(message):
"""Schreibt eine vollständige, sofort sichtbare Zeile threadsicher nach stderr."""
with _STDERR_LOCK:
sys.stderr.write(f"[glm-kimi-adapter] {message}\n")
sys.stderr.flush()
class LivenessMonitor:
"""Gibt periodisch den lokalen Zustand von Haupt- und Subagenten aus.
Ein Lebenszeichen beweist, dass der lokale Adapterprozess lebt. Beim Warten
auf eine nicht gestreamte HTTP-Antwort ist es ausdrücklich kein Nachweis für
serverseitigen Inferenzfortschritt.
"""
def __init__(self, interval_seconds=60):
self.interval_seconds = max(0, interval_seconds)
self._activities = {}
self._lock = threading.Lock()
self._stop = threading.Event()
self._thread = None
def start(self):
if self.interval_seconds <= 0:
return
self._thread = threading.Thread(
target=self._run, name="glm-kimi-liveness", daemon=True
)
self._thread.start()
def stop(self):
self._stop.set()
if self._thread:
self._thread.join(timeout=1)
def set(self, activity_id, description):
with self._lock:
self._activities[activity_id] = {
"description": description,
"since": time.monotonic(),
}
def clear(self, activity_id):
with self._lock:
self._activities.pop(activity_id, None)
def _run(self):
while not self._stop.wait(self.interval_seconds):
now = time.monotonic()
with self._lock:
activities = [
(item["description"], int(now - item["since"]))
for item in self._activities.values()
]
if activities:
states = "; ".join(
f"{description} seit {elapsed_s}s"
for description, elapsed_s in activities
)
log_stderr(
"LIFESIGN: Prozess lebt | " + states
+ " | API-Warten belegt keinen serverseitigen Fortschritt"
)
else:
log_stderr("LIFESIGN: Prozess lebt | aktuell keine blockierende Operation")
def load_cline_api_key(): def load_cline_api_key():
""" """
@@ -449,11 +525,37 @@ def execute_tool(name: str, args: dict, root: str, output_dir: str) -> str:
# Subagent # Subagent
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def post_chat_completion(url, headers, body, timeout, liveness,
activity_id, activity_label):
"""Fuehrt einen nicht gestreamten API-Aufruf mit sichtbarem Lebenszeichen aus."""
request_timeout = timeout if timeout > 0 else None
liveness.set(activity_id, f"{activity_label}: wartet auf API-Antwort")
log_stderr(f"{activity_label}: API-Aufruf gestartet")
started = time.monotonic()
try:
response = requests.post(
url, headers=headers, json=body, timeout=request_timeout
)
finally:
liveness.clear(activity_id)
elapsed_s = time.monotonic() - started
log_stderr(
f"{activity_label}: API-Antwort nach {elapsed_s:.1f}s "
f"(HTTP {response.status_code})"
)
if response.status_code != 200:
raise RuntimeError(
f"API-Fehler {response.status_code}: {response.text[:2000]}"
)
return response.json()
def run_subagent(provider, model, api_key, effort, root, description, def run_subagent(provider, model, api_key, effort, root, description,
subagent_type, temperature, timeout, max_turns=15): subagent_type, temperature, timeout, liveness, agent_id,
max_turns=0):
""" """
Startet einen Subagenten mit eigenem Kontext. Startet einen Subagenten mit eigenem Kontext.
Der Subagent erhaelt Read-Only-Tools und eine begrenzte Turn-Anzahl. Der Subagent erhaelt Read-Only-Tools. max_turns=0 bedeutet unbegrenzt.
Rueckgabe: dict mit result, usage, turns, tool_calls, status. Rueckgabe: dict mit result, usage, turns, tool_calls, status.
""" """
sys_prompt = SUBAGENT_SYSTEM_PROMPTS.get( sys_prompt = SUBAGENT_SYSTEM_PROMPTS.get(
@@ -469,14 +571,19 @@ def run_subagent(provider, model, api_key, effort, root, description,
sub_tool_calls = 0 sub_tool_calls = 0
sub_result = "" sub_result = ""
sub_errors = [] sub_errors = []
completed_normally = False
label = f"Subagent {agent_id} ({subagent_type})"
while sub_turns < max_turns: log_stderr(f"{label}: gestartet")
while max_turns <= 0 or sub_turns < max_turns:
sub_turns += 1 sub_turns += 1
try: try:
resp = call_api_subagent(provider, model, messages, api_key, resp = call_api_subagent(provider, model, messages, api_key,
effort, temperature, timeout) effort, temperature, timeout, liveness,
agent_id, subagent_type, sub_turns)
except Exception as e: except Exception as e:
sub_errors.append(str(e)) sub_errors.append(str(e))
log_stderr(f"{label}: FEHLER in Turn {sub_turns}: {e}")
break break
u = resp.get("usage", {}) u = resp.get("usage", {})
sub_usage["prompt_tokens"] += u.get("prompt_tokens", 0) sub_usage["prompt_tokens"] += u.get("prompt_tokens", 0)
@@ -484,6 +591,11 @@ def run_subagent(provider, model, api_key, effort, root, description,
sub_usage["total_tokens"] += u.get("total_tokens", 0) sub_usage["total_tokens"] += u.get("total_tokens", 0)
sub_usage["cached_tokens"] += u.get("prompt_tokens_details", {}).get("cached_tokens", 0) sub_usage["cached_tokens"] += u.get("prompt_tokens_details", {}).get("cached_tokens", 0)
sub_usage["reasoning_tokens"] += u.get("completion_tokens_details", {}).get("reasoning_tokens", 0) sub_usage["reasoning_tokens"] += u.get("completion_tokens_details", {}).get("reasoning_tokens", 0)
log_stderr(
f"{label}: Turn {sub_turns} API abgeschlossen "
f"(Antwort-Tokens: {u.get('total_tokens', 0):,}, "
f"kumuliert: {sub_usage['total_tokens']:,})"
)
choices = resp.get("choices", []) choices = resp.get("choices", [])
if not choices: if not choices:
@@ -496,6 +608,7 @@ def run_subagent(provider, model, api_key, effort, root, description,
sub_result = content sub_result = content
tool_calls = msg.get("tool_calls", []) tool_calls = msg.get("tool_calls", [])
if not tool_calls: if not tool_calls:
completed_normally = True
break break
for tc in tool_calls: for tc in tool_calls:
func = tc.get("function", {}) func = tc.get("function", {})
@@ -505,25 +618,38 @@ def run_subagent(provider, model, api_key, effort, root, description,
except json.JSONDecodeError: except json.JSONDecodeError:
targs = {} targs = {}
sub_tool_calls += 1 sub_tool_calls += 1
liveness.set(
f"subagent-{agent_id}",
f"{label}: fuehrt Tool {tname} in Turn {sub_turns} aus",
)
# Subagent darf nur Read-Only-Tools nutzen # Subagent darf nur Read-Only-Tools nutzen
try:
if tname in ("read_file", "list_directory", "search_files", "execute_command"): if tname in ("read_file", "list_directory", "search_files", "execute_command"):
tresult = execute_tool(tname, targs, root, "") tresult = execute_tool(tname, targs, root, "")
else: else:
tresult = f"FEHLER: Werkzeug '{tname}' ist fuer Subagenten nicht freigegeben." tresult = f"FEHLER: Werkzeug '{tname}' ist fuer Subagenten nicht freigegeben."
finally:
liveness.clear(f"subagent-{agent_id}")
messages.append({"role": "tool", "tool_call_id": tc.get("id", ""), messages.append({"role": "tool", "tool_call_id": tc.get("id", ""),
"name": tname, "content": tresult}) "name": tname, "content": tresult})
status = "completed" if completed_normally and not sub_errors else "failed"
log_stderr(
f"{label}: beendet (Status: {status}, Turns: {sub_turns}, "
f"Tool-Calls: {sub_tool_calls}, Tokens: {sub_usage['total_tokens']:,})"
)
return { return {
"result": sub_result or "(Subagent ohne Ergebnis)", "result": sub_result or "(Subagent ohne Ergebnis)",
"usage": sub_usage, "usage": sub_usage,
"turns": sub_turns, "turns": sub_turns,
"tool_calls": sub_tool_calls, "tool_calls": sub_tool_calls,
"errors": sub_errors, "errors": sub_errors,
"status": "completed" if sub_result else "failed", "status": status,
} }
def call_api_subagent(provider, model, messages, api_key, effort, temperature, timeout): def call_api_subagent(provider, model, messages, api_key, effort, temperature,
timeout, liveness, agent_id, subagent_type, turn):
"""API-Aufruf fuer Subagenten (mit SUBAGENT_TOOLS statt TOOLS).""" """API-Aufruf fuer Subagenten (mit SUBAGENT_TOOLS statt TOOLS)."""
url = f"{provider['base_url']}/chat/completions" url = f"{provider['base_url']}/chat/completions"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
@@ -538,18 +664,19 @@ def call_api_subagent(provider, model, messages, api_key, effort, temperature, t
body["thinking"] = {"type": "enabled", "level": effort_val} body["thinking"] = {"type": "enabled", "level": effort_val}
elif effort_type == "reasoning_effort": elif effort_type == "reasoning_effort":
body["reasoning_effort"] = effort_val body["reasoning_effort"] = effort_val
resp = requests.post(url, headers=headers, json=body, return post_chat_completion(
timeout=timeout if timeout > 0 else 1800) url, headers, body, timeout, liveness,
if resp.status_code != 200: f"subagent-{agent_id}",
raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") f"Subagent {agent_id} ({subagent_type}) Turn {turn}",
return resp.json() )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# API-Aufruf # API-Aufruf
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def call_api(provider, model, messages, api_key, effort, temperature, timeout): def call_api(provider, model, messages, api_key, effort, temperature, timeout,
liveness, activity_id="main-api", activity_label="Hauptagent"):
"""Ruft die Chat-Completions-API auf und gibt die JSON-Antwort zurueck.""" """Ruft die Chat-Completions-API auf und gibt die JSON-Antwort zurueck."""
url = f"{provider['base_url']}/chat/completions" url = f"{provider['base_url']}/chat/completions"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
@@ -565,11 +692,9 @@ def call_api(provider, model, messages, api_key, effort, temperature, timeout):
body["thinking"] = {"type": "enabled", "level": effort_val} body["thinking"] = {"type": "enabled", "level": effort_val}
elif effort_type == "reasoning_effort": elif effort_type == "reasoning_effort":
body["reasoning_effort"] = effort_val body["reasoning_effort"] = effort_val
resp = requests.post(url, headers=headers, json=body, return post_chat_completion(
timeout=timeout if timeout > 0 else 1800) url, headers, body, timeout, liveness, activity_id, activity_label
if resp.status_code != 200: )
raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}")
return resp.json()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -577,10 +702,11 @@ def call_api(provider, model, messages, api_key, effort, temperature, timeout):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort, def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
root, output_dir, max_turns, temperature, timeout, mode="solo"): root, output_dir, max_turns, temperature, timeout, liveness,
mode="solo", subagent_max_turns=0):
"""Fuehrt den Agent-Loop durch und sammelt Metriken.""" """Fuehrt den Agent-Loop durch und sammelt Metriken."""
# Tools je nach Modus waehlen # Tools je nach Modus waehlen
if mode == "builtin": if mode in ("builtin", "custom"):
active_tools = TOOLS # inklusive spawn_subagent active_tools = TOOLS # inklusive spawn_subagent
else: else:
active_tools = [t for t in TOOLS if t["function"]["name"] != "spawn_subagent"] active_tools = [t for t in TOOLS if t["function"]["name"] != "spawn_subagent"]
@@ -600,11 +726,10 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
body["thinking"] = {"type": "enabled", "level": effort_val} body["thinking"] = {"type": "enabled", "level": effort_val}
elif effort_type == "reasoning_effort": elif effort_type == "reasoning_effort":
body["reasoning_effort"] = effort_val body["reasoning_effort"] = effort_val
resp = requests.post(url, headers=headers, json=body, return post_chat_completion(
timeout=timeout if timeout > 0 else 1800) url, headers, body, timeout, liveness, "main-api",
if resp.status_code != 200: f"Hauptagent Turn {turns}",
raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") )
return resp.json()
messages = [ messages = [
{"role": "system", "content": system_prompt}, {"role": "system", "content": system_prompt},
@@ -622,50 +747,15 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
# Subagent-Tracking # Subagent-Tracking
subagent_stats = {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}} subagent_stats = {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}}
subagent_details = [] subagent_details = []
MAX_SUBAGENTS = 10 # Hartes Limit: danach wird spawn_subagent verweigert
write_file_count = 0 # Zaehlt write_file-Aufrufe
write_reminder_sent = False # Wurde schon eine Schreib-Erinnerung gesendet?
while turns < max_turns: while max_turns <= 0 or turns < max_turns:
turns += 1 turns += 1
# Schreib-Erinnerung: Wenn nach 1/3 der Turns noch kein write_file,
# oder nach 2/3 der Turns weniger als 3 write_file-Aufrufe
if not write_reminder_sent and turns >= max_turns // 3 and write_file_count == 0:
messages.append({
"role": "user",
"content": (
"WICHTIG: Du hast bisher keine Ergebnisdateien geschrieben. "
"Beginne JETZT damit, deine Analyseergebnisse mit write_file "
"in die vorgegebenen Dateien (StRS.md, SyRS.md, SwRS.md, "
"Traceability.md, Hypothesen.md, Glossar.md, Analysebericht.md) "
"zu schreiben. Schreibe nicht weiter Subagenten — formalisiere "
"deine bisherigen Erkenntnisse in Anforderungen."
),
})
write_reminder_sent = True
sys.stderr.write(f"[glm-kimi-adapter] Schreib-Erinnerung gesendet "
f"(Turn {turns}, 0 write_file-Aufrufe)\n")
elif (not write_reminder_sent and turns >= max_turns * 2 // 3
and write_file_count < 3):
messages.append({
"role": "user",
"content": (
f"WICHTIG: Du hast bisher nur {write_file_count} Ergebnisdatei(en) "
f"geschrieben. Es fehlen noch mehrere der 7 vorgegebenen Dateien "
f"(StRS.md, SyRS.md, SwRS.md, Traceability.md, Hypothesen.md, "
f"Glossar.md, Analysebericht.md). Schreibe die fehlenden Dateien "
f"JETZT mit write_file."
),
})
write_reminder_sent = True
sys.stderr.write(f"[glm-kimi-adapter] Schreib-Erinnerung gesendet "
f"(Turn {turns}, {write_file_count} write_file-Aufrufe)\n")
try: try:
response = _call_api(messages) response = _call_api(messages)
except Exception as e: except Exception as e:
errors.append(f"Turn {turns}: API-Fehler: {e}") errors.append(f"Turn {turns}: API-Fehler: {e}")
log_stderr(f"FEHLER Hauptagent Turn {turns}: {e}")
break break
usage = response.get("usage", {}) usage = response.get("usage", {})
total_usage["prompt_tokens"] += usage.get("prompt_tokens", 0) total_usage["prompt_tokens"] += usage.get("prompt_tokens", 0)
@@ -675,6 +765,11 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
total_usage["cached_tokens"] += cached total_usage["cached_tokens"] += cached
comp_details = usage.get("completion_tokens_details", {}) comp_details = usage.get("completion_tokens_details", {})
total_usage["reasoning_tokens"] += comp_details.get("reasoning_tokens", 0) total_usage["reasoning_tokens"] += comp_details.get("reasoning_tokens", 0)
log_stderr(
f"Hauptagent Turn {turns} API abgeschlossen "
f"(Antwort-Tokens: {usage.get('total_tokens', 0):,}, "
f"Gesamtlauf kumuliert: {total_usage['total_tokens']:,})"
)
if response.get("model"): if response.get("model"):
model_reported = response["model"] model_reported = response["model"]
choices = response.get("choices", []) choices = response.get("choices", [])
@@ -691,6 +786,7 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
tool_calls = msg.get("tool_calls", []) tool_calls = msg.get("tool_calls", [])
if not tool_calls: if not tool_calls:
break break
parsed_calls = []
for tc in tool_calls: for tc in tool_calls:
func = tc.get("function", {}) func = tc.get("function", {})
tool_name = func.get("name", "") tool_name = func.get("name", "")
@@ -701,62 +797,109 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
except json.JSONDecodeError: except json.JSONDecodeError:
tool_args = {} tool_args = {}
tool_calls_log.append({"turn": turns, "name": tool_name, "args": tool_args}) tool_calls_log.append({"turn": turns, "name": tool_name, "args": tool_args})
parsed_calls.append({
"tool_call": tc,
"name": tool_name,
"args": tool_args,
"id": tc_id,
})
# write_file-Zaehler erhoehen spawn_calls = [call for call in parsed_calls if call["name"] == "spawn_subagent"]
if tool_name == "write_file": results_by_id = {}
write_file_count += 1 futures = []
executor = None
# Subagent-Spawning behandeln if spawn_calls:
if tool_name == "spawn_subagent": # Bewusst kein Adapterlimit: Anzahl und Typen bestimmt allein das Modell.
if subagent_stats["spawned"] >= MAX_SUBAGENTS: executor = ThreadPoolExecutor(
# Hartes Limit erreicht: verweigere und erzwinge Schreibphase max_workers=len(spawn_calls),
result = ( thread_name_prefix=f"subagent-turn-{turns}",
"ABGELEHNT: Subagent-Limit erreicht (10/10). "
"Du hast bereits 10 Subagenten gestartet. "
"Schreibe JETZT deine Ergebnisdateien mit write_file: "
"StRS.md, SyRS.md, SwRS.md, Traceability.md, "
"Hypothesen.md, Glossar.md, Analysebericht.md. "
"Starte keine weiteren Subagenten."
) )
sys.stderr.write(f"[glm-kimi-adapter] Subagent verweigert " for call in spawn_calls:
f"(Limit {MAX_SUBAGENTS} erreicht)\n") sa_desc = call["args"].get("description", "")
else: sa_type = call["args"].get("subagent_type", "general-purpose")
sa_desc = tool_args.get("description", "")
sa_type = tool_args.get("subagent_type", "general-purpose")
subagent_stats["spawned"] += 1 subagent_stats["spawned"] += 1
subagent_stats["by_type"][sa_type] = subagent_stats["by_type"].get(sa_type, 0) + 1 agent_id = subagent_stats["spawned"]
sys.stderr.write(f"[glm-kimi-adapter] Subagent " subagent_stats["by_type"][sa_type] = (
f"{subagent_stats['spawned']}/{MAX_SUBAGENTS} " subagent_stats["by_type"].get(sa_type, 0) + 1
f"gestartet (Typ: {sa_type})\n")
try:
sa_result = run_subagent(
provider, model, api_key, effort, root,
sa_desc, sa_type, temperature, timeout
) )
subagent_stats["completed" if sa_result["status"] == "completed" else "failed"] += 1 log_stderr(
f"Subagent {agent_id} zur parallelen Ausfuehrung eingeplant "
f"(Typ: {sa_type}, Hauptagent-Turn: {turns})"
)
future = executor.submit(
run_subagent,
provider, model, api_key, effort, root, sa_desc, sa_type,
temperature, timeout, liveness, agent_id,
subagent_max_turns,
)
futures.append((call, future, agent_id, sa_type, sa_desc))
# Nicht-Subagenten-Tools laufen, waehrend die Subagenten parallel arbeiten.
for call in parsed_calls:
if call["name"] == "spawn_subagent":
continue
activity_id = f"main-tool-{call['id']}"
liveness.set(
activity_id,
f"Hauptagent Turn {turns}: fuehrt Tool {call['name']} aus",
)
try:
results_by_id[call["id"]] = execute_tool(
call["name"], call["args"], root, output_dir
)
finally:
liveness.clear(activity_id)
if futures:
liveness.set(
"main-subagent-wait",
f"Hauptagent Turn {turns}: wartet auf {len(futures)} parallele Subagenten",
)
try:
for call, future, agent_id, sa_type, sa_desc in futures:
try:
sa_result = future.result()
except Exception as e:
sa_result = {
"result": f"FEHLER: Subagent fehlgeschlagen: {e}",
"usage": {key: 0 for key in total_usage},
"turns": 0,
"tool_calls": 0,
"errors": [str(e)],
"status": "failed",
}
log_stderr(f"Subagent {agent_id}: FEHLER: {e}")
status_key = (
"completed" if sa_result["status"] == "completed" else "failed"
)
subagent_stats[status_key] += 1
sa_u = sa_result["usage"] sa_u = sa_result["usage"]
total_usage["prompt_tokens"] += sa_u["prompt_tokens"] for key in total_usage:
total_usage["completion_tokens"] += sa_u["completion_tokens"] total_usage[key] += sa_u.get(key, 0)
total_usage["total_tokens"] += sa_u["total_tokens"]
total_usage["cached_tokens"] += sa_u["cached_tokens"]
total_usage["reasoning_tokens"] += sa_u["reasoning_tokens"]
subagent_details.append({ subagent_details.append({
"id": subagent_stats["spawned"], "id": agent_id,
"type": sa_type, "type": sa_type,
"description": sa_desc[:200], "description": sa_desc[:200],
"turns": sa_result["turns"], "turns": sa_result["turns"],
"tool_calls": sa_result["tool_calls"], "tool_calls": sa_result["tool_calls"],
"tokens": sa_u["total_tokens"], "tokens": sa_u.get("total_tokens", 0),
"status": sa_result["status"], "status": sa_result["status"],
"errors": sa_result.get("errors", []),
})
results_by_id[call["id"]] = sa_result["result"]
finally:
liveness.clear("main-subagent-wait")
executor.shutdown(wait=True)
# Tool-Antworten muessen in derselben Reihenfolge wie die Tool-Calls folgen.
for call in parsed_calls:
messages.append({
"role": "tool",
"tool_call_id": call["id"],
"name": call["name"],
"content": results_by_id[call["id"]],
}) })
result = sa_result["result"]
except Exception as e:
subagent_stats["failed"] += 1
result = f"FEHLER: Subagent fehlgeschlagen: {e}"
else:
result = execute_tool(tool_name, tool_args, root, output_dir)
messages.append({"role": "tool", "tool_call_id": tc_id,
"name": tool_name, "content": result})
end_time = time.time() end_time = time.time()
duration_s = end_time - start_time duration_s = end_time - start_time
@@ -774,8 +917,8 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
name = tc["name"] name = tc["name"]
tool_call_types[name] = tool_call_types.get(name, 0) + 1 tool_call_types[name] = tool_call_types.get(name, 0) + 1
return { return {
"is_error": len(errors) > 0 and not final_content, "is_error": bool(errors),
"subtype": "success" if final_content else "error", "subtype": "error" if errors else "success",
"duration_ms": int(duration_s * 1000), "duration_ms": int(duration_s * 1000),
"duration_api_ms": int(duration_s * 1000), "duration_api_ms": int(duration_s * 1000),
"num_turns": turns, "model": model_reported, "model_requested": model, "num_turns": turns, "model": model_reported, "model_requested": model,
@@ -806,7 +949,7 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
"tool_call_types": tool_call_types, "tool_call_types": tool_call_types,
"written_files": written_files, "result": final_content, "written_files": written_files, "result": final_content,
"finish_reason": finish_reason, "errors": errors, "session_id": "", "finish_reason": finish_reason, "errors": errors, "session_id": "",
"adapter": "python-glm-kimi", "adapter_version": "1.1.0", "adapter": "python-glm-kimi", "adapter_version": ADAPTER_VERSION,
"mode": mode, "mode": mode,
"subagent_stats": subagent_stats, "subagent_stats": subagent_stats,
"subagent_details": subagent_details, "subagent_details": subagent_details,
@@ -818,19 +961,21 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def main(): def main():
parser = argparse.ArgumentParser(description="TensorX API Adapter fuer run-experiment (GLM/Kimi)") parser = argparse.ArgumentParser(description="TensorX API Adapter fuer run-experiment (GLM/Qwen/Kimi)")
parser.add_argument("--prompt", required=True, help="Pfad zur combined_prompt.md") parser.add_argument("--prompt", required=True, help="Pfad zur combined_prompt.md")
parser.add_argument("--root", required=True, help="Root-Verzeichnis der Codebasis") parser.add_argument("--root", required=True, help="Root-Verzeichnis der Codebasis")
parser.add_argument("--output", required=True, help="Ausgabeverzeichnis (Ergebnisse)") parser.add_argument("--output", required=True, help="Ausgabeverzeichnis (Ergebnisse)")
parser.add_argument("--model", required=True, help="Modell-ID (z.B. z-ai/glm-5.2, moonshotai/kimi-k3)") parser.add_argument("--model", required=True, help="Modell-ID (z.B. z-ai/glm-5.3-flash, qwen/qwen3.8-flash-next)")
parser.add_argument("--provider", default="tensorx", help="API-Provider (default: tensorx)") parser.add_argument("--provider", default="tensorx", help="API-Provider (default: tensorx)")
parser.add_argument("--api-key", default=None, help="API-Key (default: aus Cline providers.json)") parser.add_argument("--api-key", default=None, help="API-Key (default: aus Cline providers.json)")
parser.add_argument("--effort", default="high", choices=["low", "medium", "high", "xhigh", "max"]) parser.add_argument("--effort", default="high", choices=["low", "medium", "high", "xhigh", "max"])
parser.add_argument("--mode", default="solo", choices=["solo", "builtin", "custom"], help="Agentenmodus (solo=keine Subagenten, builtin=eingebaute, custom=vordefinierte Agenten aus Datei)") parser.add_argument("--mode", default="solo", choices=["solo", "builtin", "custom"], help="Agentenmodus (solo=keine Subagenten, builtin=eingebaute, custom=vordefinierte Agenten aus Datei)")
parser.add_argument("--agents", default=None, help="Pfad zu Agenten-Definitionen (JSON) fuer Modus 'custom'") parser.add_argument("--agents", default=None, help="Pfad zu Agenten-Definitionen (JSON) fuer Modus 'custom'")
parser.add_argument("--max-turns", type=int, default=50) parser.add_argument("--max-turns", type=int, default=0, help="Maximale Hauptagent-Turns (0=unbegrenzt)")
parser.add_argument("--subagent-max-turns", type=int, default=0, help="Maximale Turns je Subagent (0=unbegrenzt)")
parser.add_argument("--temperature", type=float, default=1.0) parser.add_argument("--temperature", type=float, default=1.0)
parser.add_argument("--timeout", type=int, default=0, help="Timeout in Sek (0=keins)") parser.add_argument("--timeout", type=int, default=0, help="Timeout in Sek (0=keins)")
parser.add_argument("--heartbeat-interval", type=int, default=60, help="Sekunden zwischen Lebenszeichen (0=aus)")
parser.add_argument("--result-dir", default=None, help="Verzeichnis fuer RawResult.json") parser.add_argument("--result-dir", default=None, help="Verzeichnis fuer RawResult.json")
args = parser.parse_args() args = parser.parse_args()
@@ -868,21 +1013,18 @@ def main():
"eines Legacy-ERP-Systems. Du analysierst die Codebasis im Arbeitsverzeichnis " "eines Legacy-ERP-Systems. Du analysierst die Codebasis im Arbeitsverzeichnis "
"und erstellst eine Anforderungsspezifikation nach ISO/IEC/IEEE 29148:2018.\n\n" "und erstellst eine Anforderungsspezifikation nach ISO/IEC/IEEE 29148:2018.\n\n"
"Werkzeuge: read_file, list_directory, search_files, execute_command, write_file.\n\n" "Werkzeuge: read_file, list_directory, search_files, execute_command, write_file.\n\n"
"WICHTIG: Beginne deine Arbeit mit dem Schreiben von Ergebnisdateien (write_file), "
"nicht mit dem Erkunden. Erstelle zuerst die Skelette der 7 Ergebnisdateien "
"(StRS.md, SyRS.md, SwRS.md, Traceability.md, Hypothesen.md, Glossar.md, "
"Analysebericht.md) mit Platzhalter-Inhalt, bevor du in die Details gehst. "
"Wenn du Ergebnisse hast, schreibe sie SOFORT mit write_file — beschreibe nicht, " "Wenn du Ergebnisse hast, schreibe sie SOFORT mit write_file — beschreibe nicht, "
"was du schreiben wirst, schreibe es." "was du schreiben wirst, schreibe es. Wenn du Anforderungen formuliert hast, "
"schreibe die Dateien (StRS.md, SyRS.md, SwRS.md, Traceability.md, Hypothesen.md, "
"Glossar.md, Analysebericht.md) sofort — nichtmal davor nachfragen oder zusammenfassen."
) )
if args.mode == "builtin": if args.mode == "builtin":
system_prompt += ( system_prompt += (
"\nZusaetzlich steht spawn_subagent zur Verfuegung: Starte einen " "\nZusaetzlich steht spawn_subagent fuer isolierte Teilaufgaben zur "
"Subagenten mit eigenem Kontext fuer isolierte Teilaufgaben (z.B. " "Verfuegung. Entscheide selbst, ob du Subagenten einsetzt sowie welche "
"Analyse eines einzelnen Moduls). Der Subagent kann nur lesen, nicht " "Typen und wie viele du in einem Turn parallel startest. Der Adapter "
"schreiben. Nutze Subagenten, um die Breite der Analyse zu erhoehen: " "setzt dafuer kein Anzahl- oder Turn-Limit. Subagenten koennen nur "
"delegiere Modul-Analysen an Subagenten, waehrend du die " "lesen, nicht schreiben; die Ergebnisdateien verwaltest du selbst."
"Gesamtstruktur und die Ergebnisdateien verwaltest."
) )
elif args.mode == "custom": elif args.mode == "custom":
# Custom agents laden # Custom agents laden
@@ -925,13 +1067,24 @@ def main():
sys.stderr.write(f"[glm-kimi-adapter] Modell: {args.model}\n") sys.stderr.write(f"[glm-kimi-adapter] Modell: {args.model}\n")
sys.stderr.write(f"[glm-kimi-adapter] Effort: {args.effort}\n") sys.stderr.write(f"[glm-kimi-adapter] Effort: {args.effort}\n")
sys.stderr.write(f"[glm-kimi-adapter] Mode: {args.mode}\n") sys.stderr.write(f"[glm-kimi-adapter] Mode: {args.mode}\n")
sys.stderr.write(f"[glm-kimi-adapter] Adapter-Version: {ADAPTER_VERSION}\n")
sys.stderr.write(
f"[glm-kimi-adapter] Limits: Hauptagent-Turns="
f"{'unbegrenzt' if args.max_turns <= 0 else args.max_turns}, "
f"Subagent-Turns={'unbegrenzt' if args.subagent_max_turns <= 0 else args.subagent_max_turns}, "
f"API-Timeout={'keiner' if args.timeout <= 0 else str(args.timeout) + 's'}\n"
)
liveness = LivenessMonitor(args.heartbeat_interval)
liveness.start()
try: try:
result = run_agent_loop( result = run_agent_loop(
provider=provider, model=args.model, system_prompt=system_prompt, provider=provider, model=args.model, system_prompt=system_prompt,
user_prompt=user_prompt, api_key=api_key, effort=args.effort, user_prompt=user_prompt, api_key=api_key, effort=args.effort,
root=args.root, output_dir=str(output_dir), max_turns=args.max_turns, root=args.root, output_dir=str(output_dir), max_turns=args.max_turns,
temperature=args.temperature, timeout=args.timeout, mode=args.mode, temperature=args.temperature, timeout=args.timeout, liveness=liveness,
mode=args.mode, subagent_max_turns=args.subagent_max_turns,
) )
except Exception as e: except Exception as e:
tb = traceback.format_exc() tb = traceback.format_exc()
@@ -945,11 +1098,13 @@ def main():
"reasoning_tokens": 0}, "reasoning_tokens": 0},
"modelUsage": {}, "tool_calls": [], "tool_call_count": 0, "modelUsage": {}, "tool_calls": [], "tool_call_count": 0,
"tool_call_types": {}, "written_files": [], "result": "", "tool_call_types": {}, "written_files": [], "result": "",
"errors": [str(e)], "adapter": "python-glm-kimi", "adapter_version": "1.1.0", "errors": [str(e)], "adapter": "python-glm-kimi", "adapter_version": ADAPTER_VERSION,
"mode": args.mode, "mode": args.mode,
"subagent_stats": {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}}, "subagent_stats": {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}},
"subagent_details": [], "subagent_details": [],
} }
finally:
liveness.stop()
end_iso = datetime.now(timezone.utc).isoformat() end_iso = datetime.now(timezone.utc).isoformat()
result["start_time"] = start_iso result["start_time"] = start_iso
@@ -965,6 +1120,9 @@ def main():
sa = result.get("subagent_stats", {}) sa = result.get("subagent_stats", {})
sys.stderr.write(f"[glm-kimi-adapter] Subagenten: {sa.get('spawned',0)} (completed: {sa.get('completed',0)}, failed: {sa.get('failed',0)})\n") sys.stderr.write(f"[glm-kimi-adapter] Subagenten: {sa.get('spawned',0)} (completed: {sa.get('completed',0)}, failed: {sa.get('failed',0)})\n")
sys.stderr.write(f"[glm-kimi-adapter] Ergebnisdateien: {len(result['written_files'])}\n") sys.stderr.write(f"[glm-kimi-adapter] Ergebnisdateien: {len(result['written_files'])}\n")
if result.get("errors"):
for error in result["errors"]:
sys.stderr.write(f"[glm-kimi-adapter] FEHLER: {error}\n")
sys.stderr.write(f"[glm-kimi-adapter] RawResult: {raw_result_path}\n") sys.stderr.write(f"[glm-kimi-adapter] RawResult: {raw_result_path}\n")
sys.exit(1 if result["is_error"] else 0) sys.exit(1 if result["is_error"] else 0)
@@ -0,0 +1,244 @@
import importlib.util
import tempfile
import time
import unittest
from pathlib import Path
from unittest.mock import patch
ADAPTER_PATH = Path(__file__).with_name("glm-kimi-adapter.py")
SPEC = importlib.util.spec_from_file_location("glm_kimi_adapter", ADAPTER_PATH)
ADAPTER = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(ADAPTER)
def response(message, finish_reason="tool_calls", tokens=10):
return {
"model": "test/model",
"usage": {
"prompt_tokens": tokens,
"completion_tokens": 1,
"total_tokens": tokens + 1,
"prompt_tokens_details": {"cached_tokens": 0},
"completion_tokens_details": {"reasoning_tokens": 0},
},
"choices": [{"finish_reason": finish_reason, "message": message}],
}
def subagent_result(agent_id):
return {
"result": f"Ergebnis {agent_id}",
"usage": {
"prompt_tokens": 2,
"completion_tokens": 1,
"total_tokens": 3,
"cached_tokens": 0,
"reasoning_tokens": 0,
},
"turns": 1,
"tool_calls": 0,
"errors": [],
"status": "completed",
}
class AdapterTests(unittest.TestCase):
def setUp(self):
self.provider = {
"base_url": "https://example.invalid/v1",
"__id": "test",
}
self.liveness = ADAPTER.LivenessMonitor(interval_seconds=0)
def test_timeout_zero_is_forwarded_as_no_requests_timeout(self):
captured = {}
class FakeResponse:
status_code = 200
text = ""
@staticmethod
def json():
return {"ok": True}
def fake_post(*args, **kwargs):
captured["timeout"] = kwargs["timeout"]
return FakeResponse()
with patch.object(ADAPTER.requests, "post", side_effect=fake_post):
result = ADAPTER.post_chat_completion(
"https://example.invalid", {}, {}, 0, self.liveness,
"test", "Testaufruf",
)
self.assertEqual({"ok": True}, result)
self.assertIsNone(captured["timeout"])
def test_requested_tensorx_models_have_explicit_effort_mapping(self):
self.assertEqual("thinking", ADAPTER.MODEL_EFFORT_TYPE["qwen"])
self.assertEqual("thinking", ADAPTER.MODEL_EFFORT_TYPE["z-ai"])
def test_custom_mode_exposes_spawn_subagent(self):
captured = {}
def fake_post(url, headers, body, timeout, liveness,
activity_id, activity_label):
captured["tools"] = body["tools"]
return response(
{"role": "assistant", "content": "Fertig."},
finish_reason="stop",
)
with tempfile.TemporaryDirectory() as temp_dir, \
patch.object(ADAPTER, "post_chat_completion", side_effect=fake_post):
result = ADAPTER.run_agent_loop(
provider=self.provider,
model="qwen/qwen3.8-flash-next",
system_prompt="System",
user_prompt="Aufgabe",
api_key="key",
effort="high",
root=temp_dir,
output_dir=temp_dir,
max_turns=0,
temperature=1.0,
timeout=0,
liveness=self.liveness,
mode="custom",
)
tool_names = {
tool["function"]["name"] for tool in captured["tools"]
}
self.assertIn("spawn_subagent", tool_names)
self.assertFalse(result["is_error"])
def test_liveness_monitor_reports_active_main_and_subagent_operations(self):
lines = []
monitor = ADAPTER.LivenessMonitor(interval_seconds=0.02)
with patch.object(ADAPTER, "log_stderr", side_effect=lines.append):
monitor.set("main", "Hauptagent wartet auf Subagenten")
monitor.set("subagent-1", "Subagent 1 wartet auf API-Antwort")
monitor.start()
time.sleep(0.06)
monitor.stop()
output = "\n".join(lines)
self.assertIn("LIFESIGN: Prozess lebt", output)
self.assertIn("Hauptagent wartet auf Subagenten", output)
self.assertIn("Subagent 1 wartet auf API-Antwort", output)
self.assertIn("keinen serverseitigen Fortschritt", output)
def test_subagents_from_one_turn_run_in_parallel_without_count_limit(self):
first_message = {
"role": "assistant",
"content": "Ich delegiere.",
"tool_calls": [
{
"id": f"spawn-{index}",
"type": "function",
"function": {
"name": "spawn_subagent",
"arguments": (
'{"description":"Aufgabe %d",'
'"subagent_type":"explore"}' % index
),
},
}
for index in range(12)
],
}
responses = [
response(first_message),
response(
{"role": "assistant", "content": "Fertig."},
finish_reason="stop",
),
]
starts = []
def fake_post(*args, **kwargs):
return responses.pop(0)
def fake_subagent(*args, **kwargs):
agent_id = args[10]
starts.append(time.monotonic())
time.sleep(0.15)
return subagent_result(agent_id)
with tempfile.TemporaryDirectory() as temp_dir:
started = time.monotonic()
with patch.object(ADAPTER, "post_chat_completion", side_effect=fake_post), \
patch.object(ADAPTER, "run_subagent", side_effect=fake_subagent):
result = ADAPTER.run_agent_loop(
provider=self.provider,
model="test/model",
system_prompt="System",
user_prompt="Aufgabe",
api_key="key",
effort="high",
root=temp_dir,
output_dir=temp_dir,
max_turns=0,
temperature=1.0,
timeout=0,
liveness=self.liveness,
mode="builtin",
subagent_max_turns=0,
)
elapsed = time.monotonic() - started
self.assertEqual(12, result["subagent_stats"]["spawned"])
self.assertEqual(12, result["subagent_stats"]["completed"])
self.assertEqual(0, result["subagent_stats"]["failed"])
self.assertLess(max(starts) - min(starts), 0.12)
self.assertLess(elapsed, 0.6)
def test_api_error_is_not_masked_by_earlier_content(self):
first_message = {
"role": "assistant",
"content": "Zwischenstand",
"tool_calls": [{
"id": "list-1",
"type": "function",
"function": {
"name": "list_directory",
"arguments": '{"path":""}',
},
}],
}
calls = [response(first_message), RuntimeError("Transportfehler")]
def fake_post(*args, **kwargs):
item = calls.pop(0)
if isinstance(item, Exception):
raise item
return item
with tempfile.TemporaryDirectory() as temp_dir, \
patch.object(ADAPTER, "post_chat_completion", side_effect=fake_post):
result = ADAPTER.run_agent_loop(
provider=self.provider,
model="test/model",
system_prompt="System",
user_prompt="Aufgabe",
api_key="key",
effort="high",
root=temp_dir,
output_dir=temp_dir,
max_turns=0,
temperature=1.0,
timeout=0,
liveness=self.liveness,
mode="solo",
)
self.assertEqual("Zwischenstand", result["result"])
self.assertTrue(result["is_error"])
self.assertEqual("error", result["subtype"])
self.assertIn("Transportfehler", result["errors"][0])
if __name__ == "__main__":
unittest.main()
+80 -15
View File
@@ -7,10 +7,56 @@ Rollenspezialisierte Agentendateien statt eines einzelnen Threads. Agentenmodus
| Datei | SHA-256 | Zweck | | Datei | SHA-256 | Zweck |
|---|---|---| |---|---|---|
| `01_Prompt.md` | `DCDC0E3F…B71BCF` | Analyseanweisung, Prompt-Version **02-A** | | `02_Prompt.md` | `5946FC82…C76ECD` | **aktuell** – Analyseanweisung, Prompt-Version **03-A** |
| `01_Agents.json` | `6943EECD…AF1696` | acht Agentenrollen | | `03_Agents.json` | `424DDD83…644170` | **aktuell** – acht Agentenrollen, Delegationstiefe **`unverschachtelt`** (Abschnitt *Keine Weiterdelegation* je Rolle) |
| `02_Agents.json` | `4BF0AF8F…9781F1` | acht Agentenrollen, Delegationstiefe **`verschachtelt`** – Fassung des ersten V2-Laufs; weiterhin gültige Alternative, siehe Skill 9.2.0 |
| `01_Prompt.md` | `DCDC0E3F…B71BCF` | abgelöst – Prompt-Version 02-A, kein Lauf durchgeführt |
| `01_Agents.json` | `030B2142…21A9D9` | abgelöst, kein Lauf durchgeführt. Die README nannte hier bis zum 31.08. den Hash `6943EECD…AF1696`; er passte zu keinem Stand der Datei. |
## Stand 31.08.2026: Prompt-Version 03-A und Zuständigkeitsbindung
**Prompt-Version 03-A löst 02-A ab.** 02-A leitete sich von Prompt-Version **02** aus Versuch 1
ab. Versuch 1 ist seit Iteration 8 auf Prompt-Version **03** weitergelaufen. Ein V2-Lauf gegen
02-A hätte sich von den Vergleichsläufen in **zwei** Größen unterschieden – Agentenrollen *und*
Prompt-Version – und wäre als Wirkungsnachweis der Rollen unbrauchbar gewesen. 03-A setzt auf
Prompt-Version 03 auf; einzige Ergänzung bleibt der Abschnitt *Arbeitsteilung*.
**Die Rollennutzung ist gebunden.** Der Smoke-Test vom 26.08. nutzte von acht beigestellten Rollen
nur zwei. Bleibt die Nutzung freigestellt, wird die Versuchsbedingung nicht hergestellt: Der Lauf
führte die Rollen mit, ohne sie einzusetzen. Der Prompt verlangt deshalb, dass eine Teilaufgabe,
für die ein Bearbeiter vorgesehen ist, von ihm ausgeführt wird. Die konkrete Zuordnung
`Teilaufgabe → Rolle` steht im Block *Werkzeugkontext* des Skills (Version 9.1.0), nicht im
Prompt – so bleibt dieselbe Prompt-Datei für `solo` und `builtin` gültig und die Rollenbindung
die einzige unabhängige Variable.
Gebunden ist **wer** eine Teilaufgabe ausführt. Frei bleiben Zuschnitt, Anzahl der Aufträge je
Rolle, Reihenfolge, Tiefe und Turn-Anzahl – sie bleiben Untersuchungsgegenstand wie in V1b. Die
Bindung ist vor dem Lauf statisch deklariert, in `_meta\combined_prompt.md` archiviert und über
den SHA-256 der `--agents`-Datei versioniert; sie ist damit von den in Skill 9.0.0 entfernten
**laufzeitabhängigen** Adaptereingriffen zu unterscheiden, die Lauf 34 unpoolbar machten.
Da die Bindung auf Promptebene wirkt, ist ihre Einhaltung nicht erzwungen, sondern selbst eine
Messgröße: Das Protokollfeld *Zuständigkeitsbindung* führt die Aufrufe je Rolle aus
`subagent_stats.by_type`, nennt Rollen mit null Aufrufen und gleicht sie gegen die
Dokumentationspflicht im `Analysebericht.md` ab.
**Änderungen an den Rollen (02_Agents.json).** Namen und die gemessen begründeten Regeln sind
unverändert. Neu ist:
| Rolle | Änderung | Grund |
|---|---|---|
| `strs-autor`, `syrs-autor`, `swrs-autor` | schreiben keine Ergebnisdateien, Rückgabe als Text | Im TensorX-Adapter können Subagenten grundsätzlich nicht schreiben, in der Claude-CLI erben `--agents`-Rollen alle Werkzeuge. Dieselbe Rollendatei hätte je Adapter eine andere Bedingung ergeben; drei gleichzeitig schreibende Autoren brechen zudem die 7-Datei-Regel und die ID-Disziplin. |
| `iso29148-orchestrator` | prüft und liefert einen Übergabebericht mit ausführbaren Anweisungen, statt die Struktur selbst herzustellen | Die alte Fassung verlangte Herstellungsleistungen (Umnummerierung, Ergebnisstruktur) von einem Subagenten, der nicht schreiben kann – im Adapter unausführbar, in der CLI ein zweiter Schreiber neben dem Hauptagenten. |
| `konsistenzpruefer` | Prüfpunkte 9 bis 11: 7-Datei-Regel, Anteil `nicht analysiert` über 10 %, Einhaltung der Zuständigkeitsbindung | Die Rolle prüfte gegen Prompt-Version 02; die beiden Regeln kamen mit Version 03 hinzu. Punkt 11 macht die Bindung im Lauf selbst sichtbar. |
| `belegpruefer` | Umfang ist das Zugewiesene; geprüfte Zahl und Bezugsgröße sind zu nennen | Ohne Bezugsgröße ist die Zählung nicht einzuordnen. |
| alle | „Du legst keine Dateien an" | macht die Bedingung adapterunabhängig |
## Historisch: Prompt-Version 02-A
> Dieser Abschnitt beschreibt die abgelöste Fassung `01_Prompt.md`. Er bleibt stehen, weil die
> Begründung des Abschnitts *Arbeitsteilung* unverändert gilt; maßgeblich ist der Stand vom
> 31.08.2026 weiter oben.
## Der Prompt: abgeleitet aus V1, angepasst an Agentendateien
Prompt-Version **02-A** entsteht aus `Versuche/Versuch_01/02_Prompt.md` Prompt-Version **02-A** entsteht aus `Versuche/Versuch_01/02_Prompt.md`
(`F9B2A1AA…0D7849`) durch **eine** Ergänzung: den Abschnitt **Arbeitsteilung**. Alles Übrige – (`F9B2A1AA…0D7849`) durch **eine** Ergänzung: den Abschnitt **Arbeitsteilung**. Alles Übrige –
@@ -45,10 +91,10 @@ Auftrag über V1, V2 und V3 identisch.
|---|---|---| |---|---|---|
| `modulinventar` | Schritt 0: vollständiges Inventar samt Abdeckungsbuchführung | nein | | `modulinventar` | Schritt 0: vollständiges Inventar samt Abdeckungsbuchführung | nein |
| `faktenermittler` | belegte Fakten samt durchsetzender Codestelle, je Modulausschnitt | nein | | `faktenermittler` | belegte Fakten samt durchsetzender Codestelle, je Modulausschnitt | nein |
| `strs-autor` | Stakeholder-Ebene | ja, nur StRS | | `strs-autor` | Stakeholder-Ebene | ja, nur StRS – als Rückgabetext, ohne Datei |
| `syrs-autor` | Systemebene | ja, nur SyRS | | `syrs-autor` | Systemebene | ja, nur SyRS – als Rückgabetext, ohne Datei |
| `swrs-autor` | Softwareebene, zusätzlich Konsolidierungsprüfung | ja, nur SwRS | | `swrs-autor` | Softwareebene, zusätzlich Konsolidierungsprüfung | ja, nur SwRS – als Rückgabetext, ohne Datei |
| `iso29148-orchestrator` | führt die Ebenen zur Spezifikation zusammen | nein | | `iso29148-orchestrator` | prüft den zusammengeführten Bestand und liefert den Übergabebericht | nein |
| `belegpruefer` | öffnet zitierte Stellen und prüft, ob `PRIMÄR` trägt | nein | | `belegpruefer` | öffnet zitierte Stellen und prüft, ob `PRIMÄR` trägt | nein |
| `konsistenzpruefer` | vollständige Regelprüfung gegen die Promptvorgaben | nein | | `konsistenzpruefer` | vollständige Regelprüfung gegen die Promptvorgaben | nein |
@@ -56,11 +102,12 @@ Die vier von der Arbeit genannten Rollen – Stakeholder, System, Software und
ISO-29148-Orchestrator – sind damit besetzt; `modulinventar`, `faktenermittler`, `belegpruefer` ISO-29148-Orchestrator – sind damit besetzt; `modulinventar`, `faktenermittler`, `belegpruefer`
und `konsistenzpruefer` kommen aus den Messungen hinzu. und `konsistenzpruefer` kommen aus den Messungen hinzu.
**Der Orchestrator delegiert nicht.** Er startet keine Subagenten und schreibt keine **Der Orchestrator delegiert nicht.** Er startet keine Subagenten und schreibt weder
Anforderungen, sondern stellt her, was erst am zusammengeführten Bestand entstehen kann: Anforderungen noch Dateien. Er prüft, was sich erst am zusammengeführten Bestand feststellen
durchgängige Nummerierung samt mitgezogener Verweise, beidseitig geschlossene Traceability, eine lässt – durchgängige Nummerierung samt mitzuziehender Verweise, beidseitig geschlossene
aus dem Gesamtbestand erzeugte Hypothesenliste, die Abdeckungstabelle über das gemeinsame Traceability, Deckungsgleichheit der Hypothesenliste, Abdeckung über das gemeinsame Inventar –
Inventar. Ein delegierender Orchestrator erzeugte eine zweite Ebene, deren Subagenten-Prompts und liefert die fehlenden Schritte als ausführbare Anweisungen zurück; ausgeführt werden sie vom
Hauptagenten. Ein delegierender Orchestrator erzeugte eine zweite Ebene, deren Subagenten-Prompts
nicht protokollierbar sind – in Versuch 1 blieben so 18 von 31 Aufrufen unerfassbar. nicht protokollierbar sind – in Versuch 1 blieben so 18 von 31 Aufrufen unerfassbar.
## Woraus die Rollen abgeleitet sind ## Woraus die Rollen abgeleitet sind
@@ -100,6 +147,19 @@ geprüft. Die Rolle muss ihren Risikobegriff deshalb offenlegen.
Der Skill `run-experiment` übernimmt das im Modus `custom`. Ablage der Läufe: Der Skill `run-experiment` übernimmt das im Modus `custom`. Ablage der Läufe:
`<Iteration>/<ModellID>/custom/<Effort>/`. `<Iteration>/<ModellID>/custom/<Effort>/`.
### TensorX-Modelle
Für TensorX-Läufe in Versuch 2 sind zusätzlich `qwen/qwen3.8-flash-next` und
`z-ai/glm-5.3-flash` konfiguriert. Beide IDs waren bei der Prüfung am 31.08.2026 im
`/v1/models`-Katalog des Gateways vorhanden. Der Python-Adapter lädt im Modus `custom` die
Agentendatei über `--agents` und stellt dem Hauptagenten `spawn_subagent` bereit; ohne diese
beiden Teile wäre die Agentenbedingung von Versuch 2 nicht hergestellt.
Der kleine Qwen-Kompatibilitätstest bestätigte Tool-Calling, `thinking` und Reasoning-Tokens.
Der entsprechende GLM-Test lieferte innerhalb von 90 Sekunden keine Antwort; die Modell-ID ist
vorhanden und konfiguriert, vor einem kostenintensiven Lauf ist aber ein erneuter Smoke-Test
erforderlich.
## Wichtig: `--safe-mode` ist hier nicht verwendbar ## Wichtig: `--safe-mode` ist hier nicht verwendbar
`--safe-mode` schaltet ausweislich der CLI-Hilfe „all customizations (CLAUDE.md, skills, plugins, `--safe-mode` schaltet ausweislich der CLI-Hilfe „all customizations (CLAUDE.md, skills, plugins,
@@ -134,9 +194,10 @@ Webzugriff oder MCP.
## Offene Punkte vor dem ersten Lauf ## Offene Punkte vor dem ersten Lauf
1. **`extract-subagenten.py` ist nicht gegen `custom`-Typen geprüft.** Es erwartet die 1. ~~**`extract-subagenten.py` ist nicht gegen `custom`-Typen geprüft.**~~ **Geklärt am
eingebauten Typen `Explore` und `general-purpose`; mit `--agents` heißen sie 31.08.2026 durch Inspektion:** Das Skript filtert auf die Werkzeugnamen `Task`/`Agent` und
`faktenermittler`, `strs-autor` und so fort. liest `subagent_type` generisch aus; eine Liste erwarteter Typen gibt es nicht. Custom-Typen
werden damit wie eingebaute erfasst. Bestätigung am ersten realen Lauf steht aus.
2. **Die Modellbedingung ist bei Delegation über `--model` allein nicht herstellbar.** Zwei 2. **Die Modellbedingung ist bei Delegation über `--model` allein nicht herstellbar.** Zwei
dokumentierte Fälle aus Versuch 1: bei Fable liefen die Subagenten auf `claude-opus-5[1m]`, dokumentierte Fälle aus Versuch 1: bei Fable liefen die Subagenten auf `claude-opus-5[1m]`,
bei Opus entfielen 18,5 Mio. Tokens (4,72 %) auf `claude-sonnet-5`. Nach jedem Lauf bei Opus entfielen 18,5 Mio. Tokens (4,72 %) auf `claude-sonnet-5`. Nach jedem Lauf
@@ -145,3 +206,7 @@ Webzugriff oder MCP.
`CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0`; ohne diese Einstellung bricht der Headless-Modus `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0`; ohne diese Einstellung bricht der Headless-Modus
nach 600 s ab und meldet dabei `is_error: false` bei null Ergebnisdateien. nach 600 s ab und meldet dabei `is_error: false` bei null Ergebnisdateien.
4. **Umgebungsprüfung** auf Hooks, Plugins und Output-Styles im User-Profil – siehe oben. 4. **Umgebungsprüfung** auf Hooks, Plugins und Output-Styles im User-Profil – siehe oben.
5. **Der `--safe-mode`-Smoke-Test lief auf CLI 2.1.246; installiert ist inzwischen 2.1.251.**
Der Test ist das, was verhindert, dass ein V2-Lauf stillschweigend als V1-Lauf gemessen wird.
Vor der Auswertung des ersten Laufs ist `subagent_stats.by_type` daher darauf zu prüfen, dass
die Rollennamen tatsächlich erscheinen.