mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 04:49:26 +00:00
A long multi-turn session driven through the real SessionManager with a forced 3k-token cap: repeated compactions advance the boundary, later summaries fold the previous one in, the provider verifiably receives the compacted view (summary block + verbatim tail, bounded) while the canonical transcript keeps every turn, state survives a mid-conversation rebuild, and the persisted record round-trips the final boundary. Scripted stand-in for the live-model smoke: intent survival across a real summarizer (prompt tuning) still needs a configured provider key.
101 lines
4.6 KiB
Python
101 lines
4.6 KiB
Python
"""OPE-27 smoke (4/4) — a long multi-turn session driven through the real SessionManager
|
|
across REPEATED forced compactions: the provider must actually receive the compacted
|
|
view (summary block + verbatim tail), user intent must survive every compaction, and the
|
|
state must survive a save/rebuild mid-conversation. This is the scripted stand-in for
|
|
the live-model smoke (which needs a configured provider key)."""
|
|
|
|
import json
|
|
|
|
import asyncio
|
|
|
|
from coworker.providers import AssistantTurn, ModelCapabilities, ProviderClient
|
|
from coworker.providers.base import TokenUsage
|
|
from coworker.server.manager import SessionManager
|
|
|
|
BULK = "analysis paragraph " * 400 # ~7.6k chars (~1.9k tokens) per turn → triggers by turn 2
|
|
|
|
|
|
class LongSessionProvider(ProviderClient):
|
|
"""Main turns: bulky text answers with realistic (growing) usage reporting.
|
|
Summarizer turns: a structured summary echoing the required sections."""
|
|
|
|
def __init__(self):
|
|
self.main_messages_seen: list[list[dict]] = []
|
|
self.summary_prompts: list[str] = []
|
|
|
|
def complete(self, *, model, messages, tools=None, **settings):
|
|
if messages and "compacting an AI coworker" in str(
|
|
messages[0].get("content", "")
|
|
):
|
|
self.summary_prompts.append(str(messages[1]["content"]))
|
|
return AssistantTurn(
|
|
text=(
|
|
"## Primary request and intent\nBuild the Q3 report; never email "
|
|
"it without approval.\n## Current work\nDrafting section "
|
|
f"{len(self.summary_prompts)}.\n## Next step\nContinue drafting."
|
|
),
|
|
finish_reason="stop",
|
|
)
|
|
self.main_messages_seen.append([dict(m) for m in messages])
|
|
# Usage mirrors the outbound size (chars/4), like a real provider would bill it.
|
|
prompt_tokens = sum(len(json.dumps(m, default=str)) for m in messages) // 4
|
|
return AssistantTurn(
|
|
text=f"turn {len(self.main_messages_seen)}: {BULK}",
|
|
finish_reason="stop",
|
|
usage=TokenUsage(input=prompt_tokens, output=500),
|
|
)
|
|
|
|
def capabilities(self, model):
|
|
return ModelCapabilities()
|
|
|
|
|
|
def test_long_session_survives_repeated_compaction(tmp_path):
|
|
provider = LongSessionProvider()
|
|
mgr = SessionManager(workspace=tmp_path, provider=provider)
|
|
# Force tiny windows straight through the real Settings plumbing.
|
|
mgr._prefs["compaction_cap_tokens"] = 3_000
|
|
sid = "smoke-long"
|
|
|
|
async def drive(engine, text):
|
|
async for _ in engine.run(text):
|
|
pass
|
|
|
|
engine = mgr.get_engine(sid, agent="cowork", workspace=str(tmp_path))
|
|
boundaries = []
|
|
for i in range(8):
|
|
asyncio.run(drive(engine, f"user step {i}: keep drafting the Q3 report"))
|
|
if engine.compaction_state is not None:
|
|
if not boundaries or engine.compaction_state.boundary_index != boundaries[-1]:
|
|
boundaries.append(engine.compaction_state.boundary_index)
|
|
mgr.save(sid, engine)
|
|
if i == 4: # mid-conversation restart: state must survive the rebuild
|
|
mgr._engines.pop(sid)
|
|
engine = mgr.get_engine(sid, agent="cowork", workspace=str(tmp_path))
|
|
assert engine.compaction_state is not None
|
|
|
|
# Repeated compaction actually happened, moving forward each time.
|
|
assert len(boundaries) >= 2
|
|
assert boundaries == sorted(boundaries)
|
|
# Later summarizer calls fold the previous summary in (summary is message zero).
|
|
assert any("previous compaction summary" in p for p in provider.summary_prompts)
|
|
|
|
# What the MODEL actually received after the last compaction: the block + the tail,
|
|
# bounded — not the whole ever-growing canonical history.
|
|
final_view = provider.main_messages_seen[-1]
|
|
assert final_view[0]["role"] == "system"
|
|
block = final_view[1]["content"]
|
|
assert "<compacted-history>" in block
|
|
assert "Q3 report" in block # the summary carries the intent
|
|
assert "user step 0" in block # mechanical user-message preservation, from turn 0
|
|
assert "do not recap" in block # the continuation contract
|
|
assert len(final_view) < len(engine.messages)
|
|
|
|
# Canonical transcript: untouched (every turn still present) + the divider notices.
|
|
texts = [str(m.get("content", "")) for m in engine.messages]
|
|
assert all(any(f"user step {i}" in t for t in texts) for i in range(8))
|
|
assert sum(1 for m in engine.messages if m.get("kind") == "compacted") >= 2
|
|
|
|
# The persisted record round-trips the final state.
|
|
record = mgr.session_store.load(sid)
|
|
assert record.compaction["boundary_index"] == engine.compaction_state.boundary_index
|