#!/usr/bin/env python3 """ TensorX API Adapter fuer den run-experiment Skill. Dieser Adapter fuehrt einen Headless-Versuchslauf mit einem OpenAI-kompatiblen Modell (Z.AI GLM oder Moonshot Kimi) ueber den TensorX API-Gateway durch. Er implementiert einen minimalen Agent-Loop mit Tool-Calling und erfasst Token-Metadaten (inkl. Reasoning-Tokens) aus jeder API-Antwort. Der API-Key wird automatisch aus der Cline providers.json gelesen (~/.cline/data/settings/providers.json, Provider "tensorx"). Verwendung: python glm-kimi-adapter.py \ --prompt \ --root \ --output \ --model \ --effort \ [--max-turns 50] \ [--temperature 1.0] \ [--timeout 0] Ausgaben: /RawResult.json – normalisierte Messdaten /Stderr.log – Fehler- und Debug-Ausgaben Der Adapter ist bewusst eigenstaendig (nur Python-Standardbibliothek + requests). """ import argparse import hashlib import json import os import re import subprocess import sys import time import traceback from datetime import datetime, timezone from pathlib import Path try: import requests except ImportError: sys.stderr.write("FEHLER: 'requests' ist nicht installiert.\n") sys.exit(2) # --------------------------------------------------------------------------- # Provider-Konfiguration # --------------------------------------------------------------------------- PROVIDERS = { "tensorx": { "name": "TensorX API Gateway", "base_url": "https://api.tensorx.ai/v1", "env_key": "TENSORX_API_KEY", }, } # Modell-Praefix -> Effort-Parameter-Typ # z-ai/* Modelle nutzen den 'thinking'-Parameter (level: none|low|medium|high|xhigh) # moonshotai/* Modelle nutzen 'reasoning_effort' (low|medium|high) MODEL_EFFORT_TYPE = { "z-ai": "thinking", "moonshotai": "reasoning_effort", } # Effort-Mapping: Skill-Effort -> API-Wert je Effort-Typ EFFORT_MAP = { "low": {"thinking": "low", "reasoning_effort": "low"}, "medium": {"thinking": "medium", "reasoning_effort": "medium"}, "high": {"thinking": "high", "reasoning_effort": "high"}, "xhigh": {"thinking": "xhigh", "reasoning_effort": "high"}, "max": {"thinking": "xhigh", "reasoning_effort": "high"}, } def load_cline_api_key(): """ Liest den TensorX API-Key aus der Cline providers.json. Pfad: ~/.cline/data/settings/providers.json Rueckgabe: (api_key, base_url) oder (None, None). """ home = Path.home() providers_file = home / ".cline" / "data" / "settings" / "providers.json" if not providers_file.is_file(): return None, None try: data = json.loads(providers_file.read_text(encoding="utf-8")) tx = data.get("providers", {}).get("tensorx", {}).get("settings", {}) return tx.get("apiKey"), tx.get("baseUrl") except (json.JSONDecodeError, KeyError): return None, None # Denylist fuer schreibende/bauende Kommandos DENIED_COMMAND_PATTERNS = [ r"\brm\b", r"\brmdir\b", r"\bmv\b", r"\bcp\b", r"\bdd\b", r"\btruncate\b", r"\bchmod\b", r"\bchown\b", r"\bln\b", r"\btee\b", r"\bsed\s+-i\b", r"\bgit\s+checkout\b", r"\bgit\s+restore\b", r"\bgit\s+clean\b", r"\bgit\s+reset\b", r"\bgit\s+add\b", r"\bgit\s+commit\b", r"\bgit\s+push\b", r"\bgit\s+fetch\b", r"\bdotnet\b", r"\bmsbuild\b", r"\bnpm\s+install\b", r"\bnuget\b", r"\bpip\s+install\b", r">\s*", r">>\s*", ] # --------------------------------------------------------------------------- # Tool-Definitionen (OpenAI Function Calling Format) # --------------------------------------------------------------------------- TOOLS = [ { "type": "function", "function": { "name": "read_file", "description": ( "Lies den Inhalt einer Textdatei. Der Pfad ist relativ zum " "Arbeitsverzeichnis (Root der Codebasis)." ), "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Relativer Pfad zur Datei (z.B. 'src/Program.cs')", }, }, "required": ["path"], }, }, }, { "type": "function", "function": { "name": "list_directory", "description": "Liste den Inhalt eines Verzeichnisses mit Typ-Kennzeichnung.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Relativer Pfad zum Verzeichnis (leer = Root)", }, }, "required": ["path"], }, }, }, { "type": "function", "function": { "name": "search_files", "description": ( "Durchsuche Dateien mit einem Regex-Muster (aehnlich grep -rn). " "Gibt Treffer mit Dateiname, Zeilennummer und Zeileninhalt zurueck." ), "parameters": { "type": "object", "properties": { "pattern": {"type": "string", "description": "Regex-Suchmuster"}, "path": { "type": "string", "description": "Relativer Pfad zum Startverzeichnis (leer = Root)", }, "file_pattern": { "type": "string", "description": "Dateifilter (z.B. '*.cs'), optional", }, }, "required": ["pattern"], }, }, }, { "type": "function", "function": { "name": "execute_command", "description": ( "Fuehre einen schreibgeschuetzten Shell-Befehl im Arbeitsverzeichnis " "aus. Schreibende und bauende Kommandos werden abgelehnt." ), "parameters": { "type": "object", "properties": { "command": {"type": "string", "description": "Der auszufuehrende Befehl"}, }, "required": ["command"], }, }, }, { "type": "function", "function": { "name": "write_file", "description": ( "Schreibe eine Ergebnisdatei in das Ausgabeverzeichnis. Der Pfad " "ist relativ zum Ausgabeverzeichnis (z.B. 'StRS.md')." ), "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "Relativer Pfad"}, "content": {"type": "string", "description": "Vollstaendiger Dateiinhalt"}, }, "required": ["path", "content"], }, }, }, { "type": "function", "function": { "name": "spawn_subagent", "description": ( "Starte einen Subagenten mit eigenem Kontext fuer eine isolierte Teilaufgabe. " "Der Subagent kann Dateien lesen, Verzeichnisse auflisten, suchen und " "Befehle ausfuehren – aber keine Ergebnisdateien schreiben. Verwende dies, " "um einen Teil der Codebasis parallel oder isoliert zu analysieren. " "Der Subagent erhaelt nur die Beschreibung, nicht den bisherigen " "Konversationsverlauf. Gib eine praegnante Aufgabenbeschreibung." ), "parameters": { "type": "object", "properties": { "description": { "type": "string", "description": "Die Aufgabe fuer den Subagenten (z.B. 'Analysiere alle Berechtigungspruefungen in src/backend/Centron.BL/Security und erstelle eine Zusammenfassung der gefundenen Pruefungen mit Dateipfaden und Methodennamen')", }, "subagent_type": { "type": "string", "description": "Typ des Subagenten: 'explore' fuer Code-Erkundung, 'general-purpose' fuer allgemeine Analyse", }, }, "required": ["description"], }, }, }, ] # Read-Only-Tools fuer Subagenten (kein write_file, kein spawn_subagent) SUBAGENT_TOOLS = [t for t in TOOLS if t["function"]["name"] not in ("write_file", "spawn_subagent")] # Standard-Subagent-Typen (fuer Modus 'builtin') BUILTIN_SUBAGENT_PROMPTS = { "explore": ( "Du bist ein Code-Explorations-Agent. Deine Aufgabe ist es, einen Teil der " "Codebasis zu untersuchen und eine strukturierte Zusammenfassung deiner " "Erkenntnisse zurueckzugeben. Nutze die Werkzeuge aktiv, um Dateien zu lesen " "und zu durchsuchen. Gib am Ende eine kompakte Zusammenfassung mit konkreten " "Dateipfaden, Klassennamen und Methodennamen zurueck." ), "general-purpose": ( "Du bist ein Analyse-Agent. Du untersuchst die Codebasis und beantwortest " "die dir gestellte Aufgabe. Nutze die Werkzeuge aktiv. Gib am Ende eine " "praegnante Antwort mit konkreten Belegen (Dateipfade, Methodennamen) zurueck." ), } # Aktive Subagent-Prompts (wird je Modus gesetzt) SUBAGENT_SYSTEM_PROMPTS = dict(BUILTIN_SUBAGENT_PROMPTS) def load_custom_agents(agents_file): """ Laedt Agenten-Definitionen aus einer JSON-Datei fuer Modus 'custom'. Format: { "agent_name": { "description": "...", "prompt": "..." }, ... } Rueckgabe: dict agent_name -> system_prompt """ agents_path = Path(agents_file) if not agents_path.is_file(): raise FileNotFoundError(f"Agenten-Datei nicht gefunden: {agents_file}") data = json.loads(agents_path.read_text(encoding="utf-8")) prompts = {} for name, spec in data.items(): desc = spec.get("description", "") prompt = spec.get("prompt", "") prompts[name] = prompt return prompts # --------------------------------------------------------------------------- # Pfad-Sicherheit # --------------------------------------------------------------------------- def safe_join(root: str, rel_path: str) -> Path: """Verbindet root und rel_path, verhindert Path-Traversal.""" root_resolved = Path(root).resolve() target = (root_resolved / rel_path).resolve() if not str(target).startswith(str(root_resolved)): raise ValueError(f"Pfad '{rel_path}' verlaesst das Root-Verzeichnis") return target # --------------------------------------------------------------------------- # Tool-Implementierungen # --------------------------------------------------------------------------- def tool_read_file(root: str, args: dict) -> str: path = args.get("path", "") try: full = safe_join(root, path) if not full.is_file(): return f"FEHLER: Datei nicht gefunden: {path}" content = full.read_text(encoding="utf-8", errors="replace") if len(content) > 200000: content = content[:200000] + "\n\n[... Datei abgeschnitten bei 200.000 Zeichen ...]" return content except ValueError as e: return f"FEHLER: {e}" except Exception as e: return f"FEHLER beim Lesen von {path}: {e}" def tool_list_directory(root: str, args: dict) -> str: path = args.get("path", "") try: full = safe_join(root, path) if path else Path(root).resolve() if not full.is_dir(): return f"FEHLER: Verzeichnis nicht gefunden: {path}" entries = [] for child in sorted(full.iterdir(), key=lambda c: (c.is_file(), c.name.lower())): typ = "[DIR] " if child.is_dir() else "[FILE]" size = "" if child.is_file(): try: size = f" ({child.stat().st_size:,} bytes)" except OSError: pass entries.append(f"{typ} {child.name}{size}") return "\n".join(entries) if entries else "(leeres Verzeichnis)" except ValueError as e: return f"FEHLER: {e}" except Exception as e: return f"FEHLER beim Auflisten von {path}: {e}" def tool_search_files(root: str, args: dict) -> str: pattern = args.get("pattern", "") path = args.get("path", "") file_pattern = args.get("file_pattern", "") if not pattern: return "FEHLER: Kein Suchmuster angegeben" try: regex = re.compile(pattern, re.IGNORECASE) search_root = safe_join(root, path) if path else Path(root).resolve() if not search_root.is_dir(): return f"FEHLER: Verzeichnis nicht gefunden: {path}" results = [] max_results = 500 max_file_size = 5 * 1024 * 1024 for file_path in search_root.rglob("*"): if not file_path.is_file(): continue if file_pattern: import fnmatch if not fnmatch.fnmatch(file_path.name, file_pattern): continue try: if file_path.stat().st_size > max_file_size: continue except OSError: continue try: rel = file_path.relative_to(Path(root).resolve()) except ValueError: continue try: with open(file_path, "r", encoding="utf-8", errors="replace") as f: for line_no, line in enumerate(f, 1): if regex.search(line): results.append(f"{rel}:{line_no}: {line.rstrip()[:300]}") if len(results) >= max_results: results.append(f"\n[... Suche bei {max_results} Treffern abgeschnitten ...]") return "\n".join(results) except Exception: continue return "\n".join(results) if results else "Keine Treffer." except re.error as e: return f"FEHLER: Ungueltiges Regex-Muster: {e}" except ValueError as e: return f"FEHLER: {e}" except Exception as e: return f"FEHLER bei der Suche: {e}" def tool_execute_command(root: str, args: dict) -> str: command = args.get("command", "") if not command: return "FEHLER: Kein Befehl angegeben" for pat in DENIED_COMMAND_PATTERNS: if re.search(pat, command, re.IGNORECASE): return "ABGELEHNT: Befehl enthaelt verbotenes Muster. Schreibende und bauende Kommandos sind gesperrt." try: result = subprocess.run( command, shell=True, cwd=root, capture_output=True, text=True, timeout=60, encoding="utf-8", errors="replace", ) output = result.stdout or "" if result.stderr: output += f"\n[STDERR]\n{result.stderr}" if len(output) > 100000: output = output[:100000] + "\n\n[... Ausgabe abgeschnitten ...]" return output.strip() if output.strip() else "(keine Ausgabe)" except subprocess.TimeoutExpired: return "FEHLER: Befehl nach 60 Sekunden abgebrochen" except Exception as e: return f"FEHLER bei Befehlsausfuehrung: {e}" def tool_write_file(output_dir: str, args: dict) -> str: path = args.get("path", "") content = args.get("content", "") if not path: return "FEHLER: Kein Dateipfad angegeben" # Modell gibt oft "Ergebnisse/" als Pfad – Präfix entfernen # da output_dir bereits das Ergebnisse-Verzeichnis ist. path = path.replace("\\", "/") for prefix in ("Ergebnisse/", "./Ergebnisse/", "ergebnisse/"): if path.startswith(prefix): path = path[len(prefix):] break try: base = Path(output_dir).resolve() target = (base / path).resolve() if not str(target).startswith(str(base)): return f"FEHLER: Pfad '{path}' verlaesst das Ausgabeverzeichnis" target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content, encoding="utf-8") return f"OK: Datei geschrieben: {path} ({len(content):,} Zeichen)" except Exception as e: return f"FEHLER beim Schreiben von {path}: {e}" def execute_tool(name: str, args: dict, root: str, output_dir: str) -> str: """Dispatch eines Tool-Aufrufs.""" dispatch = { "read_file": lambda a: tool_read_file(root, a), "list_directory": lambda a: tool_list_directory(root, a), "search_files": lambda a: tool_search_files(root, a), "execute_command": lambda a: tool_execute_command(root, a), "write_file": lambda a: tool_write_file(output_dir, a), } handler = dispatch.get(name) if handler: return handler(args) return f"FEHLER: Unbekanntes Werkzeug: {name}" # --------------------------------------------------------------------------- # Subagent # --------------------------------------------------------------------------- def run_subagent(provider, model, api_key, effort, root, description, subagent_type, temperature, timeout, max_turns=15): """ Startet einen Subagenten mit eigenem Kontext. Der Subagent erhaelt Read-Only-Tools und eine begrenzte Turn-Anzahl. Rueckgabe: dict mit result, usage, turns, tool_calls, status. """ sys_prompt = SUBAGENT_SYSTEM_PROMPTS.get( subagent_type, SUBAGENT_SYSTEM_PROMPTS["general-purpose"] ) messages = [ {"role": "system", "content": sys_prompt}, {"role": "user", "content": description}, ] sub_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "cached_tokens": 0, "reasoning_tokens": 0} sub_turns = 0 sub_tool_calls = 0 sub_result = "" sub_errors = [] while sub_turns < max_turns: sub_turns += 1 try: resp = call_api_subagent(provider, model, messages, api_key, effort, temperature, timeout) except Exception as e: sub_errors.append(str(e)) break u = resp.get("usage", {}) sub_usage["prompt_tokens"] += u.get("prompt_tokens", 0) sub_usage["completion_tokens"] += u.get("completion_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["reasoning_tokens"] += u.get("completion_tokens_details", {}).get("reasoning_tokens", 0) choices = resp.get("choices", []) if not choices: sub_errors.append("Keine choices in Subagent-Antwort") break msg = choices[0].get("message", {}) messages.append(msg) content = msg.get("content", "") if content: sub_result = content tool_calls = msg.get("tool_calls", []) if not tool_calls: break for tc in tool_calls: func = tc.get("function", {}) tname = func.get("name", "") try: targs = json.loads(func.get("arguments", "{}")) except json.JSONDecodeError: targs = {} sub_tool_calls += 1 # Subagent darf nur Read-Only-Tools nutzen if tname in ("read_file", "list_directory", "search_files", "execute_command"): tresult = execute_tool(tname, targs, root, "") else: tresult = f"FEHLER: Werkzeug '{tname}' ist fuer Subagenten nicht freigegeben." messages.append({"role": "tool", "tool_call_id": tc.get("id", ""), "name": tname, "content": tresult}) return { "result": sub_result or "(Subagent ohne Ergebnis)", "usage": sub_usage, "turns": sub_turns, "tool_calls": sub_tool_calls, "errors": sub_errors, "status": "completed" if sub_result else "failed", } def call_api_subagent(provider, model, messages, api_key, effort, temperature, timeout): """API-Aufruf fuer Subagenten (mit SUBAGENT_TOOLS statt TOOLS).""" url = f"{provider['base_url']}/chat/completions" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} body = { "model": model, "messages": messages, "tools": SUBAGENT_TOOLS, "tool_choice": "auto", "temperature": temperature, "stream": False, } model_prefix = model.split("/")[0] if "/" in model else "" effort_type = MODEL_EFFORT_TYPE.get(model_prefix, "thinking") effort_val = EFFORT_MAP.get(effort, {}).get(effort_type, "medium") if effort_type == "thinking": body["thinking"] = {"type": "enabled", "level": effort_val} elif effort_type == "reasoning_effort": body["reasoning_effort"] = effort_val resp = requests.post(url, headers=headers, json=body, timeout=timeout if timeout > 0 else 1800) if resp.status_code != 200: raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") return resp.json() # --------------------------------------------------------------------------- # API-Aufruf # --------------------------------------------------------------------------- def call_api(provider, model, messages, api_key, effort, temperature, timeout): """Ruft die Chat-Completions-API auf und gibt die JSON-Antwort zurueck.""" url = f"{provider['base_url']}/chat/completions" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} body = { "model": model, "messages": messages, "tools": TOOLS, "tool_choice": "auto", "temperature": temperature, "stream": False, } # Effort-Parameter anhand des Modell-Praefixes waehlen model_prefix = model.split("/")[0] if "/" in model else "" effort_type = MODEL_EFFORT_TYPE.get(model_prefix, "thinking") effort_val = EFFORT_MAP.get(effort, {}).get(effort_type, "medium") if effort_type == "thinking": body["thinking"] = {"type": "enabled", "level": effort_val} elif effort_type == "reasoning_effort": body["reasoning_effort"] = effort_val resp = requests.post(url, headers=headers, json=body, timeout=timeout if timeout > 0 else 1800) if resp.status_code != 200: raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") return resp.json() # --------------------------------------------------------------------------- # Agent-Loop # --------------------------------------------------------------------------- def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort, root, output_dir, max_turns, temperature, timeout, mode="solo"): """Fuehrt den Agent-Loop durch und sammelt Metriken.""" # Tools je nach Modus waehlen if mode == "builtin": active_tools = TOOLS # inklusive spawn_subagent else: active_tools = [t for t in TOOLS if t["function"]["name"] != "spawn_subagent"] # call_api mit den aktiven Tools parametrisieren def _call_api(messages): url = f"{provider['base_url']}/chat/completions" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} body = { "model": model, "messages": messages, "tools": active_tools, "tool_choice": "auto", "temperature": temperature, "stream": False, } model_prefix = model.split("/")[0] if "/" in model else "" effort_type = MODEL_EFFORT_TYPE.get(model_prefix, "thinking") effort_val = EFFORT_MAP.get(effort, {}).get(effort_type, "medium") if effort_type == "thinking": body["thinking"] = {"type": "enabled", "level": effort_val} elif effort_type == "reasoning_effort": body["reasoning_effort"] = effort_val resp = requests.post(url, headers=headers, json=body, timeout=timeout if timeout > 0 else 1800) if resp.status_code != 200: raise RuntimeError(f"API-Fehler {resp.status_code}: {resp.text[:2000]}") return resp.json() messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] total_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "cached_tokens": 0, "reasoning_tokens": 0} turns = 0 tool_calls_log = [] final_content = "" model_reported = model finish_reason = None errors = [] start_time = time.time() # Subagent-Tracking subagent_stats = {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}} subagent_details = [] MAX_SUBAGENTS = 10 # Hartes Limit: danach wird spawn_subagent verweigert write_file_count = 0 # Zaehlt write_file-Aufrufe write_reminder_sent = False # Wurde schon eine Schreib-Erinnerung gesendet? while turns < max_turns: turns += 1 # Schreib-Erinnerung: Wenn nach 1/3 der Turns noch kein write_file, # oder nach 2/3 der Turns weniger als 3 write_file-Aufrufe if not write_reminder_sent and turns >= max_turns // 3 and write_file_count == 0: messages.append({ "role": "user", "content": ( "WICHTIG: Du hast bisher keine Ergebnisdateien geschrieben. " "Beginne JETZT damit, deine Analyseergebnisse mit write_file " "in die vorgegebenen Dateien (StRS.md, SyRS.md, SwRS.md, " "Traceability.md, Hypothesen.md, Glossar.md, Analysebericht.md) " "zu schreiben. Schreibe nicht weiter Subagenten — formalisiere " "deine bisherigen Erkenntnisse in Anforderungen." ), }) write_reminder_sent = True sys.stderr.write(f"[glm-kimi-adapter] Schreib-Erinnerung gesendet " f"(Turn {turns}, 0 write_file-Aufrufe)\n") elif (not write_reminder_sent and turns >= max_turns * 2 // 3 and write_file_count < 3): messages.append({ "role": "user", "content": ( f"WICHTIG: Du hast bisher nur {write_file_count} Ergebnisdatei(en) " f"geschrieben. Es fehlen noch mehrere der 7 vorgegebenen Dateien " f"(StRS.md, SyRS.md, SwRS.md, Traceability.md, Hypothesen.md, " f"Glossar.md, Analysebericht.md). Schreibe die fehlenden Dateien " f"JETZT mit write_file." ), }) write_reminder_sent = True sys.stderr.write(f"[glm-kimi-adapter] Schreib-Erinnerung gesendet " f"(Turn {turns}, {write_file_count} write_file-Aufrufe)\n") try: response = _call_api(messages) except Exception as e: errors.append(f"Turn {turns}: API-Fehler: {e}") break usage = response.get("usage", {}) total_usage["prompt_tokens"] += usage.get("prompt_tokens", 0) total_usage["completion_tokens"] += usage.get("completion_tokens", 0) total_usage["total_tokens"] += usage.get("total_tokens", 0) cached = usage.get("prompt_tokens_details", {}).get("cached_tokens", 0) total_usage["cached_tokens"] += cached comp_details = usage.get("completion_tokens_details", {}) total_usage["reasoning_tokens"] += comp_details.get("reasoning_tokens", 0) if response.get("model"): model_reported = response["model"] choices = response.get("choices", []) if not choices: errors.append(f"Turn {turns}: Keine choices in API-Antwort") break choice = choices[0] finish_reason = choice.get("finish_reason") msg = choice.get("message", {}) messages.append(msg) content = msg.get("content", "") if content: final_content = content tool_calls = msg.get("tool_calls", []) if not tool_calls: break for tc in tool_calls: func = tc.get("function", {}) tool_name = func.get("name", "") tool_args_str = func.get("arguments", "{}") tc_id = tc.get("id", "") try: tool_args = json.loads(tool_args_str) except json.JSONDecodeError: tool_args = {} tool_calls_log.append({"turn": turns, "name": tool_name, "args": tool_args}) # write_file-Zaehler erhoehen if tool_name == "write_file": write_file_count += 1 # Subagent-Spawning behandeln if tool_name == "spawn_subagent": if subagent_stats["spawned"] >= MAX_SUBAGENTS: # Hartes Limit erreicht: verweigere und erzwinge Schreibphase result = ( "ABGELEHNT: Subagent-Limit erreicht (10/10). " "Du hast bereits 10 Subagenten gestartet. " "Schreibe JETZT deine Ergebnisdateien mit write_file: " "StRS.md, SyRS.md, SwRS.md, Traceability.md, " "Hypothesen.md, Glossar.md, Analysebericht.md. " "Starte keine weiteren Subagenten." ) sys.stderr.write(f"[glm-kimi-adapter] Subagent verweigert " f"(Limit {MAX_SUBAGENTS} erreicht)\n") else: sa_desc = tool_args.get("description", "") sa_type = tool_args.get("subagent_type", "general-purpose") subagent_stats["spawned"] += 1 subagent_stats["by_type"][sa_type] = subagent_stats["by_type"].get(sa_type, 0) + 1 sys.stderr.write(f"[glm-kimi-adapter] Subagent " f"{subagent_stats['spawned']}/{MAX_SUBAGENTS} " f"gestartet (Typ: {sa_type})\n") try: sa_result = run_subagent( provider, model, api_key, effort, root, sa_desc, sa_type, temperature, timeout ) subagent_stats["completed" if sa_result["status"] == "completed" else "failed"] += 1 sa_u = sa_result["usage"] total_usage["prompt_tokens"] += sa_u["prompt_tokens"] total_usage["completion_tokens"] += sa_u["completion_tokens"] total_usage["total_tokens"] += sa_u["total_tokens"] total_usage["cached_tokens"] += sa_u["cached_tokens"] total_usage["reasoning_tokens"] += sa_u["reasoning_tokens"] subagent_details.append({ "id": subagent_stats["spawned"], "type": sa_type, "description": sa_desc[:200], "turns": sa_result["turns"], "tool_calls": sa_result["tool_calls"], "tokens": sa_u["total_tokens"], "status": sa_result["status"], }) result = sa_result["result"] 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() duration_s = end_time - start_time written_files = [] if os.path.isdir(output_dir): for f in sorted(Path(output_dir).rglob("*")): if f.is_file(): try: written_files.append({"path": str(f.relative_to(output_dir)), "size": f.stat().st_size}) except OSError: pass tool_call_types = {} for tc in tool_calls_log: name = tc["name"] tool_call_types[name] = tool_call_types.get(name, 0) + 1 return { "is_error": len(errors) > 0 and not final_content, "subtype": "success" if final_content else "error", "duration_ms": int(duration_s * 1000), "duration_api_ms": int(duration_s * 1000), "num_turns": turns, "model": model_reported, "model_requested": model, "provider": provider["__id"], "usage": { "prompt_tokens": total_usage["prompt_tokens"], "completion_tokens": total_usage["completion_tokens"], "total_tokens": total_usage["total_tokens"], "cached_tokens": total_usage["cached_tokens"], "cache_read_tokens": total_usage["cached_tokens"], "cache_creation_tokens": 0, "reasoning_tokens": total_usage["reasoning_tokens"], "output_tokens_details": { "thinking_tokens": total_usage["reasoning_tokens"], }, }, "modelUsage": { model_reported: { "input_tokens": total_usage["prompt_tokens"], "output_tokens": total_usage["completion_tokens"], "cache_read_input_tokens": total_usage["cached_tokens"], "cache_creation_input_tokens": 0, "reasoning_tokens": total_usage["reasoning_tokens"], } }, "tool_calls": tool_calls_log, "tool_call_count": len(tool_calls_log), "tool_call_types": tool_call_types, "written_files": written_files, "result": final_content, "finish_reason": finish_reason, "errors": errors, "session_id": "", "adapter": "python-glm-kimi", "adapter_version": "1.1.0", "mode": mode, "subagent_stats": subagent_stats, "subagent_details": subagent_details, } # --------------------------------------------------------------------------- # Hauptprogramm # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser(description="TensorX API Adapter fuer run-experiment (GLM/Kimi)") parser.add_argument("--prompt", required=True, help="Pfad zur combined_prompt.md") parser.add_argument("--root", required=True, help="Root-Verzeichnis der Codebasis") parser.add_argument("--output", required=True, help="Ausgabeverzeichnis (Ergebnisse)") parser.add_argument("--model", required=True, help="Modell-ID (z.B. z-ai/glm-5.2, moonshotai/kimi-k3)") parser.add_argument("--provider", default="tensorx", help="API-Provider (default: tensorx)") parser.add_argument("--api-key", default=None, help="API-Key (default: aus Cline providers.json)") parser.add_argument("--effort", default="high", choices=["low", "medium", "high", "xhigh", "max"]) parser.add_argument("--mode", default="solo", choices=["solo", "builtin", "custom"], help="Agentenmodus (solo=keine Subagenten, builtin=eingebaute, custom=vordefinierte Agenten aus Datei)") parser.add_argument("--agents", default=None, help="Pfad zu Agenten-Definitionen (JSON) fuer Modus 'custom'") parser.add_argument("--max-turns", type=int, default=50) parser.add_argument("--temperature", type=float, default=1.0) parser.add_argument("--timeout", type=int, default=0, help="Timeout in Sek (0=keins)") parser.add_argument("--result-dir", default=None, help="Verzeichnis fuer RawResult.json") args = parser.parse_args() provider = PROVIDERS.get(args.provider, PROVIDERS["tensorx"]).copy() provider["__id"] = args.provider # API-Key: erst --api-key, dann Cline providers.json, dann Env-Var api_key = args.api_key base_url_override = None if not api_key: cline_key, cline_url = load_cline_api_key() if cline_key: api_key = cline_key base_url_override = cline_url sys.stderr.write("[glm-kimi-adapter] API-Key aus Cline providers.json gelesen.\n") else: api_key = os.environ.get(provider.get("env_key", ""), "") if base_url_override: provider["base_url"] = base_url_override if not api_key: sys.stderr.write( "FEHLER: Kein API-Key gefunden. Weder --api-key, noch Cline providers.json, " f"noch Umgebungsvariable {provider.get('env_key', '')}.\n" ) sys.exit(2) prompt_path = Path(args.prompt) if not prompt_path.is_file(): sys.stderr.write(f"FEHLER: Prompt-Datei nicht gefunden: {args.prompt}\n") sys.exit(2) user_prompt = prompt_path.read_text(encoding="utf-8") system_prompt = ( "Du bist ein Requirements Engineer im Reverse Requirements Engineering " "eines Legacy-ERP-Systems. Du analysierst die Codebasis im Arbeitsverzeichnis " "und erstellst eine Anforderungsspezifikation nach ISO/IEC/IEEE 29148:2018.\n\n" "Werkzeuge: read_file, list_directory, search_files, execute_command, write_file.\n\n" "WICHTIG: Beginne deine Arbeit mit dem Schreiben von Ergebnisdateien (write_file), " "nicht mit dem Erkunden. Erstelle zuerst die Skelette der 7 Ergebnisdateien " "(StRS.md, SyRS.md, SwRS.md, Traceability.md, Hypothesen.md, Glossar.md, " "Analysebericht.md) mit Platzhalter-Inhalt, bevor du in die Details gehst. " "Wenn du Ergebnisse hast, schreibe sie SOFORT mit write_file — beschreibe nicht, " "was du schreiben wirst, schreibe es." ) if args.mode == "builtin": system_prompt += ( "\nZusaetzlich steht spawn_subagent zur Verfuegung: Starte einen " "Subagenten mit eigenem Kontext fuer isolierte Teilaufgaben (z.B. " "Analyse eines einzelnen Moduls). Der Subagent kann nur lesen, nicht " "schreiben. Nutze Subagenten, um die Breite der Analyse zu erhoehen: " "delegiere Modul-Analysen an Subagenten, waehrend du die " "Gesamtstruktur und die Ergebnisdateien verwaltest." ) elif args.mode == "custom": # Custom agents laden if args.agents: custom_prompts = load_custom_agents(args.agents) SUBAGENT_SYSTEM_PROMPTS.clear() SUBAGENT_SYSTEM_PROMPTS.update(custom_prompts) agent_list = ", ".join(SUBAGENT_SYSTEM_PROMPTS.keys()) sys.stderr.write(f"[glm-kimi-adapter] Custom agents geladen: {agent_list}\n") system_prompt += ( f"\nDu orchestrierst spezialisierte Subagenten. Verfuegbare Agenten-Typen: " f"{agent_list}. Nutze spawn_subagent mit dem passenden subagent_type, " f"um Teilaufgaben zu delegieren. Vorgehen: " f"1. Starte 'modulinventar' fuer das vollstaendige Inventar. " f"2. Starte 'faktenermittler' fuer Modulausschnitte, die du analysieren willst. " f"3. Schreibe die Anforderungen selbst mit write_file (die Autoren-Agenten " f"sind nur fuer Vorbereitung da, nicht fuer das Schreiben der Ergebnisdateien). " f"4. Fuehre den Konsistenzcheck selbst durch. " f"Der Subagent kann nur lesen, nicht schreiben." ) else: sys.stderr.write("[glm-kimi-adapter] WARNUNG: Modus 'custom' ohne --agents, falle auf 'builtin' zurueck\n") args.mode = "builtin" system_prompt += ( "\nZusaetzlich steht spawn_subagent zur Verfuegung." ) system_prompt += ( "\nDie Codebasis wird ausschliesslich GELESEN. Schreibe Ergebnisdateien mit " "write_file ins Ausgabeverzeichnis. Sprache: Deutsch fuer Anforderungen." ) output_dir = Path(args.output).resolve() output_dir.mkdir(parents=True, exist_ok=True) result_dir = Path(args.result_dir) if args.result_dir else output_dir.parent result_dir.mkdir(parents=True, exist_ok=True) start_iso = datetime.now(timezone.utc).isoformat() sys.stderr.write(f"[glm-kimi-adapter] Start: {start_iso}\n") sys.stderr.write(f"[glm-kimi-adapter] Provider: {provider['name']}\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] Mode: {args.mode}\n") try: result = run_agent_loop( provider=provider, model=args.model, system_prompt=system_prompt, user_prompt=user_prompt, api_key=api_key, effort=args.effort, root=args.root, output_dir=str(output_dir), max_turns=args.max_turns, temperature=args.temperature, timeout=args.timeout, mode=args.mode, ) except Exception as e: tb = traceback.format_exc() sys.stderr.write(f"[glm-kimi-adapter] FEHLER: {e}\n{tb}\n") result = { "is_error": True, "subtype": "error", "error": str(e), "duration_ms": 0, "num_turns": 0, "model": args.model, "model_requested": args.model, "provider": args.provider, "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "cached_tokens": 0, "cache_read_tokens": 0, "cache_creation_tokens": 0, "reasoning_tokens": 0}, "modelUsage": {}, "tool_calls": [], "tool_call_count": 0, "tool_call_types": {}, "written_files": [], "result": "", "errors": [str(e)], "adapter": "python-glm-kimi", "adapter_version": "1.1.0", "mode": args.mode, "subagent_stats": {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}}, "subagent_details": [], } end_iso = datetime.now(timezone.utc).isoformat() result["start_time"] = start_iso result["end_time"] = end_iso raw_result_path = result_dir / "RawResult.json" raw_result_path.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8") sys.stderr.write(f"[glm-kimi-adapter] Ende: {end_iso}\n") sys.stderr.write(f"[glm-kimi-adapter] Turns: {result['num_turns']}\n") sys.stderr.write(f"[glm-kimi-adapter] Tokens gesamt: {result['usage']['total_tokens']:,}\n") sys.stderr.write(f"[glm-kimi-adapter] Tool-Calls: {result['tool_call_count']}\n") sa = result.get("subagent_stats", {}) sys.stderr.write(f"[glm-kimi-adapter] Subagenten: {sa.get('spawned',0)} (completed: {sa.get('completed',0)}, failed: {sa.get('failed',0)})\n") sys.stderr.write(f"[glm-kimi-adapter] Ergebnisdateien: {len(result['written_files'])}\n") sys.stderr.write(f"[glm-kimi-adapter] RawResult: {raw_result_path}\n") sys.exit(1 if result["is_error"] else 0) if __name__ == "__main__": main()