Agent: max_tokens 32k for cloud models and retry on empty turns (runaway reasoning); proxy: sap_activate fallback for PROG/FUNC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aUaQeLnwbb1zTpN7kHeat
This commit is contained in:
Kral
2026-10-03 17:07:31 +02:00
parent 0e96c6708e
commit 01e99158a9
2 changed files with 19 additions and 1 deletions

View File

@@ -79,10 +79,16 @@ class LlmAgent:
self.temperature = temperature self.temperature = temperature
self.max_seconds = max_seconds self.max_seconds = max_seconds
self.request_timeout = 3600 # local models are slow; a hung request still ends self.request_timeout = 3600 # local models are slow; a hung request still ends
# Runaway reasoning: 35 of 1965 DeepSeek turns produced 393k output tokens and no content (46 % of the
# run cost, 2026-10-03). Normal turns: p95 17.5k, max 82k. A cut turn is retried (see run).
self.max_tokens = 32000 if ":cloud" in (model or "") else None
self.empty_retries = 2
def _chat(self, messages, tools): def _chat(self, messages, tools):
body = {"model": self.model, "messages": messages, "tools": tools, body = {"model": self.model, "messages": messages, "tools": tools,
"temperature": self.temperature, "parallel_tool_calls": False} "temperature": self.temperature, "parallel_tool_calls": False}
if self.max_tokens:
body["max_tokens"] = self.max_tokens
req = urllib.request.Request(f"{self.base_url}/chat/completions", json.dumps(body).encode(), req = urllib.request.Request(f"{self.base_url}/chat/completions", json.dumps(body).encode(),
{"Content-Type": "application/json", {"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"}) "Authorization": f"Bearer {self.api_key}"})
@@ -110,6 +116,7 @@ class LlmAgent:
if ":cloud" in self.model: if ":cloud" in self.model:
check_budget() check_budget()
final = "" final = ""
empty = 0
start = time.time() start = time.time()
for _ in range(self.max_turns): for _ in range(self.max_turns):
if self.max_seconds and time.time() - start > self.max_seconds: if self.max_seconds and time.time() - start > self.max_seconds:
@@ -125,8 +132,12 @@ class LlmAgent:
"tool_calls": msg.get("tool_calls"), "usage": usage}) "tool_calls": msg.get("tool_calls"), "usage": usage})
messages.append({k: v for k, v in msg.items() if k in ("role", "content", "tool_calls")}) messages.append({k: v for k, v in msg.items() if k in ("role", "content", "tool_calls")})
calls = msg.get("tool_calls") or [] calls = msg.get("tool_calls") or []
if not calls and not (msg.get("content") or "").strip() and empty < self.empty_retries:
empty += 1 # empty turn (output limit reached in reasoning): ask the same question again
messages.pop()
continue
if not calls: if not calls:
final = msg.get("content") or "" final = msg.get("content") or ("Stopped: empty model response." if empty else "")
break break
try: try:
for c in calls: for c in calls:

View File

@@ -52,6 +52,7 @@ class ToolProxy:
self.log = open(log_path, "a") self.log = open(log_path, "a")
self.adt = None self.adt = None
self.fallbacks = [] # writes that needed the ADT activation fallback self.fallbacks = [] # writes that needed the ADT activation fallback
self.last_source = {} # last main source pushed per PROG/FUNC, for the sap_activate fallback
def schemas(self): def schemas(self):
tools = [t for t in self.mcp.list_tools() if t["name"] in MODEL_TOOLS] tools = [t for t in self.mcp.list_tools() if t["name"] in MODEL_TOOLS]
@@ -126,7 +127,13 @@ class ToolProxy:
self.activations += 1 self.activations += 1
err, text = self.mcp.call(tool, args) err, text = self.mcp.call(tool, args)
if tool == "sap_push_source": if tool == "sap_push_source":
if str(args.get("objectType", "")).upper() in ("PROG", "FUNC") and not args.get("includeType"):
self.last_source[str(args.get("objectName", "")).upper()] = args.get("source", "")
err, text = self._activation_fallback(args, err, text) err, text = self._activation_fallback(args, err, text)
elif tool == "sap_activate": # a write with activate=false, then sap_activate (G0014, G0124)
src = self.last_source.get(str(args.get("objectName", "")).upper())
if src is not None:
err, text = self._activation_fallback(dict(args, source=src), err, text)
result = (err, self._filter(tool, text)) result = (err, self._filter(tool, text))
if tool in WRITE_TOOLS: if tool in WRITE_TOOLS:
failed = err or '"success":false' in text.replace(" ", "") failed = err or '"success":false' in text.replace(" ", "")