fix natural causal objections and retain failure traces
This commit is contained in:
parent
e172207465
commit
f461f98b47
4 changed files with 123 additions and 7 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue