feat(autonomy): autonomous swarm worker orchestration, nudge triggers, and recursive loop
This commit is contained in:
+140
-2
@@ -276,9 +276,14 @@ def make_digest_id(agent):
|
||||
|
||||
# In-band response-contract footer for ACTIONABLE digests. The verbs are matched
|
||||
# by response-harvester.py to resolve followups: ACK/CLAIM acknowledge (nudge
|
||||
# suppression), RESULT/DECLINE/NO-ACTION close. ~94 chars, well under budget.
|
||||
# suppression), RESULT/DECLINE/NO-ACTION close. The closing line enforces the
|
||||
# recursive box->agent->box discipline: post the RESULT back in this thread
|
||||
# (box records it and dispatches the next chained step); never DM the next
|
||||
# agent directly. ~166 chars, within the 600-char digest budget.
|
||||
CONTRACT_FOOTER = ("Reply: [ACK id] seen | [CLAIM id] mine | "
|
||||
"[RESULT id] done | [DECLINE id] | [NO-ACTION id]")
|
||||
"[RESULT id] done | [DECLINE id] | [NO-ACTION id]. "
|
||||
"Report back here. Box dispatches the next step; "
|
||||
"do not DM the next agent directly.")
|
||||
|
||||
|
||||
def send_prompt(sender, agent, sidechat, digest):
|
||||
@@ -552,6 +557,63 @@ def check_subagents(agent, cfg, threads_meta):
|
||||
return alerts
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Brain workspace — the main loop operates its thinking in the "main-loop
|
||||
# brain" sidechat (opm's account). See bin/brain.py.
|
||||
# User directive 2026-10-04: "we need main loop to operate its brains in
|
||||
# side chat". Safety: brain posts carry [BRAIN], never [JOB]; never
|
||||
# --expect-reply; own messages skipped on read; !loop only from authorized
|
||||
# senders. All enforced in brain.py.
|
||||
# ---------------------------------------------------------------------------
|
||||
_BRAIN_LAST_IDS = {} # (agent, sidechat_name) -> last message_id seen
|
||||
|
||||
|
||||
def read_brain_messages(agent, sidechat_name, since_ts):
|
||||
"""read_fn adapter for brain.BrainWorkspace.
|
||||
|
||||
Resolves sidechat_name -> UUID via dm (never hardcoded; UUIDs rotate),
|
||||
reads via the same muse_hybrid primitive the loop uses, returns
|
||||
[{"sender", "text", "ts"}]. Tracks last message_id per (agent, name);
|
||||
first run anchors at newest with no backfill (same policy as
|
||||
new_messages for main chat).
|
||||
"""
|
||||
try:
|
||||
import dm
|
||||
uuid = dm.resolve_sidechat_target(sidechat_name, agent)
|
||||
except Exception:
|
||||
return []
|
||||
if not uuid:
|
||||
return []
|
||||
try:
|
||||
import muse_hybrid
|
||||
msgs, err = muse_hybrid.get_history(agent, thread_id=uuid, limit=10)
|
||||
except Exception:
|
||||
return []
|
||||
if err or not msgs:
|
||||
return []
|
||||
key = (agent, sidechat_name)
|
||||
last_id = _BRAIN_LAST_IDS.get(key)
|
||||
ids = [m.get("message_id") or "msg-%s" % m.get("seq") for m in msgs]
|
||||
if last_id is None:
|
||||
# First run: anchor at newest, no backfill (old !loop commands
|
||||
# must not fire on deploy).
|
||||
_BRAIN_LAST_IDS[key] = ids[-1] if ids else None
|
||||
return []
|
||||
if last_id in ids:
|
||||
new_msgs = msgs[ids.index(last_id) + 1:]
|
||||
else:
|
||||
# Watermark fell out of the read window: treat all as new.
|
||||
# Safe: brain intake skips own messages and only authorized
|
||||
# !loop senders can act.
|
||||
new_msgs = msgs
|
||||
_BRAIN_LAST_IDS[key] = ids[-1] if ids else last_id
|
||||
now = time.time()
|
||||
return [{"sender": m.get("role", "unknown"),
|
||||
"text": m.get("text", ""),
|
||||
"ts": now} for m in new_msgs]
|
||||
|
||||
|
||||
def do_check(only_agent=None):
|
||||
st = load_state()
|
||||
cfg = get_config(st)
|
||||
@@ -564,6 +626,23 @@ def do_check(only_agent=None):
|
||||
results = {}
|
||||
prompted = 0
|
||||
errors = 0
|
||||
quiet_held = 0
|
||||
|
||||
# Brain workspace: the loop thinks in the sidechat, takes !loop
|
||||
# instructions there. Non-fatal: a brain failure must never break
|
||||
# the tick.
|
||||
brain = None
|
||||
try:
|
||||
from brain import BrainWorkspace
|
||||
brain = BrainWorkspace(STATE_FILE, read_fn=read_brain_messages)
|
||||
intake = brain.intake()
|
||||
if intake.get("commands"):
|
||||
log("brain: %d commands, %d acks, %d ignored-senders" % (
|
||||
intake["commands"], intake.get("acks", 0),
|
||||
intake.get("ignored_senders", 0)))
|
||||
except Exception as e:
|
||||
log("brain init/intake failed (non-fatal): %r" % e)
|
||||
brain = None
|
||||
|
||||
import muse_hybrid
|
||||
|
||||
@@ -581,6 +660,10 @@ def do_check(only_agent=None):
|
||||
if not enabled.get(agent, True):
|
||||
results[agent] = {"ok": True, "new": 0, "disabled": True}
|
||||
continue
|
||||
if brain is not None and brain.ignored(agent):
|
||||
log("%s: skipped (brain !loop ignore active)" % agent)
|
||||
results[agent] = {"ok": True, "new": 0, "ignored": True}
|
||||
continue
|
||||
wm = agents_state.get(agent) or {}
|
||||
|
||||
# 1. Main chat check
|
||||
@@ -640,6 +723,18 @@ def do_check(only_agent=None):
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
# Brain quiet mode: hold actionable escalations. Reads continue,
|
||||
# digests are composed, but nothing escalates to the agent — the
|
||||
# thinking note in the brain carries the activity instead.
|
||||
if brain is not None and brain.quiet():
|
||||
is_act, _urg = classify_digest(digest)
|
||||
if is_act:
|
||||
log("%s: quiet mode - digest held (not escalated)" % agent)
|
||||
results[agent] = {"ok": True, "new": total_new,
|
||||
"quiet_held": True}
|
||||
quiet_held += 1
|
||||
continue
|
||||
|
||||
sent, detail, digest_id, actionable = send_prompt(cfg["sender"], agent, sidechat, digest)
|
||||
if sent:
|
||||
prompted += 1
|
||||
@@ -653,6 +748,49 @@ def do_check(only_agent=None):
|
||||
results[agent] = {"ok": False, "error": detail, "new": total_new}
|
||||
errors += 1
|
||||
|
||||
# Brain: post the tick's thinking to the sidechat workspace, then
|
||||
# persist brain state. Non-fatal on failure.
|
||||
if brain is not None:
|
||||
try:
|
||||
seen = {}
|
||||
escalated = []
|
||||
for a in agents:
|
||||
r = results.get(a, {})
|
||||
seen[a] = (r.get("new", 0), 0, 0)
|
||||
if r.get("prompted"):
|
||||
wm_a = agents_state.get(a) or {}
|
||||
did = wm_a.get("last_digest_id")
|
||||
if did and wm_a.get("last_digest_actionable"):
|
||||
escalated.append(did)
|
||||
closure_rate = None
|
||||
health = None
|
||||
try:
|
||||
health = digest_health()
|
||||
closure_rate = (health or {}).get("closure_rate")
|
||||
except Exception:
|
||||
pass
|
||||
wm_epochs = {}
|
||||
for a in agents:
|
||||
ts_s = (agents_state.get(a) or {}).get("last_ts")
|
||||
try:
|
||||
if ts_s:
|
||||
dt = datetime.fromisoformat(
|
||||
ts_s.replace("Z", "+00:00"))
|
||||
wm_epochs[a] = dt.timestamp()
|
||||
except Exception:
|
||||
pass
|
||||
brain.post_thinking(
|
||||
{"seen": seen, "escalated": escalated,
|
||||
"skipped_info": quiet_held, "errors": errors,
|
||||
"closure_rate": closure_rate},
|
||||
cfg={a: bool(enabled.get(a, True)) for a in agents},
|
||||
watermarks=wm_epochs,
|
||||
health=health,
|
||||
)
|
||||
brain.save()
|
||||
except Exception as e:
|
||||
log("brain post_thinking failed (non-fatal): %r" % e)
|
||||
|
||||
# Save under the state lock with a fresh reload: an enable/disable may
|
||||
# have landed during the slow chat reads; preserve its config changes
|
||||
# and only update the keys this run owns (watermarks, last_run/result).
|
||||
|
||||
Reference in New Issue
Block a user