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:
@@ -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:
|
||||||
|
|||||||
@@ -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(" ", "")
|
||||||
|
|||||||
Reference in New Issue
Block a user