"""Conversations on the new loop (M2.5, PR A): the `general` scope, a thread per conversation, idle close and continuation (L1, L5), and agreed work filed as a record issue whose item starts as agreed (D1, L8). Real git on the throwaway build host; a fake Gitea.""" import re from typing import Any from test_loop_milestones import DON, Forge, ScriptedModel, call, loop_agent from test_loop_milestones import world as world # noqa: PLC0414 -- the fixture from nisse_dev.canonical import Fact, ToolResult from nisse_dev.gitea import Issue from nisse_dev.loop.agent import FILE_ISSUE from nisse_dev.loop.items import CHAT, GO_AHEAD, MERGE, WAITING, loop_of PROPOSAL = call( "propose_work", project="sandbox", title="Add count_lines", summary="Add count_lines(text) to text.py, counting newline-separated lines, " "with tests. First step: the function and its tests.", ) def conversations(world: dict[str, Any]) -> list[Any]: return [i for i in world["store"].work_items() if i.scope == "general"] def facts(world: dict[str, Any], item: Any) -> list[str]: return [ e.event.text for e in world["store"].transcript(item.id, "orchestrator").read() if isinstance(e.event, Fact) ] def quiet(world: dict[str, Any]) -> None: """No assigned issue: only conversations in these tests.""" world["forge"].issues.clear() def test_a_top_level_message_is_a_conversation_in_its_own_thread( world: dict[str, Any], ) -> None: quiet(world) model = ScriptedModel( [call("reply", text="A 4090 runs a 27B model at 4-bit comfortably.")], [call("reply", text="Yes, with a smaller context.")], ) agent, transport = loop_agent(world, model) asked = transport.receive(DON, "Could a 4090 run a 27B model?") [chat] = conversations(world) loop = loop_of(chat) assert loop.thread == asked # the thread is under the operator's message assert (chat.position, loop.wait) == (WAITING, CHAT) reply = transport.outbound[-1] assert reply.thread == asked and "4-bit" in reply.text assert 'conversation "Could a 4090 run a 27B model?": talking' in agent.status() transport.receive(DON, "And a 32B?", thread=asked) assert len(conversations(world)) == 1 # the same conversation assert transport.outbound[-1].text == "Yes, with a smaller context." def test_agreed_work_is_filed_and_starts_as_agreed(world: dict[str, Any]) -> None: quiet(world) forge: Forge = world["forge"] model = ScriptedModel( [PROPOSAL], [ # the filed item's first run: straight to work, no restatement call("start_milestone", name="count_lines"), call("write_file", path="lines.py", content="L = 1\n"), call("finish_milestone", summary="count_lines", final="yes"), ], ) _, transport = loop_agent(world, model) root = transport.receive(DON, "I'd like a line counter in the sandbox.") assert transport.outbound[-1].text.startswith( 'rev-1: I\'d open an issue in don/sandbox, "Add count_lines"' ) transport.receive(DON, "go", thread=root) [filed] = forge.issues marker = re.search(r"", filed.body) assert ( marker and "count newline-separated lines" not in filed.body ) # it's the summary assert "counting newline-separated lines" in filed.body assert forge.assigned == [filed.number] work = world["store"].work_item(marker.group(1)) assert work is not None and work.scope == "sandbox" assert loop_of(work).issue == filed.number and loop_of(work).started assert (work.position, loop_of(work).wait) == (WAITING, MERGE) # it worked [pr] = forge.prs assert f"Closes #{filed.number}" in forge.prs[pr]["body"] # The conversation says where the work went and goes on. [chat] = conversations(world) assert loop_of(chat).became == (f"don/sandbox#{filed.number}",) assert loop_of(chat).wait == CHAT assert any("filed from the conversation" in f for f in facts(world, work)) def test_intake_never_takes_a_record_issue(world: dict[str, Any]) -> None: quiet(world) forge: Forge = world["forge"] body = "brief\n\n" forge.issues.append(Issue(30, "a record", body, "nisse-dev", ())) agent, _ = loop_agent(world, ScriptedModel()) agent.intake() assert world["store"].work_items() == [] def test_a_proposal_for_an_unknown_project_is_sent_back(world: dict[str, Any]) -> None: quiet(world) model = ScriptedModel( [call("propose_work", project="nope", title="t", summary="s.")], [call("reply", text="Which project should it go in?")], ) _, transport = loop_agent(world, model) transport.receive(DON, "Let's build a thing") results = [ e.event.result for e in world["store"].transcript(conversations(world)[0].id, "driver").read() if isinstance(e.event, ToolResult) ] assert any("no project 'nope': the projects are sandbox" in r for r in results) assert loop_of(conversations(world)[0]).wait == CHAT def test_changes_to_a_proposal_go_back_to_the_driver(world: dict[str, Any]) -> None: quiet(world) model = ScriptedModel([PROPOSAL], [PROPOSAL]) _, transport = loop_agent(world, model) root = transport.receive(DON, "a line counter please") transport.receive(DON, "call it line_count instead", thread=root) loop = loop_of(conversations(world)[0]) assert (loop.wait, loop.rev) == (GO_AHEAD, 2) assert world["forge"].issues == [] def test_an_idle_conversation_closes_and_a_reply_continues_it( world: dict[str, Any], ) -> None: quiet(world) model = ScriptedModel( [call("reply", text="Sure.")], [call("reply", text="Picking up again.")] ) agent, transport = loop_agent(world, model) clock = [1000.0] agent._clock = lambda: clock[0] agent._idle = 3600 root = transport.receive(DON, "thinking about a backup tool") clock[0] += 3599 agent.intake() assert conversations(world)[0].status == "open" clock[0] += 1 agent.intake() [old] = conversations(world) assert (old.status, old.outcome) == ("closed", "idle") transport.receive(DON, "back to that backup tool", thread=root) new = [c for c in conversations(world) if c.status == "open"] assert len(new) == 1 and loop_of(new[0]).follows == old.id assert loop_of(new[0]).thread == root assert any("this continues the conversation" in f for f in facts(world, new[0])) def test_a_plain_yes_files_the_work(world: dict[str, Any]) -> None: quiet(world) class Reader: def agrees(self, proposal: str, reply: str) -> bool: return reply == "sounds right" model = ScriptedModel([PROPOSAL], [call("ask_human", question="anything else?")]) agent, transport = loop_agent(world, model) agent._reader = Reader() root = transport.receive(DON, "a line counter please") transport.receive(DON, "sounds right", thread=root) assert len(world["forge"].issues) == 1 def test_a_filing_cut_off_by_a_crash_is_settled_at_boot(world: dict[str, Any]) -> None: quiet(world) forge: Forge = world["forge"] model = ScriptedModel([PROPOSAL]) _, transport = loop_agent(world, model) root = transport.receive(DON, "a line counter please") chat = conversations(world)[0] store = world["store"] # The issue was opened, then the agent died before its item existed. intent = store.begin_intent( FILE_ISSUE, {"repo": "don/sandbox", "item": "sandbox-cdead01", "title": "Add count_lines"}, cause="filing", work_item=chat.id, ) forge.issues.append( Issue( 40, "Add count_lines", "b\n\n", "x", () ) ) agent2, _ = loop_agent(world, ScriptedModel([call("ask_human", question="ok?")])) agent2.boot() work = store.work_item("sandbox-cdead01") assert work is not None and loop_of(work).issue == 40 and loop_of(work).started assert not [i for i in store.pending_intents() if i.id == intent] assert root # the conversation itself is untouched def test_a_filing_that_never_reached_gitea_is_put_up_again( world: dict[str, Any], ) -> None: quiet(world) _, transport = loop_agent(world, ScriptedModel([PROPOSAL])) transport.receive(DON, "a line counter please") chat = conversations(world)[0] world["store"].begin_intent( FILE_ISSUE, {"repo": "don/sandbox", "item": "sandbox-cnever1", "title": "Add count_lines"}, cause="filing", work_item=chat.id, ) agent2, _ = loop_agent(world, ScriptedModel()) agent2.boot() loop = loop_of(world["store"].work_item(chat.id)) assert loop.wait == GO_AHEAD and "cut off by a restart" in loop.note assert world["store"].work_item("sandbox-cnever1") is None def test_commands_alone_at_top_level_are_not_conversations( world: dict[str, Any], ) -> None: quiet(world) _, transport = loop_agent(world, ScriptedModel()) transport.receive(DON, "status") transport.receive(DON, "go") assert conversations(world) == []