The absorbed turn's map already had the dict guard; a plugin-built survivor carrying a malformed map still raised in the per-occurrence expansion. Pre-existing (the old merge raised the same way).
504 lines
25 KiB
Python
504 lines
25 KiB
Python
"""A context engine sees ``message_uid`` on every message the host hands it.
|
|
|
|
The engine-facing surfaces are ``compress()`` (the live list), ``post_llm_call``'s
|
|
``conversation_history`` (the same live dicts after the turn flush) and ``on_turn_complete``
|
|
(structural clones). The id must reach all three after a cold restore that never asked for
|
|
``_row_id``, survive the persist override and an in-place compaction commit, and never reach the
|
|
provider. A consecutive-user merge keeps the first constituent's uid and records the absorbed ones.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from contextlib import closing
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from agent.context_engine import ContextEngine
|
|
from hermes_state import SessionDB
|
|
|
|
UID_LEN = 32
|
|
|
|
|
|
class _CapturingEngine(ContextEngine):
|
|
"""Records what compress() and on_turn_complete() receive; compress() returns marker-swept copies."""
|
|
|
|
last_prompt_tokens = 0
|
|
|
|
def __init__(self) -> None:
|
|
self.compress_input = None
|
|
self.turn_complete_messages = None
|
|
self._last_compress_aborted = False
|
|
self._last_summary_error = None
|
|
self.compression_count = 1
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "capturing"
|
|
|
|
def update_from_response(self, usage):
|
|
pass
|
|
|
|
def should_compress(self, prompt_tokens=None):
|
|
return False
|
|
|
|
def compress(self, messages, current_tokens=None, focus_topic=None, force=False):
|
|
self.compress_input = [dict(m) for m in messages]
|
|
summary = {"role": "user", "content": "[CONTEXT COMPACTION] summary of prior turns"}
|
|
return [summary] + [dict(m) for m in messages[-2:]]
|
|
|
|
def on_turn_complete(self, messages, usage=None, **kwargs):
|
|
self.turn_complete_messages = messages
|
|
|
|
def _record_compression_failure_cooldown(self, *a, **k):
|
|
pass
|
|
|
|
|
|
def _make_agent(session_db, session_id):
|
|
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}):
|
|
from run_agent import AIAgent
|
|
|
|
agent = AIAgent(
|
|
api_key="test-key", base_url="https://openrouter.ai/api/v1", model="test/model", quiet_mode=True,
|
|
session_db=session_db, session_id=session_id, skip_context_files=True, skip_memory=True,
|
|
)
|
|
agent.compression_in_place = True
|
|
agent.context_compressor = _CapturingEngine()
|
|
return agent
|
|
|
|
|
|
def _seed(db, sid, n=6):
|
|
db.create_session(sid, "cli", model="test/model")
|
|
for i in range(n):
|
|
db.append_message(session_id=sid, role="user" if i % 2 == 0 else "assistant", content=f"seed msg {i}")
|
|
return [dict(r) for r in db._conn.execute(
|
|
"SELECT id, content, message_uid FROM messages WHERE session_id = ? AND active = 1 ORDER BY id",
|
|
(sid,)).fetchall()]
|
|
|
|
|
|
@pytest.fixture()
|
|
def db(tmp_path):
|
|
with closing(SessionDB(db_path=Path(tmp_path) / "state.db")) as handle:
|
|
yield handle
|
|
|
|
|
|
def test_compress_input_after_a_cold_restore_carries_every_rows_uid(db):
|
|
"""The ACP/gateway restore shape (no ``include_row_ids``) → compress() input → committed generation."""
|
|
from agent.conversation_compression import compress_context
|
|
|
|
sid = "20260928_120000_uid"
|
|
stored = _seed(db, sid)
|
|
restored = db.get_messages_as_conversation(sid, repair_alternation=True)
|
|
assert all("_row_id" not in m for m in restored)
|
|
agent = _make_agent(db, sid)
|
|
agent._session_db_created = True
|
|
agent._last_flushed_db_idx = len(restored)
|
|
|
|
compressed, _sp = compress_context(agent, restored, approx_tokens=100_000, system_message="sys")
|
|
|
|
engine = agent.context_compressor
|
|
assert [m.get("message_uid") for m in engine.compress_input] == [r["message_uid"] for r in stored]
|
|
# The committed generation: the copied tail keeps its uids, the summary got a fresh one.
|
|
active = [dict(r) for r in db._conn.execute(
|
|
"SELECT content, message_uid FROM messages WHERE session_id = ? AND active = 1 ORDER BY id", (sid,))]
|
|
assert [r["content"] for r in active] == [
|
|
"[CONTEXT COMPACTION] summary of prior turns", "seed msg 4", "seed msg 5"]
|
|
assert [r["message_uid"] for r in active[1:]] == [stored[4]["message_uid"], stored[5]["message_uid"]]
|
|
assert len(active[0]["message_uid"]) == UID_LEN
|
|
# And the live list the caller keeps carries the same uids (what the next compress()/post_llm_call sees).
|
|
assert [m.get("message_uid") for m in compressed] == [r["message_uid"] for r in active]
|
|
|
|
|
|
@pytest.mark.parametrize("in_place", [True, False], ids=["in_place", "rotation"])
|
|
def test_an_engine_authored_uid_survives_the_commit(db, in_place):
|
|
"""An engine that pre-stamps ``message_uid`` on the rows it emits gets that value written through the
|
|
commit (in place and on rotation), so it can recognise its own rows by uid afterwards."""
|
|
from agent.conversation_compression import compress_context
|
|
|
|
class _PreStamping(_CapturingEngine):
|
|
def compress(self, messages, current_tokens=None, focus_topic=None, force=False):
|
|
summary = {"role": "user", "content": "[CONTEXT COMPACTION] summary of prior turns",
|
|
"message_uid": "e" * UID_LEN}
|
|
return [summary] + [dict(m) for m in messages[-2:]]
|
|
|
|
sid = f"20260928_120050_{'inplace' if in_place else 'rotate'}"
|
|
stored = _seed(db, sid)
|
|
restored = db.get_messages_as_conversation(sid)
|
|
agent = _make_agent(db, sid)
|
|
agent.context_compressor = _PreStamping()
|
|
agent.compression_in_place = in_place
|
|
agent._session_db_created = True
|
|
agent._last_flushed_db_idx = len(restored)
|
|
|
|
compressed, _sp = compress_context(agent, restored, approx_tokens=100_000, system_message="sys")
|
|
|
|
committed_sid = agent.session_id
|
|
if not in_place:
|
|
assert committed_sid != sid # rotation published a child session
|
|
active = db.get_messages_as_conversation(committed_sid)
|
|
assert [m["content"] for m in active][:1] == ["[CONTEXT COMPACTION] summary of prior turns"]
|
|
assert active[0]["message_uid"] == "e" * UID_LEN
|
|
assert [m["message_uid"] for m in active[1:3]] == [stored[4]["message_uid"], stored[5]["message_uid"]]
|
|
assert [m.get("message_uid") for m in compressed[:3]] == [m["message_uid"] for m in active[:3]]
|
|
|
|
|
|
def test_turn_flush_stamps_uids_on_the_live_dicts_post_llm_call_hands_over(db):
|
|
"""``post_llm_call(conversation_history=list(messages))`` passes the live dicts; after the turn flush
|
|
every one of them carries the durable uid, including the current-turn user row."""
|
|
sid = "20260928_120100_flush"
|
|
db.create_session(sid, "cli", model="test/model")
|
|
agent = _make_agent(db, sid)
|
|
agent._session_db_created = True
|
|
messages = [{"role": "user", "content": "hello"}, {"role": "assistant", "content": "hi"}]
|
|
agent._persist_user_message_idx = 0
|
|
|
|
agent._persist_session(messages, conversation_history=None)
|
|
|
|
assert all(len(m.get("message_uid", "")) == UID_LEN for m in messages)
|
|
stored = [r[0] for r in db._conn.execute(
|
|
"SELECT message_uid FROM messages WHERE session_id = ? AND active = 1 ORDER BY id", (sid,))]
|
|
assert [m["message_uid"] for m in messages] == stored
|
|
|
|
|
|
def test_persist_override_keeps_the_uid(db):
|
|
"""The ACP/gateway persist override rewrites the current-turn user CONTENT in place; the id is the row's."""
|
|
sid = "20260928_120200_override"
|
|
db.create_session(sid, "cli", model="test/model")
|
|
agent = _make_agent(db, sid)
|
|
agent._session_db_created = True
|
|
messages = [{"role": "user", "content": "api-only variant with injected context"}]
|
|
agent._persist_user_message_idx = 0
|
|
agent._persist_user_message_override = "what the user typed"
|
|
agent._persist_session(messages, conversation_history=None)
|
|
uid = messages[0]["message_uid"]
|
|
|
|
agent._apply_persist_user_message_override(messages)
|
|
|
|
assert messages[0]["content"] == "what the user typed"
|
|
assert messages[0]["message_uid"] == uid
|
|
row = db._conn.execute(
|
|
"SELECT content, message_uid FROM messages WHERE session_id = ? AND active = 1", (sid,)).fetchone()
|
|
assert (row[0], row[1]) == ("what the user typed", uid)
|
|
|
|
|
|
def test_consecutive_user_merge_keeps_the_first_uid_and_records_the_absorbed_ones():
|
|
from agent.agent_runtime_helpers import _merge_consecutive_users
|
|
|
|
a = {"role": "user", "content": "first", "message_uid": "a" * UID_LEN}
|
|
b = {"role": "user", "content": "second", "message_uid": "b" * UID_LEN}
|
|
c = {"role": "user", "content": "third", "message_uid": "c" * UID_LEN}
|
|
|
|
merged, repairs = _merge_consecutive_users([a, b, c])
|
|
|
|
assert repairs == 2 and merged == [a]
|
|
assert a["content"] == "first\n\nsecond\n\nthird"
|
|
assert a["message_uid"] == "a" * UID_LEN
|
|
assert a["_absorbed_message_uids"] == ["b" * UID_LEN, "c" * UID_LEN]
|
|
# A restored (no ``_row_id``) absorbed dict still leaves its uid on the survivor.
|
|
assert "_absorbed_row_ids" not in a
|
|
|
|
|
|
def test_restart_after_a_merge_still_sees_the_composites_constituents(db):
|
|
"""A dangling user row, a restart, the next prompt merged into it, a turn flush, another restart: the
|
|
restored survivor names the absorbed row by uid instead of leaving the engine to parse ``\\n\\n``."""
|
|
sid = "20260928_120300_witness"
|
|
db.create_session(sid, "cli", model="test/model")
|
|
db.append_message(session_id=sid, role="user", content="unanswered before the crash")
|
|
dangling_uid = db.get_messages_as_conversation(sid)[0]["message_uid"]
|
|
# Restart: the ACP/gateway restore shape, then the next prompt lands and the pre-request repair merges.
|
|
history = db.get_messages_as_conversation(sid, repair_alternation=True, include_row_ids=True)
|
|
agent = _make_agent(db, sid)
|
|
agent._session_db_created = True
|
|
prompt = {"role": "user", "content": "next prompt"}
|
|
messages = history + [prompt]
|
|
agent._persist_user_message_idx = 1
|
|
agent._persist_session(messages, conversation_history=history) # the turn-start flush of the prompt
|
|
from agent.agent_runtime_helpers import repair_message_sequence
|
|
repair_message_sequence(agent, messages)
|
|
assert len(messages) == 1 and messages[0]["message_uid"] == dangling_uid
|
|
assert messages[0]["_absorbed_message_uids"] == [prompt["message_uid"]]
|
|
messages.append({"role": "assistant", "content": "reply"})
|
|
agent._persist_session(messages, conversation_history=None)
|
|
# Second restart.
|
|
again = db.get_messages_as_conversation(sid, repair_alternation=True)
|
|
survivor = next(m for m in again if m["message_uid"] == dangling_uid)
|
|
assert survivor["content"].startswith("unanswered before the crash")
|
|
assert prompt["message_uid"] in survivor["_absorbed_message_uids"]
|
|
|
|
|
|
def test_tool_result_flushed_after_its_call_pairs_through_the_live_list(db):
|
|
"""Tool round shape: the assistant(tool_calls) row is flushed first, the result in a later flush."""
|
|
sid = "20260928_120400_tool"
|
|
db.create_session(sid, "cli", model="test/model")
|
|
agent = _make_agent(db, sid)
|
|
agent._session_db_created = True
|
|
assistant = {"role": "assistant", "content": "",
|
|
"tool_calls": [{"id": "call_x", "type": "function", "function": {"name": "t", "arguments": "{}"}}]}
|
|
messages = [{"role": "user", "content": "q"}, assistant]
|
|
agent._persist_user_message_idx = 0
|
|
agent._persist_session(messages, conversation_history=None)
|
|
uid = assistant["_tool_call_uids"]["call_x"]
|
|
result = {"role": "tool", "content": "r", "tool_call_id": "call_x", "tool_name": "t"}
|
|
messages.append(result)
|
|
agent._persist_session(messages, conversation_history=None)
|
|
assert result["_tool_call_uid"] == uid
|
|
stored = db._conn.execute(
|
|
"SELECT tool_call_uid FROM messages WHERE session_id = ? AND role = 'tool'", (sid,)).fetchone()[0]
|
|
assert stored == uid
|
|
|
|
|
|
def test_the_live_list_walk_binds_a_result_to_the_nearest_call_that_names_it():
|
|
from agent.message_metadata import tool_call_uid_from_history
|
|
|
|
def call(uid=None):
|
|
msg = {"role": "assistant", "content": "",
|
|
"tool_calls": [{"id": "call_x", "type": "function", "function": {"name": "t", "arguments": "{}"}}]}
|
|
if uid:
|
|
msg["_tool_call_uids"] = {"call_x": uid}
|
|
return msg
|
|
|
|
result = {"role": "tool", "content": "r", "tool_call_id": "call_x"}
|
|
# Nearest call wins over an older one with the same provider id.
|
|
assert tool_call_uid_from_history([call("1" * UID_LEN), result, call("2" * UID_LEN), result], 3, {}) == "2" * UID_LEN
|
|
# A nearer call without a map (legacy) owns the result: no uid, never the older occurrence's.
|
|
assert tool_call_uid_from_history([call("1" * UID_LEN), result, call(), result], 3, {}) is None
|
|
# Never across a user turn.
|
|
assert tool_call_uid_from_history([call("1" * UID_LEN), {"role": "user", "content": "q"}, result], 2, {}) is None
|
|
|
|
|
|
def test_a_superseded_verification_candidate_is_not_a_witness_constituent():
|
|
"""Alternation repair DISCARDS a provisional verification candidate in favour of the final answer; its
|
|
row is retired like an absorbed row, but its uid is not a constituent of anything."""
|
|
from agent.agent_runtime_helpers import _merge_consecutive_assistants
|
|
|
|
candidate = {"role": "assistant", "content": "provisional", "finish_reason": "verification_required",
|
|
"message_uid": "p" * UID_LEN, "_row_id": 7}
|
|
final = {"role": "assistant", "content": "verified answer", "message_uid": "f" * UID_LEN}
|
|
|
|
merged, repairs = _merge_consecutive_assistants([candidate, final])
|
|
|
|
assert repairs == 1 and merged == [final]
|
|
assert final["message_uid"] == "f" * UID_LEN
|
|
assert "_absorbed_message_uids" not in final
|
|
assert final["_absorbed_row_ids"] == [7]
|
|
|
|
|
|
def test_assistant_merge_keeps_the_absorbed_turns_tool_call_uids():
|
|
from agent.agent_runtime_helpers import _merge_consecutive_assistants
|
|
|
|
first = {"role": "assistant", "content": "a",
|
|
"tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "t", "arguments": "{}"}}],
|
|
"_tool_call_uids": {"call_1": "1" * UID_LEN}}
|
|
second = {"role": "assistant", "content": "b",
|
|
"tool_calls": [{"id": "call_2", "type": "function", "function": {"name": "t", "arguments": "{}"}}],
|
|
"_tool_call_uids": {"call_2": "2" * UID_LEN}}
|
|
merged, repairs = _merge_consecutive_assistants([first, second])
|
|
assert repairs == 1 and merged == [first]
|
|
assert first["_tool_call_uids"] == {"call_1": "1" * UID_LEN, "call_2": "2" * UID_LEN}
|
|
|
|
|
|
def _reused_call(content, occurrence_uid=None):
|
|
msg = {"role": "assistant", "content": content,
|
|
"tool_calls": [{"id": "call_x", "type": "function", "function": {"name": "t", "arguments": "{}"}}]}
|
|
if occurrence_uid:
|
|
msg["_tool_call_uids"] = {"call_x": occurrence_uid}
|
|
return msg
|
|
|
|
|
|
def test_assistant_merge_keeps_every_occurrence_of_a_reused_provider_id():
|
|
"""Two folded turns that reuse a provider id (llama.cpp) both keep their call AND their occurrence uid;
|
|
the ids on the wire are untouched, and a following result pairs with the nearest (later) call."""
|
|
from agent.agent_runtime_helpers import _merge_consecutive_assistants
|
|
from agent.message_metadata import index_tool_call_uids, resolve_tool_call_uid
|
|
|
|
first, second = _reused_call("", "1" * UID_LEN), _reused_call("", "2" * UID_LEN)
|
|
merged, repairs = _merge_consecutive_assistants([first, second])
|
|
assert repairs == 1 and merged == [first]
|
|
assert [tc["id"] for tc in first["tool_calls"]] == ["call_x", "call_x"]
|
|
assert first["_tool_call_uids"] == {"call_x": ["1" * UID_LEN, "2" * UID_LEN]}
|
|
index: dict = {}
|
|
index_tool_call_uids(index, first)
|
|
assert resolve_tool_call_uid(index, "call_x") == "2" * UID_LEN
|
|
|
|
|
|
def test_a_reused_id_merge_survives_rewrite_and_restore(db):
|
|
"""The per-occurrence map a fold produces is what the survivor's rewrite persists and a restore returns;
|
|
the result keeps the uid of the call it answered."""
|
|
from agent.agent_runtime_helpers import repair_message_sequence
|
|
|
|
sid = "20260928_120600_reused"
|
|
db.create_session(sid, "cli", model="test/model")
|
|
agent = _make_agent(db, sid)
|
|
agent._session_db_created = True
|
|
first, second = _reused_call("checking"), _reused_call("")
|
|
result = {"role": "tool", "content": "r", "tool_call_id": "call_x", "tool_name": "t"}
|
|
messages = [{"role": "user", "content": "q"}, first, second, result]
|
|
agent._persist_user_message_idx = 0
|
|
agent._persist_session(messages, conversation_history=None)
|
|
occurrences = [first["_tool_call_uids"]["call_x"], second["_tool_call_uids"]["call_x"]]
|
|
assert len(set(occurrences)) == 2 and result["_tool_call_uid"] == occurrences[1]
|
|
|
|
repair_message_sequence(agent, messages)
|
|
assert first["_tool_call_uids"] == {"call_x": occurrences}
|
|
agent._persist_session(messages, conversation_history=None)
|
|
|
|
again = db.get_messages_as_conversation(sid, repair_alternation=False)
|
|
stored = next(m for m in again if m["role"] == "assistant" and m.get("tool_calls"))
|
|
assert stored["_tool_call_uids"] == {"call_x": occurrences}
|
|
assert next(m for m in again if m["role"] == "tool")["_tool_call_uid"] == occurrences[1]
|
|
|
|
|
|
@pytest.mark.parametrize("kept, dropped", [
|
|
([{"type": "text", "text": "kept"}], "UNIQUE-DROPPED-TEXT"),
|
|
("kept", [{"type": "text", "text": "UNIQUE-DROPPED-PART"}]),
|
|
([{"type": "text", "text": "kept"}], [{"type": "text", "text": "UNIQUE-DROPPED-PART"}]),
|
|
])
|
|
def test_an_assistant_fold_that_discards_the_later_text_is_no_witness(kept, dropped):
|
|
"""Multimodal content is never joined: the later turn's row is retired, but its uid must not be
|
|
claimed as a constituent of text it does not contain."""
|
|
from agent.agent_runtime_helpers import _merge_consecutive_assistants
|
|
|
|
survivor = {"role": "assistant", "content": kept, "message_uid": "a" * UID_LEN}
|
|
later = {"role": "assistant", "content": dropped, "message_uid": "b" * UID_LEN, "_row_id": 9}
|
|
merged, repairs = _merge_consecutive_assistants([survivor, later])
|
|
assert repairs == 1 and merged == [survivor] and survivor["content"] == kept
|
|
assert "_absorbed_message_uids" not in survivor
|
|
assert survivor["_absorbed_row_ids"] == [9]
|
|
|
|
|
|
def test_a_fold_ignores_a_non_dict_uid_map_on_either_turn():
|
|
"""A plugin-built dict can carry a malformed map; the fold keeps the well-formed side instead of raising."""
|
|
from agent.agent_runtime_helpers import _merge_assistant_into
|
|
|
|
def turn(call_id, uids):
|
|
return {"role": "assistant", "content": "",
|
|
"tool_calls": [{"id": call_id, "type": "function", "function": {"name": "t", "arguments": "{}"}}],
|
|
"_tool_call_uids": uids}
|
|
|
|
prev, new = turn("X", "junk"), turn("Y", {"Y": "b" * 32})
|
|
_merge_assistant_into(prev, new)
|
|
assert prev["_tool_call_uids"] == {"Y": "b" * 32}
|
|
prev, new = turn("X", {"X": "a" * 32}), turn("Y", ["junk"])
|
|
_merge_assistant_into(prev, new)
|
|
assert prev["_tool_call_uids"] == {"X": "a" * 32}
|
|
|
|
|
|
def test_a_select_context_selection_is_stripped_before_the_provider():
|
|
"""The request copy is stripped BEFORE ``select_context``; an engine that hands back the
|
|
``conversation_messages`` clones must not put the ids back on the wire."""
|
|
from agent.conversation_loop import _apply_context_engine_selection
|
|
from agent.message_metadata import PERSISTENCE_ONLY_MESSAGE_FIELDS
|
|
|
|
class _Selecting(_CapturingEngine):
|
|
def select_context(self, api_messages, conversation_messages=None, incoming_message=None, budget_tokens=0):
|
|
return list(conversation_messages)
|
|
|
|
class _Agent:
|
|
context_compressor = _Selecting()
|
|
session_id = "s"
|
|
|
|
history = [
|
|
{"role": "user", "content": "q", "message_uid": "a" * UID_LEN, "timestamp": 1.0, "_row_id": 1,
|
|
"_absorbed_message_uids": ["b" * UID_LEN]},
|
|
{"role": "assistant", "content": "", "message_uid": "c" * UID_LEN, "timestamp": 2.0,
|
|
"tool_calls": [{"id": "call_x", "type": "function", "function": {"name": "t", "arguments": "{}"}}],
|
|
"_tool_call_uids": {"call_x": "d" * UID_LEN}},
|
|
{"role": "tool", "content": "r", "tool_call_id": "call_x", "message_uid": "e" * UID_LEN, "timestamp": 3.0,
|
|
"_tool_call_uid": "d" * UID_LEN},
|
|
]
|
|
api = [{k: v for k, v in m.items() if k not in PERSISTENCE_ONLY_MESSAGE_FIELDS} for m in history]
|
|
|
|
out = _apply_context_engine_selection(_Agent(), api, history, history[0], logger=logging.getLogger("t"))
|
|
|
|
assert [m["content"] for m in out] == ["q", "", "r"]
|
|
assert out[1]["tool_calls"][0]["id"] == "call_x"
|
|
assert not any(key in m for m in out for key in PERSISTENCE_ONLY_MESSAGE_FIELDS)
|
|
assert all(m["message_uid"] for m in history) # history untouched
|
|
|
|
|
|
def test_the_inflight_task_restated_onto_the_carrier_records_its_uid():
|
|
from agent.context_compressor import (
|
|
COMPRESSED_SUMMARY_METADATA_KEY, SUMMARY_PREFIX, _INFLIGHT_TASK_REPLAY_HEADER, _SUMMARY_END_MARKER,
|
|
ContextCompressor,
|
|
)
|
|
|
|
compressor = object.__new__(ContextCompressor)
|
|
compressor.quiet_mode = True
|
|
carrier = {"role": "user", "content": f"{SUMMARY_PREFIX}\n\nwhat happened\n\n{_SUMMARY_END_MARKER}",
|
|
COMPRESSED_SUMMARY_METADATA_KEY: True, "message_uid": "s" * UID_LEN}
|
|
inflight = {"role": "user", "content": "finish the task", "message_uid": "a" * UID_LEN}
|
|
|
|
out = compressor._reappend_inflight_user_task([carrier], inflight)
|
|
|
|
assert out == [carrier] and _INFLIGHT_TASK_REPLAY_HEADER in carrier["content"]
|
|
assert carrier["message_uid"] == "s" * UID_LEN
|
|
assert carrier["_absorbed_message_uids"] == ["a" * UID_LEN]
|
|
|
|
|
|
def test_the_real_user_anchor_folded_into_scaffolding_keeps_the_anchors_uid():
|
|
"""The anchor's text leads the composite, so its uid is the composite's; the scaffolding row's is absorbed."""
|
|
from agent.conversation_compression import _insert_real_user_anchor, _merge_anchor_into_user_message
|
|
|
|
target = {"role": "user", "content": "[todo snapshot]", "message_uid": "t" * UID_LEN}
|
|
anchor = {"role": "user", "content": "the real ask", "message_uid": "a" * UID_LEN,
|
|
"_absorbed_message_uids": ["b" * UID_LEN]}
|
|
_merge_anchor_into_user_message(target, anchor)
|
|
assert target["content"] == "the real ask\n\n[todo snapshot]"
|
|
assert target["message_uid"] == "a" * UID_LEN
|
|
assert target["_absorbed_message_uids"] == ["b" * UID_LEN, "t" * UID_LEN]
|
|
|
|
# Never-persisted scaffolding (no uid) absorbs nothing but still takes the anchor's identity.
|
|
compressed = [{"role": "user", "content": "earlier"}, {"role": "assistant", "content": "ok"},
|
|
{"role": "user", "content": "[todo snapshot]"}]
|
|
assert _insert_real_user_anchor(compressed, dict(anchor, _absorbed_message_uids=[])) == "merged"
|
|
assert compressed[-1]["message_uid"] == "a" * UID_LEN
|
|
assert "_absorbed_message_uids" not in compressed[-1]
|
|
|
|
|
|
def test_micro_compactions_adjacent_user_merge_records_the_witness():
|
|
from types import SimpleNamespace
|
|
|
|
from agent.micro_compaction import MicroCompactionMixin
|
|
|
|
first = {"role": "user", "content": "one", "message_uid": "1" * UID_LEN}
|
|
second = {"role": "user", "content": "two", "message_uid": "2" * UID_LEN}
|
|
merged = MicroCompactionMixin._merge_adjacent_user_turns(SimpleNamespace(), [first, second])
|
|
|
|
assert merged == [first] and first["content"] == "one\n\ntwo"
|
|
assert first["message_uid"] == "1" * UID_LEN
|
|
assert first["_absorbed_message_uids"] == ["2" * UID_LEN]
|
|
|
|
|
|
def test_an_assistant_merge_rewrite_persists_the_unioned_tool_call_uids(db):
|
|
"""Alternation repair unions two flushed assistant turns' tool calls; the survivor's row-addressed
|
|
rewrite must carry the unioned uid map (it follows ``tool_calls``), and a restore must return it."""
|
|
from agent.agent_runtime_helpers import repair_message_sequence
|
|
|
|
sid = "20260928_120500_union"
|
|
db.create_session(sid, "cli", model="test/model")
|
|
agent = _make_agent(db, sid)
|
|
agent._session_db_created = True
|
|
survivor = {"role": "assistant", "content": "let me check"}
|
|
caller = {"role": "assistant", "content": "",
|
|
"tool_calls": [{"id": "call_2", "type": "function", "function": {"name": "t", "arguments": "{}"}}]}
|
|
result = {"role": "tool", "content": "r", "tool_call_id": "call_2", "tool_name": "t"}
|
|
messages = [{"role": "user", "content": "q"}, survivor, caller, result]
|
|
agent._persist_user_message_idx = 0
|
|
agent._persist_session(messages, conversation_history=None) # both assistants flushed with a digest
|
|
uids = dict(caller["_tool_call_uids"])
|
|
assert set(uids) == {"call_2"} and "_tool_call_uids" not in survivor and result["_tool_call_uid"] == uids["call_2"]
|
|
|
|
repair_message_sequence(agent, messages)
|
|
assert messages == [messages[0], survivor, result] and survivor["_tool_call_uids"] == uids
|
|
agent._persist_session(messages, conversation_history=None)
|
|
|
|
assert survivor["_tool_call_uids"] == uids
|
|
again = db.get_messages_as_conversation(sid, repair_alternation=False)
|
|
stored = next(m for m in again if m["role"] == "assistant" and m["content"].startswith("let me check"))
|
|
assert [tc["id"] for tc in stored["tool_calls"]] == ["call_2"]
|
|
assert stored["_tool_call_uids"] == uids
|
|
assert next(m for m in again if m["role"] == "tool")["_tool_call_uid"] == uids["call_2"]
|