diff --git a/src/agent_comms/goals.py b/src/agent_comms/goals.py index 2dd354a8f7..e22882e8d5 100644 --- a/src/agent_comms/goals.py +++ b/src/agent_comms/goals.py @@ -577,7 +577,7 @@ def goal_text(self, goal: Goal) -> str: def progress(self, goal: Goal, todos: Sequence[Todo]) -> str: """Each problem's order state and latest verdict, as Core records them on its Todo.""" - return "\n".join(" · ".join((f"{todo.id}: {todo.order.line}", + return "\n".join(" · ".join((f"{todo.id}: {todo.order.progress_line()}", *(verdict.line for verdict in todo.verdicts[-1:]))) for todo in todos) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index da81db55e5..44d0547543 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -47,23 +47,38 @@ @dataclass(frozen=True, kw_only=True) class OrderDelivery(DeclaredFamily, affix="Delivery"): at: EventTime = field(default_factory=UnrecordedEventTime) + checked_kind: ClassVar[type[OrderDelivery]] @property def work(self) -> str: return "delivered work" + @property + def check_line(self) -> str: + """A delivery saved before Core recorded the Check's print says so.""" + return "no print recorded" + async def run(self, check, *command): result = await BoundedRun.run(tuple(command), cwd=check.directory, timeout=120) if result.outcome != ExitedOutcome(0): raise ValueError(result.stderr.decode(errors="replace") or "Delivery source check failed.") return result.stdout.decode(errors="replace") + async def passed(self, check: OrderCheck) -> str: + await self.require_source(check) + result = await self.checked(check) + check.result.require(result) + return result.stdout.decode(errors="replace").strip() + async def verified(self, check: OrderCheck, deletes: OrderDeletion) -> OrderDelivery: - """The order's check and every count its deletion declares run on the delivered source.""" - for each in (check, *deletes.checks): - await self.require_source(each) - each.result.require(await self.checked(each)) - return replace(self, at=await self.delivery_time(check)) + """The order's check and every count its deletion declares run on the delivered source; + the checked member records what the order's check printed, never what a report says.""" + printed = await self.passed(check) + for each in deletes.checks: + await self.passed(each) + carried = {item.name: getattr(self, item.name) for item in fields(self)} + return self.checked_kind( + **carried | {"at": await self.delivery_time(check), "printed": printed}) @abstractmethod async def require_source(self, check): ... @@ -132,6 +147,16 @@ async def require_source(self, check: OrderCheck) -> None: f"Delivered commit {self.delivered} is on no origin branch; push it first.") +@dataclass(frozen=True, kw_only=True) +class CheckedCommitDelivery(CommitDelivery, declared_name="checked_commit"): + """A commit delivery Core verified, with what the order's Check printed on it.""" + printed: str + + @property + def check_line(self) -> str: + return f"Check printed {self.printed!r}" + + @dataclass(frozen=True, kw_only=True) class OperationDelivery(OrderDelivery): async def require_source(self, check): @@ -144,6 +169,20 @@ async def delivery_time(self, check): return EventTime.capture(time.time()) +@dataclass(frozen=True, kw_only=True) +class CheckedOperationDelivery(OperationDelivery, declared_name="checked_operation"): + """An operation delivery Core verified, with what the order's Check printed.""" + printed: str + + @property + def check_line(self) -> str: + return f"Check printed {self.printed!r}" + + +CommitDelivery.checked_kind = CheckedCommitDelivery +OperationDelivery.checked_kind = CheckedOperationDelivery + + @dataclass(frozen=True) class PullRequest: number: int @@ -929,6 +968,10 @@ def __post_init__(self): pending: ClassVar[bool] line: ClassVar[str] + + def progress_line(self) -> str: + """This order's state on the board.""" + return self.line NAMED_FILE: ClassVar[re.Pattern] = re.compile(r"([\w./-]+\.\w+):\d+") def require_transfer(self, todo: Todo) -> None: @@ -1055,6 +1098,9 @@ class DeliveredProblemOrder(ProblemOrder): pending: ClassVar[bool] = False line: ClassVar[str] = "Delivered, waiting on install" + def progress_line(self) -> str: + return f"{self.line} · {self.delivery.check_line}" + def require_transfer(self, todo: Todo) -> None: owner = (todo.assignment or todo.released[-1].previous).owner raise ValueError(f"{owner} already has {self.delivery.work}; {owner} merges it.") diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index cce6349b53..44d71c7983 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -14,7 +14,7 @@ import pytest from contextlib import closing -from dataclasses import replace +from dataclasses import fields, replace from cutover.carry import CarryChain @@ -32,7 +32,8 @@ from agent_comms.threads import Thread from agent_comms.tool_caller import ThreadCaller from agent_comms.tools import (CommsCorrectWordingTool, CommsSendTool, CommsFireTool, CommsOrderCheckTool, CommsOrderDoneTool, CommsOrderTool, - CommsOrderWorkTool, CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, CommsTodoTool, CommsVerdictTool) + CommsOrderWorkTool, CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, CommsTodoTool, CommsVerdictTool, + ThreadToolRequest) from agent_comms.todos import (AwaitingFirstCall, VerdictAudience, OrderMessage, OrderDeletion, GoalRef, CleanHunt, CommitDelivery, FirstCall, Hunt, OrderDelivery, ApplicationOrderRun, RecordedCommitDelivery, MechanicalOrderRun, TodoStore, LateObservedFirstCall, OperationDelivery, OrderedHunt, @@ -176,6 +177,19 @@ def test_failed_check_keeps_problem_pending(commander): assert comms.registry.require('worker').goal.state.active +def test_done_declares_no_wrong_measure_and_the_board_shows_the_recorded_check_result(commander): + comms, caller = commander + create(commander, 'P1') + reported = {f.name for f in fields(CommsOrderDoneTool)} - {f.name for f in fields(ThreadToolRequest)} + assert reported == {'problem', 'delivery', 'claims'} + CommsOrderDoneTool(problem='P1', delivery=OperationDelivery(), claims=working(comms)).apply(comms, caller) + delivery = comms.goals.todos.get('P1').order.delivery + assert delivery.printed == '1' and FieldCodec.decode(OrderDelivery, FieldCodec.encode(delivery)) == delivery + worker = comms.registry.require('worker') + progress = GoalSegment.capture(worker, worker.goal, comms.goals).progress + assert progress == "P1: Delivered, waiting on install · Check printed '1'" + + def test_close_records_evidence_once_and_does_not_depend_on_artifact_lifetime(commander): comms, caller = commander create(commander, 'P1') @@ -358,7 +372,7 @@ def test_a_done_record_from_before_the_split_reads_its_one_commit_as_both(): old = {'kind': 'commit', 'commit': 'd012d23b4', 'at': FieldCodec.encode(RecordedEventTime(1000))} recorded = FieldCodec.decode(OrderDelivery, old) assert type(recorded) is RecordedCommitDelivery and recorded.delivered == recorded.commit == 'd012d23b4' - assert FieldCodec.encode(recorded) == old + assert FieldCodec.encode(recorded) == old and recorded.check_line == 'no print recorded' def test_delivery_time_has_its_own_named_requirement(): @@ -1192,7 +1206,7 @@ def test_a_problem_goal_shows_only_its_live_orders_through_convert_deliver_send_ todo.assignment, 'toolu_3', reason=StrikeReason(why))).apply(comms, caller) worker = comms.registry.require('worker') progress = GoalSegment.capture(worker, worker.goal, comms.goals).progress - assert progress.splitlines() == ['P1: Delivered, waiting on install', f'P2: Open · Sent back: {why}'] + assert progress.splitlines() == ["P1: Delivered, waiting on install · Check printed '1'", f'P2: Open · Sent back: {why}'] assert (worker.goal.text, worker.goal.progress) == (ProblemGoalOrder.text, '') with pytest.raises(ValueError, match='block the problem.s Todo, or standby with wait_for'): comms.goals.update_goal('worker', BlockedGoalAction(block_reason='Waiting on lead.'), actor=ModelInvocable)