Skip to content
Merged
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
32 changes: 6 additions & 26 deletions src/agent_comms/commander/capacity.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
"""Commander load and forks (HARD RULE: as soon as the commander is overwhelmed it forks; a structural reflex).
"""Report commander load and live forks; no command refuses on capacity.

Load is measured from the files the fleet already writes, never from how the commander feels:
each open order counts 1, each overdue order 2 more, and every 10 unjudged edits 1. One commander
carries PER_COMMANDER load. Forks register by passing `--fork NAME` to order, which touches
forks/NAME.beat; a beat younger than BEAT_AGE is a live fork. While load needs more forks than are
live, order refuses the main commander's actions and rotate prints FORK NOW, so the only
way forward is forking a slice.
Each open order counts 1, each overdue order 2 more, and every 10 unjudged edits 1.
Forks register with `--fork NAME`; a heartbeat younger than BEAT_AGE is live.
"""
from __future__ import annotations

import math, os, sys, time
import os, time
from dataclasses import dataclass

from ..comms import Comms
from . import order
from .config import CommanderConfig

FORKS = f'{CommanderConfig.installed().state}/forks'
PER_COMMANDER = 6
BEAT_AGE = 10 * 60


Expand Down Expand Up @@ -52,25 +47,10 @@ def of(cls, comms: Comms, commander: str) -> Load:
def total(self) -> float:
return self.owed + 2 * self.overdue + self.unjudged / 10

@property
def needed(self) -> int:
return math.ceil(self.total / PER_COMMANDER) - 1

@property
def short(self) -> bool:
"""Live forks do not cover the load: the commander must fork now."""
return len(self.forks) < self.needed

def __str__(self) -> str:
return (f'load {self.total:.0f} ({self.owed} open orders, {self.overdue} overdue, '
f'{self.unjudged} unjudged edits) needs {self.needed} live fork(s), '
f'{len(self.forks)} live {list(self.forks)}; fork a slice now (each fork passes --fork NAME to order)')

def require(self) -> None:
if self.short:
sys.exit(f'REFUSED, nothing recorded: overloaded: {self}.')
f'{self.unjudged} unjudged edits), {len(self.forks)} live forks {list(self.forks)}')


def main(a):
load = Load.of(order.COMMS, order.ME)
print(load if load.short else f'covered: load {load.total:.0f}, forks {list(load.forks)}')
print(Load.of(order.COMMS, order.ME))
17 changes: 4 additions & 13 deletions src/agent_comms/commander/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -759,21 +759,12 @@ def fire(unit: str, why: str) -> None:
print(f'{todo.id}: released from {unit}: {why}')


class LoadedVerb(RuledVerb):
"""HARD RULE: overloaded means fork, not more orders from me; a fork is never blocked."""

def gate(self) -> None:
super().gate()
if not self.fork:
capacity.Load.of(COMMS, ME).require()


class OrderVerb(CommanderVerb):
"""A verb of `order`."""


@dataclass(frozen=True, kw_only=True)
class SendVerb(OrderVerb, LoadedVerb):
class SendVerb(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 Down Expand Up @@ -916,7 +907,7 @@ def run(self) -> None:


@dataclass(frozen=True, kw_only=True)
class SayVerb(OrderVerb, LoadedVerb):
class SayVerb(OrderVerb, RuledVerb):
help = "a non-order message; refused if it orders work"
units: str = option("units", metavar="UNIT[,UNIT]")
body_file: str = option("body_file", metavar="BODYFILE")
Expand All @@ -926,7 +917,7 @@ def run(self) -> None:


@dataclass(frozen=True, kw_only=True)
class ProblemVerb(OrderVerb, LoadedVerb):
class ProblemVerb(OrderVerb, RuledVerb):
help = "Core records the problem with its order, or nothing"
text: str = option("text")
to: str = option("--to")
Expand Down Expand Up @@ -987,7 +978,7 @@ def run(self) -> None:


@dataclass(frozen=True, kw_only=True)
class HuntVerb(OrderVerb, LoadedVerb):
class HuntVerb(OrderVerb, RuledVerb):
help = "a hot-path read: clean unless --sent names the order it became"
where: str = option("where", metavar="FILE:LINE")
why: str = option("--why")
Expand Down
17 changes: 1 addition & 16 deletions src/agent_comms/commander/standing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..threads import Thread
from ..todos import FlatTrend, OrderProgress, PullRequests, StrikeTodo, Todo
from ..turn_digests import TurnDigestRead, TurnsSince
from . import capacity, judge
from . import judge
from .config import CommanderConfig, Repo

CONFIG = CommanderConfig.installed()
Expand Down Expand Up @@ -150,21 +150,6 @@ def enforce(self, comms: Comms, commander: str) -> None:
self.assign(comms, commander, comms.registry.require(self.unit))


@dataclass(frozen=True)
class ForkNow(UndatedStandingRule):
"""The load on the asked Comms needs more live forks than there are."""

load: capacity.Load

@classmethod
def violations(cls, comms: Comms, commander: str) -> tuple[ForkNow, ...]:
load = capacity.Load.of(comms, commander)
return (cls(load),) * load.short

def line(self) -> str:
return f"FORK NOW: {self.load}"


@dataclass(frozen=True)
class Unjudged(ReasonedStandingRule, UndatedStandingRule):
limit: ClassVar[int] = 30
Expand Down
14 changes: 14 additions & 0 deletions tests/test_commander_capacity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Capacity reports the existing load formula and live forks without policy advice."""

import pytest


@pytest.mark.parametrize("unjudged", [0, 1, 395, 1_000_000])
def test_capacity_reports_load_and_forks(commander_script, unjudged):
script, _ = commander_script
load = script.capacity.Load(24, 14, unjudged, ("judge",))
assert load.total == 24 + 2 * 14 + unjudged / 10
assert str(load) == (
f"load {load.total:.0f} (24 open orders, 14 overdue, {unjudged} unjudged edits), "
"1 live forks ['judge']"
)
25 changes: 2 additions & 23 deletions tests/test_standing_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,34 +215,13 @@ def progress():
assert comms.goals.todos.get("P2").holder == "worker"


def test_fork_now_and_unjudged_follow_the_unjudged_edits(commander, standing):
def test_unjudged_follows_the_unjudged_edits(commander, standing):
comms, state, module = standing
assert lines(comms, module.ForkNow) == lines(comms, module.Unjudged) == []
assert lines(comms, module.Unjudged) == []
create(commander, 'P1')
native_judge_edits(commander, module.judge, [dict(id=f'e{n}', at=200., path='a.py',
hunks=[dict(old='before', new='after')]) for n in range(70)])
assert lines(comms, module.Unjudged) == ["UNJUDGED 70 edits -> judge or fork a judge slice"]
[fork] = lines(comms, module.ForkNow)
assert fork.startswith("FORK NOW: load 10 (1 open orders, 1 overdue, 70 unjudged edits) needs 1")


def test_fork_now_reads_the_load_of_the_bus_it_is_asked_about(commander, standing, tmp_path):
"""Three overdue orders on one bus need a fork there; a second bus without orders carries no load."""
from agent_comms.tool_caller import ThreadCaller

comms, _, _ = standing
for problem in ("P1", "P2", "P3"):
create(commander, problem)
other = Comms(tmp_path / "other-bus")
other.registry.declare(other.registry.new_thread("lead", frozenset(), str(tmp_path), created_at=1.0))

def fork_now(bus):
read = CommsStandingRulesTool().apply(bus, ThreadCaller("lead"))
return [line for line in read.broken if line.startswith("FORK NOW")]

assert [line.split(") ")[0] for line in fork_now(comms)] == [
"FORK NOW: load 9 (3 open orders, 3 overdue, 0 unjudged edits"]
assert fork_now(other) == []


def test_rules_due_until_every_drawn_rule_is_answered_and_fresh(commander, monkeypatch):
Expand Down
Loading