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