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
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>
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 "<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 |
| `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:** <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
`--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
User-Profil vorgefunden | **abweichend**: welche>
- **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
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 <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
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 = "<absoluter Pfad zum Laufverzeichnis>"
$root = "<Root-Verzeichnis der Codebasis>"
$modell = "<vom User gewählte Modell-ID, z.B. z-ai/glm-5.2>"
$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 = (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 `
--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 |
+298 -140
View File
@@ -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 <Pfad zur combined_prompt.md> \
--root <Root-Verzeichnis der Codebasis> \
--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> \
[--max-turns 50] \
[--max-turns 0] \
[--subagent-max-turns 0] \
[--temperature 1.0] \
[--timeout 0]
[--timeout 0] \
[--heartbeat-interval 60]
Ausgaben:
<Laufverzeichnis>/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)
@@ -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()