perf: preserve conversation prefixes across agent turns
This commit is contained in:
1 parent
1440a1f508
commit
f1bb5d02aa
19 files changed
+552
-205
No files matched your search
@@ -192,7 +192,17 @@ def _make(
|
||||
capsule_service=capsule,
|
||||
# No meter: the base comes from the ledger's operator-recorded prompt
|
||||
# size (None until recorded), exactly like production wiring.
|
||||
meter_getter=meter,
|
||||
meter_getter=meter
|
||||
or (
|
||||
lambda: (
|
||||
75_000
|
||||
if chunker.awaiting_chunks
|
||||
and all(
|
||||
capsule.get_summary(c.capsule_key) is not None for c in chunker.awaiting_chunks
|
||||
)
|
||||
else 40_000
|
||||
)
|
||||
),
|
||||
goal="TEST GOAL",
|
||||
)
|
||||
ledger.attach_chunker(chunker)
|
||||
@@ -443,7 +453,8 @@ def test_segment_size_threshold_triggers_without_milestone():
|
||||
assert len(chunker.chunks) >= 1
|
||||
assert chunker.chunks[0].start_step_number == 1
|
||||
total = sum(c.end_step_number - c.start_step_number + 1 for c in chunker.chunks)
|
||||
assert total >= 4
|
||||
assert total >= 3
|
||||
assert chunker.awaiting_chunks # the next prepared batch has not swapped yet
|
||||
assert len(ledger.unchunked_turns()) >= 2 # floor respected
|
||||
|
||||
|
||||
@@ -458,7 +469,7 @@ def test_source_token_threshold_triggers_chunk():
|
||||
|
||||
def test_soft_threshold_compresses_oldest_open_segment():
|
||||
steps = [_step(i) for i in range(1, 9)]
|
||||
meter = {"value": None}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, _, _ = _make(
|
||||
steps, min_active=2, max_steps=100, meter=lambda: meter["value"], auto_capsule=True
|
||||
)
|
||||
@@ -843,7 +854,7 @@ def test_hard_threshold_folds_frozen_region_into_recall_only_era_with_step_index
|
||||
merged) plus the per-step minimal index of every folded chunk — step,
|
||||
session offset and action phrase, results only via search_history."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 13)]
|
||||
meter = {"value": None}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, _, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
capsule.resolve("chunk:1-4", _capsule(1, 4))
|
||||
@@ -1083,7 +1094,7 @@ def test_failed_capsule_redispatches_and_retains_original():
|
||||
"""Failure rung of the ladder: an exhausted capsule job is re-dispatched at
|
||||
the next trigger/pressure render while the original turns stay live."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
|
||||
meter = {"value": None}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, _, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
key = chunker.awaiting_chunks[0].capsule_key
|
||||
@@ -1106,7 +1117,7 @@ def test_hard_threshold_force_swaps_pending_chunks_into_recall_only_era():
|
||||
paragraph degrades to the milestone/step-range list, and ③'s minimal
|
||||
index survives for every folded step."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
|
||||
meter = {"value": None}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, _, _ = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
assert chunker.awaiting_chunks[0].status == "pending"
|
||||
@@ -1150,7 +1161,9 @@ def test_pressure_close_waits_for_min_steps_instead_of_one_turn_chunks():
|
||||
_run_turns(ledger, chunker, 7, 8) # eligible beyond the claim: 1, then 2
|
||||
assert [c.capsule_key for c in chunker.awaiting_chunks] == ["chunk:1-3"]
|
||||
_run_turns(ledger, chunker, 9, 9)
|
||||
assert [c.capsule_key for c in chunker.awaiting_chunks] == ["chunk:1-3", "chunk:4-6"]
|
||||
assert [c.capsule_key for c in chunker.awaiting_chunks] == [
|
||||
"chunk:1-3"
|
||||
] # finish this batch first
|
||||
|
||||
|
||||
def test_size_trigger_waits_for_min_steps_even_when_one_turn_exceeds_the_tokens():
|
||||
@@ -1193,7 +1206,7 @@ def test_start_gate_holds_ready_chunk_below_start_ratio_then_swaps():
|
||||
replaced only once the operator's measured context reaches
|
||||
budget*start_ratio; until then the ready chunk waits in the queue."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 11)]
|
||||
meter = {"value": 20_000} # < 0.35 * 100k
|
||||
meter = {"value": 40_000} # < 0.35 * 100k
|
||||
ledger, chunker, engine, _ = _make(
|
||||
steps, min_active=2, meter=lambda: meter["value"], auto_capsule=True
|
||||
)
|
||||
@@ -1212,13 +1225,13 @@ def test_start_gate_holds_ready_chunk_below_start_ratio_then_swaps():
|
||||
args = held[0]["payload"]["args"]
|
||||
assert args["note"] == "held"
|
||||
assert (args["context_tokens"], args["context_budget"], args["swap_at_tokens"]) == (
|
||||
20_000,
|
||||
40_000,
|
||||
100_000,
|
||||
35_000,
|
||||
70_000,
|
||||
)
|
||||
|
||||
# Same render cadence, context now past the gate → the ready prefix swaps.
|
||||
meter["value"] = 40_000
|
||||
meter["value"] = 75_000
|
||||
_run_turns(ledger, chunker, 9, 9, _hashes(4))
|
||||
assert len(chunker.chunks) == 1
|
||||
assert chunker.chunks[0].start_step_number == 1
|
||||
@@ -1226,9 +1239,7 @@ def test_start_gate_holds_ready_chunk_below_start_ratio_then_swaps():
|
||||
assert ledger.frozen_blocks != ()
|
||||
done = engine.traces[-1]
|
||||
assert done["status"] == "success"
|
||||
assert done["payload"]["args"]["context_tokens"] == 40_000 - (
|
||||
chunker.chunks[0].source_chars // 4
|
||||
) + (chunker.chunks[0].summary_chars // 4)
|
||||
assert done["payload"]["args"]["context_tokens"] < 75_000
|
||||
|
||||
|
||||
def test_leading_stampless_turn_joins_the_next_segment_instead_of_pinning_the_queue():
|
||||
@@ -1238,13 +1249,14 @@ def test_leading_stampless_turn_joins_the_next_segment_instead_of_pinning_the_qu
|
||||
the head of the awaiting queue, so no L2 swap ever happened and the hard
|
||||
line jumped straight to the fold. It joins the following segment."""
|
||||
steps = [_step(i) for i in range(1, 13)]
|
||||
meter = {"value": 50_000} # above the start gate
|
||||
meter = {"value": 40_000} # above the start gate
|
||||
ledger, chunker, _, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
ledger.stage_turn([_observation(0), AIMessage(content="no tool call this turn")])
|
||||
_run_turns(ledger, chunker, 1, 7, _hashes(4)) # commits turn 0 without a step key
|
||||
|
||||
assert [c.capsule_key for c in chunker.awaiting_chunks] == ["chunk:1-4"]
|
||||
assert all(entry["chunk"] is not None for entry in chunker._awaiting)
|
||||
meter["value"] = 75_000
|
||||
capsule.resolve("chunk:1-4", _capsule(1, 4))
|
||||
_run_turns(ledger, chunker, 8, 8, _hashes(4))
|
||||
|
||||
@@ -1278,24 +1290,79 @@ def test_partition_folds_leading_unstamped_turns_into_the_first_segment():
|
||||
]
|
||||
|
||||
|
||||
def test_start_gate_zero_restores_swap_on_ready():
|
||||
def test_start_gate_zero_only_prepares_and_does_not_rewrite_prefix():
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
|
||||
ledger, chunker, _, _ = _make(
|
||||
steps, min_active=2, meter=lambda: 1_000, auto_capsule=True, start_ratio=0.0
|
||||
)
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
assert len(chunker.chunks) == 1
|
||||
assert chunker.awaiting_chunks == ()
|
||||
assert chunker.chunks == ()
|
||||
assert chunker.awaiting_chunks[0].status == "ready"
|
||||
|
||||
|
||||
def test_start_gate_unknown_base_falls_through_to_swap_on_ready():
|
||||
"""No measured operator call (meter None, nothing recorded on the ledger):
|
||||
the gate cannot decide and the legacy swap-on-ready behavior stands."""
|
||||
def test_unknown_usage_uses_current_prompt_estimate_and_preserves_small_prefix():
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
|
||||
ledger, chunker, _, _ = _make(steps, min_active=2, auto_capsule=True)
|
||||
assert ledger.last_prompt_tokens is None
|
||||
chunker._meter_getter = None
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
assert len(chunker.chunks) == 1
|
||||
assert ledger.last_prompt_tokens is None
|
||||
assert ledger.projected_prompt_tokens > 0
|
||||
assert chunker.chunks == ()
|
||||
assert ledger.context_phase == 0
|
||||
|
||||
|
||||
def test_phase_batches_async_summaries_then_preserves_the_new_prefix():
|
||||
steps = [_step(i) for i in range(1, 20)]
|
||||
meter = {"value": 10_000}
|
||||
ledger, chunker, _, capsule = _make(steps, meter=lambda: meter["value"])
|
||||
ledger.set_static_prefix([HumanMessage(content="Stable rules")])
|
||||
_run_turns(ledger, chunker, 1, 10, lambda i: "a" if i <= 4 else "b" if i <= 7 else "c")
|
||||
assert capsule.submitted == [] # no background LLM work below preparation gate
|
||||
tail = [_observation(11)]
|
||||
original = ledger.render(tail)
|
||||
fingerprint = [message.model_dump() for message in original]
|
||||
meter["value"] = 40_000
|
||||
ledger.render(tail)
|
||||
assert len(chunker.awaiting_chunks) == 2
|
||||
first, second = chunker.awaiting_chunks
|
||||
capsule.resolve(first.capsule_key, _capsule(first.start_step_number, first.end_step_number))
|
||||
meter["value"] = 75_000
|
||||
assert [m.model_dump() for m in ledger.render(tail)] == fingerprint
|
||||
assert ledger.context_phase == 0 # partial completion must not break the prefix
|
||||
capsule.resolve(second.capsule_key, _capsule(second.start_step_number, second.end_step_number))
|
||||
compacted = ledger.render(tail)
|
||||
assert ledger.context_phase == 1
|
||||
assert ledger.last_compaction["after_tokens"] < ledger.last_compaction["before_tokens"]
|
||||
assert [m.model_dump() for m in original] == fingerprint # prior input objects never mutated
|
||||
assert len(chunker.chunks) == 2 and not chunker.awaiting_chunks
|
||||
|
||||
meter["value"] = 10_000 # provider confirms lower occupancy after compaction
|
||||
# Commit the previous turn, then append a newly observed turn.
|
||||
ledger.commit_staged(step_key="s10")
|
||||
next_input = ledger.render(tail)
|
||||
snapshot = [m.model_dump() for m in next_input]
|
||||
turn = _turn(11)
|
||||
turn[0] = tail[0]
|
||||
ledger.stage_turn(turn)
|
||||
ledger.commit_staged(step_key="s11")
|
||||
following = ledger.render([_observation(12)])
|
||||
assert [m.model_dump() for m in following[: len(snapshot)]] == snapshot
|
||||
assert ledger.context_phase == 1
|
||||
assert compacted[0].content == "Stable rules"
|
||||
|
||||
|
||||
def test_new_large_tail_triggers_pressure_without_waiting_for_next_usage():
|
||||
steps = [_step(i) for i in range(1, 12)]
|
||||
ledger, chunker, _, _ = _make(steps, auto_capsule=True)
|
||||
chunker._meter_getter = None
|
||||
previous = _run_turns(ledger, chunker, 1, 9, _hashes(4))
|
||||
ledger.record_prompt_tokens(10_000, previous)
|
||||
assert ledger.context_phase == 0
|
||||
# New evidence already exceeds the hard line, despite a small last call.
|
||||
ledger.render([HumanMessage(content="新证据" * 40_000)])
|
||||
assert ledger.projected_prompt_tokens > 90_000
|
||||
assert ledger.context_phase == 1
|
||||
assert ledger.frozen_blocks
|
||||
|
||||
|
||||
def _big_turn(i: int, chars: int = 4000) -> list:
|
||||
@@ -1323,7 +1390,7 @@ def test_hard_threshold_swaps_ready_prefix_as_l2_when_that_is_enough():
|
||||
estimated context back under the hard line, the swap is an ordinary L2
|
||||
event — no fold, and the younger pending segment stays awaiting."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 13)]
|
||||
meter = {"value": None}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, engine, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
# Steps 1–4 (hash-a) close at the switch; steps 5–8 (hash-b) close at the
|
||||
# next switch. Turns are ~1k tokens each so the L2 swap is worth ~4k.
|
||||
@@ -1352,7 +1419,7 @@ def test_hard_threshold_escalates_to_fold_when_l2_swap_is_not_enough():
|
||||
and the whole frozen region folds into one recall-only era whose index
|
||||
lists every folded step — the pending chunk's steps too."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 13)]
|
||||
meter = {"value": None}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, engine, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
_run_turns(ledger, chunker, 1, 10, lambda i: "a" if i <= 4 else ("b" if i <= 8 else "c"))
|
||||
capsule.resolve("chunk:1-4", _capsule(1, 4))
|
||||
@@ -1394,9 +1461,10 @@ def test_hard_fold_is_monotonic_later_l2_swaps_append_after_the_folded_era():
|
||||
flips back to full chunk blocks."""
|
||||
steps = [_step(i) for i in range(1, 17)]
|
||||
hash_for = lambda i: "a" if i <= 4 else ("b" if i <= 8 else ("c" if i <= 12 else "d")) # noqa: E731
|
||||
meter = {"value": None}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, _, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
_run_turns(ledger, chunker, 1, 10, hash_for)
|
||||
meter["value"] = 75_000
|
||||
capsule.resolve("chunk:1-4", _capsule(1, 4))
|
||||
meter["value"] = 99_000
|
||||
_run_turns(ledger, chunker, 11, 11, hash_for) # hard: folds 1–4 and 5–8
|
||||
@@ -1405,10 +1473,11 @@ def test_hard_fold_is_monotonic_later_l2_swaps_append_after_the_folded_era():
|
||||
|
||||
# Pressure gone (above the start gate, below soft): the next milestone
|
||||
# switch closes 9–12; once ready it swaps as an ordinary L2 chunk.
|
||||
meter["value"] = 50_000
|
||||
meter["value"] = 40_000
|
||||
_run_turns(ledger, chunker, 12, 15, hash_for)
|
||||
pending = [c for c in chunker.awaiting_chunks if c.status == "pending"]
|
||||
assert [c.capsule_key for c in pending] == ["chunk:9-12"]
|
||||
meter["value"] = 75_000
|
||||
capsule.resolve("chunk:9-12", _capsule(9, 12))
|
||||
_run_turns(ledger, chunker, 16, 16, hash_for)
|
||||
|
||||
@@ -1430,11 +1499,13 @@ def test_hard_threshold_with_nothing_awaiting_folds_frozen_region_once():
|
||||
into a recall-only era without consuming turns, and repeated hard renders
|
||||
leave the frozen blocks byte-identical."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 13)]
|
||||
meter = {"value": 50_000}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, _, _ = _make(
|
||||
steps, min_active=2, meter=lambda: meter["value"], auto_capsule=True
|
||||
)
|
||||
_run_turns(ledger, chunker, 1, 7, _hashes(4))
|
||||
_run_turns(ledger, chunker, 1, 6, _hashes(4))
|
||||
meter["value"] = 75_000
|
||||
_run_turns(ledger, chunker, 7, 7, _hashes(4))
|
||||
# Chunk 1–4 swapped as L2 at the step-7 render; s5..s6 remain = the floor.
|
||||
assert [c.capsule_key for c in chunker.chunks] == ["chunk:1-4"]
|
||||
assert chunker.awaiting_chunks == ()
|
||||
@@ -1479,14 +1550,15 @@ def test_context_base_reads_the_ledger_operator_measurement_not_the_session_mete
|
||||
# Some other agent's small call lands on the session meter.
|
||||
get_meter("session-1").record({"prompt_tokens": 2_700, "completion_tokens": 10})
|
||||
# The operator's own measured prompt is well below the gate.
|
||||
ledger.record_prompt_tokens(20_000)
|
||||
assert chunker._context_base_tokens(ledger) == 20_000
|
||||
chunker._meter_getter = None
|
||||
ledger.record_prompt_tokens(40_000)
|
||||
assert chunker._context_base_tokens(ledger) == 40_000
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
assert chunker.chunks == () # held: 20k < 35k
|
||||
assert [c.status for c in chunker.awaiting_chunks] == ["ready"]
|
||||
|
||||
# Operator prompt grows past the gate → swap at the next render.
|
||||
ledger.record_prompt_tokens(36_000)
|
||||
ledger.record_prompt_tokens(75_000)
|
||||
_run_turns(ledger, chunker, 9, 9, _hashes(4))
|
||||
assert len(chunker.chunks) == 1
|
||||
|
||||
@@ -1507,7 +1579,7 @@ def test_held_note_is_fresh_after_a_retry():
|
||||
then readied while the start gate still holds it gets a fresh ``held``
|
||||
note: the re-dispatch resets the once-per-chunk held marker."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 14)]
|
||||
meter = {"value": 20_000} # < 0.35 * 100k: the gate holds ready chunks
|
||||
meter = {"value": 40_000} # < 0.35 * 100k: the gate holds ready chunks
|
||||
ledger, chunker, engine, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
entry = chunker._awaiting[0]
|
||||
@@ -1797,7 +1869,7 @@ def test_calibrated_ratio_scales_the_size_trigger():
|
||||
# Default ratio: the eligible text (two ~4k-char turns once turn 5 is
|
||||
# staged) is worth ~chars/4 tokens, under the 5k target → nothing closes.
|
||||
ledger, chunker, _, _ = _make(
|
||||
steps, min_active=2, min_steps=2, max_steps=100, target_tokens=5000
|
||||
steps, min_active=2, min_steps=2, max_steps=100, target_tokens=5000, meter=lambda: 40_000
|
||||
)
|
||||
ledger.record_prompt_tokens(1000, messages=[HumanMessage(content="a" * 4000)])
|
||||
_run_big_turns(ledger, chunker, 1, 5, lambda i: "hash-a")
|
||||
@@ -1807,7 +1879,7 @@ def test_calibrated_ratio_scales_the_size_trigger():
|
||||
|
||||
# Calibrated to 1 char/token: the same text is worth ~chars tokens → closes.
|
||||
ledger, chunker, _, _ = _make(
|
||||
steps, min_active=2, min_steps=2, max_steps=100, target_tokens=5000
|
||||
steps, min_active=2, min_steps=2, max_steps=100, target_tokens=5000, meter=lambda: 40_000
|
||||
)
|
||||
ledger.record_prompt_tokens(4000, messages=[HumanMessage(content="中" * 4000)])
|
||||
assert ledger.chars_per_token == pytest.approx(1.0)
|
||||
@@ -1840,6 +1912,7 @@ def test_compression_announces_one_tool_line_from_dispatch_to_swap():
|
||||
assert args["source_tokens"] > 0
|
||||
assert "context_tokens" not in args
|
||||
|
||||
meter["value"] = 75_000
|
||||
capsule.resolve("chunk:1-4", _capsule(1, 4))
|
||||
ledger.commit_staged(step_key="s8", validator_result={"status": "dispatched"})
|
||||
ledger.render([_observation(9)])
|
||||
@@ -1853,16 +1926,14 @@ def test_compression_announces_one_tool_line_from_dispatch_to_swap():
|
||||
assert done_args["summary_tokens"] > 0
|
||||
assert done_args["context_budget"] == 100_000
|
||||
assert done_args["context_estimated"] is True
|
||||
assert done_args["context_tokens"] == (
|
||||
60_000 - done_args["source_tokens"] + done_args["summary_tokens"]
|
||||
)
|
||||
assert 0 < done_args["context_tokens"] < 75_000
|
||||
assert "forced" not in done_args
|
||||
|
||||
|
||||
def test_forced_swap_announces_fold_without_a_ratio():
|
||||
"""Forced folds report estimated context usage without a summary size."""
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
|
||||
meter = {"value": None}
|
||||
meter = {"value": 40_000}
|
||||
ledger, chunker, engine, _ = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
assert any(t["status"] == "running" for t in engine.traces)
|
||||
@@ -1896,7 +1967,7 @@ def test_compression_trace_carries_a_plain_language_phase_on_every_path():
|
||||
assert COMPRESSION_PHASES == ("summarizing", "ready", "applied", "failed")
|
||||
|
||||
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 15)]
|
||||
meter = {"value": 20_000} # < 0.35 * 100k: the start gate holds ready chunks
|
||||
meter = {"value": 40_000} # < 0.35 * 100k: the start gate holds ready chunks
|
||||
ledger, chunker, engine, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
|
||||
_run_turns(ledger, chunker, 1, 8, _hashes(4))
|
||||
chunk = chunker.awaiting_chunks[0]
|
||||
@@ -1933,9 +2004,14 @@ def test_compression_trace_carries_a_plain_language_phase_on_every_path():
|
||||
assert held["payload"]["args"]["note"] == "held"
|
||||
|
||||
# Context past the gate → the swap applies the summary.
|
||||
meter["value"] = 40_000
|
||||
for pending in chunker.awaiting_chunks:
|
||||
capsule.resolve(
|
||||
pending.capsule_key, _capsule(pending.start_step_number, pending.end_step_number)
|
||||
)
|
||||
meter["value"] = 75_000
|
||||
_run_turns(ledger, chunker, 13, 13, lambda i: "hash-c")
|
||||
assert [c.start_step_number for c in chunker.chunks] == [1]
|
||||
assert chunker.chunks[0].start_step_number == 1
|
||||
assert not chunker.awaiting_chunks
|
||||
assert mine()[-1] == ("success", "applied")
|
||||
|
||||
# Every announcement on every trace carries a known phase.
|
||||
|
||||
@@ -71,9 +71,19 @@ def _turn(i: int, tool_call_count: int = 1) -> list:
|
||||
return messages
|
||||
|
||||
|
||||
def _play_turn(ledger: TranscriptLedger, i: int, *, prev_key=None, prev_result=None, **turn_kwargs):
|
||||
def _play_turn(
|
||||
ledger: TranscriptLedger,
|
||||
i: int,
|
||||
*,
|
||||
prev_key=None,
|
||||
prev_result=None,
|
||||
compact=False,
|
||||
**turn_kwargs,
|
||||
):
|
||||
"""Mimic the operator's real ordering: commit previous, render, stage."""
|
||||
ledger.commit_staged(step_key=prev_key, validator_result=prev_result)
|
||||
if compact:
|
||||
ledger.compact_active()
|
||||
turn = _turn(i, **turn_kwargs)
|
||||
rendered = ledger.render([turn[0]])
|
||||
ledger.stage_turn(turn)
|
||||
@@ -159,8 +169,8 @@ def test_recitation_and_ui_list_stripped_at_the_text_edge_and_screenshot_at_k():
|
||||
until depth K and is then resolved in place; only the live tail ever
|
||||
carries an indexed element list."""
|
||||
ledger = TranscriptLedger(step_memory=_service(), image_scrub_depth=3)
|
||||
_play_turn(ledger, 1)
|
||||
rendered = _play_turn(ledger, 2, prev_key="step-1")
|
||||
_play_turn(ledger, 1, compact=True)
|
||||
rendered = _play_turn(ledger, 2, compact=True, prev_key="step-1")
|
||||
obs1 = ledger.active_messages[0]
|
||||
# Turn 1 is now depth 2: text edge passed, screenshot kept.
|
||||
assert PLAN_RECITATION_MARKER not in str(obs1.content)
|
||||
@@ -168,10 +178,10 @@ def test_recitation_and_ui_list_stripped_at_the_text_edge_and_screenshot_at_k():
|
||||
assert _has_image(obs1)
|
||||
original_obs1 = _fingerprint(obs1)
|
||||
|
||||
rendered = _play_turn(ledger, 3, prev_key="step-2")
|
||||
rendered = _play_turn(ledger, 3, compact=True, prev_key="step-2")
|
||||
# Turn 1 reached depth 3: the screenshot is resolved.
|
||||
assert not _has_image(obs1)
|
||||
assert _fingerprint(obs1) != original_obs1
|
||||
assert not _has_image(ledger.active_messages[0])
|
||||
assert _fingerprint(ledger.active_messages[0]) != original_obs1
|
||||
# Turn 2 (depth 2): text edge passed, screenshot still there; the live tail keeps both.
|
||||
obs2 = next(m for m in ledger.active_messages if "OBSERVATION [T+00:02]" in str(m.content))
|
||||
assert PLAN_RECITATION_MARKER not in str(obs2.content)
|
||||
@@ -189,7 +199,7 @@ def test_depth_k_image_resolved_to_ready_visual_summary():
|
||||
|
||||
ledger = TranscriptLedger(step_memory=service, image_scrub_depth=3)
|
||||
for i in range(1, 5):
|
||||
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
|
||||
first_obs = ledger.active_messages[0]
|
||||
assert not _has_image(first_obs)
|
||||
@@ -212,7 +222,7 @@ def test_pending_grace_then_placeholder_never_backfilled():
|
||||
ledger = TranscriptLedger(step_memory=service, image_scrub_depth=2, pending_grace_steps=1)
|
||||
# Enough turns to push turn 1 past K + grace.
|
||||
for i in range(1, 7):
|
||||
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
|
||||
first_obs = ledger.active_messages[0]
|
||||
assert not _has_image(first_obs)
|
||||
@@ -221,7 +231,7 @@ def test_pending_grace_then_placeholder_never_backfilled():
|
||||
|
||||
# A late summary must never mutate the frozen message.
|
||||
service._summaries["step-1"] = "Too late."
|
||||
_play_turn(ledger, 7, prev_key="step-6")
|
||||
_play_turn(ledger, 7, compact=True, prev_key="step-6")
|
||||
assert _fingerprint(ledger.active_messages[0]) == frozen
|
||||
|
||||
|
||||
@@ -232,7 +242,7 @@ def test_failed_summary_becomes_unavailable_placeholder():
|
||||
|
||||
ledger = TranscriptLedger(step_memory=service, image_scrub_depth=2, pending_grace_steps=5)
|
||||
for i in range(1, 4):
|
||||
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
|
||||
first_obs = ledger.active_messages[0]
|
||||
assert not _has_image(first_obs)
|
||||
@@ -473,7 +483,7 @@ def test_relaxed_depth_below_start_gate_keeps_more_screenshots():
|
||||
assert ledger.occupancy is None
|
||||
assert ledger.effective_image_scrub_depth == 6
|
||||
for i in range(1, 10):
|
||||
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
# At the render of turn 9 its observation is the live tail (depth 1);
|
||||
# committed turns 8..5 are depths 2..5 and turn 4 (depth 6) is scrubbed.
|
||||
ledger.record_prompt_tokens(20_000) # 20% < 35%
|
||||
@@ -482,34 +492,34 @@ def test_relaxed_depth_below_start_gate_keeps_more_screenshots():
|
||||
|
||||
tight = TranscriptLedger(step_memory=_service(), image_scrub_depth=3)
|
||||
for i in range(1, 10):
|
||||
_play_turn(tight, i, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
_play_turn(tight, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
assert _image_turns(tight) == [8]
|
||||
|
||||
|
||||
def test_crossing_the_start_gate_tightens_once_and_relaxing_never_backfills():
|
||||
ledger = _banded_ledger()
|
||||
for i in range(1, 10):
|
||||
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
assert _image_turns(ledger) == [5, 6, 7, 8]
|
||||
|
||||
# Past the gate: the next render scrubs the extra depths in one pass
|
||||
# (turns 5–8 were depths 3–6 at that render; only turn 9 at depth 2 stays).
|
||||
ledger.record_prompt_tokens(40_000) # 40% >= 35%
|
||||
assert ledger.effective_image_scrub_depth == 3
|
||||
_play_turn(ledger, 10, prev_key="step-9")
|
||||
_play_turn(ledger, 10, compact=True, prev_key="step-9")
|
||||
assert _image_turns(ledger) == [9]
|
||||
|
||||
# Back under the gate: nothing is restored; new turns accumulate again
|
||||
# until four historical screenshots are in view.
|
||||
ledger.record_prompt_tokens(20_000)
|
||||
assert ledger.effective_image_scrub_depth == 6
|
||||
_play_turn(ledger, 11, prev_key="step-10")
|
||||
_play_turn(ledger, 11, compact=True, prev_key="step-10")
|
||||
assert _image_turns(ledger) == [9, 10]
|
||||
_play_turn(ledger, 12, prev_key="step-11")
|
||||
_play_turn(ledger, 12, compact=True, prev_key="step-11")
|
||||
assert _image_turns(ledger) == [9, 10, 11]
|
||||
_play_turn(ledger, 13, prev_key="step-12")
|
||||
_play_turn(ledger, 13, compact=True, prev_key="step-12")
|
||||
assert _image_turns(ledger) == [9, 10, 11, 12]
|
||||
_play_turn(ledger, 14, prev_key="step-13") # turn 9 reaches depth 6
|
||||
_play_turn(ledger, 14, compact=True, prev_key="step-13") # turn 9 reaches depth 6
|
||||
assert _image_turns(ledger) == [10, 11, 12, 13]
|
||||
|
||||
|
||||
@@ -523,10 +533,8 @@ def test_no_band_configuration_keeps_the_fixed_depth():
|
||||
assert relaxed_only.effective_image_scrub_depth == 3 # no gate → never relaxed
|
||||
|
||||
|
||||
def test_scrub_edge_runs_before_the_chunker_at_render():
|
||||
"""The chunker sees the active window after the scrub edge advanced, so a
|
||||
chunk closing over turns older than the floor never captures a live
|
||||
screenshot in its source transcript."""
|
||||
def test_chunker_sees_immutable_raw_history_before_a_phase_boundary():
|
||||
"""Preparing capsules must not scrub any previously sent observation."""
|
||||
ledger = _banded_ledger()
|
||||
seen: list[list[int]] = []
|
||||
|
||||
@@ -537,9 +545,9 @@ def test_scrub_edge_runs_before_the_chunker_at_render():
|
||||
ledger.attach_chunker(Probe())
|
||||
for i in range(1, 9):
|
||||
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
|
||||
# At the render of turn 8 the scrub edge (depth 6) had already resolved
|
||||
# turn 3 (depth 6); the probe saw turns 4–7 with images, never turn 3.
|
||||
assert seen[-1] == [4, 5, 6, 7]
|
||||
# A render below pressure preserves every historical image.
|
||||
assert seen[-1] == list(range(1, 8))
|
||||
assert ledger.context_phase == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -580,6 +588,7 @@ def test_ephemeral_blocks_vanish_at_the_text_edge_and_not_before():
|
||||
ledger.stage_turn(turn_with_notice(1))
|
||||
for i in range(2, 4):
|
||||
ledger.commit_staged(step_key=f"step-{i - 1}")
|
||||
ledger.compact_active()
|
||||
ledger.render([_observation(i)])
|
||||
ledger.stage_turn(turn_with_notice(i))
|
||||
|
||||
@@ -600,15 +609,12 @@ def test_ephemeral_blocks_vanish_at_the_text_edge_and_not_before():
|
||||
|
||||
|
||||
def test_rendering_past_the_edge_is_byte_identical_and_prefix_stable():
|
||||
"""(iii) A render with nothing new is byte-identical to the previous one,
|
||||
and across turns exactly one message — the observation reaching depth K —
|
||||
differs from the previous render's prefix, so the request prefix before
|
||||
it is what the provider's prompt cache already holds."""
|
||||
"""Every full previous prompt, including its live tail, survives unchanged."""
|
||||
ledger = TranscriptLedger(step_memory=_ready_service(12), image_scrub_depth=3)
|
||||
ledger.set_static_prefix([SystemMessage(content="STATIC")])
|
||||
|
||||
def snapshot(rendered: list) -> list[str]:
|
||||
return [_fingerprint(m) for m in rendered[:-1]] # everything but the live tail
|
||||
return [_fingerprint(m) for m in rendered] # includes the previously live observation
|
||||
|
||||
previous: list[str] = []
|
||||
for i in range(1, 11):
|
||||
@@ -620,24 +626,15 @@ def test_rendering_past_the_edge_is_byte_identical_and_prefix_stable():
|
||||
)
|
||||
current = snapshot(rendered)
|
||||
changed = [idx for idx, fp in enumerate(previous) if current[idx] != fp]
|
||||
if i >= 3:
|
||||
# The live tail is depth 1 and committed turn i-1 depth 2, so the
|
||||
# turn reaching depth 3 is i-2; its observation heads its span.
|
||||
expected = next(
|
||||
idx
|
||||
for idx, m in enumerate(rendered[:-1])
|
||||
if f"OBSERVATION [T+00:0{(i - 2) % 10}]" in str(m.content)
|
||||
)
|
||||
assert changed == [expected], (i, changed)
|
||||
else:
|
||||
assert changed == [], (i, changed)
|
||||
assert changed == [], (i, changed)
|
||||
previous = current
|
||||
|
||||
# Same state, rendered again: byte-identical, including the tail.
|
||||
ledger.commit_staged(step_key="step-10", validator_result={"status": "dispatched"})
|
||||
tail = [_observation(11)]
|
||||
again = [_fingerprint(m) for m in ledger.render(tail)]
|
||||
assert again == [_fingerprint(m) for m in ledger.render(tail)]
|
||||
assert again[:-1] == previous
|
||||
assert again[: len(previous)] == previous
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user