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
2 changes: 1 addition & 1 deletion docs/agents/behaviour-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ A done report that names its own measure instead of what its order's Check print

Checked by:

- not enforced here: P197 (#1733)
- check in `agent_comms.todos.OrderDelivery.verified`

Learned from: seed catalog pattern 4 (claude-ai message 1c8729ba0ed7); [Codex postmortem, failure 4](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md)

Expand Down
4 changes: 2 additions & 2 deletions src/agent_comms/behaviour_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from .declared_family import DeclaredFamily
from .goals import OrderCheck
from .standing_rules import BoardDisagrees, PlanIdle, StandingRule
from .todos import PlainText, StorageStrikeTodo, Todo, TodoStore, VoidStrikeTodo
from .todos import OrderDelivery, PlainText, StorageStrikeTodo, Todo, TodoStore, VoidStrikeTodo

POSTMORTEM = "https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md"
COMMANDER_SKILL = "../../src/agent_comms/skills/comms-commander/SKILL.md"
Expand Down Expand Up @@ -309,7 +309,7 @@ class WrongMeasurePattern(BehaviourPattern):
signature = (
"A done report that names its own measure instead of what its order's Check printed."
)
checks = (PendingCheck("P197", "#1733"),)
checks = (CallableCheck(OrderDelivery.verified),)
sources = (SeedCatalog(4), MeasuredWrongThingFailure())


Expand Down
12 changes: 12 additions & 0 deletions src/agent_comms/child_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,10 @@ def close(self) -> None:
class CheckPrinted(DeclaredFamily, affix="Printed"):
"""The count printed by a child, or the absence of an integer."""

@property
@abstractmethod
def line(self) -> str: ...

@staticmethod
def decoded(stdout: bytes) -> CheckPrinted:
counts = re.findall(r"\d+", stdout.decode(errors="replace"))
Expand All @@ -1040,6 +1044,10 @@ def project[T](self, counted: Callable[[int], T], uncounted: Callable[[], T]) ->
class CountPrinted(CheckPrinted):
count: int

@property
def line(self) -> str:
return f"Check printed {self.count}"

def distance(self, expected: int) -> CheckPrinted:
return CountPrinted(abs(self.count - expected))

Expand All @@ -1060,6 +1068,10 @@ def unproven(self, command: str, expected: int) -> tuple[str, ...]:

@dataclass(frozen=True)
class NoIntegerPrinted(CheckPrinted):
@property
def line(self) -> str:
return "Check printed no integer"

def distance(self, expected: int) -> CheckPrinted:
return self

Expand Down
15 changes: 4 additions & 11 deletions src/agent_comms/commander/judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from dataclasses import asdict, dataclass

from ..comms import Comms
from ..todos import AcceptedVerdict, PeerAcceptedVerdict, BluntText, OrderVerdict, SendBackVerdict, StrikeReason, Todo, Slop, Derivation, SupersededVerdict
from ..todos import AcceptedVerdict, PeerAcceptedVerdict, OrderVerdict, SendBackVerdict, Todo, Slop, Derivation, SupersededVerdict
from ..tools import CommsUnjudgedTool, CommsVerdictTool
from ..field_codec import FieldCodec
from ..tool_caller import ThreadCaller
Expand Down Expand Up @@ -160,8 +160,6 @@ def check_edits(self, edits: list[Edit]) -> None: ...
def report(self, todos: tuple[Todo, ...]) -> None: ...

def run(self) -> None:
if len(self.why.split()) < 6:
sys.exit('Refused: state which intent the edit serves or violates, and why (6+ words).')
orders = {todo.id: todo for todo in order.held_orders() if not self.msg or todo.sent_in(self.msg)}
edits = {edit.id: edit for edit in Edit.unjudged(order.COMMS, ME) if edit.problem in orders}
ids = [token.split(':')[0] for token in self.ids]
Expand Down Expand Up @@ -230,7 +228,7 @@ def report(self, todos: tuple[Todo, ...]) -> None:


@dataclass(frozen=True, kw_only=True)
class BackVerb(VerdictVerb):
class BackVerb(order.CitedState, VerdictVerb):
help = "send the edit back; the roast is posted to the verdict's audience"
msg: str = option(help="the required order message")
roast: str = option(help="blunt, why it is stupid, with because")
Expand All @@ -248,16 +246,11 @@ def slop(self) -> Slop:
def operation(self, todo: Todo, edit_id: str) -> SendBackVerdict:
return SendBackVerdict(todo.revision, order.COMMS.registry.require(ME).incarnation,
todo.held(), edit_id,
reason=StrikeReason(f'{todo.id} edit sent back: {self.why}'))
reason=f'{todo.id} edit sent back: {self.why}',
evidence=self.evidence())

def check_edits(self, edits: list[Edit]) -> None:
if len(self.roast.split()) < 12 or not re.search(r'\bbecause\b', self.roast):
sys.exit('Refused: --roast needs 12+ words with because, naming the duplicated fact and its owner')
if len(self.fix.split()) < 8:
sys.exit('Refused: --fix needs 8+ words naming the owner and replacing shape')
self.slop()
order.refuse(self.roast, BluntText)
order.refuse(' '.join((self.why, self.roast, self.fix)), *order.OUTGOING)

def report(self, todos: tuple[Todo, ...]) -> None:
for todo in todos:
Expand Down
63 changes: 35 additions & 28 deletions src/agent_comms/commander/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from ..native_tool_call import SelectedToolDenied
from ..messages import Message
from ..plan_phases import OrderPhase, UnplannedPhase
from ..progress_claims import MergedClaim, WorkingClaim
from ..progress_claims import WorkingClaim
from ..recorded_time import RecordedEventTime
from ..standing_rules import HuntDue, RulesDue
from ..tool_caller import ThreadCaller
Expand All @@ -44,7 +44,7 @@
from ..todos import (ApplicationOrderRun, VerdictAudience, InstallDeadline, PlannedInstallDeadline, OrderMessage, MechanicalOrderRun, OrderRun, BluntText, CommitDelivery, NotifiedFirstCallEvent, ObservedFirstCallEvent,
OperationDelivery, CleanHunt, OrderedHunt, PendingProblemOrder, RecordFirstCallTodo,
OrderDeletion, StrikeReason, StrikeTodo, VoidStrikeTodo, LocatedProblemFinding, UnlocatedProblemFinding, FileDeliveryEvidence, NoDeliveryEvidence,
WithdrawTodo, AssignTodo, DirectiveText, OrderProgress, Todo, Slop)
WithdrawTodo, AssignTodo, DirectiveText, OrderProgress, Todo, Slop, Evidence)
from . import capacity
from .config import Commander, CommanderConfig, CommanderRole, Repo

Expand Down Expand Up @@ -178,7 +178,7 @@ def forwarders_added(diff, sources=()):


def send(unit, due, body: str, clean=False, mechanical=False, reassign=None, new=None,
traced=None, replaces=None, disk=False, finding=UnlocatedProblemFinding()):
traced=None, replaces=None, disk=False, finding=UnlocatedProblemFinding(), evidence=()):
"""Validate an order body and record it in Core: a new problem (new=(PID, TEXT)) or the listed open problems.

Sending to a unit that already holds the problem replaces its order there; without --clean that is a send-back."""
Expand Down Expand Up @@ -307,7 +307,7 @@ def send(unit, due, body: str, clean=False, mechanical=False, reassign=None, new
# A send-back is a strike, no warnings, and every send-back is roasted in #comms.
# Done here so the commander cannot send work back and forget either.
verdict = next(l for l in body.splitlines() if BluntText.found(l)).strip()
strike(unit, group[0], f'sent back on {group[0]}: {verdict}')
strike(unit, group[0], f'sent back on {group[0]}: {verdict}', evidence=evidence)


def importers(modules: set[str], check: OrderCheck) -> list[str]:
Expand Down Expand Up @@ -400,17 +400,14 @@ def board():
owner = unit or f'unassigned: released from {last[0].previous.owner}: {last[0].reason.text}'
lines.append(f'| {t.id}: {t.text} | {t.order.owner.to_wire()} | {owner} | {t.order.id} | '
f'{hhmm(t.deadline)} | {read[t.id].line if t.id in read else "-"} | '
f'{"no" if t.pending else "yes, install pending"} | '
f'{t.order.progress_line} | '
f'{activity[unit].clock() if unit else "-"} |')
history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation)
lines += ['', '## Withdrawn orders', '',
*(f'- {line}' for t in history for line in t.order.withdrawals(t))]
open(BOARD, 'w').write('\n'.join(lines) + '\n')


STAGES = ('tool call', 'review', 'install', 'live', 'user') # where a mistake was caught; later is worse


def on_record(line: str, recorded: str, audience: VerdictAudience) -> None:
"""Put one line on the public record: the narrative file, then its audience.

Expand All @@ -431,24 +428,20 @@ def on_record(line: str, recorded: str, audience: VerdictAudience) -> None:


def strike(unit: str, task: str, why: str, caught: str = f'{ME} at review', *,
kind: type[StrikeTodo] = StrikeTodo) -> None:
evidence: tuple[Evidence, ...] = (), kind: type[StrikeTodo] = StrikeTodo) -> None:
"""HARD RULE: every failure goes on the public record, in plain words, damning and exact, so no
failure stays private and none is described in words that hide what happened."""
# What matters is how a mistake happened, how often, and how and when it was caught.
if not any(stage in caught for stage in STAGES):
sys.exit(f'Refused: --caught must say who caught it and at which stage {STAGES}, e.g. "{ME} at review".')
todo = COMMS.goals.todos.get(task)
todo.state.require_open(todo)
if todo.held().owner != unit:
sys.exit(f'Refused: {unit} does not hold {task}; strike the unit that holds it.')
recorded(unit, todo, kind(todo.revision, COMMS.registry.require(ME).incarnation, todo.held(),
f'{why} (caught by {caught})'))
f'{why} (caught by {caught})', evidence=evidence))


def recorded(unit: str, todo: Todo, strike: StrikeTodo) -> None:
"""Core records the strike on the task's Todo; it is written to the narrative file and posted
to #comms in the same step."""
refuse(strike.reason, *OUTGOING) # posted to #comms, so refused before Core records the strike
if call(CommsTodoTool(id=todo.id, operation=strike)).todo.revision == todo.revision:
return
posted(unit, COMMS.goals.todos.get(todo.id), strike)
Expand Down Expand Up @@ -500,17 +493,15 @@ def done(pid, sha, screenshot='', seen='', repo=''):
claim = WorkingClaim(evidence.artifact())
else:
delivered_commit = revision(repo, sha)
if subprocess.run(['git', '-C', repo, 'merge-base', '--is-ancestor',
delivered_commit, 'HEAD']).returncode:
sys.exit(f'Refused: {delivered_commit} is not in {repo} HEAD.')
delivery = CommitDelivery(commit=revision(repo, 'HEAD'),
delivered_commit=delivered_commit, evidence=evidence)
claim = MergedClaim(delivery.delivered)
call(CommsOrderDoneTool(problem=pid, order_id=todo.order.id, delivery=delivery, claims=(claim,)))
claims = (claim,) if operation else ()
call(CommsOrderDoneTool(problem=pid, order_id=todo.order.id, delivery=delivery, claims=claims))
delivered_todo = COMMS.goals.todos.get(pid)
for late in delivered_todo.strikes[len(todo.strikes):]:
posted(unit, delivered_todo, late)
print(f'{unit}: order {todo.order.id} delivered as {delivery.delivered if not operation else sha}')
print(f'{unit}: order {todo.order.id} delivered as {delivery.delivered if not operation else sha}'
f' · {delivered_todo.order.delivery.check_line}')


def first_calls(held, activity):
Expand Down Expand Up @@ -763,8 +754,26 @@ class OrderVerb(CommanderVerb):
"""A verb of `order`."""


class CitedState:
"""Derive command options from the Evidence declarations, not another kind roster."""

def __init_subclass__(cls) -> None:
cls.__annotations__ = {**cls.__annotations__,
**{member.form.split()[0].removeprefix('--'): tuple[str, ...]
for member in Evidence.members_with(Evidence)}}
for member in Evidence.members_with(Evidence):
flag, metavar = member.form.split()
setattr(cls, flag.removeprefix('--'), option(
flag, action='append', default=(), parser_default_factory=list, metavar=metavar))
super().__init_subclass__()

def evidence(self) -> tuple[Evidence, ...]:
return tuple(member.cited(value) for member in Evidence.members_with(Evidence)
for value in getattr(self, member.form.split()[0].removeprefix('--')))


@dataclass(frozen=True, kw_only=True)
class SendVerb(OrderVerb, RuledVerb):
class SendVerb(CitedState, OrderVerb, RuledVerb):
help = "record the order in Core and send it"
unit: str = option("unit")
due: str = option("due", help="HH:MM local time")
Expand All @@ -780,21 +789,19 @@ class SendVerb(OrderVerb, RuledVerb):
def run(self) -> None:
send(self.unit, self.due, Path(self.body_file).read_text(), clean=self.clean,
mechanical=self.mechanical, reassign=self.reassign, traced=self.traced,
replaces=self.replaces, disk=self.disk)
replaces=self.replaces, disk=self.disk, evidence=self.evidence())


@dataclass(frozen=True, kw_only=True)
class StrikeVerb(OrderVerb, RuledVerb):
class StrikeVerb(CitedState, OrderVerb, RuledVerb):
help = "a send-back, a missed deadline or a false claim"
unit: str = option("unit")
why: str = option("--why")
caught: str = option("--caught", default=f'{ME} at review', help='"WHO at STAGE"; stage is review/install/live/user')
pid: str = option("pid", help="the problem the strike is for")
caught: str = option("--caught", default=f'{ME} at review')

def run(self) -> None:
references = set(re.findall(r'\bP\d+\b', self.why))
if len(references) != 1:
sys.exit('Refused: name exactly one problem in --why; strikes count per problem.')
strike(self.unit, references.pop(), self.why, self.caught)
strike(self.unit, self.pid, self.why, self.caught, evidence=self.evidence())


@dataclass(frozen=True, kw_only=True)
Expand Down
4 changes: 2 additions & 2 deletions src/agent_comms/commander/standing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
UndatedStandingRule)
from ..goals import Goal
from ..threads import Thread
from ..todos import FlatTrend, OrderProgress, PullRequests, StrikeTodo, Todo
from ..todos import FlatTrend, OrderProgress, PullRequests, StrikeTodo, Todo, RuntimeEvidence
from ..turn_digests import TurnDigestRead, TurnsSince
from . import judge
from .config import CommanderConfig, Repo
Expand Down Expand Up @@ -124,7 +124,7 @@ def enforce(self, comms: Comms, commander: str) -> None:
comms.goals.change_problem(todo.id, StrikeTodo(
todo.revision, comms.registry.require(commander).incarnation, held,
f"{held.owner} kept working on {todo.id} while the distance to done did not fall"
" across two readings"), commander)
" across two readings", evidence=(RuntimeEvidence(str(comms.root)),)), commander)
for unit in self.idle(comms, commander)[:1]:
self.assign(comms, commander, unit)

Expand Down
11 changes: 7 additions & 4 deletions src/agent_comms/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from aiohttp import web
from pydantic import JsonValue

from .channel_targets import CommandTarget
from .cli_commands import DeclaredOptions, option
from .cli_protocol import CoreError
from .comms import Comms, client_wire
Expand All @@ -49,7 +50,7 @@
RegisteredClient, digest,
)
from .messages import Message
from .todos import DirectiveText
from .todos import AuthorizedAct, DirectiveText
from .tool_caller import ThreadCaller
from .tools import CommsChannelsTool, ReadTool, ToolRequest

Expand All @@ -62,9 +63,11 @@ class DirectorCaller(ThreadCaller):
"""claude-ai speaks for the owner: each message it sends a thread is a directive, recorded as a
request when that thread holds owner requests."""

def require_directive(self, comms: Comms, body: str) -> None:
"""A directive passes DirectiveText. claude-ai directs the commanders, so the
commander rules review does not gate it."""
def require_directive(self, comms: Comms, target: str, body: str) -> None:
"""Refuse routine permission questions to humans; the owner needs no commander review."""
for name in CommandTarget.named(target).direct_names:
for human_body in comms.registry.require(name).role.human(body):
AuthorizedAct.require(human_body)
DirectiveText.require(body)

def sent(self, comms: Comms, message: Message) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/agent_comms/goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,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)

Expand Down
Loading
Loading