feat(grid-agent): add embodied behavior controller (#125)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m42s
CI / required (push) Failing after 2m40s

This commit is contained in:
2026-08-18 00:26:24 +00:00
parent 3a0ead7eb5
commit 058ed10005
11 changed files with 2607 additions and 22 deletions

View File

@@ -156,14 +156,16 @@ async fn run_live(
})?;
let owner = LibremetaverseClientOwner::new()?;
let mut live = start_live_interactions(&config, &owner)?;
let backend = match owner.session_backend_with_services(
let backend = match owner.session_backend_with_agent_services(
connection,
live.interaction.ingress(),
live.perception.clone(),
live.behavior.ingress(),
) {
Ok(backend) => backend,
Err(error) => {
live.interaction.shutdown().await?;
live.behavior.shutdown().await?;
return Err(error.into());
}
};
@@ -212,14 +214,18 @@ async fn run_live(
if let Err(error) = readiness {
let session_result = handle.shutdown().await;
let interaction_result = live.interaction.shutdown().await;
let behavior_result = live.behavior.shutdown().await;
session_result?;
interaction_result?;
behavior_result?;
return Err(error.into());
}
let session_result = handle.shutdown().await;
let interaction_result = live.interaction.shutdown().await;
let behavior_result = live.behavior.shutdown().await;
session_result?;
interaction_result?;
behavior_result?;
println!("grid agent completed one supervised login/logout cycle");
return Ok(());
}
@@ -254,12 +260,18 @@ async fn run_live(
let Some(event) = event else { break; };
println!("grid perception event={event:?}");
}
event = live.behavior.next_observation() => {
let Some(event) = event else { break; };
println!("grid behavior event={event:?}");
}
}
}
let session_result = handle.shutdown().await;
let interaction_result = live.interaction.shutdown().await;
let behavior_result = live.behavior.shutdown().await;
session_result?;
interaction_result?;
behavior_result?;
if let Some(error) = signal_error {
return Err(error.into());
}
@@ -273,6 +285,7 @@ struct LiveInteractions {
perception: metacrate_grid_agent::PerceptionIngress,
perception_observations:
tokio::sync::mpsc::Receiver<metacrate_grid_agent::PerceptionObservation>,
behavior: metacrate_grid_agent::BehaviorHandle,
}
#[cfg(feature = "live-grid")]
@@ -281,9 +294,10 @@ fn start_live_interactions(
owner: &metacrate_grid_agent::LibremetaverseClientOwner,
) -> Result<LiveInteractions, Box<dyn Error>> {
use metacrate_grid_agent::{
AuthorizedToolBackend, ConversationStore, InteractionCoordinator, LlmClient,
LlmTransportLimits, MemoryPolicyAudit, PerceptionBackend, PolicyGateway, PolicyLimits,
PolicyLlmResponder, ToolLoopLimits, perception_policy_tools,
AuthorizedBackendRouter, AuthorizedToolBackend, BehaviorBackend, BehaviorController,
ConversationStore, InteractionCoordinator, LlmClient, LlmTransportLimits,
MemoryPolicyAudit, PerceptionBackend, PolicyGateway, PolicyLimits, PolicyLlmResponder,
ToolLoopLimits, behavior_policy_tools, perception_policy_tools,
};
let transport_limits = LlmTransportLimits {
@@ -304,11 +318,34 @@ fn start_live_interactions(
let perception_observations = perception
.take_observations()
.ok_or_else(|| CliError("perception observation receiver already claimed".into()))?;
let behavior = BehaviorController::new(
config.behavior.clone(),
Arc::new(owner.embodiment_sink()),
config.limits.control_queue,
config.limits.observable_queue,
config.timeouts.shutdown,
)?
.start();
let behavior_ingress = behavior.ingress();
let client = Arc::new(LlmClient::new(config.llm.clone(), transport_limits)?);
let audit = Arc::new(MemoryPolicyAudit::new(config.limits.observable_queue)?);
let mut tools = perception_policy_tools()?;
tools.extend(behavior_policy_tools(&config.behavior)?);
let routes = tools
.iter()
.map(|tool| tool.definition.name.as_str().to_owned())
.map(|name| {
let backend: Arc<dyn AuthorizedToolBackend> = if name.starts_with("behavior_") {
Arc::new(BehaviorBackend::new(behavior_ingress.clone()))
} else {
perception.clone()
};
(name, backend)
})
.collect::<Vec<_>>();
let gateway = Arc::new(PolicyGateway::new(
config.authorized_avatar_uuids.clone(),
perception_policy_tools()?,
tools,
PolicyLimits::default(),
audit,
)?);
@@ -325,15 +362,17 @@ fn start_live_interactions(
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_secs())
});
let perception_backend: Arc<dyn AuthorizedToolBackend> = perception;
let routed_backend: Arc<dyn AuthorizedToolBackend> =
Arc::new(AuthorizedBackendRouter::new(routes)?);
let responder = Arc::new(PolicyLlmResponder::new(
client,
gateway,
perception_backend,
routed_backend,
loop_limits,
now,
)?);
let sink = Arc::new(owner.interaction_sink());
let pacer: Arc<dyn metacrate_grid_agent::ResponsePacer> = Arc::new(behavior_ingress);
let interaction = InteractionCoordinator::new(
config.interaction.clone(),
libremetaverse_types::UUID::zero(),
@@ -345,10 +384,12 @@ fn start_live_interactions(
config.limits.observable_queue,
config.timeouts.shutdown,
)?
.with_response_pacer(pacer)
.start();
Ok(LiveInteractions {
interaction,
perception: perception_ingress,
perception_observations,
behavior,
})
}