Stabilize OpenSim agent runtime and Mentra integration
Some checks failed
CI / rust-skia (Rust only) (push) Has been cancelled
CI / required (push) Has been cancelled

This commit is contained in:
2026-08-22 10:44:09 +02:00
parent 2f5f03ac6f
commit 0dd2ca5824
28 changed files with 1765 additions and 2308 deletions

View File

@@ -1,149 +1,49 @@
use libremetaverse_types::UUID;
use libremetaverse_types::compat::CancellationTokenSource;
use metacrate_grid_agent::{
AgentConfig, ConversationChannel, ConversationKey, ConversationLimits, ConversationStore,
LlmClient, LlmTransportLimits, MemoryRecord,
ConversationChannel, ConversationKey, ConversationLimits, ConversationStore, MemoryRecord,
};
use serde_json::{Value, json};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::net::TcpListener;
use tokio::sync::mpsc;
const MAX_REQUEST_BYTES: usize = 1024 * 1024;
fn avatar(number: u64) -> UUID {
UUID::new_with_u_int64(number).expect("fixture UUID")
}
fn key(number: u64, channel: ConversationChannel) -> ConversationKey {
ConversationKey::new(avatar(number), channel).expect("nonzero fixture")
ConversationKey::new(
UUID::new_with_u_int64(number).expect("fixture UUID"),
channel,
)
.expect("nonzero fixture")
}
async fn capture_server(
request_count: usize,
) -> (String, mpsc::Receiver<Value>, tokio::task::JoinHandle<()>) {
let listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
.await
.expect("bind fake LLM");
let address = listener.local_addr().expect("listener address");
let (sender, receiver) = mpsc::channel(request_count);
let task = tokio::spawn(async move {
for _ in 0..request_count {
let (mut stream, _) = listener.accept().await.expect("LLM connection");
let request = read_request(&mut stream).await;
sender.send(request).await.expect("capture receiver");
let response = serde_json::to_vec(&json!({
"choices": [{"message": {"content": "ok", "tool_calls": []}}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
}))
.expect("response JSON");
let head = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
response.len()
);
stream
.write_all(head.as_bytes())
.await
.expect("response head");
stream.write_all(&response).await.expect("response body");
stream.shutdown().await.expect("response shutdown");
}
});
(format!("http://{address}/exact/chat"), receiver, task)
}
async fn read_request(stream: &mut tokio::net::TcpStream) -> Value {
let mut bytes = Vec::new();
let header_end = loop {
assert!(bytes.len() < MAX_REQUEST_BYTES, "bounded request headers");
let mut chunk = [0_u8; 2048];
let count = stream.read(&mut chunk).await.expect("request read");
assert!(count > 0, "complete request headers");
bytes.extend_from_slice(&chunk[..count]);
if let Some(position) = bytes.windows(4).position(|window| window == b"\r\n\r\n") {
break position + 4;
}
};
let headers = std::str::from_utf8(&bytes[..header_end]).expect("UTF-8 headers");
let content_length = headers
.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("content-length")
.then(|| value.trim().parse::<usize>().expect("content length"))
})
.expect("content-length header");
assert!(content_length <= MAX_REQUEST_BYTES, "bounded request body");
while bytes.len() - header_end < content_length {
let mut chunk = [0_u8; 4096];
let count = stream.read(&mut chunk).await.expect("request body read");
assert!(count > 0, "complete request body");
bytes.extend_from_slice(&chunk[..count]);
}
serde_json::from_slice(&bytes[header_end..header_end + content_length]).expect("request JSON")
}
#[tokio::test]
async fn actual_llm_requests_never_cross_avatar_or_channel_boundaries() {
let (endpoint, mut requests, server) = capture_server(3).await;
let config = AgentConfig::offline(&endpoint, "test-key").expect("offline config");
let limits = LlmTransportLimits {
connect_timeout: Duration::from_secs(1),
request_timeout: Duration::from_secs(2),
read_idle_timeout: Duration::from_secs(1),
pool_idle_timeout: Duration::from_secs(1),
total_timeout: Duration::from_secs(3),
max_prompt_bytes: 64 * 1024,
max_response_bytes: 64 * 1024,
max_concurrent_requests: 2,
max_retries: 0,
max_retry_delay: Duration::from_millis(10),
};
let client = Arc::new(LlmClient::new(config.llm, limits).expect("LLM client"));
#[test]
fn contexts_never_cross_avatar_or_channel_boundaries() {
let store = ConversationStore::open(ConversationLimits::default(), None).expect("store");
let alice_public = key(1, ConversationChannel::PublicChat);
let alice_direct = key(1, ConversationChannel::DirectIm);
let bob_public = key(2, ConversationChannel::PublicChat);
store
.append(
alice_public,
MemoryRecord::avatar_message("alice-public-only"),
)
.expect("alice public");
store
.append(
alice_direct,
MemoryRecord::avatar_message("alice-direct-only"),
)
.expect("alice direct");
store
.append(bob_public, MemoryRecord::avatar_message("bob-public-only"))
.expect("bob public");
for session_key in [alice_public, alice_direct, bob_public] {
let sessions = [
(key(1, ConversationChannel::PublicChat), "alice-public-only"),
(key(1, ConversationChannel::DirectIm), "alice-direct-only"),
(key(2, ConversationChannel::PublicChat), "bob-public-only"),
];
for (session, text) in sessions {
store
.append(session, MemoryRecord::avatar_message(text))
.expect("append isolated message");
}
for (session, own_text) in sessions {
let messages = store
.context(session_key)
.context(session)
.expect("isolated context")
.llm_messages()
.expect("LLM messages");
client
.complete(&messages, &[], &CancellationTokenSource::new().token())
.await
.expect("fake completion");
}
let expected = ["alice-public-only", "alice-direct-only", "bob-public-only"];
for own_text in expected {
let request = requests.recv().await.expect("captured request");
let serialized = serde_json::to_string(&request).expect("request string");
let serialized = messages
.iter()
.flat_map(|message| message.content.as_slice())
.filter_map(|part| match part {
metacrate_grid_agent::ContentPart::Text(text) => Some(text.as_str()),
metacrate_grid_agent::ContentPart::Image { .. } => None,
})
.collect::<Vec<_>>()
.join("\n");
assert!(serialized.contains(own_text));
for other_text in expected {
for (_, other_text) in sessions {
if other_text != own_text {
assert!(!serialized.contains(other_text));
}
}
}
server.await.expect("capture server");
}

View File

@@ -2,16 +2,17 @@ use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
const ALLOWED_DEPENDENCIES: [&str; 18] = [
const ALLOWED_DEPENDENCIES: [&str; 19] = [
"async-trait",
"base64",
"crossterm",
"libremetaverse",
"libremetaverse-imaging",
"libremetaverse-rendering-simple",
"metacrate-lsl-tools",
"png",
"mentra",
"jpeg-encoder",
"libremetaverse-types",
"reqwest",
"rustls",
"serde",
"serde_json",

View File

@@ -4,9 +4,8 @@ use metacrate_grid_agent::testing::{
adversarial_corpus,
};
use metacrate_grid_agent::{
BehaviorRandom, BuildGrid, BuildPrim, BuildShape, CompletionMessage, GridSessionBackend,
LandmarkGrid, LlmClient, LlmError, LlmTransportLimits, MessageRole, ReconnectPolicy,
RoamingRandom, ScriptInventory, SessionState, SessionSupervisor,
BehaviorRandom, BuildGrid, BuildPrim, BuildShape, GridSessionBackend, LandmarkGrid,
ReconnectPolicy, RoamingRandom, ScriptInventory, SessionState, SessionSupervisor,
};
use std::collections::BTreeSet;
use std::sync::Arc;
@@ -281,57 +280,3 @@ fn corpus_randomness_scripts_and_load_metrics_are_reproducible_and_redacted() {
assert_eq!(metrics.dropped_events, 2);
metrics.assert_drained().unwrap();
}
#[cfg(any(unix, windows))]
#[tokio::test]
async fn local_openai_peer_records_only_redacted_schema_and_scripts_multimodal_rejection() {
use metacrate_grid_agent::testing::FakeOpenAiEndpoint;
use metacrate_grid_agent::{BoundedText, BoundedVec, ContentPart, ImageDetail};
let endpoint = FakeOpenAiEndpoint::start(vec![FakeLlmStep::MultimodalRejected])
.await
.expect("loopback endpoint");
let config = metacrate_grid_agent::AgentConfig::offline(endpoint.url(), "SECRET_CANARY")
.expect("offline config");
let client = LlmClient::new(
config.llm,
LlmTransportLimits {
max_retries: 3,
..LlmTransportLimits::default()
},
)
.unwrap();
let message = CompletionMessage {
role: MessageRole::Avatar,
content: BoundedVec::try_from_vec(
"content",
vec![ContentPart::Image {
url: BoundedText::new("url", "data:image/png;base64,aW1hZ2U=").unwrap(),
detail: ImageDetail::Low,
}],
)
.unwrap(),
tool_call_id: None,
proposed_calls: BoundedVec::new(),
};
assert_eq!(
client
.complete(&[message], &[], &CancellationToken::default())
.await
.unwrap_err(),
LlmError::MultimodalUnsupported
);
tokio::task::yield_now().await;
let requests = endpoint.requests();
assert_eq!(requests.len(), 1, "large request is never retried");
assert!(requests[0].has_image);
assert_eq!(requests[0].body_sha256.len(), 64);
assert!(!format!("{requests:?}").contains("SECRET_CANARY"));
let reproduction = endpoint.reproduction(99);
assert!(reproduction.contains("seed=99"));
assert!(!reproduction.contains("SECRET_CANARY"));
for _ in 0..8 {
tokio::task::yield_now().await;
}
assert_eq!(endpoint.active_sockets(), 0);
drop(endpoint);
}

File diff suppressed because it is too large Load Diff