yog 0.0.62

yog: the standalone server for litany loops — the world, the balls and the conversations, behind one wire
Documentation
//! **The acts whose subject is the conversation, answered here** (REMOTE §5.4,
//! bl-dfce): the compactor's procedure pair `write_summary` /
//! `mark_for_deletion`, the four worker grants, and the two acts that read and
//! write the agent's own record — the set below is the enumeration.
//!
//! The seam inversion made the router *total* — while an injection is installed
//! it answers every invocation the agent makes, and nothing resolves a binary
//! behind it. litany has a second source of injected tool definitions besides
//! the host: the calling role's own procedure, which today is exactly these two
//! names. Under the inversion they reach yog's router like any other name.
//!
//! **They are not tools on a machine, so they are not a thrall's.** The
//! subject-locality invariant (REMOTE §5: *"a tool executes where its subject
//! lives"*) decides it and nothing else has to. `write_summary` writes the
//! conversation's own summary onto the compactor branch; `mark_for_deletion`
//! nominates that same conversation's files. Their subject is the conversation,
//! the conversation lives on the server, and the server is what yog holds — so
//! no machine is involved, no thrall is involved, and REMOTE §12's *front door
//! only* governs execution **on a machine**, which this is not. Shipping them
//! to a thrall would send a box that does not hold the world a request about
//! it. The operator's ruling states the principle it falls out of: context
//! management happens in yog.
//!
//! **litany defines what the pair does; yog only decides where.** The acts
//! themselves are the engine's (its `ARCH §2.7` compactor toolset: the summary
//! numbering, the refusal to nominate the dispatch entry, the staged `git rm`),
//! and none of it is restated here. yog performs them by re-entering the
//! engine's own front door — `<driver_target> tool <name>`, the third hop
//! litany's own resolution addressed before the inversion, with the caller
//! identity on the child's environment and the `tool_use` input on its stdin.
//! The child is yog, re-executed under the `litany` namespace by the world's
//! shim, standing in the same world; nothing of the compactor's semantics is
//! reimplemented, so the pair has exactly one definition and it is upstream's.
//!
//! **The invocation's own id rides the child's environment** (litany's §3.3
//! stdio contract as amended by upstream bl-e8d7): `LITANY_TOOL_ID` is the
//! `tool_use.id` the engine is recording this call under, so the id the child
//! reads and the `steps/<agent-id>/<NNN>/tools/<tool-id>/` directory it writes
//! beside cannot disagree. A routing host is handed that id on the call and
//! owes it on any spawn it makes, exactly as it already owes the caller
//! identity and the cwd — and `python` is the first act that cannot work
//! without it, because naming its own record directory is how it puts each
//! inner invocation's record where the engine expects one. **The routed leg
//! owes nothing here**: the spawn a machine makes is the foot's own, and the
//! id addresses a directory on the server's disk that no foot holds.
//!
//! **The tool control still sees them, and yog has no say in that.** litany
//! adjudicates in the tool window, *before* the executor is entered, for every
//! name including an injected one — so `yog tool-control` judges these two
//! exactly as it judges every other invocation, and the router (which lives
//! inside execution) could not exempt them if it wanted to. There is no
//! carve-out at the chokepoint, and none was needed.

use std::path::Path;
use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};

use ::litany::cmd::{RoutedCall, RoutedCapture};

use crate::cli_outbound::{Chunk, Cli, StreamPoll};

/// **The engine-act name set, closed and enumerated here and nowhere else.**
/// Ten rows since bl-9ced, in three families, each admitted by the
/// subject-locality invariant (REMOTE §5: *"a tool executes where its subject
/// lives"*) and by nothing else:
///
/// - the compactor's procedure pair (`write_summary`, `mark_for_deletion`,
///   REMOTE §5.4, bl-dfce) — the conversation's own summary and files;
/// - the conversation-subject worker grants (REMOTE §5.4 as amended by
///   bl-77be): `dispatch` mints and launches a child conversation on the
///   workspace the server holds, `message` deposits into another
///   conversation's inbox, `load_skill` copies a server-disk skill into the
///   agent's server-disk worktree, and `cd` writes the agent's
///   working-directory mark, a ref on the workspace. None of them is work on
///   a *machine*, so none of them is a thrall's — routing them anywhere would
///   send a box that does not hold the world a request about it.
/// - the four acts whose subject is the agent's own record, history and
///   lineage (bl-fe43, bl-81cc, bl-ebef, bl-9ced). `python` runs a program the model authored, and the
///   program composes the agent's own tools: the built-in generates a
///   `litany_tools` stub per tool this injection declares, each one a
///   `<driver_target> invoke` of the front door, and it writes each inner
///   call's record under the agent's in-flight `steps/<agent>/<NNN>/tools/`
///   (litany `docs/DESIGN_CODE_EXECUTION.md` §2.8). Both the step record and
///   the front door are the server's. `search_history` runs a fixed-string
///   pickaxe over the workspace's `agents/*` refs — every agent's transcript
///   (litany `docs/DESIGN_CONTEXT_ECONOMY.md` §4) — and a foot holds no
///   repository at all, so a routed one would search nothing and answer
///   *nothing found*, the worst answer a search can give. `remember`
///   (litany 0.0.11, upstream bl-3c11) appends one durable fact to `facts.md`
///   in a config commit on `proposal/<agent-id>` — a BRANCH of the workspace
///   repository the server holds, staged for the operator's `litany proposal
///   … --accept` — so it writes no file in the working tree at all, and a foot
///   that holds no repository could not perform it. It is also the door the
///   lineage refusal opened (upstream bl-d273, yog bl-baed): the act an agent
///   asked to remember something must reach instead of driving `litany config`,
///   which is now refused under `LITANY_TOOL_ID`. Routing it would put a
///   workspace-repository write on a box that does not hold the workspace.
///   `read_tool_output` (litany 0.0.12, upstream bl-9a6e) pages the cut middle
///   of one of *this agent's own* captures back out of
///   `steps/<agent-id>/<NNN>/tools/<tool-id>/output.json` — the diagnostic
///   record litany lands beside the step, on the server's disk, under a
///   domain bound that is one string equality against the caller's own
///   `LITANY_CONV_BRANCH`. Its subject is therefore the agent's record and
///   never a working tree, and a foot that holds no `steps/` tree could only
///   answer *no such address* — the same worst-answer `search_history` would
///   give. It is the recovery the bounded projection's cut marker names, so a
///   routed one would make the marker's own way out unreachable.
///
/// What is deliberately NOT here: `bash`, `read_file`, `apply_patch` — acts
/// at the conversation's working directory, which take the worktree lane
/// ([`super::subject`]) and reach a consenting machine when the operator
/// enrolled one. The lane's last rung calls [`perform`] below on them
/// ([`super::subject::performs`], bl-5710), so the mechanism is shared and
/// the *ordering* is what separates the two sets: an engine act never
/// consults the roster, a worktree name always does. An eleventh row is a
/// deliberate act with this audit's question asked again — never a prefix
/// test or a name shape, which is how a closed set stops being closed. The
/// strings are yog's own spelling: the engine keeps its constants
/// crate-private, so the names cross as text exactly as they do in the
/// model's `tool_use` block.
pub const NAMES: [&str; 10] = [
    "write_summary",
    "mark_for_deletion",
    "dispatch",
    "message",
    "load_skill",
    "cd",
    "python",
    "remember",
    "search_history",
    "read_tool_output",
];

/// The `litany` verb the built-in front door answers under.
const VERB: &str = "tool";

/// The workspace root a tool's caller identity is read from, litany's spelling.
const CONV_REPO: &str = "LITANY_CONV_REPO";

/// The agent id half of the same identity, litany's spelling.
const CONV_BRANCH: &str = "LITANY_CONV_BRANCH";

/// The invocation's own id — the third variable of litany's stdio contract,
/// its spelling, and the name of the record directory the child writes beside.
const TOOL_ID: &str = "LITANY_TOOL_ID";

/// How often a running child is looked at — a latency knob on the answer, not
/// on the act.
const POLL: Duration = Duration::from_millis(20);

/// Whether `name` is one of the engine acts this module performs.
pub fn is(name: &str) -> bool {
    NAMES.contains(&name)
}

/// Perform one engine act and answer what it captured, or the sentence saying
/// why nothing did. A failure is the in-band non-zero refusal every other
/// answer on this seam is — the model reads it and steps on.
pub fn perform(driver_target: &Path, deadline: Duration, call: &RoutedCall<'_>) -> RoutedCapture {
    match run(driver_target, deadline, call) {
        Ok(capture) => capture,
        Err(reason) => super::capture(call.name, Err(reason)),
    }
}

/// The re-entry itself: the identity on the environment, the input on stdin,
/// and the child's own three facts back untouched.
///
/// **Both waits are bounded** (litany's stated router obligations: carry your
/// own deadline, watch the stop flag). Returning early drops the stream, and
/// the drop is the kill — the same SIGTERM-then-SIGKILL cascade every other
/// child of this crate ends by.
fn run(
    driver_target: &Path,
    deadline: Duration,
    call: &RoutedCall<'_>,
) -> Result<RoutedCapture, String> {
    let mut stream = Cli::new(driver_target)
        .and_env(vec![
            (
                CONV_REPO.to_owned(),
                call.workspace.to_string_lossy().into_owned(),
            ),
            (CONV_BRANCH.to_owned(), call.agent.to_owned()),
            (TOOL_ID.to_owned(), call.id.to_owned()),
        ])
        .run_input(
            // The caller's resolved working directory (litany bl-ddaa), which
            // is the §3.3 contract for in-process built-ins — a relative `cd`
            // resolves against where the agent stands, not against the
            // workspace root.
            Some(call.cwd),
            call.input.to_string().as_bytes(),
            &[VERB, call.name],
        )
        .map_err(|e| e.to_string())?;
    let started = Instant::now();
    let (mut out, mut err) = (Vec::new(), Vec::new());
    loop {
        match stream.try_next() {
            StreamPoll::Ready(Chunk::Stdout(bytes)) => out.extend(bytes),
            StreamPoll::Ready(Chunk::Stderr(bytes)) => err.extend(bytes),
            StreamPoll::Ready(Chunk::Exited(info)) => {
                return Ok(RoutedCapture {
                    stdout: out,
                    stderr: err,
                    exit_code: info.shell_code(),
                });
            }
            StreamPoll::Pending => {
                if call.stop.load(Ordering::Relaxed) {
                    return Err("stopped while the engine was working".to_owned());
                }
                if started.elapsed() >= deadline {
                    return Err(format!("the engine did not answer within {deadline:?}"));
                }
                std::thread::sleep(POLL);
            }
        }
    }
}

#[cfg(test)]
mod tests;