Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8b0cdee
Record owner requests independently of commander goals
trissim Oct 21, 2026
9cf860d
Merge remote-tracking branch 'origin/main' into fix/p421-owner-requests
trissim Oct 21, 2026
39a2b2b
Merge remote-tracking branch 'origin/main' into fix/p421-owner-requests
trissim Oct 21, 2026
87353ca
Merge remote-tracking branch 'origin/main' into fix/p421-owner-requests
trissim Oct 21, 2026
45e753d
Link orders and directives to independent owner request records
trissim Oct 21, 2026
31f6073
Merge remote-tracking branch 'origin/main' into fix/p421-owner-requests
trissim Oct 21, 2026
4b21b20
Share parsed send targets and cover independent request publication
trissim Oct 21, 2026
9392ed7
Merge remote-tracking branch 'origin/main' into fix/p421-owner-requests
trissim Oct 21, 2026
f24cbff
Merge remote-tracking branch 'origin/main' into fix/p421-owner-requests
trissim Oct 21, 2026
c74c18a
Carry owner requests without losing directive history
trissim Oct 21, 2026
351d29d
Merge remote-tracking branch 'origin/main' into fix/p421-owner-requests
trissim Oct 21, 2026
d65a959
Integrate retained P421 requests with current Core owners
trissim Oct 21, 2026
7918425
Integrate main goal execution lifetime with owner requests
trissim Oct 21, 2026
82913e7
Integrate typed acting assigners and current main with owner request …
trissim Oct 21, 2026
9b9c031
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 21, 2026
c53020b
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 21, 2026
e5363c4
Own retained table rebuilds and migrate request fixtures
trissim Oct 21, 2026
b8318b1
Integrate declaration-owned schema and delete completed carry tests
trissim Oct 21, 2026
1c17ff7
Merge remote-tracking branch 'origin/fix/p447-source-native-preparati…
trissim Oct 21, 2026
d4f87c9
Integrate current main and batch request directive reads
trissim Oct 21, 2026
949c113
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 21, 2026
992a5e1
Derive request carry mechanics and preserve copied commander configur…
trissim Oct 21, 2026
1b2101b
Delete obsolete goal and todo dependencies after request migration
trissim Oct 21, 2026
9328bd6
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 21, 2026
34831da
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 22, 2026
321b2de
Factor request persistence and own historical request prefixes
trissim Oct 22, 2026
36cb004
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 22, 2026
fbadb9e
Preserve owner request identity across every parity and verdict caller
trissim Oct 22, 2026
c4bee11
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 22, 2026
14761f8
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 22, 2026
306aee0
Keep commander fixture deliveries confined to cited source
trissim Oct 22, 2026
7529af2
Merge remote-tracking branch 'origin/main' into fix/P421-owner-reques…
trissim Oct 22, 2026
d5b9c0a
Copy only the declared plan module into commander fixtures
trissim Oct 22, 2026
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
8 changes: 6 additions & 2 deletions src/agent_comms/channel_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ def contexts(self, declaration, channel):
class NamedTarget(CommandTarget):
name: str
batchable = False
direct_names = ()

@property
def direct_names(self) -> tuple[str, ...]:
return ()

guidance_scope = ""
default_audience = MessageAudience.DIRECT

Expand Down Expand Up @@ -200,7 +204,7 @@ def require_frozen_recipients(self, message, audience):
message.role.require_recipients(self, audience)

@property
def direct_names(self):
def direct_names(self) -> tuple[str, ...]:
return self.names

def canonical_name(self, canonical):
Expand Down
22 changes: 10 additions & 12 deletions src/agent_comms/commander/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from dataclasses import dataclass
from importlib.resources import files
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Self
from typing import TYPE_CHECKING, Annotated, ClassVar, Self

from pydantic import BaseModel, ConfigDict, ValidationInfo, field_validator

Expand All @@ -27,6 +27,7 @@
boundary_decoder,
)
from ..installer.backup import UpgradeRefused
from ..locked_store import DurableFile
from ..native_arguments import NativeArguments
from ..repo import Repo
from ..thread_text import AbsentText, GivenText, TextChoice
Expand All @@ -48,7 +49,8 @@ def require_commander(thread: Thread) -> None:
)


class CommanderConfig(CommanderRole, BaseModel):
class CommanderConfig(CommanderRole, BaseModel, DurableFile):
filename: ClassVar[str] = 'commander.json'
model_config = ConfigDict(frozen=True)

commander: str
Expand All @@ -60,9 +62,9 @@ class CommanderConfig(CommanderRole, BaseModel):
disk: Path
parked: frozenset[str] = frozenset()

@staticmethod
def path(home: Path | None = None) -> Path:
return (home or Path.home()) / '.config/agent-comms/commander.json'
@classmethod
def path(cls, home: Path | None = None) -> Path:
return (home or Path.home()) / '.config/agent-comms' / cls.filename

@staticmethod
def packaged_rules() -> Path:
Expand Down Expand Up @@ -91,14 +93,10 @@ def selected_root(cls, root: Path, info: ValidationInfo) -> Path:
def read(cls, path: Path | str) -> Self:
return cls.model_validate_json(Path(path).read_text())

@staticmethod
def root_path(root: Path) -> Path:
return root / 'commander.json'

@classmethod
def read_root(cls, root: Path | str) -> Self:
root = Path(root).resolve()
return cls.model_validate_json(cls.root_path(root).read_text(), context=root)
return cls.model_validate_json(cls.path_in(root).read_text(), context=root)

@classmethod
def on_machine(cls, commander: str, repos: tuple[Repo, ...], root: Path, home: Path,
Expand Down Expand Up @@ -143,7 +141,7 @@ def write(self, home: Path) -> Path:
self.require_commander(Comms(self.root).registry.require(self.commander))
path = self.path(home)
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
record = self.root_path(self.root)
record = self.path_in(self.root)
record.write_text(self.model_dump_json(indent=1) + '\n')
path.unlink(missing_ok=True)
path.symlink_to(record)
Expand All @@ -160,7 +158,7 @@ class CommanderRecord(ABC):
@boundary_decoder
def read(root: Path) -> CommanderRecord:
root = root.resolve()
if CommanderConfig.root_path(root).is_file():
if CommanderConfig.path_in(root).is_file():
return RecordedCommander(CommanderConfig.read_root(root))
return NoCommanderRecord()

Expand Down
2 changes: 1 addition & 1 deletion src/agent_comms/commander/judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ 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, todo)
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.actor_name} at review): '
Expand Down
62 changes: 33 additions & 29 deletions src/agent_comms/commander/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from agent_comms.message_reference import MessageReference
from agent_comms.messages import Message
from agent_comms.native_tool_call import SelectedToolDenied
from agent_comms.owner_requests import NewestOpenRequest, RequestSelection
from agent_comms.plan_phases import OrderPhase
from agent_comms.problem_number import ProblemNumber
from agent_comms.recorded_time import RecordedEventTime
Expand Down Expand Up @@ -324,9 +325,9 @@ def actor_identity(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.actor_name, target,
audience.addressed(target, body), answers=answers)
target = audience.target
published = self.comms.messaging.send(self.actor_name, target.name,
target.addressed(audience.threads, body), answers=answers)
return published.message.message_id

def call(self, tool):
Expand Down Expand Up @@ -477,7 +478,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 @@ -578,17 +579,17 @@ 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,
assigner=self.actor_identity)))
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.actor_identity, 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 @@ -630,20 +631,6 @@ def say(self, units: str, body_file: str,
refuse(body, *OUTGOING)
print(self.post(VerdictAudience(tuple(units.split(','))), body.rstrip(), answers))

def problem(self, order_file, unit, where, disk=False,
answers: tuple[MessageReference, ...] = ()):
"""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)
checkout = self.repo_directory(document.order.check.directory)
if checkout.is_dir():
self.comms.goals.todos.require_recurrence_declaration(
document.order, location, checkout, document.slop, time.time())
pid = self.comms.goals.todos.allocate_problem()
self.send(unit, document, new=(pid, document.defect),
disk=disk, finding=LocatedProblemFinding(location), answers=answers)
print(pid)

def close(self, pid, works, artifact=''):
"""Core owns closing: the installed evidence, the artifact and the no-open-order rule live on the Todo."""
evidence = artifact or self.comms.goals.todos.get(pid).order.closing_artifact()
Expand Down Expand Up @@ -696,7 +683,7 @@ 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, todo), 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: str, delivery: OrderDelivery) -> None:
Expand Down Expand Up @@ -748,7 +735,7 @@ 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, todo))
print(f'{unit}: {n} strike(s) on {task}')

def withdraw(self, pid: str, why: str, superseded_by: Successor = NoSuccessor()) -> None:
Expand All @@ -765,7 +752,7 @@ 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, todo))
self.post(VerdictAudience((unit,)),
f'Withdrawn: {withdrawal}. Stop that work; it counts against no one.')
print(withdrawal)
Expand Down Expand Up @@ -855,7 +842,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 @@ -894,7 +889,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 @@ -1135,7 +1130,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 @@ -1150,8 +1145,17 @@ class ProblemVerb(OrderVerb, DirectiveVerb):
disk: Annotated[bool, Argument(flags=("--disk",))] = False

def run(self, ctx: Commander) -> None:
ctx.problem(self.order, self.to, self.where, self.disk,
self.answer_references(ctx))
document = OrderFile.read(Path(self.order).read_text())
location = FileLine.from_wire(self.where)
checkout = ctx.repo_directory(document.order.check.directory)
if checkout.is_dir():
ctx.comms.goals.todos.require_recurrence_declaration(
document.order, location, checkout, document.slop, time.time())
pid = ctx.comms.goals.todos.allocate_problem()
ctx.send(self.to, document, new=(pid, document.defect), disk=self.disk,
finding=LocatedProblemFinding(location), answers=self.answer_references(ctx),
request=self.request)
print(pid)


@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 use the same bus writer as orders and directives."""

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
Loading
Loading