A pure move; behaviour unchanged. - models (L9, L7): llm, catalog, probe, render, render_anthropic, context - log (L7, L6): canonical, views - state (L8): store (was state), reconcile, worker, cancel, dump - knowledge (L10, L6.4): vault, memory - conversation (L5): transport, matrix - execution (L13): ssh (was execution), testing - authority (P3, P4, L4): actions - gitea, oplog, envfile stay at the top until each gains a second module. Every import, monkeypatch target, script and doc reference rewritten. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
240 lines
9.0 KiB
Python
240 lines
9.0 KiB
Python
"""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_core.gitea import Issue
|
|
from nisse_core.log.canonical import Fact, ToolResult
|
|
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"<!-- nisse-item: ([\w-]+) -->", 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<!-- nisse-item: sandbox-cabc123 -->"
|
|
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, model: 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<!-- nisse-item: sandbox-cdead01 -->", "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) == []
|