feat(orchestrator): hierarchy view from council offices with a separate agent order
The Agents views gain a Hierarchie view: an org chart of the chair and the line offices read from the council claims. The operator appoints, hands over and recalls an office there; the page signs each change with the passkey and a host unit (client/offices.py, offices path unit) verifies the signature, removes a live holder itself and runs a plain council.py claim for the successor. council.py loses the --operator flag, so no agent can override a live holder. The agent order is now separate from task priority: rasci.py agents order stores it in agent-order.json beside the register through the nofollow helpers and never writes a --prio pin. It only breaks ties of equal task rank, also among the queue grants. The Hierarchie view edits it as a drag list. Host units run the installed skill scripts, so orchestrator.sh keeps the offices unit off while the installed council.py, nofollow.py or rasci.py differ from the tree. Job files, council files and hub records are read with O_NOFOLLOW and O_NONBLOCK and size caps; nofollow.rewrite replaces a claim without creating directories, so a beat never revives a released office. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: 79 % 🛡️ Security: 100 % 🏆 Score: 100 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e180be256
commit
4540494c30
53 files changed
+1821
-211
No files matched your search
@@ -7,6 +7,7 @@ from .asking import ask_state
|
||||
from .cleanup import cleanup_request
|
||||
from .errors import Conflict, Missing, ToolError
|
||||
from .fields import decode, expect, field
|
||||
from .offices import office_request
|
||||
from .records import digest, shown
|
||||
from .register import REGISTER, operator_rows, read_register, runnable
|
||||
from .rules import rule_matches
|
||||
@@ -144,7 +145,12 @@ class Approving:
|
||||
payload = decode(body)
|
||||
expect(payload, ("items",))
|
||||
raw = payload["items"]
|
||||
signed = self.deletable(raw) if cleanup_request(raw) else [item["signed"] for item in self.checked(raw) if item["signed"] is not None]
|
||||
if cleanup_request(raw):
|
||||
signed = self.deletable(raw)
|
||||
elif office_request(raw):
|
||||
signed = self.appointable(raw)
|
||||
else:
|
||||
signed = [item["signed"] for item in self.checked(raw) if item["signed"] is not None]
|
||||
hashes = [signing.item_hash(item) for item in signed]
|
||||
credentials = [key["id"] for key in self.keys()]
|
||||
if not hashes:
|
||||
|
||||
@@ -18,6 +18,10 @@ MODES = ("evening", "sunset")
|
||||
RADIO_LIMIT = 5000
|
||||
GRANT_LIMIT = 50
|
||||
MAYDAY_ACTIVE = 7200
|
||||
CHAIR_OFFICE = "chair"
|
||||
GROUPS = ("work", "cluster")
|
||||
CLAIM_FILES = ("*/claim.json", "*/*/claim.json")
|
||||
OFFICE = re.compile(r"[a-z0-9][a-z0-9._-]*(?:/[a-z0-9][a-z0-9._-]*)?")
|
||||
|
||||
|
||||
def agent(name: object) -> str:
|
||||
@@ -74,10 +78,33 @@ def radio_rows(rows: list[dict], begin: float, end: float) -> list[dict]:
|
||||
return sorted(found, key=lambda row: row["ts"])[-RADIO_LIMIT:]
|
||||
|
||||
|
||||
def claim_row(data: dict) -> dict:
|
||||
def office_kind(office: str) -> tuple[str, str, str]:
|
||||
"""Args:
|
||||
office: an office of one or two segments.
|
||||
|
||||
Returns:
|
||||
(kind, dimension, line): kind is chair, line for <dimension>/<line>,
|
||||
work or cluster for their groups, other for any other single
|
||||
segment; dimension and line are empty unless the kind is line.
|
||||
"""
|
||||
head, _, rest = office.partition("/")
|
||||
if office == CHAIR_OFFICE:
|
||||
return "chair", "", ""
|
||||
if not rest:
|
||||
return "other", "", ""
|
||||
if head in GROUPS:
|
||||
return head, "", ""
|
||||
return "line", head, rest
|
||||
|
||||
|
||||
def claim_row(office: str, data: dict) -> dict:
|
||||
name = data.get("name")
|
||||
kind, dimension, line = office_kind(office)
|
||||
return {
|
||||
"office": shown(data.get("office")),
|
||||
"office": office,
|
||||
"kind": kind,
|
||||
"dimension": dimension,
|
||||
"line": line,
|
||||
"agent": agent(name),
|
||||
"address": name.strip() if isinstance(name, str) else "",
|
||||
"session": shown(data.get("session")),
|
||||
@@ -89,8 +116,14 @@ def claim_row(data: dict) -> dict:
|
||||
|
||||
|
||||
def read_claims(council: Path) -> list[dict]:
|
||||
paths = sorted([*council.glob("cluster/*/claim.json"), *council.glob("work/*/claim.json")])
|
||||
return [claim_row(data) for data in map(read_json, paths) if isinstance(data, dict)]
|
||||
found = []
|
||||
for pattern in CLAIM_FILES:
|
||||
for path in council.glob(pattern):
|
||||
office = path.parent.relative_to(council).as_posix()
|
||||
data = read_json(path)
|
||||
if OFFICE.fullmatch(office) and isinstance(data, dict):
|
||||
found.append(claim_row(office, data))
|
||||
return sorted(found, key=lambda row: row["office"])
|
||||
|
||||
|
||||
def sessions(claims: list[dict]) -> dict[str, str]:
|
||||
|
||||
@@ -2,13 +2,23 @@ from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from .council import HUMAN, agent, ordered, subagent
|
||||
from .council import CHAIR_OFFICE, HUMAN, agent, ordered, subagent
|
||||
from .names import Directory
|
||||
from .orbits import affinity
|
||||
from .records import shown
|
||||
from .register import ENDED, parties, status_of
|
||||
|
||||
CHAIR = "Maintainer"
|
||||
|
||||
def chair_of(claims: list[dict], directory: Directory, end: float) -> str:
|
||||
"""Returns:
|
||||
The node key of the chair office's holder, empty while it is vacant.
|
||||
"""
|
||||
claim = next((claim for claim in claims if claim["office"] == CHAIR_OFFICE), None)
|
||||
if claim is None:
|
||||
return ""
|
||||
if claim["session"] != "-":
|
||||
return claim["session"]
|
||||
return directory.key(claim["agent"], end)
|
||||
|
||||
|
||||
def parent_of(roles: dict[str, str], known: set[str], fallback: str) -> str:
|
||||
@@ -36,13 +46,14 @@ def edges(radio: list[dict], grant_rows: list[dict], chair: str) -> list[dict]:
|
||||
|
||||
def graph(radio: list[dict], grant_rows: list[dict], tasks: dict[str, dict], claims: list[dict], end: float, directory: Directory) -> dict:
|
||||
current = {task: row for task, row in sorted(tasks.items()) if status_of(row) not in ENDED}
|
||||
chair = directory.key(CHAIR, end)
|
||||
chair = chair_of(claims, directory, end)
|
||||
top = chair or HUMAN
|
||||
workers = {task: agent(row.get("worker")) for task, row in current.items()}
|
||||
keys = {row["source"] for row in radio} | {key for row in radio for key in row["targets"]}
|
||||
keys |= {claim["session"] for claim in claims if claim["agent"] and claim["session"] != "-"}
|
||||
keys |= {directory.key(name, end) for name in workers.values() if not subagent(name)}
|
||||
found = sorted(keys - {"", HUMAN, chair}, key=lambda key: (directory.label(key, end).lower(), key))
|
||||
known = {chair, *found}
|
||||
known = {chair, *found} - {""}
|
||||
moons: dict[str, tuple[str, str]] = {}
|
||||
holders: dict[str, dict[str, dict]] = {}
|
||||
for task, row in current.items():
|
||||
@@ -53,14 +64,15 @@ def graph(radio: list[dict], grant_rows: list[dict], tasks: dict[str, dict], cla
|
||||
worker = workers[task]
|
||||
holder = directory.key(worker, end)
|
||||
if subagent(worker):
|
||||
parent = parent_of(roles, known, chair)
|
||||
parent = parent_of(roles, known, top)
|
||||
holder = f"{worker} @ {parent}"
|
||||
moons[holder] = (parent, worker)
|
||||
if holder:
|
||||
duties(holder, task, row, holders)["worker"] = True
|
||||
for key, letters in roles.items():
|
||||
duties(key, task, row, holders)["letters"] += letters
|
||||
plan = [(HUMAN, "operator", None, HUMAN), (chair, "chair", None, directory.label(chair, end))]
|
||||
plan = [(HUMAN, "operator", None, HUMAN)]
|
||||
plan += [(chair, "chair", None, directory.label(chair, end))] if chair else []
|
||||
plan += [(key, "session", None, directory.label(key, end)) for key in found]
|
||||
plan += [(moon, "subagent", *moons[moon]) for moon in ordered(set(moons))]
|
||||
nodes = [
|
||||
@@ -77,4 +89,4 @@ def graph(radio: list[dict], grant_rows: list[dict], tasks: dict[str, dict], cla
|
||||
for key, node_kind, parent, label in plan
|
||||
]
|
||||
layout = affinity(nodes, radio, tasks, claims, end, lambda name: directory.key(name, end))
|
||||
return {"nodes": nodes, "edges": edges(radio, grant_rows, chair), "layout": layout}
|
||||
return {"nodes": nodes, "edges": edges(radio, grant_rows, top), "layout": layout}
|
||||
@@ -36,10 +36,12 @@ SIGN_ROUTE = "/api/sign"
|
||||
RULE_SIGN_ROUTE = "/api/rules/sign"
|
||||
CLEANUP_ROUTE = "/api/cleanup"
|
||||
CLEANUP_STATE = re.compile(r"/api/cleanup/([0-9a-f]{16})")
|
||||
OFFICES_ROUTE = "/api/offices"
|
||||
OFFICE_STATE = re.compile(r"/api/offices/([0-9a-f]{16})")
|
||||
VENDOR_ROUTE = re.compile(r"/vendor/([a-z0-9.-]{1,64})")
|
||||
VENDOR = {"3d-force-graph.min.js": "3d-force-graph/3d-force-graph.min.js", "three.module.js": "three/three.module.js", "three.core.js": "three/three.core.js"}
|
||||
SCRIPT_TYPE = "text/javascript; charset=utf-8"
|
||||
ACTION_POSTS = ("/api/rules", "/api/nl", SETTINGS_ROUTE, TTS_ROUTE, STT_ROUTE, SIGN_ROUTE, RULE_SIGN_ROUTE, CLEANUP_ROUTE)
|
||||
ACTION_POSTS = ("/api/rules", "/api/nl", SETTINGS_ROUTE, TTS_ROUTE, STT_ROUTE, SIGN_ROUTE, RULE_SIGN_ROUTE, CLEANUP_ROUTE, OFFICES_ROUTE)
|
||||
|
||||
|
||||
class Handler(Exchange):
|
||||
@@ -59,6 +61,7 @@ class Handler(Exchange):
|
||||
proof = PROOF_ROUTE.fullmatch(path)
|
||||
vendored = VENDOR_ROUTE.fullmatch(path)
|
||||
cleanup = CLEANUP_STATE.fullmatch(path)
|
||||
office = OFFICE_STATE.fullmatch(path)
|
||||
if self.host() is None:
|
||||
self.send_text(HTTPStatus.FORBIDDEN, "forbidden host")
|
||||
elif path == "/":
|
||||
@@ -67,6 +70,8 @@ class Handler(Exchange):
|
||||
self.vendored(VENDOR[vendored.group(1)])
|
||||
elif cleanup:
|
||||
self.answer(lambda: hub.cleanup_state(cleanup.group(1)), HTTPStatus.OK)
|
||||
elif office:
|
||||
self.answer(lambda: hub.office_state(office.group(1)), HTTPStatus.OK)
|
||||
elif proof:
|
||||
self.answer(lambda: hub.review_proof(proof.group(1)), HTTPStatus.OK)
|
||||
elif path == "/api/state":
|
||||
@@ -125,6 +130,8 @@ class Handler(Exchange):
|
||||
return lambda body: self.answer(lambda: hub.sign_rule(body), HTTPStatus.OK)
|
||||
if path == CLEANUP_ROUTE:
|
||||
return lambda body: self.answer(lambda: hub.cleanup(body), HTTPStatus.ACCEPTED)
|
||||
if path == OFFICES_ROUTE:
|
||||
return lambda body: self.answer(lambda: hub.appoint(body), HTTPStatus.ACCEPTED)
|
||||
if inbox:
|
||||
return lambda body: self.answer(lambda: hub.mark(body), HTTPStatus.OK)
|
||||
if undo:
|
||||
|
||||
@@ -22,6 +22,7 @@ from .ledger import Ledger, Marks, Reviews
|
||||
from .mcp import Mcp
|
||||
from .messaging import Messaging
|
||||
from .names import Names
|
||||
from .offices import Offices
|
||||
from .reviewing import Reviewing
|
||||
from .rules import Rules
|
||||
from .settings import Settings
|
||||
@@ -40,6 +41,7 @@ class Places:
|
||||
timers: Path
|
||||
vendor: Path
|
||||
cleanup: Path
|
||||
offices: Path
|
||||
|
||||
|
||||
def environment(templates: Path) -> jinja2.Environment:
|
||||
@@ -55,7 +57,7 @@ def environment(templates: Path) -> jinja2.Environment:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Hub(Mcp, Reviewing, Asking, Messaging, Approving, Cleaning, Inbox, Rules, Jobs, Sources, Timeline, View, Language):
|
||||
class Hub(Mcp, Reviewing, Asking, Messaging, Approving, Cleaning, Offices, Inbox, Rules, Jobs, Sources, Timeline, View, Language):
|
||||
council: Path
|
||||
pages: Path
|
||||
public_port: int
|
||||
@@ -84,6 +86,7 @@ class Hub(Mcp, Reviewing, Asking, Messaging, Approving, Cleaning, Inbox, Rules,
|
||||
vendor: Path
|
||||
icons: Markup
|
||||
spool: Spool
|
||||
office_spool: Spool
|
||||
touched: Touched
|
||||
|
||||
def hosts(self) -> tuple[str, str]:
|
||||
@@ -101,6 +104,7 @@ def open_hub(council: Path, pages: Path, public_port: int, state: Path, host_sta
|
||||
vendor=places.vendor,
|
||||
icons=sheet(places.vendor, templates),
|
||||
spool=Spool(places.cleanup).ready(),
|
||||
office_spool=Spool(places.offices).ready(),
|
||||
touched=Touched(reviews.stored),
|
||||
approvers=approvers,
|
||||
speech=Speech(engines, state / "tts"),
|
||||
|
||||
@@ -11,7 +11,7 @@ from .errors import ToolError
|
||||
from .events import VIEWS
|
||||
from .fields import bounded, decode, expect, message_text
|
||||
from .records import read_jsonl
|
||||
from .register import REGISTER, priority, read_register
|
||||
from .register import REGISTER, agent_order, read_register
|
||||
|
||||
NL_LIMIT = 2000
|
||||
NL_ACTION_LIMIT = 20
|
||||
@@ -183,6 +183,6 @@ class Language:
|
||||
directory = self.directory(claims, read_jsonl(self.council / NEWS), now)
|
||||
self.watch.see(tasks, now)
|
||||
items = self.inbox(now, tasks, directory)
|
||||
agents = ordered({claim["agent"] for claim in claims if claim["agent"]} | set(priority(tasks)))
|
||||
agents = ordered({claim["agent"] for claim in claims if claim["agent"]} | set(agent_order(tasks, [], [])))
|
||||
shown_items = [{"id": item["id"], "kind": item["kind"], "title": item["title"][:200]} for item in items]
|
||||
return nl_actions(self.model.complete(nl_messages(text, shown_items, agents)), items, agents)
|
||||
@@ -8,6 +8,7 @@ from .council import HUMAN, ordered, read_claims
|
||||
from .errors import Missing, ToolError
|
||||
from .fields import NAME_LIMIT, ORDER_NAME, decode, expect, field, message_text, single
|
||||
from .records import write_atomic
|
||||
from .register import REGISTER, agent_order, read_register
|
||||
from .skill_scripts import NIGHT, RADIO, RASCI
|
||||
|
||||
NIGHTS = {
|
||||
@@ -111,7 +112,11 @@ class Messaging:
|
||||
raise ToolError("agents must be a non-empty list of agent names without commas")
|
||||
if len(set(names)) != len(names):
|
||||
raise ToolError("agents must be distinct")
|
||||
runs = [self.scripts.run(RASCI, ["order", ",".join(names)])]
|
||||
known = agent_order(read_register(self.council / REGISTER), read_claims(self.council), [])
|
||||
unknown = [name for name in names if name not in known]
|
||||
if unknown:
|
||||
raise ToolError(f"unknown agents: {', '.join(unknown)[:300]}; the register and the council know {', '.join(known)[:600] or 'none'}")
|
||||
runs = [self.scripts.run(RASCI, ["agents", "order", ",".join(names)])]
|
||||
elif action in NIGHTS:
|
||||
command, text = NIGHTS[action]
|
||||
runs = [self.scripts.run(NIGHT, [*command, "--session", HUMAN])]
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from .council import HUMAN, office_kind, read_claims
|
||||
from .errors import Conflict, Missing, ToolError
|
||||
from .fields import decode, expect
|
||||
from .records import read_json, stamp
|
||||
from .signed import signing
|
||||
|
||||
ITEM_LIMIT = 20
|
||||
JOB_VERSION = 1
|
||||
LABELS = {"appoint": "besetzt", "takeover": "übergeben", "recall": "abberufen"}
|
||||
|
||||
|
||||
def office_request(raw: object) -> bool:
|
||||
return isinstance(raw, list) and bool(raw) and all(isinstance(entry, dict) and entry.get("kind") == "office" for entry in raw)
|
||||
|
||||
|
||||
class Offices:
|
||||
def office_sessions(self, claims: list[dict]) -> set[str]:
|
||||
named = {entry["session"] for entry in self.names.listed()} | {claim["session"] for claim in claims}
|
||||
return {session for session in named if signing.SESSION.fullmatch(session) and session != HUMAN}
|
||||
|
||||
def appointable(self, raw: object) -> list[dict]:
|
||||
"""Args:
|
||||
raw: the orders the page chose, each {"kind": "office", "action",
|
||||
"office", "holder", "session", "work"}.
|
||||
|
||||
Returns:
|
||||
The signed items: each order well formed, its holder the office's
|
||||
current one, its successor a known session that represents no
|
||||
other line of the same dimension.
|
||||
"""
|
||||
if not isinstance(raw, list) or not 1 <= len(raw) <= ITEM_LIMIT:
|
||||
raise ToolError(f"items must be a list of 1 to {ITEM_LIMIT} objects")
|
||||
claims = read_claims(self.council)
|
||||
holders = {claim["office"]: claim["session"] for claim in claims}
|
||||
sessions = self.office_sessions(claims)
|
||||
chosen = []
|
||||
for entry in raw:
|
||||
if not isinstance(entry, dict):
|
||||
raise ToolError("every item must be an object with kind, action, office, holder, session and work")
|
||||
expect(entry, ("kind", *signing.OFFICE_KEYS))
|
||||
if entry["kind"] != "office":
|
||||
raise ToolError("kind must be office")
|
||||
order = {name: entry[name] for name in signing.OFFICE_KEYS}
|
||||
problem = signing.office_problem(order)
|
||||
if problem:
|
||||
raise ToolError(problem)
|
||||
if order["holder"] != holders.get(order["office"], ""):
|
||||
raise Conflict({"office": order["office"], "holder": holders.get(order["office"], "")})
|
||||
if order["action"] != "recall":
|
||||
self.successor(order, sessions, claims)
|
||||
item = signing.office_item(**order)
|
||||
if item in chosen:
|
||||
raise ToolError("items must be distinct")
|
||||
chosen.append(item)
|
||||
return chosen
|
||||
|
||||
def successor(self, order: dict, sessions: set[str], claims: list[dict]) -> None:
|
||||
if order["session"] not in sessions:
|
||||
raise ToolError("session must be a session the Agent Orchestrator knows")
|
||||
kind, dimension, _ = office_kind(order["office"])
|
||||
others = [claim["office"] for claim in claims if claim["session"] == order["session"] and claim["dimension"] == dimension and claim["office"] != order["office"]]
|
||||
if kind == "line" and others:
|
||||
raise ToolError(f"{order['session']} represents {others[0]} already: one line per dimension")
|
||||
|
||||
def appoint(self, body: bytes) -> dict:
|
||||
payload = decode(body)
|
||||
expect(payload, ("items", "proof"))
|
||||
items = self.appointable(payload["items"])
|
||||
proof = self.covered(payload["proof"], items, time.time())
|
||||
job_id = secrets.token_hex(8)
|
||||
job = {"v": JOB_VERSION, "id": job_id, "created": stamp(), "items": [{name: item[name] for name in signing.OFFICE_KEYS} for item in items], "proof": proof}
|
||||
self.office_spool.put(job_id, job)
|
||||
names = ", ".join(f"{item['office']} {LABELS[item['action']]}" for item in items)
|
||||
lines = "\n".join(f"{item['action']} {item['office']}: {item['holder'] or '-'} -> {item['session'] or '-'}" for item in items)
|
||||
self.log.add({"kind": "run", "title": f"Amt signiert: {names}"[:200], "text": lines, "ref": {"offices": job_id, "status": "queued"}})
|
||||
return {"id": job_id, "status": "queued", "items": job["items"]}
|
||||
|
||||
def office_state(self, job_id: str) -> dict:
|
||||
result = read_json(self.office_spool.results / f"{job_id}.json")
|
||||
if isinstance(result, dict) and isinstance(result.get("items"), list):
|
||||
return {**result, "id": job_id, "status": "done"}
|
||||
if (self.office_spool.jobs / f"{job_id}.json").is_file():
|
||||
return {"id": job_id, "status": "queued", "items": []}
|
||||
raise Missing(job_id)
|
||||
@@ -1,12 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
READ_FLAGS = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC
|
||||
JSON_LIMIT = 1 << 24
|
||||
|
||||
|
||||
def stamp() -> str:
|
||||
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
|
||||
@@ -20,9 +25,27 @@ def write_atomic(path: Path, data: bytes) -> None:
|
||||
os.replace(handle.name, path)
|
||||
|
||||
|
||||
def read_regular(path: Path, limit: int | None) -> str:
|
||||
"""Args:
|
||||
path: the file; a link, FIFO or other non-regular file raises OSError
|
||||
without blocking.
|
||||
limit: the most bytes to accept, more raise OSError; None for any size.
|
||||
|
||||
Returns:
|
||||
The file's text, UTF-8 decoded.
|
||||
"""
|
||||
with open(os.open(path, READ_FLAGS), "rb") as handle:
|
||||
if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
|
||||
raise OSError(errno.EINVAL, "not a regular file", str(path))
|
||||
data = handle.read() if limit is None else handle.read(limit + 1)
|
||||
if limit is not None and len(data) > limit:
|
||||
raise OSError(errno.EFBIG, f"larger than {limit} bytes", str(path))
|
||||
return data.decode("utf-8")
|
||||
|
||||
|
||||
def read_json(path: Path) -> object:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
return json.loads(read_regular(path, JSON_LIMIT))
|
||||
except (OSError, ValueError, RecursionError):
|
||||
return None
|
||||
|
||||
@@ -37,7 +60,7 @@ def parse_line(line: str) -> dict | None:
|
||||
|
||||
def read_jsonl(path: Path) -> list[dict]:
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
lines = read_regular(path, None).splitlines()
|
||||
except (OSError, ValueError):
|
||||
return []
|
||||
return [row for row in map(parse_line, lines) if row is not None]
|
||||
|
||||
@@ -2,11 +2,12 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .council import agent
|
||||
from .council import agent, ordered
|
||||
from .fields import ORDER_NAME
|
||||
from .records import read_json, shown
|
||||
|
||||
REGISTER = "rasci.json"
|
||||
AGENT_ORDER = "agent-order.json"
|
||||
OPERATOR = "operator/"
|
||||
ENDED = ("done", "stopped")
|
||||
|
||||
@@ -98,10 +99,31 @@ def register_rows(tasks: dict[str, dict]) -> list[dict]:
|
||||
return [register_row(task, row) for task, row in sorted(tasks.items(), key=pinned) if is_open(task, row, False)]
|
||||
|
||||
|
||||
def priority(tasks: dict[str, dict]) -> list[str]:
|
||||
def stored_order(path: Path) -> list[str]:
|
||||
data = read_json(path)
|
||||
names = data.get("agents") if isinstance(data, dict) else None
|
||||
if not isinstance(names, list) or not all(isinstance(name, str) for name in names):
|
||||
return []
|
||||
return names
|
||||
|
||||
|
||||
def agent_order(tasks: dict[str, dict], claims: list[dict], stored: list[str]) -> list[str]:
|
||||
"""Args:
|
||||
tasks: the register.
|
||||
claims: every office claim; its holder is an agent too.
|
||||
stored: the operator's agent order, as `rasci.py agents order` wrote it.
|
||||
|
||||
Returns:
|
||||
The agents `rasci.py agents order` accepts: the stored ones it still
|
||||
knows first, in their order, then the workers of open tasks by pin,
|
||||
then the remaining office holders by name.
|
||||
"""
|
||||
names: list[str] = []
|
||||
for _, row in sorted(tasks.items(), key=pinned):
|
||||
worker = row.get("worker")
|
||||
if status_of(row) not in ENDED and isinstance(worker, str) and ORDER_NAME.fullmatch(worker) and worker not in names:
|
||||
if status_of(row) not in ENDED and isinstance(worker, str) and worker not in names:
|
||||
names.append(worker)
|
||||
return names
|
||||
names += [name for name in ordered({claim["address"] or claim["session"] for claim in claims}) if name not in names]
|
||||
known = [name for name in names if ORDER_NAME.fullmatch(name)]
|
||||
listed = [name for name in dict.fromkeys(stored) if name in known]
|
||||
return [*listed, *(name for name in known if name not in listed)]
|
||||
@@ -22,7 +22,15 @@ from .messaging import message_row
|
||||
from .orbits import TRACK_STEPS, track
|
||||
from .phrasing import phrase
|
||||
from .records import epoch, iso_epoch, read_jsonl
|
||||
from .register import REGISTER, operator_rows, priority, read_register, register_rows
|
||||
from .register import (
|
||||
AGENT_ORDER,
|
||||
REGISTER,
|
||||
agent_order,
|
||||
operator_rows,
|
||||
read_register,
|
||||
register_rows,
|
||||
stored_order,
|
||||
)
|
||||
from .reviewing import review_row
|
||||
from .settings import media_origins
|
||||
from .skill_scripts import OUTPUT_LIMIT
|
||||
@@ -117,7 +125,7 @@ class View:
|
||||
"messages": [message_row(message) for message in self.messages.listed() if begin <= message["created_ts"] <= end],
|
||||
"operator": operator_rows(tasks),
|
||||
"register": register_rows(tasks),
|
||||
"priority": priority(tasks),
|
||||
"agent_order": agent_order(tasks, claims, stored_order(self.council / AGENT_ORDER)),
|
||||
"radio": radio,
|
||||
"claims": claims,
|
||||
"grants": grant_rows,
|
||||
|
||||
Reference in new issue
Block a user