feat(grid-agent): add bounded world perception tools (#124)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m48s
CI / required (push) Failing after 2m43s

This commit is contained in:
2026-08-17 23:52:33 +00:00
parent 26fd2bf714
commit 3a0ead7eb5
7 changed files with 2269 additions and 37 deletions

View File

@@ -22,24 +22,6 @@ impl fmt::Display for CliError {
impl Error for CliError {}
#[cfg(feature = "live-grid")]
#[derive(Debug)]
struct DenyUnregisteredTools;
#[cfg(feature = "live-grid")]
impl metacrate_grid_agent::AuthorizedToolBackend for DenyUnregisteredTools {
fn apply(
&self,
_action: metacrate_grid_agent::AuthorizedAction,
_cancellation: libremetaverse_types::compat::CancellationToken,
) -> metacrate_grid_agent::BackendFuture<
'_,
Result<metacrate_grid_agent::ToolCallOutcome, metacrate_grid_agent::BackendError>,
> {
Box::pin(async { Err(metacrate_grid_agent::BackendError::RejectedMutation) })
}
}
#[derive(Default)]
struct Options {
config: Option<PathBuf>,
@@ -173,11 +155,15 @@ async fn run_live(
CliError("validated live configuration did not contain a grid connection".into())
})?;
let owner = LibremetaverseClientOwner::new()?;
let mut interaction = start_live_interactions(&config, &owner)?;
let backend = match owner.session_backend_with_interaction(connection, interaction.ingress()) {
let mut live = start_live_interactions(&config, &owner)?;
let backend = match owner.session_backend_with_services(
connection,
live.interaction.ingress(),
live.perception.clone(),
) {
Ok(backend) => backend,
Err(error) => {
interaction.shutdown().await?;
live.interaction.shutdown().await?;
return Err(error.into());
}
};
@@ -225,13 +211,13 @@ async fn run_live(
};
if let Err(error) = readiness {
let session_result = handle.shutdown().await;
let interaction_result = interaction.shutdown().await;
let interaction_result = live.interaction.shutdown().await;
session_result?;
interaction_result?;
return Err(error.into());
}
let session_result = handle.shutdown().await;
let interaction_result = interaction.shutdown().await;
let interaction_result = live.interaction.shutdown().await;
session_result?;
interaction_result?;
println!("grid agent completed one supervised login/logout cycle");
@@ -260,14 +246,18 @@ async fn run_live(
);
}
}
event = interaction.next_observation() => {
event = live.interaction.next_observation() => {
let Some(event) = event else { break; };
println!("grid interaction event={event:?}");
}
event = live.perception_observations.recv() => {
let Some(event) = event else { break; };
println!("grid perception event={event:?}");
}
}
}
let session_result = handle.shutdown().await;
let interaction_result = interaction.shutdown().await;
let interaction_result = live.interaction.shutdown().await;
session_result?;
interaction_result?;
if let Some(error) = signal_error {
@@ -277,14 +267,23 @@ async fn run_live(
Ok(())
}
#[cfg(feature = "live-grid")]
struct LiveInteractions {
interaction: metacrate_grid_agent::InteractionHandle,
perception: metacrate_grid_agent::PerceptionIngress,
perception_observations:
tokio::sync::mpsc::Receiver<metacrate_grid_agent::PerceptionObservation>,
}
#[cfg(feature = "live-grid")]
fn start_live_interactions(
config: &metacrate_grid_agent::AgentConfig,
owner: &metacrate_grid_agent::LibremetaverseClientOwner,
) -> Result<metacrate_grid_agent::InteractionHandle, Box<dyn Error>> {
) -> Result<LiveInteractions, Box<dyn Error>> {
use metacrate_grid_agent::{
ConversationStore, InteractionCoordinator, LlmClient, LlmTransportLimits,
MemoryPolicyAudit, PolicyGateway, PolicyLimits, PolicyLlmResponder, ToolLoopLimits,
AuthorizedToolBackend, ConversationStore, InteractionCoordinator, LlmClient,
LlmTransportLimits, MemoryPolicyAudit, PerceptionBackend, PolicyGateway, PolicyLimits,
PolicyLlmResponder, ToolLoopLimits, perception_policy_tools,
};
let transport_limits = LlmTransportLimits {
@@ -295,11 +294,21 @@ fn start_live_interactions(
max_concurrent_requests: config.interaction.max_concurrent_inference,
..LlmTransportLimits::default()
};
let conversation = Arc::new(ConversationStore::from_config(config)?);
let perception = Arc::new(PerceptionBackend::new(
Arc::new(owner.world_snapshot_source()),
Arc::clone(&conversation),
config.limits.observable_queue,
)?);
let perception_ingress = perception.ingress();
let perception_observations = perception
.take_observations()
.ok_or_else(|| CliError("perception observation receiver already claimed".into()))?;
let client = Arc::new(LlmClient::new(config.llm.clone(), transport_limits)?);
let audit = Arc::new(MemoryPolicyAudit::new(config.limits.observable_queue)?);
let gateway = Arc::new(PolicyGateway::new(
config.authorized_avatar_uuids.clone(),
Vec::new(),
perception_policy_tools()?,
PolicyLimits::default(),
audit,
)?);
@@ -316,16 +325,16 @@ fn start_live_interactions(
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_secs())
});
let perception_backend: Arc<dyn AuthorizedToolBackend> = perception;
let responder = Arc::new(PolicyLlmResponder::new(
client,
gateway,
Arc::new(DenyUnregisteredTools),
perception_backend,
loop_limits,
now,
)?);
let conversation = Arc::new(ConversationStore::from_config(config)?);
let sink = Arc::new(owner.interaction_sink());
Ok(InteractionCoordinator::new(
let interaction = InteractionCoordinator::new(
config.interaction.clone(),
libremetaverse_types::UUID::zero(),
config.authorized_avatar_uuids.clone(),
@@ -336,5 +345,10 @@ fn start_live_interactions(
config.limits.observable_queue,
config.timeouts.shutdown,
)?
.start())
.start();
Ok(LiveInteractions {
interaction,
perception: perception_ingress,
perception_observations,
})
}