diff --git a/Cargo.lock b/Cargo.lock index 7cb8da4e92..c8740a18e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -373,6 +373,7 @@ dependencies = [ name = "agentos-vfs" version = "0.0.1" dependencies = [ + "agentos-runtime", "agentos-vfs-core", "async-trait", "aws-config", diff --git a/crates/agentos-sidecar/src/acp/mod.rs b/crates/agentos-sidecar/src/acp/mod.rs index bf31fb67fa..21da3884f5 100644 --- a/crates/agentos-sidecar/src/acp/mod.rs +++ b/crates/agentos-sidecar/src/acp/mod.rs @@ -1308,8 +1308,10 @@ mod tests { #[test] fn configured_acp_limit_errors_preserve_stable_wire_codes() { - let mut limits = AcpLimits::default(); - limits.max_prompt_bytes = 3; + let mut limits = AcpLimits { + max_prompt_bytes: 3, + ..AcpLimits::default() + }; let bytes_error = parse_content_blocks("[{}]", "main", &limits) .expect_err("prompt bytes must be bounded"); assert_eq!(error_code(&bytes_error), "acp_prompt_bytes_limit"); diff --git a/crates/agentos-sidecar/src/acp/restore.rs b/crates/agentos-sidecar/src/acp/restore.rs index 89f6e4ea5a..4433cdadba 100644 --- a/crates/agentos-sidecar/src/acp/restore.rs +++ b/crates/agentos-sidecar/src/acp/restore.rs @@ -35,7 +35,7 @@ impl AcpExtension { .restore_acp_runtime( ctx, RestoreRuntimeRequest { - acp_session_id: acp_session_id, + acp_session_id, agent_type: session.agent.clone(), cwd: session.cwd.clone(), env, diff --git a/crates/agentos-sidecar/src/acp/runtime.rs b/crates/agentos-sidecar/src/acp/runtime.rs index 1d2a10f892..3302d77da6 100644 --- a/crates/agentos-sidecar/src/acp/runtime.rs +++ b/crates/agentos-sidecar/src/acp/runtime.rs @@ -355,9 +355,9 @@ impl AcpExtension { .await; matches!(&sigterm, Err(error) if is_process_already_gone_error(error)) }; - let terminated = if adapter_already_gone { - true - } else if wait_for_process_exit(ctx, &session.process_id, SESSION_CLOSE_TIMEOUT).await { + let terminated = if adapter_already_gone + || wait_for_process_exit(ctx, &session.process_id, SESSION_CLOSE_TIMEOUT).await + { true } else { let sigkill = ctx @@ -404,12 +404,13 @@ impl AcpExtension { Ok(()) } + #[allow(clippy::needless_option_as_deref)] pub(super) async fn send_runtime_request_with_sink( &self, ctx: &mut ExtensionContext<'_>, request: AcpSessionRequest, mut durable_sink: Option<&mut DurableUpdateSink>, - mut cancellation: Option<&mut tokio::sync::watch::Receiver>, + cancellation: Option<&mut tokio::sync::watch::Receiver>, ) -> AcpHandlerOutput { let params = match request .params @@ -488,7 +489,7 @@ impl AcpExtension { &mut stdout_buffer, Some(&acp_session_id), durable_sink.as_deref_mut(), - cancellation.as_deref_mut(), + cancellation, ) .await { @@ -564,7 +565,7 @@ impl AcpExtension { }; match synthetic { Ok(Some(notification)) => { - let handled = if let Some(sink) = durable_sink.as_deref_mut() { + let handled = if let Some(sink) = durable_sink { match serde_json::from_str::(¬ification) { Ok(notification) => match sink .handle_notification(ctx, ¬ification, &mut exchange.events) diff --git a/crates/agentos-sidecar/src/acp/turn.rs b/crates/agentos-sidecar/src/acp/turn.rs index 8c87b200c2..8b298a2d44 100644 --- a/crates/agentos-sidecar/src/acp/turn.rs +++ b/crates/agentos-sidecar/src/acp/turn.rs @@ -553,6 +553,36 @@ impl AcpExtension { if output.response.is_err() { return output; } + let response = match &output.response { + Ok(AcpResponse::AcpSessionRpcResponse(response)) => { + match serde_json::from_str::(&response.response) { + Ok(response) => response, + Err(error) => { + return AcpHandlerOutput { + response: Err(SidecarError::InvalidState(format!( + "invalid ACP config update response JSON: {error}" + ))), + events: output.events, + }; + } + } + } + Ok(_) => { + return AcpHandlerOutput { + response: Err(SidecarError::InvalidState(String::from( + "invalid ACP config update response", + ))), + events: output.events, + }; + } + Err(_) => unreachable!("ACP transport errors returned above"), + }; + if let Err(error) = response_result(response, "ACP session/set_config_option") { + return AcpHandlerOutput { + response: Err(error), + events: output.events, + }; + } if let Err(error) = sink.flush(ctx, &mut output.events).await { return AcpHandlerOutput { response: Err(error), @@ -723,6 +753,7 @@ impl DurableUpdateSink { }) } + #[allow(clippy::too_many_arguments)] pub(super) async fn handle_permission_request( &mut self, ctx: &mut ExtensionContext<'_>, @@ -1144,6 +1175,7 @@ pub(super) fn decode_durable_event(event_json: &str) -> Result, process_id: &str, diff --git a/crates/agentos-sidecar/src/session_store.rs b/crates/agentos-sidecar/src/session_store.rs index 85b5036c01..980d435a22 100644 --- a/crates/agentos-sidecar/src/session_store.rs +++ b/crates/agentos-sidecar/src/session_store.rs @@ -262,7 +262,12 @@ impl SessionStore { vec![SqlValue::SqlText(session_id.to_owned())], )) .await?; - match result.rows.first().map(decode_session).transpose()? { + match result + .rows + .first() + .map(|row| decode_session(row)) + .transpose()? + { Some(mut session) => { self.hydrate_pending_state(&mut session).await?; Ok(Some(session)) @@ -271,6 +276,7 @@ impl SessionStore { } } + #[allow(clippy::too_many_arguments)] pub async fn create( &self, session_id: &str, @@ -409,7 +415,7 @@ impl SessionStore { let mut sessions = result .rows .iter() - .map(decode_session_summary) + .map(|row| decode_session_summary(row)) .collect::, _>>()?; self.hydrate_pending_summaries(&mut sessions).await?; Ok(sessions) @@ -457,7 +463,11 @@ impl SessionStore { vec![text(session_id), text(idempotency_key)], )) .await?; - result.rows.first().map(decode_prompt).transpose() + result + .rows + .first() + .map(|row| decode_prompt(row)) + .transpose() } pub async fn accept_prompt( @@ -550,9 +560,7 @@ impl SessionStore { let results = match self.database.transaction(statements).await { Ok(results) => results, Err(error @ VmSqliteError::UnexpectedChanges { .. }) => { - if let Err(limit_error) = self.ensure_prompt_capacity(session_id).await { - return Err(limit_error); - } + self.ensure_prompt_capacity(session_id).await?; return Err(error); } Err(error) => return Err(error), @@ -763,9 +771,7 @@ impl SessionStore { { Ok(results) => results, Err(error @ VmSqliteError::UnexpectedChanges { .. }) => { - if let Err(limit_error) = self.ensure_pending_capacity(session_id).await { - return Err(limit_error); - } + self.ensure_pending_capacity(session_id).await?; return Err(error); } Err(error) => return Err(error), @@ -1840,15 +1846,13 @@ fn validate_event(event: &Value) -> Result<&str, VmSqliteError> { event.get("reason").and_then(Value::as_str), ) { (Some("accepted"), None) => {} - (Some("not_pending"), Some(reason)) - if matches!( - reason, - "already_resolved" - | "prompt_cancelled" - | "adapter_exited" - | "session_deleted" - | "vm_shutdown" - ) => {} + ( + Some("not_pending"), + Some( + "already_resolved" | "prompt_cancelled" | "adapter_exited" + | "session_deleted" | "vm_shutdown", + ), + ) => {} _ => { return Err(VmSqliteError::InvalidResult( "permission_response event status/reason combination is invalid".to_owned(), @@ -2070,7 +2074,7 @@ fn derive_state_json( serde_json::to_string(&value).map_err(|error| VmSqliteError::InvalidResult(error.to_string())) } -fn decode_session(row: &Vec) -> Result { +fn decode_session(row: &[SqlValue]) -> Result { if row.len() != 26 { return Err(VmSqliteError::InvalidResult(format!( "session row has {} columns, expected 26", @@ -2127,7 +2131,7 @@ fn decode_session(row: &Vec) -> Result { }) } -fn decode_session_summary(row: &Vec) -> Result { +fn decode_session_summary(row: &[SqlValue]) -> Result { if row.len() != 12 { return Err(VmSqliteError::InvalidResult(format!( "session summary row has {} columns, expected 12", @@ -2229,7 +2233,7 @@ fn decode_pending_resolution( Ok(PendingRequestResolution::Terminal { reason, event }) } -fn decode_prompt(row: &Vec) -> Result { +fn decode_prompt(row: &[SqlValue]) -> Result { if row.len() != 5 { return Err(VmSqliteError::InvalidResult(format!( "prompt row has {} columns, expected 5", @@ -2559,9 +2563,11 @@ mod tests { .await .expect("database"); - let mut limits = AcpLimits::default(); - limits.max_session_history_events = 3; - limits.max_session_history_bytes = 1_000_000; + let mut limits = AcpLimits { + max_session_history_events: 3, + max_session_history_bytes: 1_000_000, + ..AcpLimits::default() + }; let store = SessionStore::open(database.clone()) .await .expect("store") @@ -2749,8 +2755,10 @@ mod tests { .expect("session"); assert_eq!(stale.oldest_retained_sequence, 1); - let mut limits = AcpLimits::default(); - limits.max_session_history_events = 2; + let limits = AcpLimits { + max_session_history_events: 2, + ..AcpLimits::default() + }; let request_store = SessionStore::from_database(database).with_limits(&limits); let refreshed = request_store .enforce_history_retention("main") @@ -3613,14 +3621,16 @@ mod tests { ) .await .expect("database"); - let mut limits = AcpLimits::default(); - limits.max_sessions_per_vm = 1; - limits.max_prompts_per_session = 1; - limits.max_prompts_per_vm = 1; - limits.max_pending_permissions_per_session = 1; - limits.max_pending_permissions_per_vm = 1; - limits.max_permission_outcomes_per_session = 1; - limits.max_permission_outcomes_per_vm = 1; + let limits = AcpLimits { + max_sessions_per_vm: 1, + max_prompts_per_session: 1, + max_prompts_per_vm: 1, + max_pending_permissions_per_session: 1, + max_pending_permissions_per_vm: 1, + max_permission_outcomes_per_session: 1, + max_permission_outcomes_per_vm: 1, + ..AcpLimits::default() + }; let store = SessionStore::open(database).await.expect("store").with_limits(&limits); store.create("main", "agent", "native", "/workspace", r#"{"permissionPolicy":"ask"}"#, None, None, "[]").await.expect("create"); assert!(matches!( @@ -3660,9 +3670,11 @@ mod tests { ) .await .expect("database"); - let mut limits = AcpLimits::default(); - limits.max_prompts_per_session = 1; - limits.max_prompts_per_vm = 1; + let mut limits = AcpLimits { + max_prompts_per_session: 1, + max_prompts_per_vm: 1, + ..AcpLimits::default() + }; let prompt_store = SessionStore::open(database.clone()) .await .expect("store") diff --git a/crates/agentos-sidecar/src/session_store/performance_tests.rs b/crates/agentos-sidecar/src/session_store/performance_tests.rs index 6418e54499..ff5e8bc1c3 100644 --- a/crates/agentos-sidecar/src/session_store/performance_tests.rs +++ b/crates/agentos-sidecar/src/session_store/performance_tests.rs @@ -448,9 +448,11 @@ async fn exercise_near_limit_append( populate_history(&database, HISTORY_COMPLEXITY_LIMIT - 1).await; let recording = RecordingDatabase::wrap(database); - let mut limits = AcpLimits::default(); - limits.max_session_history_events = HISTORY_COMPLEXITY_LIMIT; - limits.max_session_history_bytes = 16 * 1024 * 1024; + let limits = AcpLimits { + max_session_history_events: HISTORY_COMPLEXITY_LIMIT, + max_session_history_bytes: 16 * 1024 * 1024, + ..AcpLimits::default() + }; let store: SharedVmSqliteDatabase = recording.clone(); let store = SessionStore::from_database(store).with_limits(&limits); if let Some(metrics) = wire_metrics { diff --git a/crates/client/src/fs.rs b/crates/client/src/fs.rs index 6d2424be59..069a498083 100644 --- a/crates/client/src/fs.rs +++ b/crates/client/src/fs.rs @@ -926,12 +926,13 @@ impl AgentOs { async fn reconfigure_dynamic_mounts(&self) -> Result<()> { let inner = self.inner(); let config = &inner.config; + let mounts = inner.dynamic_mounts.lock().clone(); let response = self .transport() .request_wire( self.fs_vm_scope(), wire::RequestPayload::ConfigureVmRequest(wire::ConfigureVmRequest { - mounts: inner.dynamic_mounts.lock().clone(), + mounts, software: Vec::new(), permissions: Some(crate::agent_os::permissions_policy(config)), module_access_cwd: None, diff --git a/crates/kernel/src/kernel.rs b/crates/kernel/src/kernel.rs index 593d08b5b2..2f69643fef 100644 --- a/crates/kernel/src/kernel.rs +++ b/crates/kernel/src/kernel.rs @@ -2284,6 +2284,7 @@ impl KernelVm { .set_xattr(path, name, value, flags, follow_symlinks)?) } + #[allow(clippy::too_many_arguments)] pub fn set_xattr_for_process( &mut self, requester_driver: &str, @@ -9078,7 +9079,7 @@ struct PosixAcl { impl PosixAcl { fn parse(value: &[u8], path: &str) -> KernelResult { - if value.len() < 4 || (value.len() - 4) % 8 != 0 { + if value.len() < 4 || !(value.len() - 4).is_multiple_of(8) { return Err(invalid_acl(path, "invalid xattr length")); } let entry_count = (value.len() - 4) / 8; diff --git a/crates/native-sidecar-core/src/limits.rs b/crates/native-sidecar-core/src/limits.rs index 9b1ef9773c..72554dff45 100644 --- a/crates/native-sidecar-core/src/limits.rs +++ b/crates/native-sidecar-core/src/limits.rs @@ -1928,7 +1928,8 @@ mod tests { .to_string() .contains("limits.resources.maxSocketBufferedBytes")); - let acp_relationship_cases: [(&str, &str, fn(&mut VmLimits)); 3] = [ + type AcpRelationshipCase = (&'static str, &'static str, fn(&mut VmLimits)); + let acp_relationship_cases: [AcpRelationshipCase; 3] = [ ( "limits.acp.maxPromptsPerSession", "limits.acp.maxPromptsPerVm", diff --git a/crates/native-sidecar/src/filesystem.rs b/crates/native-sidecar/src/filesystem.rs index 57719cf4f5..5a2a808223 100644 --- a/crates/native-sidecar/src/filesystem.rs +++ b/crates/native-sidecar/src/filesystem.rs @@ -2658,16 +2658,12 @@ pub(crate) fn service_javascript_fs_sync_rpc( } else { kernel.chown_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path, uid, gid, true) }; - if is_lchown && result.as_ref().is_err_and(|error| error.code() == "ENOENT") { - if materialize_process_shadow_symlink(kernel, process, kernel_pid, path)? { - result = kernel.lchown_for_process( - EXECUTION_DRIVER_NAME, - kernel_pid, - path, - uid, - gid, - ); - } + if is_lchown + && result.as_ref().is_err_and(|error| error.code() == "ENOENT") + && materialize_process_shadow_symlink(kernel, process, kernel_pid, path)? + { + result = + kernel.lchown_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path, uid, gid); } result.map(|()| Value::Null).map_err(kernel_error) } @@ -2943,7 +2939,7 @@ fn normalize_process_filesystem_rpc_path(process: &ActiveProcess, path: &str) -> return guest_path; } if let Some(sandbox_root) = process.shadow_root.as_ref() { - if let Ok(suffix) = normalized_host_path.strip_prefix(&sandbox_root) { + if let Ok(suffix) = normalized_host_path.strip_prefix(sandbox_root) { let suffix = suffix.to_string_lossy(); return normalize_path(&format!("/{}", suffix.trim_start_matches('/'))); } @@ -3933,14 +3929,14 @@ fn mapped_child_rename_at2( { let flags = nix::fcntl::RenameFlags::from_bits(flags) .ok_or_else(|| std::io::Error::from_raw_os_error(libc::EINVAL))?; - return nix::fcntl::renameat2( + nix::fcntl::renameat2( Some(source.directory.as_raw_fd()), source.child_name.as_os_str(), Some(destination.directory.as_raw_fd()), destination.child_name.as_os_str(), flags, ) - .map_err(errno_to_io); + .map_err(errno_to_io) } #[cfg(not(all(target_os = "linux", target_env = "gnu")))] diff --git a/crates/native-sidecar/src/plugins/object_s3.rs b/crates/native-sidecar/src/plugins/object_s3.rs index 61b1e9cc35..c1802b50e3 100644 --- a/crates/native-sidecar/src/plugins/object_s3.rs +++ b/crates/native-sidecar/src/plugins/object_s3.rs @@ -70,7 +70,6 @@ impl FileSystemPluginFactory> for ObjectS3MountPlugin { gid: config.gid.unwrap_or(0), file_mode: config.file_mode.unwrap_or(0o644), dir_mode: config.dir_mode.unwrap_or(0o755), - ..ObjectFsOptions::default() }, ); Ok(Box::new(MountedEngineFileSystem::with_runtime_context( diff --git a/crates/native-sidecar/tests/stdio_binary.rs b/crates/native-sidecar/tests/stdio_binary.rs index 4a019e35e9..ec9ee68b72 100644 --- a/crates/native-sidecar/tests/stdio_binary.rs +++ b/crates/native-sidecar/tests/stdio_binary.rs @@ -744,7 +744,11 @@ fn native_sidecar_binary_runs_the_framed_protocol_over_stdio() { wire_request( 13, wire_vm(&connection_id, &session_id, &vm_id), - RequestPayload::SnapshotRootFilesystemRequest, + RequestPayload::SnapshotRootFilesystemRequest( + agentos_native_sidecar::wire::SnapshotRootFilesystemRequest { + max_bytes: 64 * 1024 * 1024, + }, + ), ), ); let snapshot = recv_response(&mut control, &codec, 13, &mut buffered_events); @@ -985,6 +989,31 @@ fn native_sidecar_binary_supports_js_bridge_host_filesystem_access() { ("realpath", "/existing.txt") => { js_bridge_result(call, Some(json!("/existing.txt")), None) } + ("stat" | "lstat", "/existing.txt") => { + let metadata = + fs::metadata(host_root.join("existing.txt")).expect("stat host file"); + js_bridge_result( + call, + Some(json!({ + "mode": 0o644, + "size": metadata.len(), + "blocks": 0, + "dev": 1, + "rdev": 0, + "isDirectory": false, + "isSymbolicLink": false, + "atimeMs": 0, + "mtimeMs": 0, + "ctimeMs": 0, + "birthtimeMs": 0, + "ino": 2, + "nlink": 1, + "uid": 0, + "gid": 0, + })), + None, + ) + } ("readFile", "/existing.txt") => js_bridge_result( call, Some(serde_json::Value::String( @@ -994,6 +1023,9 @@ fn native_sidecar_binary_supports_js_bridge_host_filesystem_access() { )), None, ), + ("utimes", "/existing.txt") => { + js_bridge_result(call, Some(serde_json::Value::Null), None) + } other => panic!("unexpected js bridge read callback: {other:?}"), } }, diff --git a/crates/native-sidecar/tests/support/mod.rs b/crates/native-sidecar/tests/support/mod.rs index f199082312..7e9411410f 100644 --- a/crates/native-sidecar/tests/support/mod.rs +++ b/crates/native-sidecar/tests/support/mod.rs @@ -11,6 +11,7 @@ use agentos_native_sidecar::protocol::{ }; use agentos_native_sidecar::{DispatchResult, NativeSidecar, NativeSidecarConfig}; pub use bridge_support::RecordingBridge; +#[allow(unused_imports)] pub(crate) use s3_common::test_support::MockS3Server; use std::collections::{BTreeMap, HashMap}; use std::fs; diff --git a/crates/native-sidecar/tests/xfstests_correctness.rs b/crates/native-sidecar/tests/xfstests_correctness.rs index 9f9675b788..175fcdf938 100644 --- a/crates/native-sidecar/tests/xfstests_correctness.rs +++ b/crates/native-sidecar/tests/xfstests_correctness.rs @@ -2405,7 +2405,7 @@ fn xfstests_looptest_scaled_throughput_probe() { &connection_id, &session_id, &vm_id, - xfstests_mounts(&source, root.path(), &backend, s3_endpoint.as_deref()), + xfstests_mounts(&source, root.path(), &backend, s3_endpoint), ); let truncate_arg = if truncate { "-t" } else { "" }; @@ -5420,7 +5420,7 @@ fn xfstests_truncfile_scaled_throughput_regression() { let connection_id = authenticate_wire(&mut sidecar, "conn-xfstests-truncfile-throughput"); let session_id = open_session_wire(&mut sidecar, 2, &connection_id); let vm_id = create_xfstests_vm_wire(&mut sidecar, 3, &connection_id, &session_id, &cwd); - let mut mounts = xfstests_mounts(&source, root.path(), &backend, s3_endpoint.as_deref()); + let mut mounts = xfstests_mounts(&source, root.path(), &backend, s3_endpoint); mounts.push(host_dir_mount( &format!("/__secure_exec/commands/{}", COMMAND_PACKAGES.len()), &source.join("src"), @@ -6364,8 +6364,7 @@ fn quick_tests(source: &Path) -> Vec { .filter_map(|line| { let fields = line.split_whitespace().collect::>(); fields - .iter() - .any(|field| *field == "quick") + .contains(&"quick") .then(|| format!("generic/{}", fields[0])) }) .collect::>(); diff --git a/crates/vfs-store/Cargo.toml b/crates/vfs-store/Cargo.toml index 2c3dd8e101..3252ceedd4 100644 --- a/crates/vfs-store/Cargo.toml +++ b/crates/vfs-store/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0" vfs = { workspace = true } [dev-dependencies] +agentos-runtime = { workspace = true } aws-config = "1" aws-credential-types = "1" tempfile = "3" diff --git a/crates/vfs-store/src/local/sqlite_metadata_store.rs b/crates/vfs-store/src/local/sqlite_metadata_store.rs index 3dbf70a580..89990d4b5e 100644 --- a/crates/vfs-store/src/local/sqlite_metadata_store.rs +++ b/crates/vfs-store/src/local/sqlite_metadata_store.rs @@ -267,13 +267,19 @@ impl SqliteMetadataStore { upsert_inode(connection, meta)?; if storage_changed { connection - .execute("DELETE FROM agentos_fs_chunks WHERE ino = ?", params![meta.ino]) + .execute( + "DELETE FROM agentos_fs_chunks WHERE ino = ?", + params![meta.ino], + ) .map_err(|err| VfsError::eio(format!("delete setattr chunks: {err}")))?; for key in affected_keys { let refcount = self.inner.refcount(&key); if refcount == 0 { connection - .execute("DELETE FROM agentos_fs_block_refs WHERE block_key = ?", params![key.0]) + .execute( + "DELETE FROM agentos_fs_block_refs WHERE block_key = ?", + params![key.0], + ) .map_err(|err| { VfsError::eio(format!("delete setattr block ref {}: {err}", key.0)) })?; @@ -399,7 +405,9 @@ impl SqliteMetadataStore { if new_size < old_size { connection - .prepare_cached("DELETE FROM agentos_fs_chunks WHERE ino = ? AND chunk_index >= ?") + .prepare_cached( + "DELETE FROM agentos_fs_chunks WHERE ino = ? AND chunk_index >= ?", + ) .map_err(|err| VfsError::eio(format!("prepare truncated chunk delete: {err}")))? .execute(params![ino, keep_chunks]) .map_err(|err| { @@ -425,7 +433,10 @@ impl SqliteMetadataStore { let refcount = self.inner.refcount(&key); if refcount == 0 { connection - .execute("DELETE FROM agentos_fs_block_refs WHERE block_key = ?", params![key.0]) + .execute( + "DELETE FROM agentos_fs_block_refs WHERE block_key = ?", + params![key.0], + ) .map_err(|err| { VfsError::eio(format!("delete SQLite block ref {}: {err}", key.0)) })?; diff --git a/crates/vfs-store/tests/local.rs b/crates/vfs-store/tests/local.rs index 09703a4511..4cbe726a7e 100644 --- a/crates/vfs-store/tests/local.rs +++ b/crates/vfs-store/tests/local.rs @@ -436,11 +436,14 @@ async fn chunked_local_reopens_and_cleans_stale_blocks() { #[test] fn chunked_local_mounted_adapter_creates_exclusive_files_with_modes() { let temp = tempfile::tempdir().unwrap(); + let runtime = + agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + .expect("create test runtime"); let fs = ChunkedFs::new( SqliteMetadataStore::open(temp.path().join("metadata.sqlite")).unwrap(), FileBlockStore::new(temp.path().join("blocks")).unwrap(), ); - let mut mounted = MountedEngineFileSystem::new(fs).unwrap(); + let mut mounted = MountedEngineFileSystem::with_runtime_context(fs, runtime.context()); mounted.mkdir("/nested", false).unwrap(); mounted @@ -457,11 +460,14 @@ fn chunked_local_mounted_adapter_creates_exclusive_files_with_modes() { #[test] fn chunked_local_mounted_adapter_preserves_sparse_pwrite_allocation() { let temp = tempfile::tempdir().unwrap(); + let runtime = + agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + .expect("create test runtime"); let fs = ChunkedFs::new( SqliteMetadataStore::open(temp.path().join("metadata.sqlite")).unwrap(), FileBlockStore::new(temp.path().join("blocks")).unwrap(), ); - let mut mounted = MountedEngineFileSystem::new(fs).unwrap(); + let mut mounted = MountedEngineFileSystem::with_runtime_context(fs, runtime.context()); mounted.write_file("/sparse", Vec::new()).unwrap(); mounted @@ -476,11 +482,14 @@ fn chunked_local_mounted_adapter_preserves_sparse_pwrite_allocation() { #[test] fn chunked_local_mount_table_preserves_sparse_pwrite_allocation() { let temp = tempfile::tempdir().unwrap(); + let runtime = + agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default()) + .expect("create test runtime"); let fs = ChunkedFs::new( SqliteMetadataStore::open(temp.path().join("metadata.sqlite")).unwrap(), FileBlockStore::new(temp.path().join("blocks")).unwrap(), ); - let mounted = MountedEngineFileSystem::new(fs).unwrap(); + let mounted = MountedEngineFileSystem::with_runtime_context(fs, runtime.context()); let mut table = MountTable::new(MemoryFileSystem::new()); vfs::posix::VirtualFileSystem::mkdir(&mut table, "/mnt", false).unwrap(); table diff --git a/crates/vfs/src/posix/mount_table.rs b/crates/vfs/src/posix/mount_table.rs index 1be43d39fa..3446b021cc 100644 --- a/crates/vfs/src/posix/mount_table.rs +++ b/crates/vfs/src/posix/mount_table.rs @@ -1487,13 +1487,13 @@ impl MountTable { } } } - (Some(before), Some(after)) if (before.dev, before.ino) == (after.dev, after.ino) => { - if !before.is_directory { - usage.total_bytes = usage - .total_bytes - .saturating_sub(before.size) - .saturating_add(after.size); - } + (Some(before), Some(after)) + if (before.dev, before.ino) == (after.dev, after.ino) && !before.is_directory => + { + usage.total_bytes = usage + .total_bytes + .saturating_sub(before.size) + .saturating_add(after.size); } _ => {} } diff --git a/justfile b/justfile index 695eea012c..f8a011d661 100644 --- a/justfile +++ b/justfile @@ -91,7 +91,7 @@ shell *args: done pnpm "${registry_filters[@]}" build fi - if [[ ! -e registry/software/common/dist/index.js ]]; then + if [[ ! -e software/common/dist/index.js ]]; then pnpm --filter @agentos-software/common build fi if [[ ! -e packages/runtime-core/dist/index.js \ diff --git a/packages/core/tests/acp-reactor-regression.test.ts b/packages/core/tests/acp-reactor-regression.test.ts index 7222160726..98049a1db9 100644 --- a/packages/core/tests/acp-reactor-regression.test.ts +++ b/packages/core/tests/acp-reactor-regression.test.ts @@ -37,7 +37,7 @@ function writeUpdate(text) { sessionId: "reactor-session-1", update: { sessionUpdate: "agent_message_chunk", - content: { text }, + content: { type: "text", text }, }, }, }); @@ -242,29 +242,38 @@ describe("ACP adapter reactor regression", () => { await vm.openSession({ sessionId, agent: "acp-reactor-regression" }); const updateTexts: string[] = []; const unsubscribe = vm.onSessionEvent(sessionId, (event) => { - if (event.method !== "session/update") return; - const params = event.params as { - update?: { content?: { text?: unknown } }; - }; - const text = params.update?.content?.text; - if (typeof text === "string") updateTexts.push(text); + if ( + event.durability === "ephemeral" && + event.type === "agent_message_chunk" && + event.content.type === "text" + ) { + updateTexts.push(event.content.text); + } }); try { - const firstPrompt = await vm.prompt( + const firstPrompt = await vm.prompt({ sessionId, - "Exercise the saturated ordinary event lane.", - ); - expect(firstPrompt.response.error).toBeUndefined(); - expect( - (firstPrompt.response.result as { stopReason?: string }).stopReason, - ).toBe("end_turn"); - await waitFor( - () => - updateTexts.filter((text) => text.startsWith("reactor-update:")) - .length === UPDATE_COUNT && - updateTexts.includes("reactor-tool-result:42"), - ); + content: [ + { + type: "text", + text: "Exercise the saturated ordinary event lane.", + }, + ], + }); + expect(firstPrompt.stopReason).toBe("end_turn"); + try { + await waitFor( + () => + updateTexts.filter((text) => text.startsWith("reactor-update:")) + .length === UPDATE_COUNT && + updateTexts.includes("reactor-tool-result:42"), + ); + } catch (error) { + throw new Error( + `${String(error)}; received ${updateTexts.length} updates; stderr: ${stderrChunks.join("")}`, + ); + } const numberedUpdates = updateTexts .filter((text) => text.startsWith("reactor-update:")) @@ -276,14 +285,16 @@ describe("ACP adapter reactor regression", () => { expect(hostToolCalls).toBe(1); expect(hostToolInputs).toEqual([{ a: 19, b: 23 }]); - const secondPrompt = await vm.prompt( + const secondPrompt = await vm.prompt({ sessionId, - "Prove that the same adapter session remains reusable.", - ); - expect(secondPrompt.response.error).toBeUndefined(); - expect( - (secondPrompt.response.result as { stopReason?: string }).stopReason, - ).toBe("end_turn"); + content: [ + { + type: "text", + text: "Prove that the same adapter session remains reusable.", + }, + ], + }); + expect(secondPrompt.stopReason).toBe("end_turn"); await waitFor(() => updateTexts.includes("reactor-session-reused:2")); expect(hostToolCalls).toBe(1); expect(unexpectedAgentExits).toEqual([]); @@ -295,7 +306,7 @@ describe("ACP adapter reactor regression", () => { expect(stderr).not.toMatch(/session evicted/i); } finally { unsubscribe(); - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } }, 120_000); }); diff --git a/packages/core/tests/agent-config-environment.test.ts b/packages/core/tests/agent-config-environment.test.ts index 635e7e36a0..987e07b9f8 100644 --- a/packages/core/tests/agent-config-environment.test.ts +++ b/packages/core/tests/agent-config-environment.test.ts @@ -1,7 +1,8 @@ import { resolve } from "node:path"; import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import { describe, expect, test } from "vitest"; -import { AgentOs, type AgentInfo } from "../src/agent-os.js"; +import { AgentOs } from "../src/agent-os.js"; +import type { SessionAgentInfo } from "../src/session-api.js"; import { createProjectedAgentPackage } from "./helpers/projected-agent-package.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); @@ -74,7 +75,7 @@ process.stdin.on("data", (chunk) => { }); `; -type LaunchProbe = AgentInfo & { +type LaunchProbe = SessionAgentInfo & { argv?: string[]; env?: Partial>; }; @@ -101,10 +102,10 @@ async function inspectLaunch( try { sessionId = `launch-probe-${agentType}`; await vm.openSession({ sessionId, agent: agentType }); - return vm.getSessionAgentInfo(sessionId) as LaunchProbe; + return (await vm.getSessionAgentInfo({ sessionId })) as LaunchProbe; } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); agentPackage.cleanup(); diff --git a/packages/core/tests/agentos-package-agent-vm.test.ts b/packages/core/tests/agentos-package-agent-vm.test.ts index 1a72928e79..19b06d2081 100644 --- a/packages/core/tests/agentos-package-agent-vm.test.ts +++ b/packages/core/tests/agentos-package-agent-vm.test.ts @@ -52,10 +52,7 @@ process.stdin.on("data", (chunk) => { case "session/prompt": writeMessage({ jsonrpc: "2.0", method: "session/update", params: { sessionId: "opt-agentos-session-1", - update: { sessionUpdate: "agent_message_chunk", content: { text: "opt-agentos-agent-ok" } } } }); - writeMessage({ jsonrpc: "2.0", method: "session/update", params: { - sessionId: "opt-agentos-session-1", - update: { sessionUpdate: "completed", stopReason: "end_turn" } } }); + update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "opt-agentos-agent-ok" } } } }); writeResponse(msg.id, { stopReason: "end_turn" }); break; case "session/cancel": diff --git a/packages/core/tests/binding-reference.test.ts b/packages/core/tests/binding-reference.test.ts index beefba7c51..9df4e0d454 100644 --- a/packages/core/tests/binding-reference.test.ts +++ b/packages/core/tests/binding-reference.test.ts @@ -107,7 +107,7 @@ describe("binding reference registration", () => { test("openSession injects the registered binding reference into the system prompt", async () => { const sessionId = "binding-reference"; await vm.openSession({ sessionId, agent: "pi" }); - const agentInfo = vm.getSessionAgentInfo(sessionId) as { + const agentInfo = (await vm.getSessionAgentInfo({ sessionId })) as { systemPrompt?: string; }; const prompt = agentInfo.systemPrompt ?? ""; @@ -115,6 +115,6 @@ describe("binding reference registration", () => { expect(prompt).toContain("`agentos-math add --a --b `"); expect(prompt).toContain("### math"); - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); }); }); diff --git a/packages/core/tests/claude-session.test.ts b/packages/core/tests/claude-session.test.ts index 4deb56b3ca..7ebdc4f9ed 100644 --- a/packages/core/tests/claude-session.test.ts +++ b/packages/core/tests/claude-session.test.ts @@ -192,9 +192,7 @@ describe("full openSession({ agent: 'claude' })", () => { expect(events.length).toBeGreaterThanOrEqual(1); expect( - events.some((event) => - JSON.stringify(event).includes("tool_call"), - ), + events.some((event) => JSON.stringify(event).includes("tool_call")), ).toBe(true); expect( events.some((event) => @@ -252,9 +250,7 @@ describe("full openSession({ agent: 'claude' })", () => { ), ).toBe(true); expect( - events.some((event) => - JSON.stringify(event).includes("tool_call"), - ), + events.some((event) => JSON.stringify(event).includes("tool_call")), ).toBe(false); } finally { if (sessionId) { @@ -313,9 +309,7 @@ describe("full openSession({ agent: 'claude' })", () => { ); expect( - events.some((event) => - JSON.stringify(event).includes("tool_call"), - ), + events.some((event) => JSON.stringify(event).includes("tool_call")), ).toBe(true); expect( events.some((event) => @@ -382,9 +376,7 @@ describe("full openSession({ agent: 'claude' })", () => { ).toBe(true); expect( - events.some((event) => - JSON.stringify(event).includes("tool_call"), - ), + events.some((event) => JSON.stringify(event).includes("tool_call")), ).toBe(true); expect( events.some((event) => @@ -427,20 +419,16 @@ describe("full openSession({ agent: 'claude' })", () => { }); const capabilities = await vm.getSessionCapabilities({ sessionId }); - expect(capabilities?.prompt).toMatchObject({ - audio: false, - embeddedContext: false, - image: true, - }); + expect(capabilities?.prompt?.image).toBe(true); + expect(capabilities?.prompt?.audio).toBeUndefined(); + expect(capabilities?.prompt?.embeddedContext).toBeUndefined(); const config = await vm.getSessionConfig({ sessionId }); - const modes = config.options.find((option) => option.id === "mode"); - expect(modes?.type).toBe("select"); - if (modes?.type !== "select") throw new Error("missing mode selector"); - expect(modes.currentValue).toBe("default"); - expect(modes.options.map((mode) => mode.value)).toEqual( - expect.arrayContaining(["default", "plan", "dontAsk"]), - ); + expect(config.revision).toBe(0); + expect(config.options).toEqual(expect.any(Array)); + // Claude currently advertises legacy ACP `modes`, not native + // `configOptions`; AgentOS deliberately does not invent a mapping. + expect(config.options.some((option) => option.id === "mode")).toBe(false); const closedSessionId = sessionId; await vm.unloadSession({ sessionId: closedSessionId }); @@ -484,7 +472,7 @@ describe("full openSession({ agent: 'claude' })", () => { ); }, 120_000); - test("Claude sessions reflect native ACP configuration changes", async () => { + test("Claude sessions surface unsupported native ACP configuration changes", async () => { let sessionId: string | undefined; try { @@ -498,26 +486,16 @@ describe("full openSession({ agent: 'claude' })", () => { ANTHROPIC_BASE_URL: mockUrl, }, }); + const initialConfig = await vm.getSessionConfig({ sessionId }); - const modeEvents: unknown[] = []; - const unsubscribeEvents = vm.onSessionEvent(sessionId, (event) => { - if ( - event.type === "current_mode_update" && - JSON.stringify(event).includes("current_mode_update") - ) { - modeEvents.push(event); - } - }); - const response = await vm.setSessionConfigOption({ - sessionId, - configId: "mode", - value: "plan", - }); - unsubscribeEvents(); - const modes = response.options.find((option) => option.id === "mode"); - expect(modes?.type === "select" && modes.currentValue).toBe("plan"); - - expect(modeEvents.length).toBeGreaterThanOrEqual(1); + await expect( + vm.setSessionConfigOption({ + sessionId, + configId: "mode", + value: "plan", + }), + ).rejects.toThrow(/session\/set_config_option.*method not found/i); + expect(await vm.getSessionConfig({ sessionId })).toEqual(initialConfig); } finally { if (sessionId) { await vm.unloadSession({ sessionId }); diff --git a/packages/core/tests/helpers/session-result.ts b/packages/core/tests/helpers/session-result.ts new file mode 100644 index 0000000000..10678c37c2 --- /dev/null +++ b/packages/core/tests/helpers/session-result.ts @@ -0,0 +1,15 @@ +import type { PromptResult } from "../../src/session-api.js"; + +type TextContentBlock = Extract< + NonNullable["content"][number], + { type: "text" } +>; + +export function promptResultText(result: PromptResult): string { + return ( + result.message?.content + .filter((block): block is TextContentBlock => block.type === "text") + .map((block) => block.text) + .join("") ?? "" + ); +} diff --git a/packages/core/tests/migration-parity.test.ts b/packages/core/tests/migration-parity.test.ts index 26db5fff8e..911c26bc1f 100644 --- a/packages/core/tests/migration-parity.test.ts +++ b/packages/core/tests/migration-parity.test.ts @@ -5,7 +5,9 @@ import common from "@agentos-software/common"; import { afterEach, describe, expect, test } from "vitest"; import { z } from "zod"; import { AgentOs, binding, bindings } from "../src/index.js"; +import type { SessionStreamEntry } from "../src/session-api.js"; import { createProjectedAgentPackage } from "./helpers/projected-agent-package.js"; +import { promptResultText } from "./helpers/session-result.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); const textDecoder = new TextDecoder(); @@ -78,6 +80,7 @@ process.stdin.on("data", (chunk) => { update: { sessionUpdate: "agent_message_chunk", content: { + type: "text", text: "mock-parity-flow-ok", }, }, @@ -90,22 +93,13 @@ process.stdin.on("data", (chunk) => { sessionId: "mock-session-1", update: { sessionUpdate: "tool_call", + toolCallId: "synthetic-tool-1", title: "synthetic-tool", + kind: "other", status: "completed", }, }, }); - writeMessage({ - jsonrpc: "2.0", - method: "session/update", - params: { - sessionId: "mock-session-1", - update: { - sessionUpdate: "completed", - stopReason: "end_turn", - }, - }, - }); writeResponse(msg.id, { stopReason: "end_turn" }); break; case "session/cancel": @@ -146,8 +140,10 @@ function assertNativeSidecar(vm: AgentOs): void { expect(vm.sidecar.describe()).toMatchObject({ state: "ready", }); - expect("kernel" in (vm as Record)).toBe(false); - expect((vm as Record).kernel).toBeUndefined(); + expect("kernel" in (vm as unknown as Record)).toBe(false); + expect( + (vm as unknown as Record).kernel, + ).toBeUndefined(); } async function runSpawnedProcess( @@ -155,22 +151,7 @@ async function runSpawnedProcess( command: string, args: string[], ): Promise<{ exitCode: number; stdout: string; stderr: string }> { - const stdoutChunks: string[] = []; - const stderrChunks: string[] = []; - const { pid } = vm.spawn(command, args, { - onStdout: (chunk) => { - stdoutChunks.push(textDecoder.decode(chunk)); - }, - onStderr: (chunk) => { - stderrChunks.push(textDecoder.decode(chunk)); - }, - }); - - return { - exitCode: await vm.waitProcess(pid), - stdout: stdoutChunks.join(""), - stderr: stderrChunks.join(""), - }; + return vm.execArgv(command, args); } function getRequestPath(req: IncomingMessage): string { @@ -223,7 +204,9 @@ describe("native sidecar migration parity gate", () => { textDecoder.decode(await vm.readFile("/workspace/process.txt")), ).toBe("filesystem-ok:process-ok"); - const snapshot = await vm.exportRootFilesystem({ maxBytes: 64 * 1024 * 1024 }); + const snapshot = await vm.exportRootFilesystem({ + maxBytes: 64 * 1024 * 1024, + }); const clonedVm = await AgentOs.create({ rootFilesystem: { disableDefaultBaseLayer: true, @@ -384,36 +367,22 @@ describe("native sidecar migration parity gate", () => { const sessionId = "migration-parity"; await vm.openSession({ sessionId, agent: "migration-parity" }); - const events: { method: string; params?: unknown }[] = []; + const events: SessionStreamEntry[] = []; const unsubscribeEvents = vm.onSessionEvent(sessionId, (event) => { events.push(event); }); - const { response, text } = await vm.prompt( + const result = await vm.prompt({ sessionId, - "Run the migration parity prompt flow.", - ); + content: [ + { type: "text", text: "Run the migration parity prompt flow." }, + ], + }); unsubscribeEvents(); - expect(response.error).toBeUndefined(); - expect((response.result as { stopReason?: string }).stopReason).toBe( - "end_turn", - ); - expect(text).toContain("mock-parity-flow-ok"); + expect(result.stopReason).toBe("end_turn"); + expect(promptResultText(result)).toContain("mock-parity-flow-ok"); - expect( - events.some( - (event) => - event.method === "session/update" && - JSON.stringify(event.params).includes("tool_call"), - ), - ).toBe(true); - expect( - events.some( - (event) => - event.method === "session/update" && - JSON.stringify(event.params).includes('"completed"'), - ), - ).toBe(true); + expect(events.some((event) => event.type === "tool_call")).toBe(true); await vm.deleteSession({ sessionId }); }, 120_000); diff --git a/packages/core/tests/opencode-real-session.test.ts b/packages/core/tests/opencode-real-session.test.ts index dcd1ea654f..9607d6228a 100644 --- a/packages/core/tests/opencode-real-session.test.ts +++ b/packages/core/tests/opencode-real-session.test.ts @@ -1,7 +1,6 @@ import { resolve } from "node:path"; import opencode from "@agentos-software/opencode"; import { describe, expect, test } from "vitest"; -import type { AgentCapabilities, AgentInfo } from "../src/agent-os.js"; import { AgentOs } from "../src/agent-os.js"; import { DEFAULT_TEXT_FIXTURE, @@ -44,31 +43,27 @@ describe("real openSession({ agent: 'opencode' })", () => { }, }); - const agentInfo = vm.getSessionAgentInfo(sessionId) as AgentInfo; + const agentInfo = await vm.getSessionAgentInfo({ sessionId }); expect(agentInfo.name).toBe("OpenCode"); expect(agentInfo.version).toBeTruthy(); - const capabilities = vm.getSessionCapabilities( - sessionId, - ) as AgentCapabilities; - expect(capabilities.promptCapabilities).toMatchObject({ + const capabilities = await vm.getSessionCapabilities({ sessionId }); + expect(capabilities.prompt).toMatchObject({ embeddedContext: true, image: true, }); - const modes = vm.getSessionModes(sessionId); - expect(modes?.currentModeId).toBe("build"); - expect(modes?.availableModes.map((mode) => mode.id)).toEqual( - expect.arrayContaining(["build", "plan"]), - ); + const config = await vm.getSessionConfig({ sessionId }); + // OpenCode currently advertises legacy ACP `modes`, not native + // `configOptions`; AgentOS deliberately does not invent a mapping. + expect(config.options.some((option) => option.id === "mode")).toBe(false); - expect(vm.listSessions()).toContainEqual({ - sessionId, - agentType: "opencode", - }); + expect((await vm.listSessions()).sessions).toContainEqual( + expect.objectContaining({ sessionId, agent: "opencode" }), + ); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); diff --git a/packages/core/tests/opencode-session.test.ts b/packages/core/tests/opencode-session.test.ts index d7f1e1b740..970950cc46 100644 --- a/packages/core/tests/opencode-session.test.ts +++ b/packages/core/tests/opencode-session.test.ts @@ -649,7 +649,7 @@ describe("OpenCode session API integration", () => { } }, 120_000); - test("surfaces OpenCode cancelSession() honestly through the Agent OS session API", async () => { + test("surfaces OpenCode cancelPrompt() honestly through the Agent OS session API", async () => { const { mock, url } = await startLlmock([ { match: { predicate: () => true }, diff --git a/packages/core/tests/pi-cli-headless.test.ts b/packages/core/tests/pi-cli-headless.test.ts index 691a528680..515840c4eb 100644 --- a/packages/core/tests/pi-cli-headless.test.ts +++ b/packages/core/tests/pi-cli-headless.test.ts @@ -4,6 +4,7 @@ import piCli from "@agentos-software/pi-cli"; import type { Fixture, ToolCall } from "@copilotkit/llmock"; import { describe, expect, test } from "vitest"; import { AgentOs } from "../src/agent-os.js"; +import type { SessionStreamEntry } from "../src/session-api.js"; import { createAnthropicFixture, startLlmock, @@ -11,6 +12,7 @@ import { } from "./helpers/llmock-helper.js"; import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import { hasBuiltRegistryCommands } from "./helpers/registry-command-availability.js"; +import { promptResultText } from "./helpers/session-result.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); const registryCommandsAvailable = hasBuiltRegistryCommands(common); @@ -116,40 +118,34 @@ describe("full openSession({ agent: 'pi-cli' }) inside the VM", () => { }, }); - const events: { method: string; params?: unknown }[] = []; + const events: SessionStreamEntry[] = []; const unsubscribeEvents = vm.onSessionEvent(sessionId, (event) => { events.push(event); }); - const { response, text } = await vm.prompt( + const result = await vm.prompt({ sessionId, - `Create ${workspacePath} with the text hello from pi cli write.`, - ); + content: [ + { + type: "text", + text: `Create ${workspacePath} with the text hello from pi cli write.`, + }, + ], + }); unsubscribeEvents(); - expect(response.error).toBeUndefined(); - expect(text).toContain("notes.txt was created successfully."); + expect(result.stopReason).toBe("end_turn"); + expect(promptResultText(result)).toContain( + "notes.txt was created successfully.", + ); expect(new TextDecoder().decode(await vm.readFile(workspacePath))).toBe( "hello from pi cli write", ); expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); - expect( - events.some( - (event) => - event.method === "session/update" && - JSON.stringify(event.params).includes("tool_call"), - ), - ).toBe(true); - expect( - events.some( - (event) => - event.method === "session/update" && - JSON.stringify(event.params).includes('"completed"'), - ), - ).toBe(true); + expect(events.some((event) => event.type === "tool_call")).toBe(true); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); @@ -190,20 +186,27 @@ describe("full openSession({ agent: 'pi-cli' }) inside the VM", () => { }, }); - const { response, text } = await vm.prompt( + const result = await vm.prompt({ sessionId, - `Use bash to write bash-ok into ${workspacePath}.`, - ); + content: [ + { + type: "text", + text: `Use bash to write bash-ok into ${workspacePath}.`, + }, + ], + }); - expect(response.error).toBeUndefined(); - expect(text).toContain("bash-output.txt was written successfully."); + expect(result.stopReason).toBe("end_turn"); + expect(promptResultText(result)).toContain( + "bash-output.txt was written successfully.", + ); expect(new TextDecoder().decode(await vm.readFile(workspacePath))).toBe( "bash-ok", ); expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); diff --git a/packages/core/tests/pi-extensions.test.ts b/packages/core/tests/pi-extensions.test.ts index 4af6aca1e0..d72e63fcfd 100644 --- a/packages/core/tests/pi-extensions.test.ts +++ b/packages/core/tests/pi-extensions.test.ts @@ -9,6 +9,7 @@ import { stopLlmock, } from "./helpers/llmock-helper.js"; import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; +import { promptResultText } from "./helpers/session-result.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); const HOME_DIR = "/home/agentos"; @@ -112,20 +113,22 @@ describe("Pi extensions quickstart truth test", () => { }, }); - const { response, text } = await vm.prompt( + const result = await vm.prompt({ sessionId, - "What is 2 + 2? Reply with just the number.", - ); + content: [ + { type: "text", text: "What is 2 + 2? Reply with just the number." }, + ], + }); - expect(response.error).toBeUndefined(); - expect(text).toContain(EXPECTED_REPLY); + expect(result.stopReason).toBe("end_turn"); + expect(promptResultText(result)).toContain(EXPECTED_REPLY); expect(mock.getRequests().length).toBeGreaterThanOrEqual(1); expect(mock.getRequests().some(requestIncludesExtensionMarker)).toBe( true, ); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); diff --git a/packages/core/tests/pi-headless.test.ts b/packages/core/tests/pi-headless.test.ts index 8fb3ff5b0f..47f414de84 100644 --- a/packages/core/tests/pi-headless.test.ts +++ b/packages/core/tests/pi-headless.test.ts @@ -3,8 +3,8 @@ import common from "@agentos-software/common"; import pi from "@agentos-software/pi"; import type { Fixture, ToolCall } from "@copilotkit/llmock"; import { describe, expect, test } from "vitest"; -import type { AgentCapabilities, AgentInfo } from "../src/agent-os.js"; import { AgentOs } from "../src/agent-os.js"; +import type { SessionStreamEntry } from "../src/session-api.js"; import { createAnthropicFixture, startLlmock, @@ -12,6 +12,7 @@ import { } from "./helpers/llmock-helper.js"; import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import { hasBuiltRegistryCommands } from "./helpers/registry-command-availability.js"; +import { promptResultText } from "./helpers/session-result.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); const registryCommandsAvailable = hasBuiltRegistryCommands(common); @@ -109,11 +110,13 @@ describe("full openSession({ agent: 'pi' }) inside the VM", () => { expect(sessionId).toBeTruthy(); expect( - vm.listSessions().some((entry) => entry.sessionId === sessionId), + (await vm.listSessions()).sessions.some( + (entry) => entry.sessionId === sessionId, + ), ).toBe(true); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); @@ -151,36 +154,40 @@ describe("full openSession({ agent: 'pi' }) inside the VM", () => { }, }); - const agentInfo = vm.getSessionAgentInfo(sessionId) as AgentInfo; + const agentInfo = await vm.getSessionAgentInfo({ sessionId }); expect(agentInfo.name).toBe("pi-sdk-acp"); expect(agentInfo.title).toBe("Pi SDK ACP adapter"); expect(agentInfo.version).toBeTruthy(); - const capabilities = vm.getSessionCapabilities( - sessionId, - ) as AgentCapabilities; - expect(capabilities.promptCapabilities).toMatchObject({ - image: true, - audio: false, - embeddedContext: false, - }); + const capabilities = await vm.getSessionCapabilities({ sessionId }); + expect(capabilities.prompt?.image).toBe(true); + expect(capabilities.prompt?.audio).toBeUndefined(); + expect(capabilities.prompt?.embeddedContext).toBeUndefined(); - const modes = vm.getSessionModes(sessionId); - expect(modes?.currentModeId).toBeTruthy(); - expect(modes?.availableModes.length).toBeGreaterThan(0); + const config = await vm.getSessionConfig({ sessionId }); + // Pi currently advertises legacy ACP `modes`, not native + // `configOptions`; AgentOS deliberately does not invent a mapping. + expect(config.options.some((option) => option.id === "mode")).toBe(false); - const events: { method: string; params?: unknown }[] = []; + const events: SessionStreamEntry[] = []; const unsubscribeEvents = vm.onSessionEvent(sessionId, (event) => { events.push(event); }); - const { response, text } = await vm.prompt( + const result = await vm.prompt({ sessionId, - "Create notes.txt with the text hello from pi write.", - ); + content: [ + { + type: "text", + text: "Create notes.txt with the text hello from pi write.", + }, + ], + }); unsubscribeEvents(); - expect(response.error).toBeUndefined(); - expect(text).toContain("notes.txt was created successfully."); + expect(result.stopReason).toBe("end_turn"); + expect(promptResultText(result)).toContain( + "notes.txt was created successfully.", + ); expect( new TextDecoder().decode( await vm.readFile(`${workspaceDir}/notes.txt`), @@ -188,23 +195,10 @@ describe("full openSession({ agent: 'pi' }) inside the VM", () => { ).toBe("hello from pi write"); expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); - expect( - events.some( - (event) => - event.method === "session/update" && - JSON.stringify(event.params).includes("tool_call"), - ), - ).toBe(true); - expect( - events.some( - (event) => - event.method === "session/update" && - JSON.stringify(event.params).includes('"completed"'), - ), - ).toBe(true); + expect(events.some((event) => event.type === "tool_call")).toBe(true); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); @@ -244,13 +238,20 @@ describe("full openSession({ agent: 'pi' }) inside the VM", () => { }, }); - const { response, text } = await vm.prompt( + const result = await vm.prompt({ sessionId, - "Use bash to write bash-ok into bash-output.txt.", - ); + content: [ + { + type: "text", + text: "Use bash to write bash-ok into bash-output.txt.", + }, + ], + }); - expect(response.error).toBeUndefined(); - expect(text).toContain("bash-output.txt was written successfully."); + expect(result.stopReason).toBe("end_turn"); + expect(promptResultText(result)).toContain( + "bash-output.txt was written successfully.", + ); expect( new TextDecoder().decode( await vm.readFile(`${workspaceDir}/bash-output.txt`), @@ -259,7 +260,7 @@ describe("full openSession({ agent: 'pi' }) inside the VM", () => { expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); diff --git a/packages/core/tests/pi-tool-llmock.test.ts b/packages/core/tests/pi-tool-llmock.test.ts index 7b27f66d3b..34fa65ae79 100644 --- a/packages/core/tests/pi-tool-llmock.test.ts +++ b/packages/core/tests/pi-tool-llmock.test.ts @@ -4,12 +4,14 @@ import pi from "@agentos-software/pi"; import type { Fixture, ToolCall } from "@copilotkit/llmock"; import { describe, expect, test } from "vitest"; import { AgentOs } from "../src/agent-os.js"; +import type { SessionStreamEntry } from "../src/session-api.js"; import { createAnthropicFixture, startLlmock, stopLlmock, } from "./helpers/llmock-helper.js"; import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; +import { promptResultText } from "./helpers/session-result.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); @@ -113,40 +115,34 @@ describe("pi tool execution (llmock)", () => { }, }); - const events: { method: string; params?: unknown }[] = []; + const events: SessionStreamEntry[] = []; const unsubscribeEvents = vm.onSessionEvent(sessionId, (event) => { events.push(event); }); - const { response, text } = await vm.prompt( + const result = await vm.prompt({ sessionId, - "Write the text 'tool-test-ok' to tool-verify.txt. Do not explain, just do it.", - ); + content: [ + { + type: "text", + text: "Write the text 'tool-test-ok' to tool-verify.txt. Do not explain, just do it.", + }, + ], + }); unsubscribeEvents(); - expect(response.error).toBeUndefined(); - expect(text).toContain("tool-verify.txt was created successfully."); + expect(result.stopReason).toBe("end_turn"); + expect(promptResultText(result)).toContain( + "tool-verify.txt was created successfully.", + ); expect(new TextDecoder().decode(await vm.readFile(workspacePath))).toBe( "tool-test-ok", ); expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); - expect( - events.some( - (event) => - event.method === "session/update" && - JSON.stringify(event.params).includes("tool_call"), - ), - ).toBe(true); - expect( - events.some( - (event) => - event.method === "session/update" && - JSON.stringify(event.params).includes('"completed"'), - ), - ).toBe(true); + expect(events.some((event) => event.type === "tool_call")).toBe(true); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); diff --git a/packages/core/tests/pi-vanilla-bash.test.ts b/packages/core/tests/pi-vanilla-bash.test.ts index 2212ae1d0c..9ba646c0bb 100644 --- a/packages/core/tests/pi-vanilla-bash.test.ts +++ b/packages/core/tests/pi-vanilla-bash.test.ts @@ -98,7 +98,7 @@ function captureSessionEventText( } { const events: string[] = []; const unsubscribe = vm.onSessionEvent(sessionId, (event) => { - events.push(JSON.stringify(event.params)); + events.push(JSON.stringify(event)); }); return { text: () => events.join("\n"), @@ -106,6 +106,10 @@ function captureSessionEventText( }; } +function textPrompt(vm: AgentOs, sessionId: string, text: string) { + return vm.prompt({ sessionId, content: [{ type: "text", text }] }); +} + /** * Vanilla Pi bash coverage: these tests use the unmodified Pi SDK bash backend * (`createLocalBashOperations()` spawning the shell directly with @@ -141,13 +145,13 @@ describe("vanilla Pi bash tool inside the VM", () => { }); const eventText = captureSessionEventText(vm, sessionId); - const { response } = await vm.prompt(sessionId, "Run pwd."); + const result = await textPrompt(vm, sessionId, "Run pwd."); eventText.unsubscribe(); - expect(response.error).toBeUndefined(); + expect(result.stopReason).toBe("end_turn"); expect(eventText.text()).toContain(workspaceDir); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); @@ -180,16 +184,17 @@ describe("vanilla Pi bash tool inside the VM", () => { }); const eventText = captureSessionEventText(vm, sessionId); - const { response } = await vm.prompt( + const result = await textPrompt( + vm, sessionId, "Echo the APP_TEST_FLAG variable.", ); eventText.unsubscribe(); - expect(response.error).toBeUndefined(); + expect(result.stopReason).toBe("end_turn"); expect(eventText.text()).toContain("vanilla"); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); @@ -224,19 +229,20 @@ describe("vanilla Pi bash tool inside the VM", () => { }); const eventText = captureSessionEventText(vm, sessionId); - const { response } = await vm.prompt( + const result = await textPrompt( + vm, sessionId, "Run a command that writes to stdout and stderr and exits nonzero.", ); eventText.unsubscribe(); - expect(response.error).toBeUndefined(); + expect(result.stopReason).toBe("end_turn"); const events = eventText.text(); expect(events).toContain("out-line"); expect(events).toContain("err-line"); expect(events).toContain("3"); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); @@ -267,17 +273,18 @@ describe("vanilla Pi bash tool inside the VM", () => { }, }); - const { response } = await vm.prompt( + const result = await textPrompt( + vm, sessionId, "Use bash to write ok into out.txt.", ); - expect(response.error).toBeUndefined(); + expect(result.stopReason).toBe("end_turn"); expect( new TextDecoder().decode(await vm.readFile(`${workspaceDir}/out.txt`)), ).toBe("ok"); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); @@ -310,19 +317,20 @@ describe("vanilla Pi bash tool inside the VM", () => { }); const eventText = captureSessionEventText(vm, sessionId); - const { response } = await vm.prompt( + const result = await textPrompt( + vm, sessionId, "Run sleep 30 with a 1 second timeout.", ); eventText.unsubscribe(); - expect(response.error).toBeUndefined(); + expect(result.stopReason).toBe("end_turn"); // The kill must actually fire: completing in seconds (not ~30s) proves // the timeout killed the sleep instead of waiting for it to finish. expect(Date.now() - startedAt).toBeLessThan(20_000); expect(eventText.text().toLowerCase()).toContain("timed out"); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); @@ -361,7 +369,7 @@ describe("vanilla Pi bash tool inside the VM", () => { const activeSessionId = sessionId; const sawInProgress = new Promise((resolveInProgress) => { const unsubscribe = vm.onSessionEvent(activeSessionId, (event) => { - const serialized = JSON.stringify(event.params); + const serialized = JSON.stringify(event); if ( serialized.includes('"in_progress"') && serialized.includes("bash") @@ -372,15 +380,17 @@ describe("vanilla Pi bash tool inside the VM", () => { }); }); - const promptPromise = vm.prompt(activeSessionId, "Run sleep 60 in bash."); + const promptPromise = textPrompt( + vm, + activeSessionId, + "Run sleep 60 in bash.", + ); await sawInProgress; - await vm.cancelSession(activeSessionId); + await vm.cancelPrompt({ sessionId: activeSessionId }); - const { response } = await promptPromise; - const stopReason = (response.result as { stopReason?: string }) - ?.stopReason; - expect(stopReason).toBe("cancelled"); + const result = await promptPromise; + expect(result.stopReason).toBe("cancelled"); const lingering = vm .allProcesses() @@ -393,7 +403,7 @@ describe("vanilla Pi bash tool inside the VM", () => { expect(lingering).toEqual([]); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); await stopLlmock(mock); diff --git a/packages/core/tests/session-cleanup.test.ts b/packages/core/tests/session-cleanup.test.ts index e3568796d3..73e9daedba 100644 --- a/packages/core/tests/session-cleanup.test.ts +++ b/packages/core/tests/session-cleanup.test.ts @@ -25,6 +25,7 @@ import { createVmOpenCodeHome, } from "./helpers/opencode-helper.js"; import { REGISTRY_SOFTWARE } from "./helpers/registry-commands.js"; +import { promptResultText } from "./helpers/session-result.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); const PROMPT_TEXT = "Reply with exactly cleanup-ok."; @@ -732,12 +733,12 @@ function registerSharedCleanupCoverage(agents: SessionCleanupAgent[]): void { ); expect(activePids.length).toBeGreaterThan(0); - const { stopReason, text } = await vm.prompt({ + const result = await vm.prompt({ sessionId, content: [{ type: "text", text: PROMPT_TEXT }], }); - expect(stopReason).toBeDefined(); - expect(text).toContain(PROMPT_RESPONSE); + expect(result.stopReason).toBeDefined(); + expect(promptResultText(result)).toContain(PROMPT_RESPONSE); const vmResourcesBeforeClose = await snapshotVmResources(vm); expect(vmResourcesBeforeClose.processCount).toBeGreaterThanOrEqual( baselineVmResources.processCount + 1, @@ -800,12 +801,12 @@ describe("session cleanup", () => { (pid) => !baselineVmResources.pids.includes(pid), ); expect(activePids.length).toBeGreaterThan(0); - const { stopReason, text } = await vm.prompt({ + const result = await vm.prompt({ sessionId, content: [{ type: "text", text: PROMPT_TEXT }], }); - expect(stopReason).toBeDefined(); - expect(text).toContain(PROMPT_RESPONSE); + expect(result.stopReason).toBeDefined(); + expect(promptResultText(result)).toContain(PROMPT_RESPONSE); await unloadTestSession(vm, sessionId); await assertSessionResourcesReleased( diff --git a/packages/core/tests/session-resume.test.ts b/packages/core/tests/session-resume.test.ts index 42f81350a0..5e9a023793 100644 --- a/packages/core/tests/session-resume.test.ts +++ b/packages/core/tests/session-resume.test.ts @@ -1,6 +1,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { AgentOs } from "../src/agent-os.js"; import { createProjectedAgentPackage } from "./helpers/projected-agent-package.js"; +import { promptResultText } from "./helpers/session-result.js"; // Exercise lazy durable restoration through the public API against the real // sidecar and a mock ACP adapter. `unloadSession` hides the private ACP id; @@ -192,7 +193,7 @@ describe("sidecar resume orchestration (mock ACP adapter)", () => { await vm.unloadSession({ sessionId }); const restored = await textPrompt(sessionId, "after unload"); expect(restored.sessionId).toBe(sessionId); - expect(JSON.parse(restored.text)).toEqual([ + expect(JSON.parse(promptResultText(restored))).toEqual([ { type: "text", text: "after unload" }, ]); } finally { @@ -214,7 +215,7 @@ describe("sidecar resume orchestration (mock ACP adapter)", () => { await textPrompt(sessionId, "first turn"); await vm.unloadSession({ sessionId }); const restored = await textPrompt(sessionId, "second turn"); - const blocks = JSON.parse(restored.text) as Array<{ + const blocks = JSON.parse(promptResultText(restored)) as Array<{ type: string; text: string; }>; diff --git a/packages/core/tests/session-update-live.test.ts b/packages/core/tests/session-update-live.test.ts index f40fdef6c2..ab0f5bc374 100644 --- a/packages/core/tests/session-update-live.test.ts +++ b/packages/core/tests/session-update-live.test.ts @@ -4,12 +4,14 @@ import pi from "@agentos-software/pi"; import type { Fixture, ToolCall } from "@copilotkit/llmock"; import { describe, expect, test } from "vitest"; import { AgentOs } from "../src/agent-os.js"; +import type { SessionStreamEntry } from "../src/session-api.js"; import { createAnthropicFixture, startLlmock, stopLlmock, } from "./helpers/llmock-helper.js"; import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; +import { promptResultText } from "./helpers/session-result.js"; /** * REPRO / REGRESSION: "onSessionUpdate not delivered live mid-turn with Pi". @@ -41,11 +43,7 @@ import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); const RESPONSE_LATENCY_MS = 1500; -interface TimedEvent { - method: string; - params?: unknown; - t: number; -} +type TimedEvent = SessionStreamEntry & { t: number }; function getRequestBody(req: unknown): Record { const direct = req as Record; @@ -64,7 +62,9 @@ function isPostToolResultRequest( } function isSessionUpdate(event: TimedEvent): boolean { - return event.method === "session/update"; + return ( + event.type !== "permission_request" && event.type !== "permission_response" + ); } describe("REPRO: Pi session/update live delivery", () => { @@ -137,24 +137,28 @@ describe("REPRO: Pi session/update live delivery", () => { const unsubscribe = vm.onSessionEvent(sessionId, (event) => { events.push({ - method: event.method, - params: event.params, + ...event, t: performance.now() - promptStart, }); }); promptStart = performance.now(); - const { response, text } = await vm.prompt( + const result = await vm.prompt({ sessionId, - "Write the text 'tool-test-ok' to tool-verify.txt. Do not explain, just do it.", - ); + content: [ + { + type: "text", + text: "Write the text 'tool-test-ok' to tool-verify.txt. Do not explain, just do it.", + }, + ], + }); const promptResolved = performance.now() - promptStart; unsubscribe(); // Sanity: the turn completed correctly and actually exercised the // latency hold (so the mid-turn window really existed). - expect(response.error).toBeUndefined(); - expect(text).toContain(finalText); + expect(result.stopReason).toBe("end_turn"); + expect(promptResultText(result)).toContain(finalText); expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); expect( promptResolved, @@ -180,7 +184,7 @@ describe("REPRO: Pi session/update live delivery", () => { "BUG: first update arrived at ~the same time as resolution — events are batched, not streamed", ).toBeGreaterThan(RESPONSE_LATENCY_MS * 0.5); } finally { - if (sessionId) vm.unloadSession({ sessionId }); + if (sessionId) await vm.unloadSession({ sessionId }); await vm.dispose(); await stopLlmock(mock); } diff --git a/packages/core/tests/synthetic-session-updates.test.ts b/packages/core/tests/synthetic-session-updates.test.ts index 4c41ca20bc..f01bc9aace 100644 --- a/packages/core/tests/synthetic-session-updates.test.ts +++ b/packages/core/tests/synthetic-session-updates.test.ts @@ -8,6 +8,17 @@ let buffer = ""; const sessionState = { modeId: "default", configOptions: [ + { + id: "mode", + category: "mode", + label: "Mode", + type: "select", + currentValue: "default", + options: [ + { value: "default", name: "Default" }, + { value: "plan", name: "Plan" }, + ], + }, { id: "model", category: "model", @@ -147,19 +158,33 @@ describe("synthetic session/update compatibility", () => { const receivedEvents: string[] = []; const unsubscribe = vm.onSessionEvent(sessionId, (event) => { - if (event.method === "session/update") { - receivedEvents.push(JSON.stringify(event.params)); + if (event.type === "config_option_update") { + receivedEvents.push(JSON.stringify(event)); } }); - await vm.setSessionModel(sessionId, "gpt-5-codex"); - await vm.setSessionThoughtLevel(sessionId, "high"); - await vm.setSessionMode(sessionId, "plan"); + await vm.setSessionConfigOption({ + sessionId, + configId: "model", + value: "gpt-5-codex", + }); + await vm.setSessionConfigOption({ + sessionId, + configId: "thought_level", + value: "high", + }); + await vm.setSessionConfigOption({ + sessionId, + configId: "mode", + value: "plan", + }); await new Promise((resolve) => queueMicrotask(resolve)); unsubscribe(); - expect(vm.getSessionModes(sessionId)?.currentModeId).toBe("plan"); - const configOptions = vm.getSessionConfigOptions(sessionId); + const configOptions = (await vm.getSessionConfig({ sessionId })).options; + expect( + configOptions.find((option) => option.id === "mode")?.currentValue, + ).toBe("plan"); expect( configOptions.find((option) => option.category === "model") ?.currentValue, @@ -169,19 +194,14 @@ describe("synthetic session/update compatibility", () => { ?.currentValue, ).toBe("high"); - expect( - receivedEvents.some((event) => - event.includes('"sessionUpdate":"current_mode_update"'), - ), - ).toBe(true); expect( receivedEvents.filter((event) => - event.includes('"sessionUpdate":"config_option_update"'), + event.includes('"type":"config_option_update"'), ).length, - ).toBeGreaterThanOrEqual(2); + ).toBeGreaterThanOrEqual(3); } finally { if (sessionId) { - vm.unloadSession({ sessionId }); + await vm.unloadSession({ sessionId }); } await vm.dispose(); agentPackage.cleanup(); diff --git a/registry/software/codex-cli/bin/codex-exec b/registry/software/codex-cli/bin/codex-exec deleted file mode 100644 index 5c27fbc846..0000000000 Binary files a/registry/software/codex-cli/bin/codex-exec and /dev/null differ diff --git a/scripts/verify-check-types.mjs b/scripts/verify-check-types.mjs index 25f4525f08..50ba6ae683 100644 --- a/scripts/verify-check-types.mjs +++ b/scripts/verify-check-types.mjs @@ -24,7 +24,7 @@ const found = execSync( "\\(", "-type d", "\\(", - '-name node_modules -o -name dist -o -name .astro -o -name .cache -o -name .turbo -o -name vendor -o -name target -o -name .git -o -name .jj', + '-name node_modules -o -name dist -o -name .astro -o -name .cache -o -name .turbo -o -name vendor -o -name target -o -name .git -o -name .jj -o -name .claude', '-o -path "./packages/runtime-core/tests/integration/projects"', '-o -path "./crates/execution/assets/undici-shims"', "\\)", diff --git a/software/acl/agentos-package.json b/software/acl/agentos-package.json index 748f66971b..783432cf7d 100644 --- a/software/acl/agentos-package.json +++ b/software/acl/agentos-package.json @@ -7,6 +7,7 @@ "registry": { "title": "POSIX ACL tools", "description": "Inspect and modify POSIX access and default ACLs.", + "category": "core", "priority": 42 } } diff --git a/software/acl/test/manifest.test.ts b/software/acl/test/manifest.test.ts new file mode 100644 index 0000000000..3d69c80d0f --- /dev/null +++ b/software/acl/test/manifest.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares the POSIX ACL commands", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands).toEqual(["chacl", "getfacl", "setfacl"]); + }); +}); diff --git a/software/attr/agentos-package.json b/software/attr/agentos-package.json index f10f378a60..d96815c340 100644 --- a/software/attr/agentos-package.json +++ b/software/attr/agentos-package.json @@ -7,6 +7,7 @@ "registry": { "title": "attr", "description": "Inspect and modify filesystem extended attributes.", + "category": "core", "priority": 41 } } diff --git a/software/attr/test/manifest.test.ts b/software/attr/test/manifest.test.ts new file mode 100644 index 0000000000..d415bb5228 --- /dev/null +++ b/software/attr/test/manifest.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares the extended-attribute commands", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands).toEqual(["attr", "getfattr", "setfattr"]); + }); +}); diff --git a/software/xfsprogs/agentos-package.json b/software/xfsprogs/agentos-package.json index f0ceae297d..786ef78f38 100644 --- a/software/xfsprogs/agentos-package.json +++ b/software/xfsprogs/agentos-package.json @@ -5,6 +5,7 @@ "registry": { "title": "xfs_io", "description": "Scriptable low-level filesystem I/O commands used by xfstests.", + "category": "core", "priority": 43 } } diff --git a/software/xfsprogs/test/manifest.test.ts b/software/xfsprogs/test/manifest.test.ts new file mode 100644 index 0000000000..4f18303914 --- /dev/null +++ b/software/xfsprogs/test/manifest.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares xfs_io", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands).toEqual(["xfs_io"]); + }); +});