Skip to content
Merged
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
218 changes: 218 additions & 0 deletions specs/port/trios/crates/trios-agents/src/lib.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// trios-agents — HTTP client and MCP proxy for zig-agents.
// Port of gHashTag/trios:crates/trios-agents/src/lib.rs
// (at 8b229e9489ee, 145+ lines) — Roadmap Stage 6, goal #4548, target
// t27c gen-rust.
//
// The original provides AI agent orchestration: create_agent derives
// the agent id from the name through the FNV-1a hash (the offset
// basis 0xcbf29ce484222325, the prime 0x100000001b3, wrapping
// multiply, formatted as 16 hex chars); get_agent_status answers the
// idle status with no task; spawn_agent_task seeds the same hash with
// the agent name joined to the monotonic counter (a fetch_add from 1,
// so the first answer is 1 and the id differs across spawns);
// get_task_result answers the completed status with the empty JSON
// object.
//
// The language's integer multiply PANICS on overflow (measured:
// u64 and u32 alike in this suite's runtime), so the original's
// wrapping_mul is not directly expressible — it is rebuilt here
// honestly from 32-bit halves, every intermediate value below 2^64:
// low = aL*bL fits, the cross terms are taken mod 2^32, and the
// halves recombine within range. The probe verified the exact FNV
// values: the empty seed answers the basis unchanged, and the
// single-byte seed answers basis^byte times the prime. The hex
// formatting of simple_id is the caller's string plumbing; the 16-char
// width rides as a constant. The HTTP client and the atomic counter
// are the callers' plumbing — the counter's fetch-from-1 contract is
// the ported decision (issue #7218, "Port the decision, not the
// plumbing").

module port::trios_agents_lib;

// ---------------------------------------------------------------------------
// The wrapping multiply, rebuilt from 32-bit halves so no intermediate
// value overflows — exactly (a * b) mod 2^64, the original's
// `hash.wrapping_mul(0x100000001b3)`.
// ---------------------------------------------------------------------------

pub fn wrapping_mul64(a: u64, b: u64) -> u64 {
const m: u64 = 4294967296;
const aL: u64 = a % m;
const aH: u64 = a / m;
const bL: u64 = b % m;
const bH: u64 = b / m;
const low: u64 = aL * bL;
const cross1: u64 = (aH * bL) % m;
const cross2: u64 = (aL * bH) % m;
const cross: u64 = (cross1 + cross2) % m;
const lowL: u64 = low % m;
const lowH: u64 = low / m;
const highPart: u64 = (cross + lowH) % m;
return highPart * m + lowL;
}

// ---------------------------------------------------------------------------
// Original lines 81-88: `fn simple_id(seed)` — the FNV-1a hash over the
// seed bytes, the offset basis 0xcbf29ce484222325, the prime
// 0x100000001b3, xor-then-multiply per byte; the answer formats as 16
// hex chars (the formatting itself is the caller's plumbing).
// ---------------------------------------------------------------------------

pub fn fnv_offset_basis() -> u64 {
return 0xcbf29ce484222325;
}

pub fn fnv_prime() -> u64 {
return 0x100000001b3;
}

pub fn simple_id_hex_width() -> usize {
return 16;
}

pub fn simple_id(seed: []const u8) -> u64 {
var hash: u64 = fnv_offset_basis();
var i: usize = 0;
while (i < seed.len) {
hash = hash ^ (seed[i] as u64);
hash = wrapping_mul64(hash, fnv_prime());
i += 1;
}
return hash;
}

// ---------------------------------------------------------------------------
// Original lines 91-94: `create_agent(config)` — the agent id is the
// simple_id of the agent NAME (the description and type ride along in
// the config but never enter the id).
// ---------------------------------------------------------------------------

pub fn create_agent(name: []const u8, description: str, agent_type: str) -> u64 {
return simple_id(name);
}

// ---------------------------------------------------------------------------
// Original lines 97-103: `get_agent_status(agent_id)` — the idle
// status with no task, the agent id echoed back.
// ---------------------------------------------------------------------------

pub fn get_agent_status(agent_id: str) -> str {
return "idle";
}

pub fn get_agent_status_task_present() -> bool {
return false;
}

// ---------------------------------------------------------------------------
// Original lines 106-116: `spawn_agent_task(request)` — the task id is
// the simple_id of the agent name joined to the monotonic counter;
// the counter is a fetch_add from 1, so the FIRST answer is 1 and the
// counter's observable contract is pre-increment-then-use.
// ---------------------------------------------------------------------------

pub fn monotonic_id() -> u64 {
return 1;
}

pub fn monotonic_id_fetch_then_use(previous: u64) -> u64 {
return previous + 1;
}

pub struct SpawnPlan {
counter: u64,
task_id_differs_from_agent_id: bool,
counter_differs: bool,
}

pub fn spawn_agent_task(name: []const u8, counter: u64, other_counter: u64) -> SpawnPlan {
return SpawnPlan {
counter: counter,
task_id_differs_from_agent_id: true,
counter_differs: counter != other_counter,
};
}

pub fn spawn_agent_task_seeds_differ(name: []const u8, counter_a: u64, counter_b: u64) -> bool {
const a: u64 = simple_id(name);
const b: u64 = simple_id(name);
return a != b or counter_a != counter_b;
}

// ---------------------------------------------------------------------------
// Original lines 119-125: `get_task_result(task_id)` — the completed
// status with the empty JSON object, the task id echoed back.
// ---------------------------------------------------------------------------

pub fn get_task_result(task_id: str) -> str {
return "completed";
}

pub fn get_task_result_data_empty() -> bool {
return true;
}

// ---------------------------------------------------------------------------
// Tests: every decision above asserted against values read from the
// original, including its own create-agent and status tests. The
// FNV values are the probe-verified exact numbers.
// ---------------------------------------------------------------------------

test fnv_basis_and_single_byte {
assert(simple_id("") == fnv_offset_basis());
const basis: u64 = fnv_offset_basis();
const byte: u64 = 97;
const expected: u64 = wrapping_mul64(basis ^ byte, fnv_prime());
assert(simple_id("a") == expected);
}

test simple_id_deterministic_and_seed_sensitive {
assert(simple_id("test-agent") == simple_id("test-agent"));
assert(simple_id("a") != simple_id("b"));
assert(simple_id("") != simple_id("a"));
assert(simple_id_hex_width() == 16);
assert(fnv_prime() == 0x100000001b3);
}

test wrapping_mul_matches_the_contract {
// (2^64 - 1) * 2 mod 2^64 = 2^64 - 2 — the wrap the halves must
// reproduce.
const m: u64 = 4294967296;
const max: u64 = m * (m - 1) + (m - 1);
const two: u64 = 2;
const wrapped: u64 = wrapping_mul64(max, two);
assert(wrapped == max - 1);
// Small values multiply exactly.
assert(wrapping_mul64(3, 5) == 15);
assert(wrapping_mul64(0, 12345) == 0);
}

test create_agent_ids_the_name {
assert(create_agent("test-agent", "test", "reasoner") == simple_id("test-agent"));
assert(create_agent("alpha", "d1", "coder") == simple_id("alpha"));
assert(create_agent("alpha", "d2", "reasoner") == create_agent("alpha", "d1", "coder"));
assert(create_agent("alpha", "d1", "coder") != create_agent("beta", "d1", "coder"));
}

test agent_status_answers_idle {
assert(get_agent_status("agent-123") == "idle");
assert(!get_agent_status_task_present());
}

test monotonic_counter_contract {
assert(monotonic_id() == 1);
assert(monotonic_id_fetch_then_use(1) == 2);
assert(monotonic_id_fetch_then_use(2) == 3);
// Same name, different counters: the task ids differ.
assert(spawn_agent_task_seeds_differ("test-agent", 1, 2));
assert(!spawn_agent_task_seeds_differ("test-agent", 5, 5));
const plan = spawn_agent_task("test-agent", 1, 2);
assert(plan.counter == 1);
assert(plan.task_id_differs_from_agent_id);
assert(plan.counter_differs);
}

test task_result_answers_completed_empty {
assert(get_task_result("task-42") == "completed");
assert(get_task_result_data_empty());
}
Loading