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