aion-server 0.31.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! The grounding pack a spawned assistant agent can read.
//!
//! An assistant agent is asked about Aion and AWL from its first turn, and an
//! agent grounded only in its training data answers from an Aion that no
//! longer exists. The server carries the real answers already — the language
//! reference is embedded in `aion-awl`, and the authoring, worker, command,
//! environment and best-practices guides ship in this crate's
//! `grounding-embed/` copy (git-tracked under the crate root, exactly as
//! `aion-awl` tracks `guide-embed/`, so `cargo package` carries them;
//! `grounding_gate_tests` pins each byte-identical to its authored original
//! under `docs/assistant/grounding/`).
//!
//! At every spawn the pack is REWRITTEN into `$AION_HOME/assistant/grounding/`
//! — rewritten, not written-once, so the files are version-true from the
//! binary that is serving, never from whichever binary ran first. The first
//! turn's prompt then names the directory once (see
//! [`crate::assistant::sessions`]); the agent reads the files with its own
//! tools, which is the dynamic half — a pointer costs a sentence, the
//! documents cost nothing until the agent actually needs one.

use std::io::Write as _;
use std::path::{Path, PathBuf};

use crate::config::aion_home;

/// The pack: file name → content, embedded at compile time.
///
/// `AWL-REFERENCE.md` comes from `aion-awl`'s own embed (one copy, one gate —
/// this crate re-embedding it would be a second truth); the other five are
/// this crate's `grounding-embed/` copies.
pub(crate) fn documents() -> [(&'static str, &'static str); 6] {
    [
        ("AWL-REFERENCE.md", aion_awl::guide::reference_text()),
        (
            "AWL-AUTHORING.md",
            include_str!("../../grounding-embed/AWL-AUTHORING.md"),
        ),
        (
            "WORKERS.md",
            include_str!("../../grounding-embed/WORKERS.md"),
        ),
        (
            "COMMANDS.md",
            include_str!("../../grounding-embed/COMMANDS.md"),
        ),
        (
            "ENVIRONMENT.md",
            include_str!("../../grounding-embed/ENVIRONMENT.md"),
        ),
        (
            "AWL-BEST-PRACTICES.md",
            include_str!("../../grounding-embed/AWL-BEST-PRACTICES.md"),
        ),
    ]
}

/// Where the pack lives: `$AION_HOME/assistant/grounding`.
///
/// Resolved through the one typed home resolver — the same answer the rest of
/// the server gives for "where is Aion's own state" — and never invented.
///
/// # Errors
///
/// Whatever [`aion_home`] reports: no home is a configuration fact the
/// operator has to fix, not one this module may paper over.
pub(crate) fn directory() -> Result<PathBuf, String> {
    let home = aion_home().map_err(|error| error.to_string())?;
    Ok(home.path.join("assistant").join("grounding"))
}

/// Rewrite the pack into `dir`, owner-only.
///
/// Every file is truncated and rewritten on every call: the pack must state
/// what THIS binary knows, and a stale file from an older binary would be a
/// reference that quietly disagrees with the server reading it. The directory
/// is created `0700` (and an existing directory is tightened to it) for the
/// same reason the rest of `$AION_HOME` is owner-only.
///
/// # Errors
///
/// The first filesystem refusal, with the path in the message. Nothing is
/// swallowed: a pack that cannot be written surfaces on the turn that needed
/// it rather than as an agent that silently knows nothing.
pub(crate) fn materialize(dir: &Path) -> Result<(), String> {
    std::fs::create_dir_all(dir)
        .map_err(|error| format!("cannot create {}: {error}", dir.display()))?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt as _;
        std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
            .map_err(|error| format!("cannot restrict {}: {error}", dir.display()))?;
    }
    for (name, content) in documents() {
        let path = dir.join(name);
        let mut file = std::fs::File::create(&path)
            .map_err(|error| format!("cannot write {}: {error}", path.display()))?;
        file.write_all(content.as_bytes())
            .map_err(|error| format!("cannot write {}: {error}", path.display()))?;
    }
    Ok(())
}

/// The first-turn preamble: where the agent is, and where the answers are.
///
/// Three sentences, on the FIRST turn of a session only. Every turn would be
/// noise the agent learns to read past; the first turn is where an agent
/// decides what it is standing in. This is SERVER-side grounding, not operator
/// context — it deliberately does not pass through the turn-context formatter,
/// whose byte-for-byte mirror contract with the console is about what the
/// operator's screen said and nothing else.
pub(crate) fn preamble(dir: &Path) -> String {
    format!(
        "You are the assistant inside an Aion server's ops console. Aion is a durable workflow \
         engine whose workflows are written in AWL; the complete, version-true reference for \
         this server — the AWL language reference and the authoring, worker, command, \
         environment and best-practice guides — is in {}, and reading the relevant file before \
         answering an AWL or Aion question beats recalling. The `assistant_context` tool \
         fetches what is on the operator's screen right now; `assistant_document_edit` edits \
         the document they are editing in place, and `assistant_document_check` runs their \
         editor's own AWL check over it.",
        dir.display()
    )
}