feat(grid-agent): add portable control plane (#126)
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user