Proxy: ADT REST write+activate fallback when EPOD does not activate a second PROG/FUNC write

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 14:29:37 +02:00
parent 39769afce8
commit 428b3565b2
4 changed files with 61 additions and 1 deletions

View File

@@ -70,3 +70,31 @@ class AdtClient:
out[uri.group(1) if uri else "?"] = (ok is not None and ok.group(1) == "true", out[uri.group(1) if uri else "?"] = (ok is not None and ok.group(1) == "true",
msg.group(1) if msg else "") msg.group(1) if msg else "")
return out return out
def write_activate(self, uri, name, source):
"""Lock, write source/main, unlock, activate. Fallback for EPOD: a second sap_push_source on a
PROG or FUNC writes an inactive version that is not in the inactive list, so no activation runs
(probe Z0ZZZ999_PROBE_PROG, 2026-10-03). Returns (activated, [(severity, text)])."""
if not self.csrf:
self.fetch_csrf()
st = {"X-sap-adt-sessiontype": "stateful"}
_, _, t = self._req("POST", uri + "?_action=LOCK&accessMode=MODIFY", None,
dict(st, Accept="application/vnd.sap.as+xml;charset=UTF-8;"
"dataname=com.sap.adt.lock.result"))
lh = re.search(r"<LOCK_HANDLE>(.*?)</LOCK_HANDLE>", t).group(1)
try:
self._req("PUT", f"{uri}/source/main?lockHandle={lh}", source,
dict(st, **{"Content-Type": "text/plain; charset=utf-8"}))
finally:
self._req("POST", f"{uri}?_action=UNLOCK&lockHandle={lh}", None, st)
body = ('<?xml version="1.0" encoding="UTF-8"?><adtcore:objectReferences '
'xmlns:adtcore="http://www.sap.com/adt/core">'
f'<adtcore:objectReference adtcore:uri={quoteattr(uri)} adtcore:name={quoteattr(name)}/>'
'</adtcore:objectReferences>')
_, _, t = self._req("POST", "/sap/bc/adt/activation?method=activate&preauditRequested=true", body,
{"Content-Type": "application/xml", "Accept": "application/xml"})
msgs = [(m.group(1), re.sub(r"<[^>]+>", "", m.group(2)).strip())
for m in re.finditer(r'<msg\b[^>]*\btype="(\w)"[^>]*>(.*?)</msg>', t, re.S)]
done = 'activationExecuted="true"' in t and not any(sev in ("E", "A") for sev, _ in msgs)
return done, msgs

View File

@@ -495,7 +495,7 @@ def make_k_variant(src_id, new_id, style, model, base_url, run_base, tool_schema
src_dir, task_dir = os.path.join(pool_root, src_id), os.path.join(pool_root, new_id) src_dir, task_dir = os.path.join(pool_root, src_id), os.path.join(pool_root, new_id)
b = bundle_of(src_dir) b = bundle_of(src_dir)
b["files"] = {k: v for k, v in b["files"].items() b["files"] = {k: v for k, v in b["files"].items()
if not (k.startswith("faulty/") or k in ("mutation.json",))} if not (k.startswith("faulty/") or k in ("mutation.json", "review.json", "empirical.json"))}
spec = b["files"]["spec.md"] spec = b["files"]["spec.md"]
terms = contract_terms(b["task"]) terms = contract_terms(b["task"])
messages = [{"role": "user", "content": messages = [{"role": "user", "content":

View File

@@ -50,6 +50,8 @@ class ToolProxy:
self.fail_streak = 0 self.fail_streak = 0
self.max_fail_streak = 0 self.max_fail_streak = 0
self.log = open(log_path, "a") self.log = open(log_path, "a")
self.adt = None
self.fallbacks = [] # writes that needed the ADT activation 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]
@@ -123,6 +125,8 @@ class ToolProxy:
if tool in WRITE_TOOLS and (tool == "sap_activate" or args.get("activate", True)): if tool in WRITE_TOOLS and (tool == "sap_activate" or args.get("activate", True)):
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":
err, text = self._activation_fallback(args, 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(" ", "")
@@ -133,6 +137,32 @@ class ToolProxy:
self.log.flush() self.log.flush()
return result return result
def _activation_fallback(self, args, err, text):
"""EPOD does not activate a second write of a PROG or FUNC (outcome notExecuted, and sap_activate
cannot help). Write and activate the same source through ADT REST; the agent sees one normal result."""
otype = str(args.get("objectType", "")).upper()
if otype not in ("PROG", "FUNC") or args.get("includeType") or args.get("activate") is False \
or '"outcome":"notExecuted"' not in text.replace(" ", ""):
return err, text
name = str(args.get("objectName", "")).lower()
uri = (f"/sap/bc/adt/programs/programs/{name}" if otype == "PROG" else
f"/sap/bc/adt/functions/groups/{str(args.get('functionGroup', '')).lower()}/fmodules/{name}")
try:
if self.adt is None:
from .adt_client import AdtClient
self.adt = AdtClient()
done, msgs = self.adt.write_activate(uri, name.upper(), args.get("source", ""))
except Exception as e: # noqa: BLE001 keep the EPOD result when the fallback fails
self.fallbacks.append({"object": name, "error": str(e)[:200]})
return err, text
self.fallbacks.append({"object": name, "activated": done})
out = {"success": done,
"message": "Source written and activation requested." if done else
"Source written. Activation failed. See the activation messages.",
"activation": {"messages": [{"severity": sv, "message": tx} for sv, tx in msgs],
"activationExecuted": done, "success": done}}
return False, json.dumps(out)
def note(self, kind, content): def note(self, kind, content):
self.log.write(json.dumps({"t": time.time(), "kind": kind, "content": content}) + "\n") self.log.write(json.dumps({"t": time.time(), "kind": kind, "content": content}) + "\n")
self.log.flush() self.log.flush()

View File

@@ -207,6 +207,8 @@ class Runner:
rep["agent_seconds"] = round(time.time() - t1, 1) rep["agent_seconds"] = round(time.time() - t1, 1)
rep["tool_calls"], rep["activations"] = proxy.calls, proxy.activations rep["tool_calls"], rep["activations"] = proxy.calls, proxy.activations
rep["max_fail_streak"] = proxy.max_fail_streak rep["max_fail_streak"] = proxy.max_fail_streak
if proxy.fallbacks:
rep["adt_fallbacks"] = proxy.fallbacks
# 3 collect # 3 collect
hidden_names = {o["name"].upper() for o in task.objects("hidden_tests")} hidden_names = {o["name"].upper() for o in task.objects("hidden_tests")}