1135 lines
47 KiB
Python
1135 lines
47 KiB
Python
#!/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, Qwen oder Moonshot Kimi) ueber den TensorX API-Gateway durch.
|
||
Er implementiert einen minimalen Agent-Loop mit Tool-Calling und erfasst
|
||
Token-Metadaten (inkl. Reasoning-Tokens) aus jeder API-Antwort.
|
||
|
||
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 <Pfad zur combined_prompt.md> \
|
||
--root <Root-Verzeichnis der Codebasis> \
|
||
--output <Laufverzeichnis/Ergebnisse> \
|
||
--model <Modell-ID, z.B. z-ai/glm-5.3-flash oder qwen/qwen3.8-flash-next> \
|
||
--effort <low|medium|high|xhigh> \
|
||
[--max-turns 0] \
|
||
[--subagent-max-turns 0] \
|
||
[--temperature 1.0] \
|
||
[--timeout 0] \
|
||
[--heartbeat-interval 60]
|
||
|
||
Ausgaben:
|
||
<Laufverzeichnis>/RawResult.json – normalisierte Messdaten
|
||
<Laufverzeichnis>/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 threading
|
||
import time
|
||
import traceback
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
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)
|
||
# qwen/* Modelle nutzen ebenfalls den 'thinking'-Parameter
|
||
# moonshotai/* Modelle nutzen 'reasoning_effort' (low|medium|high)
|
||
MODEL_EFFORT_TYPE = {
|
||
"z-ai": "thinking",
|
||
"qwen": "thinking",
|
||
"moonshotai": "reasoning_effort",
|
||
}
|
||
|
||
# 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"},
|
||
}
|
||
|
||
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():
|
||
"""
|
||
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/<name>" 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 post_chat_completion(url, headers, body, timeout, liveness,
|
||
activity_id, activity_label):
|
||
"""Fuehrt einen nicht gestreamten API-Aufruf mit sichtbarem Lebenszeichen aus."""
|
||
request_timeout = timeout if timeout > 0 else None
|
||
liveness.set(activity_id, f"{activity_label}: wartet auf API-Antwort")
|
||
log_stderr(f"{activity_label}: API-Aufruf gestartet")
|
||
started = time.monotonic()
|
||
try:
|
||
response = requests.post(
|
||
url, headers=headers, json=body, timeout=request_timeout
|
||
)
|
||
finally:
|
||
liveness.clear(activity_id)
|
||
elapsed_s = time.monotonic() - started
|
||
log_stderr(
|
||
f"{activity_label}: API-Antwort nach {elapsed_s:.1f}s "
|
||
f"(HTTP {response.status_code})"
|
||
)
|
||
if response.status_code != 200:
|
||
raise RuntimeError(
|
||
f"API-Fehler {response.status_code}: {response.text[:2000]}"
|
||
)
|
||
return response.json()
|
||
|
||
|
||
def run_subagent(provider, model, api_key, effort, root, description,
|
||
subagent_type, temperature, timeout, liveness, agent_id,
|
||
max_turns=0):
|
||
"""
|
||
Startet einen Subagenten mit eigenem Kontext.
|
||
Der Subagent erhaelt Read-Only-Tools. max_turns=0 bedeutet unbegrenzt.
|
||
Rueckgabe: dict mit result, usage, turns, tool_calls, status.
|
||
"""
|
||
sys_prompt = SUBAGENT_SYSTEM_PROMPTS.get(
|
||
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 = []
|
||
completed_normally = False
|
||
label = f"Subagent {agent_id} ({subagent_type})"
|
||
|
||
log_stderr(f"{label}: gestartet")
|
||
while max_turns <= 0 or sub_turns < max_turns:
|
||
sub_turns += 1
|
||
try:
|
||
resp = call_api_subagent(provider, model, messages, api_key,
|
||
effort, temperature, timeout, liveness,
|
||
agent_id, subagent_type, sub_turns)
|
||
except Exception as e:
|
||
sub_errors.append(str(e))
|
||
log_stderr(f"{label}: FEHLER in Turn {sub_turns}: {e}")
|
||
break
|
||
u = resp.get("usage", {})
|
||
sub_usage["prompt_tokens"] += u.get("prompt_tokens", 0)
|
||
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)
|
||
log_stderr(
|
||
f"{label}: Turn {sub_turns} API abgeschlossen "
|
||
f"(Antwort-Tokens: {u.get('total_tokens', 0):,}, "
|
||
f"kumuliert: {sub_usage['total_tokens']:,})"
|
||
)
|
||
|
||
choices = resp.get("choices", [])
|
||
if not choices:
|
||
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:
|
||
completed_normally = True
|
||
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
|
||
liveness.set(
|
||
f"subagent-{agent_id}",
|
||
f"{label}: fuehrt Tool {tname} in Turn {sub_turns} aus",
|
||
)
|
||
# Subagent darf nur Read-Only-Tools nutzen
|
||
try:
|
||
if tname in ("read_file", "list_directory", "search_files", "execute_command"):
|
||
tresult = execute_tool(tname, targs, root, "")
|
||
else:
|
||
tresult = f"FEHLER: Werkzeug '{tname}' ist fuer Subagenten nicht freigegeben."
|
||
finally:
|
||
liveness.clear(f"subagent-{agent_id}")
|
||
messages.append({"role": "tool", "tool_call_id": tc.get("id", ""),
|
||
"name": tname, "content": tresult})
|
||
|
||
status = "completed" if completed_normally and not sub_errors else "failed"
|
||
log_stderr(
|
||
f"{label}: beendet (Status: {status}, Turns: {sub_turns}, "
|
||
f"Tool-Calls: {sub_tool_calls}, Tokens: {sub_usage['total_tokens']:,})"
|
||
)
|
||
return {
|
||
"result": sub_result or "(Subagent ohne Ergebnis)",
|
||
"usage": sub_usage,
|
||
"turns": sub_turns,
|
||
"tool_calls": sub_tool_calls,
|
||
"errors": sub_errors,
|
||
"status": status,
|
||
}
|
||
|
||
|
||
def call_api_subagent(provider, model, messages, api_key, effort, temperature,
|
||
timeout, liveness, agent_id, subagent_type, turn):
|
||
"""API-Aufruf fuer Subagenten (mit SUBAGENT_TOOLS statt TOOLS)."""
|
||
url = f"{provider['base_url']}/chat/completions"
|
||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||
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
|
||
return post_chat_completion(
|
||
url, headers, body, timeout, liveness,
|
||
f"subagent-{agent_id}",
|
||
f"Subagent {agent_id} ({subagent_type}) Turn {turn}",
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# API-Aufruf
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def call_api(provider, model, messages, api_key, effort, temperature, timeout,
|
||
liveness, activity_id="main-api", activity_label="Hauptagent"):
|
||
"""Ruft die Chat-Completions-API auf und gibt die JSON-Antwort zurueck."""
|
||
url = f"{provider['base_url']}/chat/completions"
|
||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||
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
|
||
return post_chat_completion(
|
||
url, headers, body, timeout, liveness, activity_id, activity_label
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Agent-Loop
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def run_agent_loop(provider, model, system_prompt, user_prompt, api_key, effort,
|
||
root, output_dir, max_turns, temperature, timeout, liveness,
|
||
mode="solo", subagent_max_turns=0):
|
||
"""Fuehrt den Agent-Loop durch und sammelt Metriken."""
|
||
# Tools je nach Modus waehlen
|
||
if mode in ("builtin", "custom"):
|
||
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
|
||
return post_chat_completion(
|
||
url, headers, body, timeout, liveness, "main-api",
|
||
f"Hauptagent Turn {turns}",
|
||
)
|
||
|
||
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 = []
|
||
|
||
while max_turns <= 0 or turns < max_turns:
|
||
turns += 1
|
||
|
||
try:
|
||
response = _call_api(messages)
|
||
except Exception as e:
|
||
errors.append(f"Turn {turns}: API-Fehler: {e}")
|
||
log_stderr(f"FEHLER Hauptagent Turn {turns}: {e}")
|
||
break
|
||
usage = response.get("usage", {})
|
||
total_usage["prompt_tokens"] += usage.get("prompt_tokens", 0)
|
||
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)
|
||
log_stderr(
|
||
f"Hauptagent Turn {turns} API abgeschlossen "
|
||
f"(Antwort-Tokens: {usage.get('total_tokens', 0):,}, "
|
||
f"Gesamtlauf kumuliert: {total_usage['total_tokens']:,})"
|
||
)
|
||
if response.get("model"):
|
||
model_reported = response["model"]
|
||
choices = response.get("choices", [])
|
||
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
|
||
parsed_calls = []
|
||
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})
|
||
parsed_calls.append({
|
||
"tool_call": tc,
|
||
"name": tool_name,
|
||
"args": tool_args,
|
||
"id": tc_id,
|
||
})
|
||
|
||
spawn_calls = [call for call in parsed_calls if call["name"] == "spawn_subagent"]
|
||
results_by_id = {}
|
||
futures = []
|
||
executor = None
|
||
|
||
if spawn_calls:
|
||
# Bewusst kein Adapterlimit: Anzahl und Typen bestimmt allein das Modell.
|
||
executor = ThreadPoolExecutor(
|
||
max_workers=len(spawn_calls),
|
||
thread_name_prefix=f"subagent-turn-{turns}",
|
||
)
|
||
for call in spawn_calls:
|
||
sa_desc = call["args"].get("description", "")
|
||
sa_type = call["args"].get("subagent_type", "general-purpose")
|
||
subagent_stats["spawned"] += 1
|
||
agent_id = subagent_stats["spawned"]
|
||
subagent_stats["by_type"][sa_type] = (
|
||
subagent_stats["by_type"].get(sa_type, 0) + 1
|
||
)
|
||
log_stderr(
|
||
f"Subagent {agent_id} zur parallelen Ausfuehrung eingeplant "
|
||
f"(Typ: {sa_type}, Hauptagent-Turn: {turns})"
|
||
)
|
||
future = executor.submit(
|
||
run_subagent,
|
||
provider, model, api_key, effort, root, sa_desc, sa_type,
|
||
temperature, timeout, liveness, agent_id,
|
||
subagent_max_turns,
|
||
)
|
||
futures.append((call, future, agent_id, sa_type, sa_desc))
|
||
|
||
# Nicht-Subagenten-Tools laufen, waehrend die Subagenten parallel arbeiten.
|
||
for call in parsed_calls:
|
||
if call["name"] == "spawn_subagent":
|
||
continue
|
||
activity_id = f"main-tool-{call['id']}"
|
||
liveness.set(
|
||
activity_id,
|
||
f"Hauptagent Turn {turns}: fuehrt Tool {call['name']} aus",
|
||
)
|
||
try:
|
||
results_by_id[call["id"]] = execute_tool(
|
||
call["name"], call["args"], root, output_dir
|
||
)
|
||
finally:
|
||
liveness.clear(activity_id)
|
||
|
||
if futures:
|
||
liveness.set(
|
||
"main-subagent-wait",
|
||
f"Hauptagent Turn {turns}: wartet auf {len(futures)} parallele Subagenten",
|
||
)
|
||
try:
|
||
for call, future, agent_id, sa_type, sa_desc in futures:
|
||
try:
|
||
sa_result = 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"]
|
||
for key in total_usage:
|
||
total_usage[key] += sa_u.get(key, 0)
|
||
subagent_details.append({
|
||
"id": agent_id,
|
||
"type": sa_type,
|
||
"description": sa_desc[:200],
|
||
"turns": sa_result["turns"],
|
||
"tool_calls": sa_result["tool_calls"],
|
||
"tokens": sa_u.get("total_tokens", 0),
|
||
"status": sa_result["status"],
|
||
"errors": sa_result.get("errors", []),
|
||
})
|
||
results_by_id[call["id"]] = sa_result["result"]
|
||
finally:
|
||
liveness.clear("main-subagent-wait")
|
||
executor.shutdown(wait=True)
|
||
|
||
# Tool-Antworten muessen in derselben Reihenfolge wie die Tool-Calls folgen.
|
||
for call in parsed_calls:
|
||
messages.append({
|
||
"role": "tool",
|
||
"tool_call_id": call["id"],
|
||
"name": call["name"],
|
||
"content": results_by_id[call["id"]],
|
||
})
|
||
|
||
end_time = time.time()
|
||
duration_s = end_time - start_time
|
||
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": bool(errors),
|
||
"subtype": "error" if errors else "success",
|
||
"duration_ms": int(duration_s * 1000),
|
||
"duration_api_ms": int(duration_s * 1000),
|
||
"num_turns": turns, "model": model_reported, "model_requested": model,
|
||
"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": ADAPTER_VERSION,
|
||
"mode": mode,
|
||
"subagent_stats": subagent_stats,
|
||
"subagent_details": subagent_details,
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Hauptprogramm
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="TensorX API Adapter fuer run-experiment (GLM/Qwen/Kimi)")
|
||
parser.add_argument("--prompt", required=True, help="Pfad zur combined_prompt.md")
|
||
parser.add_argument("--root", required=True, help="Root-Verzeichnis der Codebasis")
|
||
parser.add_argument("--output", required=True, help="Ausgabeverzeichnis (Ergebnisse)")
|
||
parser.add_argument("--model", required=True, help="Modell-ID (z.B. z-ai/glm-5.3-flash, qwen/qwen3.8-flash-next)")
|
||
parser.add_argument("--provider", default="tensorx", help="API-Provider (default: tensorx)")
|
||
parser.add_argument("--api-key", default=None, help="API-Key (default: aus Cline providers.json)")
|
||
parser.add_argument("--effort", default="high", choices=["low", "medium", "high", "xhigh", "max"])
|
||
parser.add_argument("--mode", default="solo", choices=["solo", "builtin", "custom"], help="Agentenmodus (solo=keine Subagenten, builtin=eingebaute, custom=vordefinierte Agenten aus Datei)")
|
||
parser.add_argument("--agents", default=None, help="Pfad zu Agenten-Definitionen (JSON) fuer Modus 'custom'")
|
||
parser.add_argument("--max-turns", type=int, default=0, help="Maximale Hauptagent-Turns (0=unbegrenzt)")
|
||
parser.add_argument("--subagent-max-turns", type=int, default=0, help="Maximale Turns je Subagent (0=unbegrenzt)")
|
||
parser.add_argument("--temperature", type=float, default=1.0)
|
||
parser.add_argument("--timeout", type=int, default=0, help="Timeout in Sek (0=keins)")
|
||
parser.add_argument("--heartbeat-interval", type=int, default=60, help="Sekunden zwischen Lebenszeichen (0=aus)")
|
||
parser.add_argument("--result-dir", default=None, help="Verzeichnis fuer RawResult.json")
|
||
args = parser.parse_args()
|
||
|
||
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"
|
||
"Wenn du Ergebnisse hast, schreibe sie SOFORT mit write_file — beschreibe nicht, "
|
||
"was du schreiben wirst, schreibe es. Wenn du Anforderungen formuliert hast, "
|
||
"schreibe die Dateien (StRS.md, SyRS.md, SwRS.md, Traceability.md, Hypothesen.md, "
|
||
"Glossar.md, Analysebericht.md) sofort — nichtmal davor nachfragen oder zusammenfassen."
|
||
)
|
||
if args.mode == "builtin":
|
||
system_prompt += (
|
||
"\nZusaetzlich steht spawn_subagent fuer isolierte Teilaufgaben zur "
|
||
"Verfuegung. Entscheide selbst, ob du Subagenten einsetzt sowie welche "
|
||
"Typen und wie viele du in einem Turn parallel startest. Der Adapter "
|
||
"setzt dafuer kein Anzahl- oder Turn-Limit. Subagenten koennen nur "
|
||
"lesen, nicht schreiben; die Ergebnisdateien verwaltest du selbst."
|
||
)
|
||
elif args.mode == "custom":
|
||
# Custom agents laden
|
||
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")
|
||
sys.stderr.write(f"[glm-kimi-adapter] Adapter-Version: {ADAPTER_VERSION}\n")
|
||
sys.stderr.write(
|
||
f"[glm-kimi-adapter] Limits: Hauptagent-Turns="
|
||
f"{'unbegrenzt' if args.max_turns <= 0 else args.max_turns}, "
|
||
f"Subagent-Turns={'unbegrenzt' if args.subagent_max_turns <= 0 else args.subagent_max_turns}, "
|
||
f"API-Timeout={'keiner' if args.timeout <= 0 else str(args.timeout) + 's'}\n"
|
||
)
|
||
|
||
liveness = LivenessMonitor(args.heartbeat_interval)
|
||
liveness.start()
|
||
|
||
try:
|
||
result = run_agent_loop(
|
||
provider=provider, model=args.model, system_prompt=system_prompt,
|
||
user_prompt=user_prompt, api_key=api_key, effort=args.effort,
|
||
root=args.root, output_dir=str(output_dir), max_turns=args.max_turns,
|
||
temperature=args.temperature, timeout=args.timeout, liveness=liveness,
|
||
mode=args.mode, subagent_max_turns=args.subagent_max_turns,
|
||
)
|
||
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": ADAPTER_VERSION,
|
||
"mode": args.mode,
|
||
"subagent_stats": {"spawned": 0, "completed": 0, "failed": 0, "by_type": {}},
|
||
"subagent_details": [],
|
||
}
|
||
finally:
|
||
liveness.stop()
|
||
|
||
end_iso = datetime.now(timezone.utc).isoformat()
|
||
result["start_time"] = start_iso
|
||
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")
|
||
if result.get("errors"):
|
||
for error in result["errors"]:
|
||
sys.stderr.write(f"[glm-kimi-adapter] FEHLER: {error}\n")
|
||
sys.stderr.write(f"[glm-kimi-adapter] RawResult: {raw_result_path}\n")
|
||
|
||
sys.exit(1 if result["is_error"] else 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
|