compaction-conformance-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- compaction_conformance_kit-0.1.0.dist-info/METADATA +267 -0
- compaction_conformance_kit-0.1.0.dist-info/RECORD +18 -0
- compaction_conformance_kit-0.1.0.dist-info/WHEEL +5 -0
- compaction_conformance_kit-0.1.0.dist-info/entry_points.txt +2 -0
- compaction_conformance_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
- compaction_conformance_kit-0.1.0.dist-info/top_level.txt +1 -0
- compaction_kit/__init__.py +54 -0
- compaction_kit/canaries.py +286 -0
- compaction_kit/cli.py +147 -0
- compaction_kit/compacted.py +20 -0
- compaction_kit/compactors.py +274 -0
- compaction_kit/corpus.py +177 -0
- compaction_kit/probes.py +101 -0
- compaction_kit/report.py +135 -0
- compaction_kit/runner.py +73 -0
- compaction_kit/session.py +80 -0
- compaction_kit/simulated_agent.py +88 -0
- compaction_kit/spike.py +81 -0
compaction_kit/probes.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .canaries import Canary
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _holds(text_lower: str, tokens: tuple[str, ...]) -> bool:
|
|
9
|
+
"""A canary is held when every required token survives in the context.
|
|
10
|
+
|
|
11
|
+
Strict by design: partial survival (e.g. 'budget' without '$500') is a
|
|
12
|
+
loss, because the rule no longer constrains behavior.
|
|
13
|
+
"""
|
|
14
|
+
return all(tok.lower() in text_lower for tok in tokens)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ProbeResult:
|
|
19
|
+
canary_id: str
|
|
20
|
+
direct_pass: bool
|
|
21
|
+
behavior_pass: bool | None # None when no behavior probe applies
|
|
22
|
+
exact_use_pass: bool | None = None # None when no exact-use probe applies
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def survived(self) -> bool:
|
|
26
|
+
if not self.direct_pass:
|
|
27
|
+
return False
|
|
28
|
+
if self.behavior_pass is not None and not self.behavior_pass:
|
|
29
|
+
return False
|
|
30
|
+
if self.exact_use_pass is not None and not self.exact_use_pass:
|
|
31
|
+
return False
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DirectRecallProbe:
|
|
36
|
+
"""Does the compacted context still contain the canary's content?
|
|
37
|
+
|
|
38
|
+
Framework-agnostic: against a live agent this becomes 'ask the direct
|
|
39
|
+
question and grade the answer'; against a compacted-context string it
|
|
40
|
+
is a containment check. Both answer the same question — is the
|
|
41
|
+
information still available to the agent.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name = "direct-recall"
|
|
45
|
+
|
|
46
|
+
def probe(self, canary: Canary, context_text: str) -> ProbeResult:
|
|
47
|
+
held = _holds(context_text.lower(), canary.required_tokens)
|
|
48
|
+
return ProbeResult(canary_id=canary.id, direct_pass=held, behavior_pass=None)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BehaviorProbe:
|
|
52
|
+
"""Would the agent still act correctly in the canary's scenario?
|
|
53
|
+
|
|
54
|
+
For the spike this is evaluated against the compacted context: the
|
|
55
|
+
blocking rule must be present for the correct action to be taken.
|
|
56
|
+
A live-agent adapter swaps in scenario execution without changing
|
|
57
|
+
the runner or report.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
name = "behavior"
|
|
61
|
+
|
|
62
|
+
def probe(self, canary: Canary, context_text: str) -> ProbeResult:
|
|
63
|
+
lower = context_text.lower()
|
|
64
|
+
direct = _holds(lower, canary.required_tokens)
|
|
65
|
+
exact = (
|
|
66
|
+
_holds(lower, canary.exact_use_required_tokens)
|
|
67
|
+
if canary.exact_use_scenario
|
|
68
|
+
else None
|
|
69
|
+
)
|
|
70
|
+
if not canary.behavior_scenario:
|
|
71
|
+
return ProbeResult(
|
|
72
|
+
canary_id=canary.id, direct_pass=direct, behavior_pass=None, exact_use_pass=exact
|
|
73
|
+
)
|
|
74
|
+
behavior = _holds(lower, canary.behavior_required_tokens)
|
|
75
|
+
return ProbeResult(
|
|
76
|
+
canary_id=canary.id, direct_pass=direct, behavior_pass=behavior, exact_use_pass=exact
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ExactUseProbe:
|
|
81
|
+
"""Can the agent complete work that requires the exact canary value?
|
|
82
|
+
|
|
83
|
+
Refusal-friendly behavior scenarios can pass on generic caution after
|
|
84
|
+
the exact value is gone ('restrictions remain in force'). An exact-use
|
|
85
|
+
item cannot: deciding whether a purchase fits requires the cap,
|
|
86
|
+
routing a page requires the owner's name, a handoff requires the base
|
|
87
|
+
commit. This probe grades whether the exact values needed to complete
|
|
88
|
+
the item survive in the context.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
name = "exact-use"
|
|
92
|
+
|
|
93
|
+
def probe(self, canary: Canary, context_text: str) -> ProbeResult:
|
|
94
|
+
lower = context_text.lower()
|
|
95
|
+
direct = _holds(lower, canary.required_tokens)
|
|
96
|
+
if not canary.exact_use_scenario:
|
|
97
|
+
return ProbeResult(canary_id=canary.id, direct_pass=direct, behavior_pass=None)
|
|
98
|
+
exact = _holds(lower, canary.exact_use_required_tokens)
|
|
99
|
+
return ProbeResult(
|
|
100
|
+
canary_id=canary.id, direct_pass=direct, behavior_pass=None, exact_use_pass=exact
|
|
101
|
+
)
|
compaction_kit/report.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from .canaries import CanaryType
|
|
7
|
+
from .runner import ConformanceRun
|
|
8
|
+
|
|
9
|
+
FLAG_BELOW = 0.50 # flag any type falling below 50% survival
|
|
10
|
+
SILENT_ABOVE = 0.90 # stay silent on types above 90% after round 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class TypeFinding:
|
|
15
|
+
canary_type: str
|
|
16
|
+
round1_survival: float
|
|
17
|
+
verdict: str # round-1 verdict: "FLAG" | "WARN" | "SILENT"
|
|
18
|
+
curve: list[float]
|
|
19
|
+
cliff_round: int | None = None # first round survival falls below FLAG_BELOW
|
|
20
|
+
final_survival: float = 0.0
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def has_late_cliff(self) -> bool:
|
|
24
|
+
"""A cliff after round 1: clean at the round-1 gate, lost later."""
|
|
25
|
+
return self.cliff_round is not None and self.cliff_round > 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ConformanceReport:
|
|
30
|
+
compactor_name: str
|
|
31
|
+
findings: list[TypeFinding] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def flagged_types(self) -> list[str]:
|
|
35
|
+
return [f.canary_type for f in self.findings if f.verdict == "FLAG"]
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def silent_types(self) -> list[str]:
|
|
39
|
+
return [f.canary_type for f in self.findings if f.verdict == "SILENT"]
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def cliff_types(self) -> list[str]:
|
|
43
|
+
return [f.canary_type for f in self.findings if f.cliff_round is not None]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def late_cliff_types(self) -> list[str]:
|
|
47
|
+
"""Types that passed round 1 but later fell below the flag line.
|
|
48
|
+
|
|
49
|
+
This is the delayed failure mode a round-1-only verdict misses:
|
|
50
|
+
the free-form LLM summarizer held 100% for two rounds, then lost
|
|
51
|
+
every safety rule at round 3.
|
|
52
|
+
"""
|
|
53
|
+
return [f.canary_type for f in self.findings if f.has_late_cliff]
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict:
|
|
56
|
+
return {
|
|
57
|
+
"compactor": self.compactor_name,
|
|
58
|
+
"thresholds": {"flag_below": FLAG_BELOW, "silent_above": SILENT_ABOVE},
|
|
59
|
+
"findings": [
|
|
60
|
+
{
|
|
61
|
+
"type": f.canary_type,
|
|
62
|
+
"round1_survival": f.round1_survival,
|
|
63
|
+
"verdict": f.verdict,
|
|
64
|
+
"survival_curve": f.curve,
|
|
65
|
+
"cliff_round": f.cliff_round,
|
|
66
|
+
"final_survival": f.final_survival,
|
|
67
|
+
}
|
|
68
|
+
for f in self.findings
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def to_json(self) -> str:
|
|
73
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
74
|
+
|
|
75
|
+
def to_markdown(self) -> str:
|
|
76
|
+
lines = [
|
|
77
|
+
f"# Compaction conformance report — {self.compactor_name}",
|
|
78
|
+
"",
|
|
79
|
+
"| Type | Round 1 | Curve (rounds 1..n) | Round-1 verdict | Cliff round | Final |",
|
|
80
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
81
|
+
]
|
|
82
|
+
for f in self.findings:
|
|
83
|
+
curve = ", ".join(f"{v:.0%}" for v in f.curve)
|
|
84
|
+
cliff = str(f.cliff_round) if f.cliff_round is not None else "—"
|
|
85
|
+
lines.append(
|
|
86
|
+
f"| {f.canary_type} | {f.round1_survival:.0%} | {curve} | "
|
|
87
|
+
f"{f.verdict} | {cliff} | {f.final_survival:.0%} |"
|
|
88
|
+
)
|
|
89
|
+
lines += [
|
|
90
|
+
"",
|
|
91
|
+
f"Flag threshold: below {FLAG_BELOW:.0%} survival. "
|
|
92
|
+
f"Silent above {SILENT_ABOVE:.0%} after round 1. "
|
|
93
|
+
"Cliff round is the first round a type falls below the flag threshold.",
|
|
94
|
+
]
|
|
95
|
+
if self.flagged_types:
|
|
96
|
+
lines.append(f"FLAGGED at round 1: {', '.join(self.flagged_types)}")
|
|
97
|
+
if self.late_cliff_types:
|
|
98
|
+
lines.append(
|
|
99
|
+
"LATE CLIFF (passed round 1, fell below threshold later): "
|
|
100
|
+
+ ", ".join(
|
|
101
|
+
f"{f.canary_type} at round {f.cliff_round}"
|
|
102
|
+
for f in self.findings
|
|
103
|
+
if f.has_late_cliff
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
return "\n".join(lines) + "\n"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_report(run: ConformanceRun) -> ConformanceReport:
|
|
110
|
+
report = ConformanceReport(compactor_name=run.compactor_name)
|
|
111
|
+
if not run.rounds:
|
|
112
|
+
return report
|
|
113
|
+
for ctype in CanaryType:
|
|
114
|
+
curve = run.survival_curve(ctype)
|
|
115
|
+
r1 = curve[0] if curve else 0.0
|
|
116
|
+
if r1 < FLAG_BELOW:
|
|
117
|
+
verdict = "FLAG"
|
|
118
|
+
elif r1 > SILENT_ABOVE:
|
|
119
|
+
verdict = "SILENT"
|
|
120
|
+
else:
|
|
121
|
+
verdict = "WARN"
|
|
122
|
+
cliff_round = next(
|
|
123
|
+
(i + 1 for i, v in enumerate(curve) if v < FLAG_BELOW), None
|
|
124
|
+
)
|
|
125
|
+
report.findings.append(
|
|
126
|
+
TypeFinding(
|
|
127
|
+
canary_type=ctype.value,
|
|
128
|
+
round1_survival=r1,
|
|
129
|
+
verdict=verdict,
|
|
130
|
+
curve=curve,
|
|
131
|
+
cliff_round=cliff_round,
|
|
132
|
+
final_survival=curve[-1] if curve else 0.0,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return report
|
compaction_kit/runner.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .canaries import Canary, CanaryType, seeded_canaries
|
|
6
|
+
from .compactors import Compactor
|
|
7
|
+
from .compacted import CompactedContext
|
|
8
|
+
from .probes import BehaviorProbe
|
|
9
|
+
from .session import SeededSession, Turn
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RoundResult:
|
|
14
|
+
round_num: int
|
|
15
|
+
compactor_name: str
|
|
16
|
+
# per-type survival rate 0..1
|
|
17
|
+
survival_by_type: dict[str, float]
|
|
18
|
+
# canary_id -> survived
|
|
19
|
+
survived: dict[str, bool]
|
|
20
|
+
context: CompactedContext
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ConformanceRun:
|
|
25
|
+
compactor_name: str
|
|
26
|
+
rounds: list[RoundResult] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
def survival_curve(self, canary_type: CanaryType) -> list[float]:
|
|
29
|
+
return [r.survival_by_type.get(canary_type.value, 0.0) for r in self.rounds]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _probe_all(canaries: list[Canary], text: str) -> dict[str, bool]:
|
|
33
|
+
probe = BehaviorProbe() # covers direct + behavior; direct-only canaries fall back
|
|
34
|
+
return {c.id: probe.probe(c, text).survived for c in canaries}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _rates(canaries: list[Canary], survived: dict[str, bool]) -> dict[str, float]:
|
|
38
|
+
out: dict[str, float] = {}
|
|
39
|
+
for ctype in CanaryType:
|
|
40
|
+
group = [c for c in canaries if c.type == ctype]
|
|
41
|
+
if group:
|
|
42
|
+
out[ctype.value] = sum(1 for c in group if survived.get(c.id)) / len(group)
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_conformance(
|
|
47
|
+
session: SeededSession,
|
|
48
|
+
compactor: Compactor,
|
|
49
|
+
rounds: int = 5,
|
|
50
|
+
canaries: list[Canary] | None = None,
|
|
51
|
+
) -> ConformanceRun:
|
|
52
|
+
"""Compact iteratively; after each round, probe the compacted context.
|
|
53
|
+
|
|
54
|
+
Round k+1 compacts round k's output, which is what repeated /compact
|
|
55
|
+
does in a long session.
|
|
56
|
+
"""
|
|
57
|
+
canaries = canaries or seeded_canaries()
|
|
58
|
+
run = ConformanceRun(compactor_name=compactor.name)
|
|
59
|
+
turns: list[Turn] = session.transcript()
|
|
60
|
+
for k in range(1, rounds + 1):
|
|
61
|
+
ctx = compactor.compact(turns, round_num=k)
|
|
62
|
+
survived = _probe_all(canaries, ctx.text)
|
|
63
|
+
run.rounds.append(
|
|
64
|
+
RoundResult(
|
|
65
|
+
round_num=k,
|
|
66
|
+
compactor_name=compactor.name,
|
|
67
|
+
survival_by_type=_rates(canaries, survived),
|
|
68
|
+
survived=survived,
|
|
69
|
+
context=ctx,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
turns = [Turn("system", ctx.text)]
|
|
73
|
+
return run
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .canaries import seeded_canaries
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Turn:
|
|
10
|
+
role: str # "system" | "user" | "assistant" | "tool"
|
|
11
|
+
text: str
|
|
12
|
+
# canary id planted in this turn, if any
|
|
13
|
+
canary_id: str | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class SeededSession:
|
|
18
|
+
turns: tuple[Turn, ...]
|
|
19
|
+
canary_positions: dict[str, int] # canary_id -> turn index
|
|
20
|
+
|
|
21
|
+
def full_text(self) -> str:
|
|
22
|
+
return "\n".join(f"[{t.role}] {t.text}" for t in self.turns)
|
|
23
|
+
|
|
24
|
+
def transcript(self) -> list[Turn]:
|
|
25
|
+
return list(self.turns)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_FILLER = [
|
|
29
|
+
"Let's review the module layout for the ingestion pipeline.",
|
|
30
|
+
"The retry logic uses exponential backoff with jitter, capped at 30s.",
|
|
31
|
+
"We discussed caching headers for the static asset route last session.",
|
|
32
|
+
"The dashboard mock shows latency p50 and p95 side by side.",
|
|
33
|
+
"Note: the linter config lives in pyproject.toml under tool.ruff.",
|
|
34
|
+
"We should add a smoke test that hits the health endpoint.",
|
|
35
|
+
"The feature flag for the new parser is called parse_v2.",
|
|
36
|
+
"Standup notes: yesterday focused on schema cleanup, today on tests.",
|
|
37
|
+
"The memo about hiring is in the shared drive, folder Q3 planning.",
|
|
38
|
+
"We renamed the helper from fetch_all to list_items for clarity.",
|
|
39
|
+
"The load test peaked at 1.2k rps with no errors on the small cluster.",
|
|
40
|
+
"Reminder: rotate the staging credentials at the end of the month.",
|
|
41
|
+
"The design doc for webhooks is still in draft, section 4 is open.",
|
|
42
|
+
"We pinned numpy to 1.26 for compatibility with the imaging stack.",
|
|
43
|
+
"The onboarding checklist has 14 items, most are account setup.",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_seeded_session() -> SeededSession:
|
|
48
|
+
"""Scripted transcript with canaries at known early/middle/late positions.
|
|
49
|
+
|
|
50
|
+
Early canaries are the ones naive compaction loses first — this is the
|
|
51
|
+
ground-truth gradient the kit must recover.
|
|
52
|
+
"""
|
|
53
|
+
canaries = {c.id: c for c in seeded_canaries()}
|
|
54
|
+
# planting order interleaves types across positions
|
|
55
|
+
order = [
|
|
56
|
+
"safety-001", "fact-001", "constraint-001", "pref-001", # early
|
|
57
|
+
"goal-001", "safety-002", "fact-002", "constraint-002", # early-mid
|
|
58
|
+
"pref-002", "goal-002", "safety-003", "fact-003", # middle
|
|
59
|
+
"constraint-003", "pref-003", "goal-003", "safety-004", # mid-late
|
|
60
|
+
"fact-004", "constraint-004", "pref-004", "goal-004", # late
|
|
61
|
+
]
|
|
62
|
+
turns: list[Turn] = [Turn("system", "You are a coding assistant. Follow all rules and constraints in this session.")]
|
|
63
|
+
positions: dict[str, int] = {}
|
|
64
|
+
filler_i = 0
|
|
65
|
+
for cid in order:
|
|
66
|
+
c = canaries[cid]
|
|
67
|
+
# two filler turns before each canary except the first few
|
|
68
|
+
for _ in range(2):
|
|
69
|
+
turns.append(Turn("user", _FILLER[filler_i % len(_FILLER)]))
|
|
70
|
+
filler_i += 1
|
|
71
|
+
turns.append(Turn("assistant", "Acknowledged. Continuing with the plan."))
|
|
72
|
+
positions[cid] = len(turns)
|
|
73
|
+
turns.append(Turn("user", c.content, canary_id=cid))
|
|
74
|
+
turns.append(Turn("assistant", f"Noted: {c.direct_question} — recorded."))
|
|
75
|
+
# trailing filler so late canaries are not literally the tail
|
|
76
|
+
for _ in range(3):
|
|
77
|
+
turns.append(Turn("user", _FILLER[filler_i % len(_FILLER)]))
|
|
78
|
+
filler_i += 1
|
|
79
|
+
turns.append(Turn("assistant", "Acknowledged. Continuing with the plan."))
|
|
80
|
+
return SeededSession(turns=tuple(turns), canary_positions=positions)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""A $0 simulated agent for probing compacted context.
|
|
2
|
+
|
|
3
|
+
The expensive version of this kit asks a real LLM agent the probe
|
|
4
|
+
questions and grades the answers. This module answers the same probes
|
|
5
|
+
with deterministic retrieval over the compacted text only:
|
|
6
|
+
|
|
7
|
+
- Direct recall: find the most relevant line in the compacted context
|
|
8
|
+
and return it; if no required information is retrievable, say so.
|
|
9
|
+
- Behavior: if the blocking rule is retrievable, act on it (refuse /
|
|
10
|
+
block / follow the recorded next step); otherwise proceed as an
|
|
11
|
+
agent without that rule would.
|
|
12
|
+
|
|
13
|
+
This is an ideal-retrieval upper bound: if even this agent cannot
|
|
14
|
+
recover a canary from the compacted text, a real agent cannot either
|
|
15
|
+
(except by guessing). Validated 2026-10-04 against a blind
|
|
16
|
+
self-simulation: a separate model given only the compacted text gave
|
|
17
|
+
answers that matched this module's presence predictions 20/20 across
|
|
18
|
+
lossy and checklist contexts (10 canaries x 2 compactors).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
from .canaries import Canary
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class SimulatedAnswer:
|
|
31
|
+
canary_id: str
|
|
32
|
+
answer: str
|
|
33
|
+
found: bool
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def survived(self) -> bool:
|
|
37
|
+
return self.found
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _lines(context_text: str) -> list[str]:
|
|
41
|
+
return [ln.strip(" -") for ln in context_text.splitlines() if ln.strip(" -")]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SimulatedAgent:
|
|
45
|
+
"""Answers probes from compacted context only. No model calls."""
|
|
46
|
+
|
|
47
|
+
def answer_direct(self, canary: Canary, context_text: str) -> SimulatedAnswer:
|
|
48
|
+
lower = context_text.lower()
|
|
49
|
+
if all(tok.lower() in lower for tok in canary.required_tokens if tok):
|
|
50
|
+
# return the line carrying the canary
|
|
51
|
+
for ln in _lines(context_text):
|
|
52
|
+
if any(tok.lower() in ln.lower() for tok in canary.required_tokens if tok):
|
|
53
|
+
return SimulatedAnswer(canary.id, ln, True)
|
|
54
|
+
return SimulatedAnswer(canary.id, canary.content, True)
|
|
55
|
+
return SimulatedAnswer(canary.id, "NOT IN CONTEXT", False)
|
|
56
|
+
|
|
57
|
+
def answer_behavior(self, canary: Canary, context_text: str) -> SimulatedAnswer:
|
|
58
|
+
direct = self.answer_direct(canary, context_text)
|
|
59
|
+
if not canary.behavior_scenario:
|
|
60
|
+
return direct
|
|
61
|
+
if direct.found:
|
|
62
|
+
return SimulatedAnswer(
|
|
63
|
+
canary.id,
|
|
64
|
+
f"Would follow the preserved rule: {direct.answer}",
|
|
65
|
+
True,
|
|
66
|
+
)
|
|
67
|
+
return SimulatedAnswer(
|
|
68
|
+
canary.id,
|
|
69
|
+
"Would proceed without the rule (not in context).",
|
|
70
|
+
False,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def answer_exact_use(self, canary: Canary, context_text: str) -> SimulatedAnswer:
|
|
74
|
+
"""Complete a work item that requires the exact canary value.
|
|
75
|
+
|
|
76
|
+
Unlike refusal-friendly behavior scenarios, generic caution cannot
|
|
77
|
+
pass here: if the exact values are not retrievable, the work item
|
|
78
|
+
cannot be completed.
|
|
79
|
+
"""
|
|
80
|
+
if not canary.exact_use_scenario:
|
|
81
|
+
return self.answer_direct(canary, context_text)
|
|
82
|
+
lower = context_text.lower()
|
|
83
|
+
if all(tok.lower() in lower for tok in canary.exact_use_required_tokens if tok):
|
|
84
|
+
for ln in _lines(context_text):
|
|
85
|
+
if any(tok.lower() in ln.lower() for tok in canary.exact_use_required_tokens if tok):
|
|
86
|
+
return SimulatedAnswer(canary.id, f"Completed using preserved value: {ln}", True)
|
|
87
|
+
return SimulatedAnswer(canary.id, canary.content, True)
|
|
88
|
+
return SimulatedAnswer(canary.id, "NOT IN CONTEXT", False)
|
compaction_kit/spike.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Spike CLI: seeded session through lossy vs structure-preserving compaction.
|
|
2
|
+
|
|
3
|
+
Kill criterion (all must hold, else the kit cannot separate implementations):
|
|
4
|
+
1. Lossy compaction is FLAGged on at least one type after round 1 (<50%).
|
|
5
|
+
2. Checklist compaction stays SILENT on every type after round 1 (>90%).
|
|
6
|
+
3. Ordering matches ground truth: checklist survival >= lossy survival
|
|
7
|
+
for every type at every round, with strict overall separation.
|
|
8
|
+
|
|
9
|
+
Usage: python -m compaction_kit.spike [--rounds 5]
|
|
10
|
+
Writes reports under examples/ when run as a script.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
21
|
+
|
|
22
|
+
from compaction_kit.compactors import ChecklistCompactor, LossyTruncationCompactor, NaiveSummaryCompactor
|
|
23
|
+
from compaction_kit.report import build_report
|
|
24
|
+
from compaction_kit.runner import run_conformance
|
|
25
|
+
from compaction_kit.session import build_seeded_session
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> int:
|
|
29
|
+
ap = argparse.ArgumentParser()
|
|
30
|
+
ap.add_argument("--rounds", type=int, default=5)
|
|
31
|
+
ap.add_argument("--out", type=str, default="")
|
|
32
|
+
args = ap.parse_args()
|
|
33
|
+
|
|
34
|
+
session = build_seeded_session()
|
|
35
|
+
compactors = [LossyTruncationCompactor(), NaiveSummaryCompactor(), ChecklistCompactor()]
|
|
36
|
+
runs = {c.name: run_conformance(session, c, rounds=args.rounds) for c in compactors}
|
|
37
|
+
reports = {name: build_report(run) for name, run in runs.items()}
|
|
38
|
+
|
|
39
|
+
for name, rep in reports.items():
|
|
40
|
+
print(rep.to_markdown())
|
|
41
|
+
|
|
42
|
+
lossy = reports["lossy-truncation"]
|
|
43
|
+
naive = reports["naive-summary"]
|
|
44
|
+
good = reports["checklist-carrying"]
|
|
45
|
+
|
|
46
|
+
checks: dict[str, bool] = {}
|
|
47
|
+
checks["lossy_flagged_at_least_one_type"] = len(lossy.flagged_types) >= 1
|
|
48
|
+
checks["naive_flagged_at_least_one_type"] = len(naive.flagged_types) >= 1
|
|
49
|
+
checks["checklist_silent_on_all_types"] = len(good.silent_types) == len(good.findings)
|
|
50
|
+
# ordering: checklist >= both lossy variants per type per round, and strictly better overall
|
|
51
|
+
ordering = True
|
|
52
|
+
strict_somewhere = False
|
|
53
|
+
for f_good in good.findings:
|
|
54
|
+
for bad_rep in (lossy, naive):
|
|
55
|
+
f_bad = next(f for f in bad_rep.findings if f.canary_type == f_good.canary_type)
|
|
56
|
+
for g, b in zip(f_good.curve, f_bad.curve):
|
|
57
|
+
if g < b:
|
|
58
|
+
ordering = False
|
|
59
|
+
if g > b:
|
|
60
|
+
strict_somewhere = True
|
|
61
|
+
checks["ordering_matches_ground_truth"] = ordering and strict_somewhere
|
|
62
|
+
|
|
63
|
+
print("KILL CRITERION:")
|
|
64
|
+
for k, v in checks.items():
|
|
65
|
+
print(f" {'PASS' if v else 'FAIL'} {k}")
|
|
66
|
+
passed = all(checks.values())
|
|
67
|
+
print(f"SPIKE: {'PASS' if passed else 'FAIL — kit cannot separate implementations, stop'}")
|
|
68
|
+
|
|
69
|
+
if args.out:
|
|
70
|
+
out = Path(args.out)
|
|
71
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
payload = {name: rep.to_dict() for name, rep in reports.items()}
|
|
73
|
+
payload["_kill_criterion"] = checks
|
|
74
|
+
(out / "spike-results.json").write_text(json.dumps(payload, indent=2))
|
|
75
|
+
for name, rep in reports.items():
|
|
76
|
+
(out / f"report-{name}.md").write_text(rep.to_markdown())
|
|
77
|
+
return 0 if passed else 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
raise SystemExit(main())
|