feat(grid-agent): establish architecture and config (#118)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m47s
CI / required (push) Failing after 2m44s

This commit is contained in:
2026-08-17 20:15:37 +00:00
parent 1254cf24e1
commit 1e1e95a58a
14 changed files with 2647 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
//! Narrow injected boundaries between orchestration and grid/world I/O.
use crate::types::{GridEvent, GridEventKind, PolicyDecision, ProposedToolCall, ToolCallOutcome};
use libremetaverse_types::compat::CancellationToken;
use std::error::Error;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use tokio::sync::mpsc;
pub type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BackendError {
Configuration { component: &'static str },
EventQueueClosed,
RejectedMutation,
Operation { operation: &'static str },
}
impl fmt::Display for BackendError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Configuration { component } => {
write!(formatter, "backend configuration failed for {component}")
}
Self::EventQueueClosed => formatter.write_str("grid-event owner closed its queue"),
Self::RejectedMutation => {
formatter.write_str("world mutation lacks an approved policy decision")
}
Self::Operation { operation } => {
write!(formatter, "backend operation failed: {operation}")
}
}
}
}
impl Error for BackendError {}
/// One owned grid event source. Implementations send into the coordinator-owned
/// bounded queue and must finish when cancellation is requested.
pub trait GridBackend: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn run(
&self,
events: mpsc::Sender<GridEvent>,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<(), BackendError>>;
}
/// The sole world-mutation boundary. Later tool implementations cannot bypass
/// the policy decision passed to this trait, and fake/live implementations use
/// the same call shape.
pub trait WorldMutator: Send + Sync + 'static {
fn apply(
&self,
call: ProposedToolCall,
decision: PolicyDecision,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<ToolCallOutcome, BackendError>>;
}
/// Inert deterministic backend used by the foundational offline service.
///
/// It performs no login or network operation and is available without the
/// opt-in live-grid dependency graph.
#[derive(Clone, Copy, Debug, Default)]
pub struct OfflineGridBackend;
impl OfflineGridBackend {
#[must_use]
pub const fn new() -> Self {
Self
}
}
/// Live-feature composition owner that guarantees production adapters reuse
/// the existing `libremetaverse` manager/client graph.
#[cfg(feature = "live-grid")]
#[derive(Debug)]
pub struct LibremetaverseClientOwner {
client: libremetaverse::GridClient,
}
#[cfg(feature = "live-grid")]
impl LibremetaverseClientOwner {
/// Builds the shared client composition root without starting login or I/O.
///
/// # Errors
///
/// Returns a backend configuration error if the shared client defaults are invalid.
pub fn new() -> Result<Self, BackendError> {
let client = libremetaverse::GridClientBuilder::default()
.build()
.map_err(|_| BackendError::Configuration {
component: "libremetaverse client defaults",
})?;
Ok(Self { client })
}
#[must_use]
pub const fn client(&self) -> &libremetaverse::GridClient {
&self.client
}
}
impl GridBackend for OfflineGridBackend {
fn name(&self) -> &'static str {
"offline-fake"
}
fn run(
&self,
events: mpsc::Sender<GridEvent>,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<(), BackendError>> {
Box::pin(async move {
let ready = GridEvent {
sequence: 1,
kind: GridEventKind::BackendReady,
};
tokio::select! {
() = cancellation.cancelled() => return Ok(()),
result = events.send(ready) => {
result.map_err(|_| BackendError::EventQueueClosed)?;
}
}
cancellation.cancelled().await;
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use libremetaverse_types::compat::CancellationTokenSource;
#[tokio::test]
async fn offline_backend_stops_on_cancellation() {
let backend = OfflineGridBackend::new();
let (sender, mut receiver) = mpsc::channel(1);
let cancellation = CancellationTokenSource::new();
let run = backend.run(sender, cancellation.token());
tokio::pin!(run);
tokio::select! {
event = receiver.recv() => {
assert_eq!(event.expect("ready event").kind, GridEventKind::BackendReady);
}
result = &mut run => panic!("backend exited before ready: {result:?}"),
}
cancellation.cancel();
run.await.expect("clean cancellation");
}
}