Files
buchwerk/backend/agenten.py
team3 705ff5d876 Workflow-Werkzeug für Agenten sperren
Ein Konsistenz-Agent startete einen Hintergrund-Workflow und endete ohne
Bericht; die Pipeline hielt die Stufe für fertig.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 19:10:26 +02:00

77 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Ein Agent = eine Claude-Agent-SDK-Query mit dem Themenverzeichnis als Arbeitsverzeichnis."""
import asyncio
import logging
from claude_agent_sdk import (AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, ToolUseBlock, query)
from . import config, state
log = logging.getLogger("buchwerk")
SYSTEM_ZUSATZ = (
"Du bist ein Agent in einer Pipeline, die ein deutschsprachiges Sachbuch erstellt. Das aktuelle Arbeitsverzeichnis "
"ist das Buchprojekt; verlasse es nicht (außer für Klone und Downloads, die du unter quellen/ ablegst). "
"Es gibt keinen menschlichen Ansprechpartner: Rückfragen sind nicht möglich, entscheide selbst. "
"Arbeite gründlich und bis zum Ende der Aufgabe. Antworte auf Deutsch. Deine letzte Nachricht ist der Abschlussbericht."
)
def _kurz(block: ToolUseBlock) -> str:
i = block.input or {}
for k in ("file_path", "command", "pattern", "query", "url", "description"):
if k in i:
return f"{block.name} {str(i[k])[:160]}"
return block.name
async def laufen(name: str, agent_id: str, stufe_id: str, titel: str, prompt: str, *,
effort: str = config.EFFORT, max_turns: int = config.MAX_TURNS) -> dict:
"""Führt einen Agenten aus; schreibt Ereignisse und Bericht. → {"status", "bericht", "kosten", "tokens"}"""
a = state.agent_eintrag(name, agent_id, stufe_id, titel)
state.ereignis(name, agent_id, "start", titel)
options = ClaudeAgentOptions(
model=config.MODELL, effort=effort, cwd=str(state.verzeichnis(name)),
permission_mode="bypassPermissions", max_turns=max_turns,
system_prompt={"type": "preset", "preset": "claude_code", "append": SYSTEM_ZUSATZ},
# Workflow läuft im Hintergrund und ließ einen Agenten ohne Bericht enden – gesperrt.
disallowed_tools=["Workflow"],
)
letzter_text, kosten, tokens, status, fehler = "", 0.0, 0, "fehler", ""
try:
async for m in query(prompt=prompt, options=options):
if isinstance(m, AssistantMessage):
for b in m.content:
if isinstance(b, ToolUseBlock):
a["werkzeuge"] += 1
if a["werkzeuge"] % 5 == 1:
state.ereignis(name, agent_id, "tool", _kurz(b))
elif isinstance(b, TextBlock) and b.text.strip():
letzter_text = b.text
elif isinstance(m, ResultMessage):
kosten = m.total_cost_usd or 0.0
u = m.usage or {}
tokens = sum(int(u.get(k) or 0) for k in
("input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens"))
if m.result and m.result.strip():
letzter_text = m.result
if m.is_error or m.subtype != "success":
fehler = f"{m.subtype}: " + "; ".join(str(e) for e in (m.errors or [])[:3])
else:
status = "fertig"
except asyncio.CancelledError:
state.agent_fertig(name, a, "abgebrochen", letzter_text, kosten, tokens)
state.ereignis(name, agent_id, "ende", "abgebrochen")
raise
except Exception as e: # ProcessError, CLIConnectionError, …
fehler = f"{type(e).__name__}: {e}"
log.exception("Agent %s", agent_id)
bericht = letzter_text.strip()
if status != "fertig":
bericht = f"**Fehler:** {fehler}\n\n{bericht}".strip()
state.agent_fertig(name, a, status, bericht, kosten, tokens)
if status == "fertig":
state.bericht_schreiben(name, agent_id, bericht)
state.ereignis(name, agent_id, "ende" if status == "fertig" else "fehler",
f"{status} · {kosten:.2f} USD · {a['werkzeuge']} Werkzeugaufrufe" + (f" · {fehler}" if fehler else ""))
return {"status": status, "bericht": bericht, "kosten": kosten, "tokens": tokens}