feat(grid-agent): add portable control plane (#126)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m44s
CI / required (push) Failing after 2m43s

This commit is contained in:
2026-08-18 04:29:12 +00:00
parent 058ed10005
commit 962d17257d
16 changed files with 4023 additions and 31 deletions

View File

@@ -147,8 +147,10 @@ async fn run_live(
run_once: bool,
) -> Result<(), Box<dyn Error>> {
use metacrate_grid_agent::{
GridSessionBackend, LibremetaverseClientOwner, SessionObservation, SessionState,
SessionSupervisor,
AgentControlTarget, BehaviorObservation, ControlEventKind, ControlPlane, ControlTarget,
GridSessionBackend, LibremetaverseClientOwner, OperatingMode, RuntimeControlCommand,
SessionControl, SessionObservation, SessionState, SessionSupervisor, TcpControlConfig,
TcpControlServer,
};
let connection = config.grid.clone().ok_or_else(|| {
@@ -230,8 +232,56 @@ async fn run_live(
return Ok(());
}
let (control_target, mut control_commands) = AgentControlTarget::new(
live.conversations.clone(),
live.policy.clone(),
live.audit.clone(),
live.behavior.ingress(),
config.limits.control_queue,
)?;
control_target.update_session(handle.status());
let erased_target: Arc<dyn ControlTarget> = control_target.clone();
let (control_plane, _integrated_client, control_server) = match config.mode {
OperatingMode::Integrated => {
let (plane, client) = ControlPlane::integrated(erased_target, config.control.limits)?;
(plane, Some(client), None)
}
OperatingMode::SplitService => {
let operator = config.control.operator_token.clone().ok_or_else(|| {
CliError("validated split configuration omitted its operator token".into())
})?;
let plane = ControlPlane::new(
erased_target,
operator,
config.control.observer_token.clone(),
config.control.limits,
)?;
let transport = TcpControlConfig {
listen: config.control.listen,
limits: config.control.limits,
};
let server = if let Some(tls) = &config.control.remote_tls {
eprintln!(
"WARNING: remote control is enabled with TLS; protect operator tokens and certificate keys"
);
TcpControlServer::bind_tls(plane.clone(), transport, tls.server_config()?).await?
} else {
TcpControlServer::bind(plane.clone(), transport).await?
};
println!(
"grid agent control plane listening on {}",
server.local_addr()
);
(plane, None, Some(server))
}
OperatingMode::OfflineFake => {
return Err(CliError("offline mode reached the live control plane".into()).into());
}
};
println!("grid agent session supervisor started; press Ctrl-C to stop");
let mut signal_error = None;
let mut control_failure = None;
loop {
tokio::select! {
signal = tokio::signal::ctrl_c() => {
@@ -243,6 +293,11 @@ async fn run_live(
event = handle.next_observation() => {
let Some(event) = event else { break; };
if let SessionObservation::Transition { status, reason, retry_in } = event {
control_target.update_session(status);
control_plane.publish(ControlEventKind::StateChanged {
component: "session".into(),
state: status.state.as_str().into(),
});
println!(
"grid session state={} generation={} transport_connected={} agent_ready={} reason={reason:?} retry_in={retry_in:?}",
status.state.as_str(),
@@ -262,19 +317,66 @@ async fn run_live(
}
event = live.behavior.next_observation() => {
let Some(event) = event else { break; };
if let BehaviorObservation::Transition { to, .. } = &event {
control_target.update_behavior(*to);
control_plane.publish(ControlEventKind::StateChanged {
component: "behavior".into(),
state: format!("{to:?}").to_ascii_lowercase(),
});
}
println!("grid behavior event={event:?}");
}
command = control_commands.recv() => {
let Some(command) = command else { break; };
match command {
RuntimeControlCommand::Pause => {
if let Err(error) = handle.control(SessionControl::Pause).await {
control_failure = Some(error.to_string());
break;
}
}
RuntimeControlCommand::Resume => {
if let Err(error) = handle.control(SessionControl::Resume).await {
control_failure = Some(error.to_string());
break;
}
}
RuntimeControlCommand::ForceReconnect => {
if let Err(error) = handle.control(SessionControl::ForceReconnect).await {
control_failure = Some(error.to_string());
break;
}
}
RuntimeControlCommand::OperatorMessage { message } => {
control_plane.publish(ControlEventKind::StateChanged {
component: "operator_message".into(),
state: format!("accepted_{}_bytes", message.len()),
});
}
RuntimeControlCommand::GracefulShutdown => break,
}
}
}
}
control_target.mark_stopping();
let session_result = handle.shutdown().await;
let interaction_result = live.interaction.shutdown().await;
let behavior_result = live.behavior.shutdown().await;
let control_result = if let Some(server) = control_server {
server.shutdown().await
} else {
Ok(())
};
session_result?;
interaction_result?;
behavior_result?;
control_result?;
if let Some(error) = signal_error {
return Err(error.into());
}
if let Some(error) = control_failure {
return Err(CliError(format!("control command failed: {error}")).into());
}
println!("grid agent stopped cleanly");
Ok(())
}
@@ -286,9 +388,13 @@ struct LiveInteractions {
perception_observations:
tokio::sync::mpsc::Receiver<metacrate_grid_agent::PerceptionObservation>,
behavior: metacrate_grid_agent::BehaviorHandle,
conversations: Arc<metacrate_grid_agent::ConversationStore>,
policy: Arc<metacrate_grid_agent::PolicyGateway>,
audit: Arc<metacrate_grid_agent::MemoryPolicyAudit>,
}
#[cfg(feature = "live-grid")]
#[allow(clippy::too_many_lines)]
fn start_live_interactions(
config: &metacrate_grid_agent::AgentConfig,
owner: &metacrate_grid_agent::LibremetaverseClientOwner,
@@ -347,7 +453,7 @@ fn start_live_interactions(
config.authorized_avatar_uuids.clone(),
tools,
PolicyLimits::default(),
audit,
audit.clone(),
)?);
let loop_limits = ToolLoopLimits {
max_tool_calls_per_turn: config.limits.max_tool_calls,
@@ -366,7 +472,7 @@ fn start_live_interactions(
Arc::new(AuthorizedBackendRouter::new(routes)?);
let responder = Arc::new(PolicyLlmResponder::new(
client,
gateway,
gateway.clone(),
routed_backend,
loop_limits,
now,
@@ -377,7 +483,7 @@ fn start_live_interactions(
config.interaction.clone(),
libremetaverse_types::UUID::zero(),
config.authorized_avatar_uuids.clone(),
conversation,
conversation.clone(),
responder,
sink,
config.limits.grid_event_queue,
@@ -391,5 +497,8 @@ fn start_live_interactions(
perception: perception_ingress,
perception_observations,
behavior,
conversations: conversation,
policy: gateway,
audit,
})
}