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>
77 lines
3.9 KiB
Python
77 lines
3.9 KiB
Python
"""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}
|