feat(conversations,api,ui): rewind takes the last turns of a thread back, the cut session stays in the store
This commit is contained in:
1 parent
b0e5aaf599
commit
2a47c6771d
14 files changed
+583
-9
No files matched your search
@@ -504,6 +504,105 @@ async def test_copy_seed_forks_parent_with_window(world: World) -> None:
|
||||
assert (await world.conversations.get(child.external_id)).flags["seed"] is None
|
||||
|
||||
|
||||
async def _three_turns(world: World) -> tuple[str, list[dict[str, Any]]]:
|
||||
sid = str(uuid.uuid4())
|
||||
history = [
|
||||
{"role": "user", "content": "one"},
|
||||
{"role": "assistant", "content": "done one"},
|
||||
{"role": "user", "content": "two"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "tool_use", "id": "t1", "name": "Read", "input": {}}],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": "x"}],
|
||||
},
|
||||
{"role": "assistant", "content": "done two"},
|
||||
{"role": "user", "content": "three"},
|
||||
{"role": "assistant", "content": "refused"},
|
||||
]
|
||||
entries = build_entries(history, session_id=sid, cwd=str(world.root), model="m")
|
||||
entries[-1]["isApiErrorMessage"] = True
|
||||
note = {
|
||||
**entries[2],
|
||||
"uuid": str(uuid.uuid4()),
|
||||
"message": {
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "[Request interrupted by user]"}],
|
||||
},
|
||||
}
|
||||
del note["promptSource"]
|
||||
entries.insert(6, note)
|
||||
await world.store.append(world.key(sid), entries)
|
||||
return sid, entries
|
||||
|
||||
|
||||
async def test_rewind_moves_the_thread_to_a_fork_that_ends_before_the_turn(
|
||||
world: World,
|
||||
) -> None:
|
||||
sid, entries = await _three_turns(world)
|
||||
conv = await world.conversations.create(
|
||||
kind="master", agent="a", origin="test", session_id=sid
|
||||
)
|
||||
await world.conversations.post(conv, "live")
|
||||
await world.settle(conv, 1)
|
||||
assert world.pool.get(conv.external_id) is not None
|
||||
|
||||
turns = await world.conversations.turns(conv)
|
||||
assert [(t["text"], t["reply"], t["failed"]) for t in turns] == [
|
||||
("two", "done two", False),
|
||||
("three", "refused", True),
|
||||
]
|
||||
assert [t["id"] for t in turns] == [entries[2]["uuid"], entries[7]["uuid"]]
|
||||
|
||||
result = await world.conversations.rewind(conv, turns[1]["id"])
|
||||
|
||||
assert result.turns == 1 and result.previous_session == sid
|
||||
moved = result.conversation.session_id
|
||||
assert moved != sid
|
||||
assert (await world.conversations.get(conv.external_id)).session_id == moved
|
||||
assert await world.store.load(world.key(sid)) == entries
|
||||
kept = [
|
||||
e["message"]["content"]
|
||||
for e in await world.store.load(world.key(moved))
|
||||
if e["type"] in ("user", "assistant")
|
||||
]
|
||||
assert len(kept) == 7 and kept[0] == "one"
|
||||
assert kept[-1] == [{"type": "text", "text": "[Request interrupted by user]"}]
|
||||
assert world.pool.get(conv.external_id) is None
|
||||
|
||||
await world.conversations.run_text_turn(conv, "after", origin="user")
|
||||
assert conv.session_id == sid
|
||||
assert ScriptedClient.instances[-1].options.resume == moved
|
||||
|
||||
(two,) = await world.conversations.turns(result.conversation)
|
||||
assert two["text"] == "two"
|
||||
again = await world.conversations.rewind(conv, two["id"])
|
||||
assert again.turns == 1 and again.previous_session == moved
|
||||
assert await world.conversations.turns(again.conversation) == []
|
||||
first = (await world.store.load(world.key(again.conversation.session_id)))[0]
|
||||
with pytest.raises(LookupError):
|
||||
await world.conversations.rewind(conv, first["uuid"])
|
||||
with pytest.raises(LookupError):
|
||||
await world.conversations.rewind(conv, str(uuid.uuid4()))
|
||||
|
||||
|
||||
async def test_rewind_waits_for_no_turn(world: World) -> None:
|
||||
sid, entries = await _three_turns(world)
|
||||
conv = await world.conversations.create(
|
||||
kind="master", agent="a", origin="test", session_id=sid
|
||||
)
|
||||
ScriptedClient.hold = asyncio.Event()
|
||||
await world.conversations.post(conv, "busy")
|
||||
await asyncio.sleep(0.2)
|
||||
with pytest.raises(RuntimeError, match="mid-turn"):
|
||||
await world.conversations.rewind(conv, entries[7]["uuid"])
|
||||
ScriptedClient.hold.set()
|
||||
await world.settle(conv, 1)
|
||||
assert (await world.conversations.get(conv.external_id)).session_id == sid
|
||||
|
||||
|
||||
async def test_merge_injects_summary_into_parent(world: World) -> None:
|
||||
sid = str(uuid.uuid4())
|
||||
await world.store.append(
|
||||
|
||||
Reference in new issue
Block a user