Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/agent_comms/commander/judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,8 @@ def check_edits(self, edits: list[Edit]) -> None:

def report(self, ctx: order.Commander, todos: tuple[Todo, ...]) -> None:
for todo in todos:
audience = todo.verdicts[-1].audience.of(todo.held().owner, todo.goal)
audience = todo.verdicts[-1].audience.of(
todo.held().owner, ctx.comms.goals.todos.owner_request(todo.request.require()))
roast(ctx, audience, todo.holder, self.lines, self.roast, self.fix, self.slop())
line = (f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} '
f'(strike {todo.strike_count} on {todo.id}; caught by {ctx.commander} at review): '
Expand Down
54 changes: 33 additions & 21 deletions src/agent_comms/commander/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from agent_comms.standing_rules import HuntDue
from agent_comms.storage_rules import UnitStorage
from agent_comms.thread_identity import ThreadIncarnation
from agent_comms.owner_requests import NewestOpenRequest, RequestSelection
from agent_comms.todos import (
BluntText,
CleanHunt,
Expand Down Expand Up @@ -262,9 +263,9 @@ def incarnation(self) -> ThreadIncarnation:

def post(self, audience: VerdictAudience, body: str,
answers: tuple[MessageReference, ...] = ()) -> str:
target = audience.threads[0] if len(audience.threads) == 1 else '#comms'
published = self.comms.messaging.send(self.commander, target,
audience.addressed(target, body), answers=answers)
target = audience.target
published = self.comms.messaging.send(self.commander, target.name,
target.addressed(audience.threads, body), answers=answers)
return published.message.message_id

def call(self, tool):
Expand Down Expand Up @@ -396,7 +397,7 @@ def send(self, unit, document: OrderFile, clean=False,
reassign: Reassignment = NoReassignment(), new=None,
traced: FileLine | NoSourceReference = NoSourceReference(),
disk=False, finding=UnlocatedProblemFinding(),
answers: tuple[MessageReference, ...] = ()):
answers: tuple[MessageReference, ...] = (), *, request: RequestSelection):
"""Record the decoded order, or report every failed prerequisite before any write."""
problems = document.problems_found(clean)
check = replace(document.order.check,
Expand Down Expand Up @@ -495,16 +496,16 @@ def send(self, unit, document: OrderFile, clean=False,
struck_already = replacing and listed[group[0]].struck_on_order(unit)
measured = MeasuredReview.read(self.comms.goals.todos)
review_at = measured.review_at(order)
request = order.request(self.comms.registry.require(self.commander), tuple(group))
request_id = request.for_order(self.comms.goals.todos, self.incarnation)
if new:
self.call(CommsTodoCreateTool(owner=unit, order=CreateProblemGoalAction(
problem=new[0], repo=Repo.of(Path(check.directory)).github, text=new[1],
order=order, request=request, review_at=review_at, commander=self.commander)))
order=order, request=request_id, review_at=review_at, commander=self.commander)))
for pid in [] if new else group:
self.call(CommsOrderTool(owner=unit, order=OrderGoalAction(
problem=pid, expected_revision=self.comms.goals.todos.get(pid).revision,
assigner=self.incarnation, traced=traced,
order=order, request=request, review_at=review_at, commander=self.commander)))
order=order, request=request_id, review_at=review_at, commander=self.commander)))
message += "\n" + "\n".join(line for pid in group for line in order.scope.instructions(pid))
sent = self.post(VerdictAudience((unit,)), message, answers)
# The message is each order's recorded link; a verdict citing it lands on that order.
Expand Down Expand Up @@ -546,7 +547,7 @@ def say(self, units: str, body_file: str,
print(self.post(VerdictAudience(tuple(units.split(','))), body.rstrip(), answers))

def problem(self, order_file, unit, where, disk=False,
answers: tuple[MessageReference, ...] = ()):
answers: tuple[MessageReference, ...] = (), *, request: RequestSelection):
"""A problem is born owned: Core records it with its order in one step, or not at all."""
document = OrderFile.read(Path(order_file).read_text())
location = FileLine.from_wire(where)
Expand All @@ -557,7 +558,8 @@ def problem(self, order_file, unit, where, disk=False,
history = self.comms.goals.todos.history_for_commander(self.incarnation)
pid = f'P{max([int(t.id[1:]) for t in history if t.id[1:].isdigit()] + [0]) + 1}'
self.send(unit, document, new=(pid, document.defect),
disk=disk, finding=LocatedProblemFinding(location), answers=answers)
disk=disk, finding=LocatedProblemFinding(location), answers=answers,
request=request)
print(pid)

def close(self, pid, works, artifact=''):
Expand Down Expand Up @@ -630,7 +632,8 @@ def posted(self, unit: str, todo: Todo, strike: StrikeTodo,
n = struck(todo, unit)
line = (f'{time.strftime("%Y-%m-%d")} {strike.at.clock()} {unit} (strike {n} on {todo.id}'
f'{f", {todo.id} taken away" if n >= 2 else ""}): {strike.reason}')
self.on_record(line, 'Strike', VerdictAudience.of(unit, todo.goal), answers)
self.on_record(line, 'Strike', VerdictAudience.of(
unit, self.comms.goals.todos.owner_request(todo.request.require())), answers)
print(f'{unit}: {n} strike(s) on {todo.id}' + (f'. Reassign {todo.id} now and freeze its branch.' if n >= 2 else ''))

def done(self, pid, sha, screenshot='', seen=''):
Expand Down Expand Up @@ -701,7 +704,8 @@ def unstrike(self, task: str, why: str, at: StrikeSelector = NewestStrike()) ->
n = struck(self.comms.goals.todos.get(task), unit)
line = (f'{time.strftime("%Y-%m-%d %H:%M")} {unit} (strike voided on {task}, '
f'recorded {voided.at.clock()}; {n} strike(s) remain): {why}')
self.on_record(line, 'Void', VerdictAudience.of(unit, todo.goal))
self.on_record(line, 'Void', VerdictAudience.of(
unit, self.comms.goals.todos.owner_request(todo.request.require())))
print(f'{unit}: {n} strike(s) on {task}')

def withdraw(self, pid: str, why: str, superseded_by: Successor = NoSuccessor()) -> None:
Expand All @@ -718,7 +722,8 @@ def withdraw(self, pid: str, why: str, superseded_by: Successor = NoSuccessor())
unit = previous.owner
withdrawal, = self.comms.goals.todos.get(pid).order.withdrawals(todo)
self.on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {withdrawal}', 'Withdrawal',
VerdictAudience.of(unit, todo.goal))
VerdictAudience.of(
unit, self.comms.goals.todos.owner_request(todo.request.require())))
self.post(VerdictAudience((unit,)),
f'Withdrawn: {withdrawal}. Stop that work; it counts against no one.')
print(withdrawal)
Expand Down Expand Up @@ -778,8 +783,8 @@ class DirectiveVerb(CommanderVerb):

def answer_references(self, ctx: Commander) -> tuple[MessageReference, ...]:
thread = ctx.comms.registry.require(self.who(ctx))
answerable = (*(message for goal in thread.goal.active()
for message in goal.order.directive_messages(ctx.comms.goals)),
answerable = (*(message for request in ctx.comms.goals.todos.open_requests(thread.incarnation)
for message in request.directive_messages(ctx.comms.goals)),
*UnitAskUnanswered.asks(ctx.comms, thread.name, time.time()))
originals = tuple(message for message in answerable if message.message_id in self.answers)
if set(self.answers) != {message.message_id for message in originals}:
Expand All @@ -791,9 +796,8 @@ def gate(self, ctx: Commander) -> None:
answers = self.answer_references(ctx)
thread = ctx.comms.registry.require(self.who(ctx))
overdue = tuple(directive.original(ctx.comms.goals)
for goal in thread.goal.active()
for directive in goal.order.overdue_directives(
goal, ctx.comms.goals, thread.incarnation, time.time()))
for request in ctx.comms.goals.todos.open_requests(thread.incarnation)
for directive in request.overdue_directives(ctx.comms.goals, time.time()))
if overdue and not answers:
lines = '\n'.join(f'{message.message_id}: {message.body[:80]}' for message in overdue)
sys.exit('REFUSED, nothing recorded: overdue owner directives:\n' + lines
Expand All @@ -806,7 +810,15 @@ class OrderVerb(CommanderVerb):


@dataclass(frozen=True, kw_only=True)
class SendVerb(OrderVerb, DirectiveVerb):
class RequestedOrderVerb(OrderVerb, DirectiveVerb, declared_name=None):
request: Annotated[
RequestSelection, Argument(flags=("--request",),
options={"help": "owner request ID; defaults to newest open"}),
] = NewestOpenRequest()


@dataclass(frozen=True, kw_only=True)
class SendVerb(RequestedOrderVerb):
help = "record the order in Core and send it"
unit: Annotated[str, Argument(flags=("unit",))]
body_file: Annotated[
Expand Down Expand Up @@ -845,7 +857,7 @@ class SendVerb(OrderVerb, DirectiveVerb):
def run(self, ctx: Commander) -> None:
ctx.send(self.unit, OrderFile.read(Path(self.body_file).read_text()), clean=self.clean,
reassign=self.reassign, traced=self.traced,
disk=self.disk, answers=self.answer_references(ctx))
disk=self.disk, answers=self.answer_references(ctx), request=self.request)


@dataclass(frozen=True, kw_only=True)
Expand Down Expand Up @@ -1093,7 +1105,7 @@ def run(self, ctx: Commander) -> None:


@dataclass(frozen=True, kw_only=True)
class ProblemVerb(OrderVerb, DirectiveVerb):
class ProblemVerb(RequestedOrderVerb):
help = "Core records the problem with its order, or nothing"
order: Annotated[
str, Argument(flags=("order",), options={"metavar": "ORDER.json"})
Expand All @@ -1109,7 +1121,7 @@ class ProblemVerb(OrderVerb, DirectiveVerb):

def run(self, ctx: Commander) -> None:
ctx.problem(self.order, self.to, self.where, self.disk,
self.answer_references(ctx))
self.answer_references(ctx), request=self.request)


@dataclass(frozen=True, kw_only=True)
Expand Down
68 changes: 32 additions & 36 deletions src/agent_comms/commander/request.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,67 @@
"""What the owner asked for, recorded as the commander's own Goal so no request is forgotten.
"""Record, list and complete independent owner requests.

`request add "TEXT"` records one and prints its id; `request complete ID --why "..."` closes
it, refused while an order serving it is open; `request list` shows the open request with its
open orders. A thread holds one Goal, so one request is open at a time. Every order names the
request it serves (`order send|problem --request ID`); rotation prints REQUEST STALLED for an
open request that no open order serves.
Every request keeps its own id and serving orders. Completing one request never
replaces the commander's current goal or another request.
"""

from __future__ import annotations

from typing import Annotated
from dataclasses import dataclass
from functools import partial
from typing import Annotated

from agent_comms import field_codec
from agent_comms.commander.order import Commander, CommanderVerb
from agent_comms.message_bus import HeldBusWrite
from agent_comms.owner_requests import OwnerRequestId

from ..goal_actions import CompletedGoalAction, GoalPrecondition, SetGoalAction
from ..goal_states import ActiveGoal
from ..goals import Goal, RequestGoalOrder
from ..todos import Todo
from .order import Commander, CommanderVerb

class RequestVerb(CommanderVerb):
"""A verb of `request`."""

def open_requests(ctx: Commander) -> tuple[tuple[Goal, tuple[Todo, ...]], ...]:
"""The commander's open owner requests and their undelivered orders."""
thread = ctx.comms.registry.require(ctx.commander)
return tuple(request for goal in thread.goal.active()
for request in goal.order.open_requests(goal, ctx.comms.goals, thread.incarnation))

class RequestMutation(HeldBusWrite, RequestVerb, declared_name=None):
"""Request mutations share the same admission as order and directive publication."""

class RequestVerb(CommanderVerb):
"""A verb of `request`."""
def apply(self, ctx: Commander) -> None:
self.write_when_open(ctx.comms, partial(super().apply, ctx))


@dataclass(frozen=True, kw_only=True)
class RequestAddVerb(RequestVerb, declared_name="add"):
help = "record an owner request as the commander's Goal; prints its id"
class RequestAddVerb(RequestMutation, declared_name="add"):
help = "record an owner request; prints its id"
text: Annotated[str, field_codec.Argument(flags=("text",))]

def run(self, ctx: Commander) -> None:
goal = ctx.comms.goals.update_goal(ctx.commander,
SetGoalAction(text=self.text, order=RequestGoalOrder()))
print(goal.require().id)
commander = ctx.comms.registry.require(self.who(ctx)).incarnation
request = ctx.comms.goals.todos.add_request(commander, self.text)
print(request.id.value)


@dataclass(frozen=True, kw_only=True)
class RequestCompleteVerb(RequestVerb, declared_name="complete"):
help = "close a request; refused while an order serving it is open"
id: Annotated[str, field_codec.Argument(flags=("id",))]
class RequestCompleteVerb(RequestMutation, declared_name="complete"):
help = "close a request; refused while an order serving it is pending"
id: Annotated[OwnerRequestId, field_codec.Argument(flags=("id",))]
why: Annotated[str, field_codec.Argument(flags=("--why",))]

def run(self, ctx: Commander) -> None:
expect = GoalPrecondition(goal_id=self.id, expected_state=ActiveGoal())
ctx.comms.goals.update_goal(ctx.commander,
CompletedGoalAction(progress=self.why, expect=expect))
print(f"{self.id} done")
commander = ctx.comms.registry.require(self.who(ctx)).incarnation
ctx.comms.goals.todos.complete_request(commander, self.id, self.why)
print(f"{self.id.value} done")


class RequestListVerb(RequestVerb, declared_name="list"):
help = "the open request and the open orders serving it"
help = "every open request and its pending orders"

def run(self, ctx: Commander) -> None:
requests = open_requests(ctx)
commander = ctx.comms.registry.require(self.who(ctx)).incarnation
store = ctx.comms.goals.todos
requests = store.open_requests(commander)
if not requests:
print("No open request.")
for goal, served in requests:
print(f"{goal.id} {goal.text}")
for todo in served:
for request in requests:
print(f"{request.id.value} {request.text}")
for todo in store.serving_orders(request.id):
print(f" {todo.id} review {todo.review_at.clock()}: {todo.text}")


Expand Down
15 changes: 7 additions & 8 deletions src/agent_comms/commander/standing.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from agent_comms.storage_rules import OrderCheckoutWork
from agent_comms.thread_identity import CoreWatcherIncarnation, ThreadIncarnation
from agent_comms.threads import Thread
from agent_comms.todos import Todo
from agent_comms.todos import OwnerRequest, Todo
from agent_comms.turn_digests import OrderEditCountRead, TurnDigestRead, TurnsSince
from agent_comms.work_branches import WorkBranches
from agent_comms.work_dispatch import ReusedWorkDispatch, WorkDispatch
Expand Down Expand Up @@ -184,18 +184,17 @@ def line(self) -> str:

@dataclass(frozen=True)
class RequestStalled(UndatedStandingRule):
goal: Goal
request: OwnerRequest

@classmethod
def violations(cls, comms: Comms, commander: str) -> tuple[RequestStalled, ...]:
thread = comms.registry.require(commander)
return tuple(cls(request) for goal in thread.goal.active()
for request, served in goal.order.open_requests(
goal, comms.goals, thread.incarnation)
if not served)
store = comms.goals.todos
return tuple(cls(request) for request in store.open_requests(thread.incarnation)
if not store.serving_orders(request.id))

def line(self) -> str:
return (f"REQUEST STALLED: {self.goal.id} {self.goal.text}"
return (f"REQUEST STALLED: {self.request.id.value} {self.request.text}"
" -> order its next step or mark it done")


Expand Down Expand Up @@ -308,7 +307,7 @@ def order_for(todo: Todo, commander: str, assigner: ThreadIncarnation) -> Reissu
now = time.time()
return ReissuedOrderGoalAction(
problem=todo.id, expected_revision=todo.revision,
order=todo.order.reissued(now), request=todo.goal.request(),
order=todo.order.reissued(now), request=todo.request.require(),
review_at=todo.reissued_review(now), commander=commander, assigner=assigner,
traced=todo.order.owner)

Expand Down
4 changes: 1 addition & 3 deletions src/agent_comms/commander_heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@ def commanders(cls, registry: Registration, goals: Goals) -> tuple[Thread, ...]:
thread
for thread in registry.all_threads().values()
if thread.process_alive
and any(
goal.order.open_requests(goal, goals, thread.incarnation) for goal in thread.goal
)
and goals.todos.open_requests(thread.incarnation, limit=1)
)

def observe(self, comms: Comms) -> tuple[CommanderObservation, ...]:
Expand Down
Loading
Loading