feat(grid-agent): establish architecture and config (#118)
This commit is contained in:
156
crates/metacrate-grid-agent/src/backend.rs
Normal file
156
crates/metacrate-grid-agent/src/backend.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user