File size: 7,753 Bytes
cfb152f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | //! The free-agent loop cycle, composed from the proven parts (bigpickle asolaria-loop.mjs:26-103).
//!
//! One cycle: revolver -> room select + rename-before-load (`row=project_swap`) -> mock free agent ->
//! hookwall pass (the SCORE/Fischer lanes called through [`GateLanes`]) -> second prism PID +
//! `row=prism_in` -> GC emit. The live opencode spawn and the filesystem writes (the `ROOM.hbp` marker,
//! :43-46, and the prism append, :69-78) are the integrator's lanes; the rows they write are returned,
//! built with a supplied timestamp. `runLoop`'s per-verdict tally (:104-118) is kept by the caller from
//! each [`CycleOutcome`].
use std::path::Path;
use crate::dispatcher::run_free_agent_mock;
use crate::district_fabric::DISTRICTS;
use crate::gc_runtime::{CapStatus, GcRuntime, DEFAULT_GC_EVERY_MESSAGES};
use crate::hookwall::{pass, Envelope, GateLanes, PassOpts, Verdict};
use crate::revolver::PidChainRevolver;
use crate::room_router::{ProjectRoomRouter, RoomSwapPlan};
use crate::sha::sha8;
/// Rooms of the named district at `scale`, 2000 for a name outside the catalog (asolaria-loop.mjs:26-29).
pub fn district_room_count(name: &str, scale: u64) -> u64 {
DISTRICTS
.iter()
.find(|d| d.name == name)
.map_or(2000, |d| d.rooms)
* scale
}
/// Loop context: the proven modules plus the rolling observation hash and the optional prism rotor.
pub struct LoopCtx {
pub revolver: PidChainRevolver,
pub router: ProjectRoomRouter,
pub gc: GcRuntime,
pub district: String,
pub room_count: u64,
pub prev_hash: String,
pub prism_revolver: Option<PidChainRevolver>,
pub prism_room_count: u64,
}
impl LoopCtx {
/// The context `runLoop` builds (asolaria-loop.mjs:90-103): anchor `anchor` or
/// `ASOLARIA-LOOP-<UPPER>`, prism rotor `ASOLARIA-PRISM-ROTATOR-<UPPER>`, room counts from
/// [`district_room_count`] (prism rooms from `prism`), GC window `gc_every` or 2000, active room 0.
/// `scale` is upstream's `opts.scale` (default `ASOLARIA_ROOM_SCALE` or 1, read by the caller).
pub fn for_district(
district: &str,
anchor: Option<&str>,
scale: u64,
gc_every: Option<u64>,
) -> Self {
let upper = district.to_uppercase();
let anchor = match anchor {
Some(a) if !a.is_empty() => a.to_string(),
_ => format!("ASOLARIA-LOOP-{upper}"),
};
let prism_anchor = format!("ASOLARIA-PRISM-ROTATOR-{upper}");
LoopCtx {
revolver: PidChainRevolver::new(&anchor).expect("the anchor is non-empty"),
router: ProjectRoomRouter::new(),
gc: GcRuntime::with_gc_every(gc_every.unwrap_or(DEFAULT_GC_EVERY_MESSAGES)),
district: district.to_string(),
room_count: district_room_count(district, scale),
prev_hash: "0000000000000000".to_string(),
prism_revolver: Some(
PidChainRevolver::new(&prism_anchor).expect("the anchor is non-empty"),
),
prism_room_count: district_room_count("prism", scale),
}
}
}
/// Per-cycle options (asolaria-loop.mjs:32 `opts`). Under upstream `dryRun` both ledger paths are `None`.
pub struct CycleOpts<'a> {
/// `opts.question`; `None` or `""` takes the default probe (:48 `||`).
pub question: Option<&'a str>,
/// `opts.observeOnly`, spread into the hookwall pass (:51-54, asolaria-hookwall.mjs:84).
pub observe_only: bool,
/// Row timestamp (upstream `new Date().toISOString()`).
pub ts: &'a str,
/// `ctx.ledgerPath`, the observation append (:53).
pub ledger_path: Option<&'a Path>,
/// `opts.fischerLedgerPath`, the Fischer row append (asolaria-hookwall.mjs:75-80).
pub fischer_ledger_path: Option<&'a Path>,
}
/// The outcome of one cycle, including the two emitted rows and the observation row.
pub struct CycleOutcome {
pub agent_pid: String,
pub room_id: u32,
pub rotation: u64,
/// `!swap.noop`. Upstream's `renamed` (:42-46) is this when the `project_swap_row` append to an
/// existing room's `ROOM.hbp` succeeds, and `false` under `dryRun`; that append is the caller's.
pub would_rename: bool,
pub mock: bool,
pub verdict: Verdict,
pub mark: String,
pub score: String,
pub l0_real: bool,
pub gc_runs: u64,
pub cap_status: CapStatus,
pub project_swap_row: String,
pub prism_in_row: String,
pub observation_row: String,
}
/// Run one full loop cycle (asolaria-loop.mjs:32-86). The gate calls `lanes` for SCORE (pid, answer)
/// and Fischer (pid, envelope, score, `ctx.prev_hash`) as upstream's hookwall pass does.
pub fn loop_cycle(ctx: &mut LoopCtx, lanes: &mut dyn GateLanes, opts: &CycleOpts) -> CycleOutcome {
// 1. PID emitter
let agent_pid = ctx.revolver.next_pid();
let rotation = ctx.revolver.counter - 1;
// 2. room selection + rename-before-load marker
let room_id = ctx
.router
.room_for_pid(&agent_pid)
.expect("revolver PIDs are 16 hex digits")
% (ctx.room_count as u32);
let swap = ctx.router.plan_swap_to(room_id);
let would_rename = !matches!(swap, RoomSwapPlan::Noop { .. });
let ts = opts.ts;
let project_swap_row = format!(
"HBPv1|row=project_swap|agent_pid={agent_pid}|room={room_id}|rotation={rotation}|invariant=rename-before-load|ts={ts}|json=0"
);
// 3. free agent in the unique project (mock)
let default_question = format!(
"[{}] cycle {rotation}: produce one genius/mistake mark for this lane",
ctx.district
);
let question = match opts.question {
Some(q) if !q.is_empty() => q,
_ => &default_question,
};
let agent = run_free_agent_mock("opencode-coder", question, &agent_pid);
// 4. hookwall gate (SCORE/Fischer lanes)
let env = Envelope {
pid: Some(&agent_pid),
actor: &ctx.district,
verb: "answer",
target: "prism",
payload: Some(&agent.answer),
payload_sha16: "",
};
let pass_opts = PassOpts {
observe_only: opts.observe_only,
prev_hash: &ctx.prev_hash,
ts,
ledger_path: opts.ledger_path,
fischer_ledger_path: opts.fischer_ledger_path,
};
let gate = pass(&env, lanes, &pass_opts);
ctx.prev_hash = gate.row_hash.clone();
// 5. second prism PID + prism_in row
let mut prism_segment = String::new();
if let Some(prism_rev) = ctx.prism_revolver.as_mut() {
if ctx.prism_room_count > 0 {
let prism_rotation = prism_rev.counter;
let prism_pid = prism_rev.next_pid();
let prism_room_id = ctx
.router
.room_for_pid(&prism_pid)
.expect("revolver PIDs are 16 hex digits")
% (ctx.prism_room_count as u32);
prism_segment = format!(
"|prism_room={prism_room_id}|prism_pid={prism_pid}|prism_rotation={prism_rotation}"
);
}
}
let prism_in_row = format!(
"HBPv1|row=prism_in|from_pid={agent_pid}|from_room={room_id}{prism_segment}|mark={}|score={}|gnn_lane=reverse_gain_gnn|answer_sha16={}|ts={ts}|json=0",
gate.mark,
gate.score,
sha8(&agent.answer)
);
// 6. GC emit
let gc_status = ctx.gc.emit();
CycleOutcome {
agent_pid,
room_id,
rotation,
would_rename,
mock: agent.mock,
verdict: gate.verdict,
mark: gate.mark,
score: gate.score,
l0_real: gate.l0_real,
gc_runs: gc_status.runs,
cap_status: gc_status.cap_status,
project_swap_row,
prism_in_row,
observation_row: gate.observation,
}
}
|