Skip to main content

xtalforge_agent/
lib.rs

1//! Agent layer for xtalforge: session, verbs, inspection, diagnostics,
2//! change tracking and pictures (Phase 8).
3//!
4//! The layer is what an AI assistant drives. Every public operation is a
5//! named verb in [`verbs::catalogue`], takes JSON arguments and returns a
6//! typed, printable [`answers::Answer`]. The CLI, the Python package, the
7//! WebAssembly package and the MCP server read the same catalogue, so there
8//! is one list of capabilities and no second list to drift.
9//!
10//! Mutations never bypass [`xtalforge_commands::Document`]: the session
11//! applies them through commands with [`xtalforge_commands::Origin::Agent`],
12//! so a user can review or undo the assistant's work as a unit
13//! ([`changes`]).
14//!
15//! # Example
16//!
17//! ```
18//! use xtalforge_agent::{Session, answers::Answer};
19//!
20//! let mut session = Session::new();
21//! let answer = session.run("capabilities", &serde_json::json!({})).unwrap();
22//! assert!(matches!(answer, Answer::Capabilities { .. }));
23//! ```
24//!
25//! Ported from Crystal-Builder `xtal/agent/` (MIT), split by verb group; the
26//! reference is recorded in `docs/algorithms/agent.md`.
27
28pub mod answers;
29pub mod batch;
30pub mod capabilities;
31pub mod changes;
32pub mod context;
33pub mod diagnostics;
34pub mod inspect;
35pub mod mcp;
36mod ops;
37mod ops_analysis;
38mod ops_ff;
39mod ops_picture;
40pub mod png;
41pub mod render;
42mod run;
43pub mod session;
44pub mod skill;
45pub mod verbs;
46
47pub use answers::Answer;
48pub use batch::run_verbs;
49pub use capabilities::{Capabilities, capability_report};
50pub use changes::{ChangeBurst, changes_for};
51pub use diagnostics::{Diagnostic, DiagnosticReport, Severity, diagnose};
52pub use inspect::inspect_structure;
53pub use mcp::{McpServer, serve_stdio};
54pub use render::{RenderOptions, render_png, render_svg};
55pub use session::{Session, SessionOptions};
56pub use skill::{SkillDocument, default_directory};
57pub use verbs::{ParameterKind, VerbInfo, catalogue, lookup};
58
59/// Errors returned by the agent layer.
60#[derive(Debug, Clone, PartialEq, thiserror::Error)]
61pub enum Error {
62    /// The verb name is not in the catalogue.
63    #[error("unknown verb {0}")]
64    UnknownVerb(String),
65    /// A required argument is missing or has the wrong JSON type.
66    #[error("invalid argument {name}: {reason}")]
67    Argument {
68        /// Argument name.
69        name: String,
70        /// What is wrong.
71        reason: String,
72    },
73    /// The document name is not loaded in the session.
74    #[error("unknown document {0}")]
75    UnknownDocument(String),
76    /// A verb needs a document and none is active.
77    #[error("no active document; call `load` first")]
78    NoDocument,
79    /// File input or output failed.
80    #[error("io: {0}")]
81    Io(String),
82    /// An error from the core crate.
83    #[error("core: {0}")]
84    Core(#[from] xtalforge_core::Error),
85    /// An error from the analysis crate.
86    #[error("analysis: {0}")]
87    Analysis(String),
88    /// An error from the force-field crate.
89    #[error("force field: {0}")]
90    ForceField(#[from] xtalforge_ff::Error),
91    /// A JSON value could not be encoded or decoded.
92    #[error("json: {0}")]
93    Json(String),
94}
95
96impl Error {
97    /// Stable `snake_case` code for this error, suitable for bindings and JSON.
98    #[must_use]
99    pub fn code(&self) -> &'static str {
100        match self {
101            Self::UnknownVerb(_) => "unknown_verb",
102            Self::Argument { .. } => "invalid_argument",
103            Self::UnknownDocument(_) => "unknown_document",
104            Self::NoDocument => "no_document",
105            Self::Io(_) => "io",
106            Self::Core(error) => error.code(),
107            Self::Analysis(_) => "analysis",
108            Self::ForceField(error) => error.code(),
109            Self::Json(_) => "json",
110        }
111    }
112}