From ca52aa4701693528c097f3377edb2c2b07ec5149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=B6rer?= Date: Mon, 31 Aug 2026 16:51:35 +0200 Subject: [PATCH] Add TensorX models for Versuch 2 --- .claude/skills/run-experiment/SKILL.md | 150 +++++- .../skills/run-experiment/glm-kimi-adapter.py | 438 ++++++++++++------ .../run-experiment/test_glm_kimi_adapter.py | 244 ++++++++++ Versuche/Versuch_02/README.md | 95 +++- 4 files changed, 751 insertions(+), 176 deletions(-) create mode 100644 .claude/skills/run-experiment/test_glm_kimi_adapter.py diff --git a/.claude/skills/run-experiment/SKILL.md b/.claude/skills/run-experiment/SKILL.md index b949df13..ba477c1a 100644 --- a/.claude/skills/run-experiment/SKILL.md +++ b/.claude/skills/run-experiment/SKILL.md @@ -1,8 +1,8 @@ --- 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 " 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 " oder wenn der User einen Versuch/ein Experiment ausführen und tracken will. argument-hint: -version: 8.0.0 +version: 9.3.0 --- # 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 Teil gilt unabhängig davon, mit welchem LLM oder welcher CLI gearbeitet 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. **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. 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/*` 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. 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 | |---|---| | `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 | 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 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 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 @@ -262,7 +288,7 @@ per stdin übergebenen Text angehängt (siehe Schritt „Prompt zusammenstellen" Sort-Object { [int]($_.Name -replace '\D','') } $iteration = if ($iterationen) { $iterationen[-1].Name } else { 'Iteration 1' } $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 { $id4 = '{0:x4}' -f (Get-Random -Maximum 65536) $lauf = Join-Path "\$zelle" "_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 | | `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 der Codebasis: @@ -772,6 +842,15 @@ Vorlage: - **MCP-Server / Agentendateien:** +- **Delegationstiefe (nur `custom`):** 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):** - **Subagenten:** @@ -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 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** (`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 | |---|---|---| | `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) | -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. **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 wird **nicht** in Laufartefakten gespeichert. -**Unterstützter Agentenmodus:** `solo` (V1) und `builtin` (V1b). Der Modus wird per -`--mode solo|builtin` gesteuert. Im Modus `solo` steht das `spawn_subagent`-Tool nicht +**Unterstützte Agentenmodi:** `solo` (V1), `builtin` (V1b) und `custom` (V2). Der Modus wird per +`--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 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 -`general-purpose`) bestimmt den System-Prompt. Subagent-Token fließen vollständig in -`usage` und `modelUsage` ein. `custom` ist nicht freigegeben und führt zum Abbruch. +eine eigene, vom Hauptagenten unabhängige Konversation. Anzahl und Typen bestimmt allein +das Modell; der Adapter setzt weder ein Anzahl- noch standardmäßig ein Turn-Limit. Fordert +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 ` 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 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: -| Skill-Effort | GLM `thinking.level` | Kimi `reasoning_effort` | -|---|---|---| -| `low` | `low` | `low` | -| `medium` | `medium` | `medium` | -| `high` | `high` | `high` | -| `xhigh` | `xhigh` | `high` (höchste verfügbare Stufe) | -| `max` | `xhigh` | `high` | +| Skill-Effort | GLM `thinking.level` | Qwen `thinking.level` | Kimi `reasoning_effort` | +|---|---|---|---| +| `low` | `low` | `low` | `low` | +| `medium` | `medium` | `medium` | `medium` | +| `high` | `high` | `high` | `high` | +| `xhigh` | `xhigh` | `xhigh` | `high` (höchste verfügbare Stufe) | +| `max` | `xhigh` | `xhigh` | `high` | **Der Aufruf** (als Background-Task starten): @@ -1059,6 +1156,8 @@ $lauf = "" $root = "" $modell = "" $effort = "" +$modus = "" +$agents = "" # Prompt zusammenstellen (wie bei den anderen Adaptern) $prompt = (Get-Content "" -Raw) + "`n`n`n`n" @@ -1073,7 +1172,11 @@ python "$skillDir\glm-kimi-adapter.py" ` --model $modell ` --effort $effort ` --mode $modus ` - --max-turns 80 ` + --agents $agents ` + --max-turns 0 ` + --subagent-max-turns 0 ` + --timeout 0 ` + --heartbeat-interval 60 ` --result-dir $lauf ` 2> "$lauf\Stderr.log" @@ -1095,10 +1198,11 @@ Set-Content -Path "$lauf\_meta\endzeit.txt" -Value (Get-Date -Format o) | Agent-Turns | `num_turns` | | Tatsächlich eingesetztes Modell | `model` (aus API-Antwort; mit `model_requested` abzugleichen) | | 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) | | Erzeugte Artefakte | `written_files` (Pfad und Größe je Datei) | | Abschlusstext | `result` | +| Lebenszeichen | periodische `LIFESIGN`-Zeilen in `Stderr.log` | **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 @@ -1179,6 +1283,10 @@ der Historie unten – im selben Arbeitsschritt. | 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 | | **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 | diff --git a/.claude/skills/run-experiment/glm-kimi-adapter.py b/.claude/skills/run-experiment/glm-kimi-adapter.py index b0273131..7c2c1417 100644 --- a/.claude/skills/run-experiment/glm-kimi-adapter.py +++ b/.claude/skills/run-experiment/glm-kimi-adapter.py @@ -3,7 +3,7 @@ TensorX API Adapter fuer den run-experiment Skill. 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 Token-Metadaten (inkl. Reasoning-Tokens) aus jeder API-Antwort. @@ -15,11 +15,13 @@ Verwendung: --prompt \ --root \ --output \ - --model \ + --model \ --effort \ - [--max-turns 50] \ + [--max-turns 0] \ + [--subagent-max-turns 0] \ [--temperature 1.0] \ - [--timeout 0] + [--timeout 0] \ + [--heartbeat-interval 60] Ausgaben: /RawResult.json – normalisierte Messdaten @@ -35,8 +37,10 @@ import os import re import subprocess import sys +import threading import time import traceback +from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timezone from pathlib import Path @@ -60,9 +64,11 @@ PROVIDERS = { # Modell-Praefix -> Effort-Parameter-Typ # 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) MODEL_EFFORT_TYPE = { "z-ai": "thinking", + "qwen": "thinking", "moonshotai": "reasoning_effort", } @@ -75,6 +81,76 @@ EFFORT_MAP = { "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(): """ @@ -449,11 +525,37 @@ def execute_tool(name: str, args: dict, root: str, output_dir: str) -> str: # 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, - subagent_type, temperature, timeout, max_turns=15): + subagent_type, temperature, timeout, liveness, agent_id, + max_turns=0): """ 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. """ 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_result = "" 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 try: 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: sub_errors.append(str(e)) + log_stderr(f"{label}: FEHLER in Turn {sub_turns}: {e}") break u = resp.get("usage", {}) 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["cached_tokens"] += u.get("prompt_tokens_details", {}).get("cached_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", []) if not choices: @@ -496,6 +608,7 @@ def run_subagent(provider, model, api_key, effort, root, description, sub_result = content tool_calls = msg.get("tool_calls", []) if not tool_calls: + completed_normally = True break for tc in tool_calls: func = tc.get("function", {}) @@ -505,25 +618,38 @@ def run_subagent(provider, model, api_key, effort, root, description, except json.JSONDecodeError: targs = {} 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 - if tname in ("read_file", "list_directory", "search_files", "execute_command"): - tresult = execute_tool(tname, targs, root, "") - else: - tresult = f"FEHLER: Werkzeug '{tname}' ist fuer Subagenten nicht freigegeben." + try: + if tname in ("read_file", "list_directory", "search_files", "execute_command"): + tresult = execute_tool(tname, targs, root, "") + else: + 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", ""), "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 { "result": sub_result or "(Subagent ohne Ergebnis)", "usage": sub_usage, "turns": sub_turns, "tool_calls": sub_tool_calls, "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).""" url = f"{provider['base_url']}/chat/completions" 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} elif effort_type == "reasoning_effort": body["reasoning_effort"] = effort_val - resp = requests.post(url, headers=headers, json=body, - timeout=timeout if timeout > 0 else 1800) - if resp.status_code != 200: - raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") - return resp.json() + return post_chat_completion( + url, headers, body, timeout, liveness, + f"subagent-{agent_id}", + f"Subagent {agent_id} ({subagent_type}) Turn {turn}", + ) # --------------------------------------------------------------------------- # 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.""" url = f"{provider['base_url']}/chat/completions" 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} elif effort_type == "reasoning_effort": body["reasoning_effort"] = effort_val - resp = requests.post(url, headers=headers, json=body, - timeout=timeout if timeout > 0 else 1800) - if resp.status_code != 200: - raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") - return resp.json() + return post_chat_completion( + url, headers, body, timeout, liveness, activity_id, activity_label + ) # --------------------------------------------------------------------------- @@ -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, - 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.""" # Tools je nach Modus waehlen - if mode == "builtin": + if mode in ("builtin", "custom"): active_tools = TOOLS # inklusive spawn_subagent else: 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} elif effort_type == "reasoning_effort": body["reasoning_effort"] = effort_val - resp = requests.post(url, headers=headers, json=body, - timeout=timeout if timeout > 0 else 1800) - if resp.status_code != 200: - raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") - return resp.json() + return post_chat_completion( + url, headers, body, timeout, liveness, "main-api", + f"Hauptagent Turn {turns}", + ) messages = [ {"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_stats = {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}} 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 - # 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: response = _call_api(messages) except Exception as e: errors.append(f"Turn {turns}: API-Fehler: {e}") + log_stderr(f"FEHLER Hauptagent Turn {turns}: {e}") break usage = response.get("usage", {}) 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 comp_details = usage.get("completion_tokens_details", {}) 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"): model_reported = response["model"] 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", []) if not tool_calls: break + parsed_calls = [] for tc in tool_calls: func = tc.get("function", {}) 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: 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 - if tool_name == "write_file": - write_file_count += 1 + spawn_calls = [call for call in parsed_calls if call["name"] == "spawn_subagent"] + results_by_id = {} + futures = [] + executor = None - # Subagent-Spawning behandeln - if tool_name == "spawn_subagent": - if subagent_stats["spawned"] >= MAX_SUBAGENTS: - # Hartes Limit erreicht: verweigere und erzwinge Schreibphase - result = ( - "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 " - f"(Limit {MAX_SUBAGENTS} erreicht)\n") - else: - sa_desc = tool_args.get("description", "") - sa_type = tool_args.get("subagent_type", "general-purpose") - subagent_stats["spawned"] += 1 - subagent_stats["by_type"][sa_type] = subagent_stats["by_type"].get(sa_type, 0) + 1 - sys.stderr.write(f"[glm-kimi-adapter] Subagent " - f"{subagent_stats['spawned']}/{MAX_SUBAGENTS} " - f"gestartet (Typ: {sa_type})\n") + if spawn_calls: + # Bewusst kein Adapterlimit: Anzahl und Typen bestimmt allein das Modell. + executor = ThreadPoolExecutor( + max_workers=len(spawn_calls), + thread_name_prefix=f"subagent-turn-{turns}", + ) + for call in spawn_calls: + sa_desc = call["args"].get("description", "") + sa_type = call["args"].get("subagent_type", "general-purpose") + subagent_stats["spawned"] += 1 + agent_id = subagent_stats["spawned"] + subagent_stats["by_type"][sa_type] = ( + subagent_stats["by_type"].get(sa_type, 0) + 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 = 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 - sa_u = sa_result["usage"] - total_usage["prompt_tokens"] += sa_u["prompt_tokens"] - total_usage["completion_tokens"] += sa_u["completion_tokens"] - 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({ - "id": subagent_stats["spawned"], - "type": sa_type, - "description": sa_desc[:200], - "turns": sa_result["turns"], - "tool_calls": sa_result["tool_calls"], - "tokens": sa_u["total_tokens"], - "status": sa_result["status"], - }) - result = sa_result["result"] + sa_result = future.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}) + 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"] + for key in total_usage: + total_usage[key] += sa_u.get(key, 0) + subagent_details.append({ + "id": agent_id, + "type": sa_type, + "description": sa_desc[:200], + "turns": sa_result["turns"], + "tool_calls": sa_result["tool_calls"], + "tokens": sa_u.get("total_tokens", 0), + "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"]], + }) end_time = time.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"] tool_call_types[name] = tool_call_types.get(name, 0) + 1 return { - "is_error": len(errors) > 0 and not final_content, - "subtype": "success" if final_content else "error", + "is_error": bool(errors), + "subtype": "error" if errors else "success", "duration_ms": int(duration_s * 1000), "duration_api_ms": int(duration_s * 1000), "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, "written_files": written_files, "result": final_content, "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, "subagent_stats": subagent_stats, "subagent_details": subagent_details, @@ -818,19 +961,21 @@ def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort, # --------------------------------------------------------------------------- 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("--root", required=True, help="Root-Verzeichnis der Codebasis") 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("--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("--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("--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("--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") args = parser.parse_args() @@ -868,21 +1013,18 @@ def main(): "eines Legacy-ERP-Systems. Du analysierst die Codebasis im Arbeitsverzeichnis " "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" - "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, " - "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": system_prompt += ( - "\nZusaetzlich steht spawn_subagent zur Verfuegung: Starte einen " - "Subagenten mit eigenem Kontext fuer isolierte Teilaufgaben (z.B. " - "Analyse eines einzelnen Moduls). Der Subagent kann nur lesen, nicht " - "schreiben. Nutze Subagenten, um die Breite der Analyse zu erhoehen: " - "delegiere Modul-Analysen an Subagenten, waehrend du die " - "Gesamtstruktur und die Ergebnisdateien verwaltest." + "\nZusaetzlich steht spawn_subagent fuer isolierte Teilaufgaben zur " + "Verfuegung. Entscheide selbst, ob du Subagenten einsetzt sowie welche " + "Typen und wie viele du in einem Turn parallel startest. Der Adapter " + "setzt dafuer kein Anzahl- oder Turn-Limit. Subagenten koennen nur " + "lesen, nicht schreiben; die Ergebnisdateien verwaltest du selbst." ) elif args.mode == "custom": # 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] Effort: {args.effort}\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: result = run_agent_loop( provider=provider, model=args.model, system_prompt=system_prompt, user_prompt=user_prompt, api_key=api_key, effort=args.effort, 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: tb = traceback.format_exc() @@ -945,11 +1098,13 @@ def main(): "reasoning_tokens": 0}, "modelUsage": {}, "tool_calls": [], "tool_call_count": 0, "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, "subagent_stats": {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}}, "subagent_details": [], } + finally: + liveness.stop() end_iso = datetime.now(timezone.utc).isoformat() result["start_time"] = start_iso @@ -965,6 +1120,9 @@ def main(): 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] 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.exit(1 if result["is_error"] else 0) diff --git a/.claude/skills/run-experiment/test_glm_kimi_adapter.py b/.claude/skills/run-experiment/test_glm_kimi_adapter.py new file mode 100644 index 00000000..d6738ead --- /dev/null +++ b/.claude/skills/run-experiment/test_glm_kimi_adapter.py @@ -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() diff --git a/Versuche/Versuch_02/README.md b/Versuche/Versuch_02/README.md index ac916ad3..d3837451 100644 --- a/Versuche/Versuch_02/README.md +++ b/Versuche/Versuch_02/README.md @@ -7,10 +7,56 @@ Rollenspezialisierte Agentendateien statt eines einzelnen Threads. Agentenmodus | Datei | SHA-256 | Zweck | |---|---|---| -| `01_Prompt.md` | `DCDC0E3F…B71BCF` | Analyseanweisung, Prompt-Version **02-A** | -| `01_Agents.json` | `6943EECD…AF1696` | acht Agentenrollen | +| `02_Prompt.md` | `5946FC82…C76ECD` | **aktuell** – Analyseanweisung, Prompt-Version **03-A** | +| `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` (`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 | | `faktenermittler` | belegte Fakten samt durchsetzender Codestelle, je Modulausschnitt | nein | -| `strs-autor` | Stakeholder-Ebene | ja, nur StRS | -| `syrs-autor` | Systemebene | ja, nur SyRS | -| `swrs-autor` | Softwareebene, zusätzlich Konsolidierungsprüfung | ja, nur SwRS | -| `iso29148-orchestrator` | führt die Ebenen zur Spezifikation zusammen | nein | +| `strs-autor` | Stakeholder-Ebene | ja, nur StRS – als Rückgabetext, ohne Datei | +| `syrs-autor` | Systemebene | ja, nur SyRS – als Rückgabetext, ohne Datei | +| `swrs-autor` | Softwareebene, zusätzlich Konsolidierungsprüfung | ja, nur SwRS – als Rückgabetext, ohne Datei | +| `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 | | `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` und `konsistenzpruefer` kommen aus den Messungen hinzu. -**Der Orchestrator delegiert nicht.** Er startet keine Subagenten und schreibt keine -Anforderungen, sondern stellt her, was erst am zusammengeführten Bestand entstehen kann: -durchgängige Nummerierung samt mitgezogener Verweise, beidseitig geschlossene Traceability, eine -aus dem Gesamtbestand erzeugte Hypothesenliste, die Abdeckungstabelle über das gemeinsame -Inventar. Ein delegierender Orchestrator erzeugte eine zweite Ebene, deren Subagenten-Prompts +**Der Orchestrator delegiert nicht.** Er startet keine Subagenten und schreibt weder +Anforderungen noch Dateien. Er prüft, was sich erst am zusammengeführten Bestand feststellen +lässt – durchgängige Nummerierung samt mitzuziehender Verweise, beidseitig geschlossene +Traceability, Deckungsgleichheit der Hypothesenliste, Abdeckung über das gemeinsame Inventar – +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. ## 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: `//custom//`. +### 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 `--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 -1. **`extract-subagenten.py` ist nicht gegen `custom`-Typen geprüft.** Es erwartet die - eingebauten Typen `Explore` und `general-purpose`; mit `--agents` heißen sie - `faktenermittler`, `strs-autor` und so fort. +1. ~~**`extract-subagenten.py` ist nicht gegen `custom`-Typen geprüft.**~~ **Geklärt am + 31.08.2026 durch Inspektion:** Das Skript filtert auf die Werkzeugnamen `Task`/`Agent` und + 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 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 @@ -145,3 +206,7 @@ Webzugriff oder MCP. `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. 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.