fix natural causal objections and retain failure traces

This commit is contained in:
twentyOne2x 2026-07-15 10:33:27 +02:00
parent e172207465
commit f461f98b47
4 changed files with 123 additions and 7 deletions

View file

@ -348,8 +348,8 @@ def evaluate_challenger_objection(candidate: str, message: str) -> dict[str, boo
objection_prose = QUOTED_RE.sub(" ", message) objection_prose = QUOTED_RE.sub(" ", message)
mechanism = r"(?:mechanism|intervention|tooling|protocol)" mechanism = r"(?:mechanism|intervention|tooling|protocol)"
outcome = ( outcome = (
r"(?:adoption|business\s+outcome|downstream\s+outcome|latency|organizational\s+outcome|outcome|" r"(?:adoption|business\s+outcome|downstream\s+outcome|failures?|latency|organizational\s+outcome|outcome|"
r"performance|productivity|result|retention|revenue)" r"performance|underperformance|productivity|result|retention|revenue)"
) )
causal_verb = ( causal_verb = (
r"(?:caus(?:e|es)|driv(?:e|es)|improv(?:e|es)|increas(?:e|es)|lead(?:s)?\s+to|produc(?:e|es)|" r"(?:caus(?:e|es)|driv(?:e|es)|improv(?:e|es)|increas(?:e|es)|lead(?:s)?\s+to|produc(?:e|es)|"
@ -364,6 +364,15 @@ def evaluate_challenger_objection(candidate: str, message: str) -> dict[str, boo
r"(?:unsupported|unproven|unsubstantiated|without\s+causal\s+evidence|lacks?\s+causal\s+evidence|" r"(?:unsupported|unproven|unsubstantiated|without\s+causal\s+evidence|lacks?\s+causal\s+evidence|"
r"missing\s+causal\s+evidence)" r"missing\s+causal\s+evidence)"
) )
evidence_denial = (
r"(?:(?:does\s+not|doesn't|cannot|can't|fails?\s+to)(?:\s*,\s*by\s+itself\s*,)?\s+"
r"(?:establish|show|demonstrate|support|infer|prove|justify)(?:\s+(?:that|whether))?|"
r"(?:does\s+not|doesn't|cannot|can't)\s+provide\s+evidence\s+(?:for|of|that)|"
r"without\s+(?:showing|demonstrating|establishing|proving)(?:\s+that)?)"
)
measurement_term = (
r"(?:calibrat\w*|instrument\w*|measur\w*|methodolog\w*|operational\s+definition|rater\w*|timer\w*|timing)"
)
mechanism_relation = rf"\b{mechanism}\b(?:\s+[a-z-]+){{0,4}}\s+\b{causal_verb}\b.{{0,100}}\b{outcome}\b" mechanism_relation = rf"\b{mechanism}\b(?:\s+[a-z-]+){{0,4}}\s+\b{causal_verb}\b.{{0,100}}\b{outcome}\b"
jump_relation = ( jump_relation = (
rf"\b(?:jumps?|leaps?|moves?)\s+from\b.{{0,160}}\b{mechanism}\b.{{0,160}}\bto\b" rf"\b(?:jumps?|leaps?|moves?)\s+from\b.{{0,160}}\b{mechanism}\b.{{0,160}}\bto\b"
@ -384,10 +393,27 @@ def evaluate_challenger_objection(candidate: str, message: str) -> dict[str, boo
rf"\b(?:data|evidence|observation|study)\b.{{0,160}}\b{denial}\b.{{0,120}}{mechanism_relation}", rf"\b(?:data|evidence|observation|study)\b.{{0,160}}\b{denial}\b.{{0,120}}{mechanism_relation}",
rf"{mechanism_relation}.{{0,120}}\b{causal_gap}\b", rf"{mechanism_relation}.{{0,120}}\b{causal_gap}\b",
) )
denied_causal_relation = rf"\b{evidence_denial}\b.{{0,200}}\b{causal_verb}\b.{{0,100}}\b{outcome}\b"
causal_identification_gap = (
r"\b(?:without|lacks?|missing)\b.{0,80}\b(?:detail\w*|specif\w*|identif\w*|isolat\w*|separat\w*)\b"
r".{0,120}\b(?:causal\s+(?:effect|pathway|relationship)|attribution|counterfactual|identification\s+strategy)\b"
)
quoted_candidate = bool(candidate and _quoted_from(message, candidate)) quoted_candidate = bool(candidate and _quoted_from(message, candidate))
explicit_objection = bool(re.search(r"\bi\s+(?:object|disagree)\b", objection_prose, re.IGNORECASE)) explicit_objection = bool(re.search(r"\bi\s+(?:object|disagree)\b", objection_prose, re.IGNORECASE))
causal_bridge = any(re.search(pattern, objection_prose, re.IGNORECASE) for pattern in bridge_patterns) causal_bridge = any(re.search(pattern, objection_prose, re.IGNORECASE) for pattern in bridge_patterns)
evidence_gap = any(re.search(pattern, objection_prose, re.IGNORECASE) for pattern in evidence_gap_patterns) bound_denial = any(
not re.search(
rf"\b{measurement_term}\b",
objection_prose[max(0, match.start() - 100) : min(len(objection_prose), match.end() + 40)],
re.IGNORECASE,
)
for match in re.finditer(denied_causal_relation, objection_prose, re.IGNORECASE)
)
evidence_gap = (
bound_denial
or any(re.search(pattern, objection_prose, re.IGNORECASE) for pattern in evidence_gap_patterns)
or bool(causal_bridge and re.search(causal_identification_gap, objection_prose, re.IGNORECASE))
)
required_axis = causal_bridge and evidence_gap required_axis = causal_bridge and evidence_gap
return { return {
"quoted_candidate": quoted_candidate, "quoted_candidate": quoted_candidate,

View file

@ -37,11 +37,11 @@ REMOTE_REPORT_FETCH_TIMEOUT_SECONDS = 45
LOCAL_RECOVERY_DIR = Path(tempfile.gettempdir()) / "teleo-agent-challenger-loop-recovery" LOCAL_RECOVERY_DIR = Path(tempfile.gettempdir()) / "teleo-agent-challenger-loop-recovery"
RETENTION_PROJECTION_SCHEMA = "livingip.leoAgentChallengerRetentionProjection.v1" RETENTION_PROJECTION_SCHEMA = "livingip.leoAgentChallengerRetentionProjection.v1"
BEHAVIOR_RETENTION_SCHEMA = "livingip.leoBehaviorManifestRetentionProjection.v1" BEHAVIOR_RETENTION_SCHEMA = "livingip.leoBehaviorManifestRetentionProjection.v1"
RETENTION_ABSOLUTE_RUNTIME_PATH_RE = re.compile(
r"(?<![A-Za-z0-9])/(?:Users|private|home|root|opt|tmp|var|etc)/[^\s\"']+"
)
RETENTION_FORBIDDEN_PATTERNS = ( RETENTION_FORBIDDEN_PATTERNS = (
( ("absolute runtime path", RETENTION_ABSOLUTE_RUNTIME_PATH_RE),
"absolute runtime path",
re.compile(r"(?<![A-Za-z0-9])/(?:Users|private|home|root|opt|tmp|var|etc)/[^\s\"']+"),
),
("PEM private key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")), ("PEM private key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")),
("credentialed URL", re.compile(r"https?://[^/\s:@]+:[^/\s@]+@")), ("credentialed URL", re.compile(r"https?://[^/\s:@]+:[^/\s@]+@")),
("JWT", re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b")), ("JWT", re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b")),
@ -1521,6 +1521,25 @@ def sanitize_report_for_retention(report: dict[str, Any]) -> dict[str, Any]:
if remote_report_path: if remote_report_path:
redacted_fields.append("remote_report_path") redacted_fields.append("remote_report_path")
traceback_projection: dict[str, Any] = {}
traceback_tail = retained.get("traceback_tail")
if isinstance(traceback_tail, list):
raw_traceback_tail = [str(line) for line in traceback_tail]
redacted_traceback_tail: list[str] = []
path_occurrence_count = 0
for line in raw_traceback_tail:
redacted_line, count = RETENTION_ABSOLUTE_RUNTIME_PATH_RE.subn("[absolute-runtime-path-redacted]", line)
redacted_traceback_tail.append(redacted_line)
path_occurrence_count += count
retained["traceback_tail"] = redacted_traceback_tail
traceback_projection = {
"raw_traceback_tail_sha256": protocol.canonical_sha256(raw_traceback_tail),
"line_count": len(raw_traceback_tail),
"absolute_runtime_path_occurrence_count": path_occurrence_count,
}
if path_occurrence_count:
redacted_fields.append("traceback_tail[*].absolute_runtime_path")
transport = retained.get("execution_transport") transport = retained.get("execution_transport")
if isinstance(transport, dict) and isinstance(transport.get("recovery"), dict): if isinstance(transport, dict) and isinstance(transport.get("recovery"), dict):
recovery = transport["recovery"] recovery = transport["recovery"]
@ -1552,6 +1571,7 @@ def sanitize_report_for_retention(report: dict[str, Any]) -> dict[str, Any]:
"raw_remote_report_sha256": (recovery.get("recovered_report_sha256") if isinstance(recovery, dict) else None), "raw_remote_report_sha256": (recovery.get("recovered_report_sha256") if isinstance(recovery, dict) else None),
"pre_projection_report_sha256": pre_projection_report_sha256, "pre_projection_report_sha256": pre_projection_report_sha256,
"behavior_manifests": behavior_projection, "behavior_manifests": behavior_projection,
"traceback_tail": traceback_projection,
"redacted_fields": sorted(redacted_fields), "redacted_fields": sorted(redacted_fields),
"redacted_field_count": len(redacted_fields), "redacted_field_count": len(redacted_fields),
"profile_realpath_hash_verified": profile_realpath_hash_verified, "profile_realpath_hash_verified": profile_realpath_hash_verified,

View file

@ -4,6 +4,8 @@ import copy
import json import json
from pathlib import Path from pathlib import Path
import pytest
from scripts import leo_agent_challenger_protocol as protocol from scripts import leo_agent_challenger_protocol as protocol
CLAIM_ID = "fd159490-280d-4ede-84ef-faa169cff766" CLAIM_ID = "fd159490-280d-4ede-84ef-faa169cff766"
@ -504,6 +506,61 @@ def test_natural_association_does_not_establish_causation_objection_passes_axis(
assert mechanism_gate["passes"] is True assert mechanism_gate["passes"] is True
@pytest.mark.parametrize(
"objection",
(
(
'Leo, I object to Candidate A because "adoption failures attributable to coordination deficits" '
"does not establish that coordination deficits are the cause of technological underperformance. "
"You've identified an association, but not a causal link."
),
(
'Leo, I object to Candidate A because "adoption failures attributable to coordination deficits" '
"does not establish that coordination deficits cause technological underperformance or failure. "
"The observation does not provide evidence for a causal link to the downstream outcome."
),
(
'Leo, I object to Candidate A. Your claim that "adoption failures attributable to coordination deficits" '
"does not provide evidence that these coordination deficits cause the adoption failures. You are asserting "
"a causal link without demonstrating that the former directly leads to the latter."
),
(
'I object to Candidate A. The claim that "governance frameworks have consistently been enacted after" '
"does not, by itself, establish that this temporal "
"sequence causes any specific downstream outcome or business impact. You have not provided evidence to "
"bridge the observed pattern and its consequence or establish a causal link."
),
(
'I object to "Coordination handoff tooling reduces transfer latency" because this is a '
"mechanism-to-business-outcome causal overreach. It asserts a primary proximate cause without detailing "
"the causal pathway or isolating the effect from other factors."
),
),
)
def test_live_natural_denied_causal_relation_passes_axis(objection: str) -> None:
candidate = (
"Major AI governance frameworks have consistently been enacted after, not before, the capability classes "
"they address"
if "governance frameworks" in objection
else (
"Coordination handoff tooling reduces transfer latency"
if "Coordination handoff tooling" in objection
else "adoption failures attributable to coordination deficits"
)
)
gate = protocol.evaluate_challenger_objection(candidate, objection)
assert gate == {
"quoted_candidate": True,
"explicit_objection": True,
"causal_bridge": True,
"evidence_gap": True,
"required_axis": True,
"passes": True,
}
def test_rejected_c3_is_regenerated_and_only_accepted_attempt_enters_transcript() -> None: def test_rejected_c3_is_regenerated_and_only_accepted_attempt_enters_transcript() -> None:
report = passing_report() report = passing_report()
c3 = next(turn for turn in report["turns"] if turn["turn_id"] == "C3") c3 = next(turn for turn in report["turns"] if turn["turn_id"] == "C3")

View file

@ -183,6 +183,10 @@ def test_retention_projection_removes_runtime_paths_without_losing_hash_proof()
original_contract = role["contract_sha256"] original_contract = role["contract_sha256"]
raw = { raw = {
"remote_report_path": "/tmp/leo-agent-challenger-loop-test.json", "remote_report_path": "/tmp/leo-agent-challenger-loop-test.json",
"traceback_tail": [
' File "/home/teleo/private/runtime.py", line 42, in exchange',
"RuntimeError: semantic generation failed",
],
"execution_transport": { "execution_transport": {
"recovery": { "recovery": {
"remote_path": "/tmp/leo-agent-challenger-loop-test.json", "remote_path": "/tmp/leo-agent-challenger-loop-test.json",
@ -217,6 +221,15 @@ def test_retention_projection_removes_runtime_paths_without_losing_hash_proof()
assert retained_role["contract_sha256"] == runner.protocol.compute_runtime_role_contract_sha256(retained_role) assert retained_role["contract_sha256"] == runner.protocol.compute_runtime_role_contract_sha256(retained_role)
assert retained["retention_projection"]["original_runtime_role_contract_sha256"]["leo"] == (original_contract) assert retained["retention_projection"]["original_runtime_role_contract_sha256"]["leo"] == (original_contract)
assert retained["retention_projection"]["raw_remote_report_sha256"] == "5" * 64 assert retained["retention_projection"]["raw_remote_report_sha256"] == "5" * 64
assert retained["traceback_tail"] == [
' File "[absolute-runtime-path-redacted]", line 42, in exchange',
"RuntimeError: semantic generation failed",
]
assert retained["retention_projection"]["traceback_tail"] == {
"raw_traceback_tail_sha256": runner.protocol.canonical_sha256(raw["traceback_tail"]),
"line_count": 2,
"absolute_runtime_path_occurrence_count": 1,
}
assert retained["retention_projection"]["profile_realpath_hash_verified"] == {"leo": True} assert retained["retention_projection"]["profile_realpath_hash_verified"] == {"leo": True}
assert runner.sanitize_report_for_retention(retained) == retained assert runner.sanitize_report_for_retention(retained) == retained
retained["remote_stderr"] = "traceback from /home/teleo/private/runtime.py" retained["remote_stderr"] = "traceback from /home/teleo/private/runtime.py"