diff --git a/rsworkspace/crates/acp-nats-agent/src/connection.rs b/rsworkspace/crates/acp-nats-agent/src/connection.rs index 89511dff5..83553aaae 100644 --- a/rsworkspace/crates/acp-nats-agent/src/connection.rs +++ b/rsworkspace/crates/acp-nats-agent/src/connection.rs @@ -115,11 +115,8 @@ where N: SubscribeClient + PublishClient + FlushClient + Clone + 'static, A: Agent + 'static, { - // TODO: These two wildcards overlap when session_id == "agent", causing duplicate - // dispatch. A single {prefix}.> avoids duplicates but consumes client messages. - // Revisit the subject topology to eliminate both problems. let global_wildcard = acp_nats::nats::agent::wildcards::all(prefix); - let session_wildcard = acp_nats::nats::agent::wildcards::all_sessions(prefix); + let session_wildcard = acp_nats::nats::session::wildcards::all_agent(prefix); info!( global = %global_wildcard, @@ -461,8 +458,8 @@ mod tests { #[tokio::test] async fn dispatch_cancel_is_notification_no_reply_published() { let (nats, agent) = dispatch( - "acp.s1.agent.session.cancel", - &CancelNotification::new("sess-1"), + "acp.session.s1.agent.cancel", + &CancelNotification::new("s1"), None, ) .await; @@ -496,8 +493,8 @@ mod tests { #[tokio::test] async fn dispatch_prompt_returns_stop_reason() { let (nats, _) = dispatch( - "acp.s1.agent.session.prompt", - &PromptRequest::new("sess-1", vec![]), + "acp.session.s1.agent.prompt", + &PromptRequest::new("s1", vec![]), Some("_INBOX.3"), ) .await; @@ -599,8 +596,8 @@ mod tests { #[tokio::test] async fn dispatch_session_load_publishes_response() { assert_dispatch_publishes( - "acp.s1.agent.session.load", - &LoadSessionRequest::new("sess-1", "/tmp"), + "acp.session.s1.agent.load", + &LoadSessionRequest::new("s1", "/tmp"), ) .await; } @@ -613,8 +610,8 @@ mod tests { #[tokio::test] async fn dispatch_set_session_mode_publishes_response() { assert_dispatch_publishes( - "acp.s1.agent.session.set_mode", - &SetSessionModeRequest::new("sess-1", "code"), + "acp.session.s1.agent.set_mode", + &SetSessionModeRequest::new("s1", "code"), ) .await; } @@ -622,8 +619,8 @@ mod tests { #[tokio::test] async fn dispatch_set_session_config_option_publishes_response() { assert_dispatch_publishes( - "acp.s1.agent.session.set_config_option", - &SetSessionConfigOptionRequest::new("sess-1", "key", "val"), + "acp.session.s1.agent.set_config_option", + &SetSessionConfigOptionRequest::new("s1", "key", "val"), ) .await; } @@ -631,8 +628,8 @@ mod tests { #[tokio::test] async fn dispatch_set_session_model_publishes_response() { assert_dispatch_publishes( - "acp.s1.agent.session.set_model", - &SetSessionModelRequest::new("sess-1", "gpt-4"), + "acp.session.s1.agent.set_model", + &SetSessionModelRequest::new("s1", "gpt-4"), ) .await; } @@ -640,8 +637,8 @@ mod tests { #[tokio::test] async fn dispatch_fork_session_publishes_response() { assert_dispatch_publishes( - "acp.s1.agent.session.fork", - &ForkSessionRequest::new("sess-1", "/tmp"), + "acp.session.s1.agent.fork", + &ForkSessionRequest::new("s1", "/tmp"), ) .await; } @@ -649,8 +646,8 @@ mod tests { #[tokio::test] async fn dispatch_resume_session_publishes_response() { assert_dispatch_publishes( - "acp.s1.agent.session.resume", - &ResumeSessionRequest::new("sess-1", "/tmp"), + "acp.session.s1.agent.resume", + &ResumeSessionRequest::new("s1", "/tmp"), ) .await; } @@ -658,8 +655,8 @@ mod tests { #[tokio::test] async fn dispatch_close_session_publishes_response() { assert_dispatch_publishes( - "acp.s1.agent.session.close", - &CloseSessionRequest::new("sess-1"), + "acp.session.s1.agent.close", + &CloseSessionRequest::new("s1"), ) .await; } @@ -832,7 +829,7 @@ mod tests { let subjects = nats.subscribed_to(); assert!(subjects.contains(&"myprefix.agent.>".to_string())); - assert!(subjects.contains(&"myprefix.*.agent.>".to_string())); + assert!(subjects.contains(&"myprefix.session.*.agent.>".to_string())); }) .await; } diff --git a/rsworkspace/crates/acp-nats/src/agent/bridge.rs b/rsworkspace/crates/acp-nats/src/agent/bridge.rs index 9af819de9..f2da8fe16 100644 --- a/rsworkspace/crates/acp-nats/src/agent/bridge.rs +++ b/rsworkspace/crates/acp-nats/src/agent/bridge.rs @@ -3,7 +3,7 @@ use std::cell::RefCell; use crate::config::Config; use crate::nats::{ self, ExtSessionReady, FlushClient, FlushPolicy, PublishClient, PublishOptions, RequestClient, - RetryPolicy, SubscribeClient, agent, + RetryPolicy, SubscribeClient, session, }; use crate::pending_prompt_waiters::PendingSessionPromptResponseWaiters; use crate::telemetry::metrics::Metrics; @@ -96,7 +96,7 @@ async fn publish_session_ready( ) { tokio::time::sleep(SESSION_READY_DELAY).await; - let subject = agent::ext_session_ready(prefix, &session_id.to_string()); + let subject = session::agent::ext_ready(prefix, &session_id.to_string()); info!(session_id = %session_id, subject = %subject, "Publishing session.ready"); let message = ExtSessionReady::new(session_id.clone()); diff --git a/rsworkspace/crates/acp-nats/src/agent/cancel.rs b/rsworkspace/crates/acp-nats/src/agent/cancel.rs index 118033056..135761ab3 100644 --- a/rsworkspace/crates/acp-nats/src/agent/cancel.rs +++ b/rsworkspace/crates/acp-nats/src/agent/cancel.rs @@ -1,5 +1,5 @@ use super::Bridge; -use crate::nats::{self, FlushClient, PublishClient, agent}; +use crate::nats::{self, FlushClient, PublishClient, session}; use crate::session_id::AcpSessionId; use agent_client_protocol::{CancelNotification, Error, ErrorCode, Result}; use tracing::{info, instrument, warn}; @@ -34,7 +34,7 @@ pub async fn handle( ) })?; - let subject = agent::session_cancel(bridge.config.acp_prefix(), &args.session_id.to_string()); + let subject = session::agent::cancel(bridge.config.acp_prefix(), &args.session_id.to_string()); let publish_result = nats::publish( bridge.nats(), @@ -58,7 +58,7 @@ pub async fn handle( } let cancelled_subject = - agent::session_cancelled(bridge.config.acp_prefix(), &args.session_id.to_string()); + session::agent::cancelled(bridge.config.acp_prefix(), &args.session_id.to_string()); if let Err(e) = bridge .nats() .publish_with_headers( @@ -116,8 +116,8 @@ mod tests { let published = mock.published_messages(); assert!( - published.contains(&"acp.s1.agent.session.cancel".to_string()), - "expected publish to acp.s1.agent.session.cancel, got: {:?}", + published.contains(&"acp.session.s1.agent.cancel".to_string()), + "expected publish to acp.session.s1.agent.cancel, got: {:?}", published ); } @@ -130,8 +130,8 @@ mod tests { let published = mock.published_messages(); assert!( - published.contains(&"acp.s1.agent.session.cancelled".to_string()), - "expected publish to acp.s1.agent.session.cancelled (prompt broadcast), got: {:?}", + published.contains(&"acp.session.s1.agent.cancelled".to_string()), + "expected publish to acp.session.s1.agent.cancelled (prompt broadcast), got: {:?}", published ); } diff --git a/rsworkspace/crates/acp-nats/src/agent/close_session.rs b/rsworkspace/crates/acp-nats/src/agent/close_session.rs index 78c78f0a7..b04b1af84 100644 --- a/rsworkspace/crates/acp-nats/src/agent/close_session.rs +++ b/rsworkspace/crates/acp-nats/src/agent/close_session.rs @@ -1,6 +1,6 @@ use super::Bridge; use crate::error::map_nats_error; -use crate::nats::{self, RequestClient, agent}; +use crate::nats::{self, RequestClient, session}; use crate::session_id::AcpSessionId; use agent_client_protocol::{CloseSessionRequest, CloseSessionResponse, Error, ErrorCode, Result}; use tracing::{info, instrument}; @@ -29,7 +29,7 @@ pub async fn handle( ) })?; let nats = bridge.nats(); - let subject = agent::session_close(bridge.config.acp_prefix(), session_id.as_str()); + let subject = session::agent::close(bridge.config.acp_prefix(), session_id.as_str()); let result = nats::request_with_timeout::( nats, @@ -61,7 +61,7 @@ mod tests { async fn close_session_forwards_request_and_returns_response() { let (mock, bridge) = mock_bridge(); let expected = CloseSessionResponse::new(); - set_json_response(&mock, "acp.s1.agent.session.close", &expected); + set_json_response(&mock, "acp.session.s1.agent.close", &expected); let request = CloseSessionRequest::new("s1"); let result = bridge.close_session(request).await; @@ -82,7 +82,7 @@ mod tests { #[tokio::test] async fn close_session_returns_error_when_response_is_invalid_json() { let (mock, bridge) = mock_bridge(); - mock.set_response("acp.s1.agent.session.close", "not json".into()); + mock.set_response("acp.session.s1.agent.close", "not json".into()); let request = CloseSessionRequest::new("s1"); let err = bridge.close_session(request).await.unwrap_err(); @@ -105,7 +105,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.close", + "acp.session.s1.agent.close", &CloseSessionResponse::new(), ); diff --git a/rsworkspace/crates/acp-nats/src/agent/fork_session.rs b/rsworkspace/crates/acp-nats/src/agent/fork_session.rs index 5a75b5ab1..a87737325 100644 --- a/rsworkspace/crates/acp-nats/src/agent/fork_session.rs +++ b/rsworkspace/crates/acp-nats/src/agent/fork_session.rs @@ -1,6 +1,6 @@ use super::Bridge; use crate::error::map_nats_error; -use crate::nats::{self, FlushClient, PublishClient, RequestClient, agent}; +use crate::nats::{self, FlushClient, PublishClient, RequestClient, session}; use crate::session_id::AcpSessionId; use agent_client_protocol::{Error, ErrorCode, ForkSessionRequest, ForkSessionResponse, Result}; use tracing::{Span, info, instrument}; @@ -29,7 +29,7 @@ pub async fn handle( nats, @@ -73,7 +73,7 @@ mod tests { let (mock, bridge) = mock_bridge(); let new_session_id = SessionId::from("forked-session-1"); let expected = ForkSessionResponse::new(new_session_id.clone()); - set_json_response(&mock, "acp.s1.agent.session.fork", &expected); + set_json_response(&mock, "acp.session.s1.agent.fork", &expected); let request = ForkSessionRequest::new("s1", "."); let result = bridge.fork_session(request).await; @@ -97,7 +97,7 @@ mod tests { #[tokio::test] async fn fork_session_returns_error_when_response_is_invalid_json() { let (mock, bridge) = mock_bridge(); - mock.set_response("acp.s1.agent.session.fork", "not json".into()); + mock.set_response("acp.session.s1.agent.fork", "not json".into()); let request = ForkSessionRequest::new("s1", "."); let err = bridge.fork_session(request).await.unwrap_err(); @@ -121,7 +121,7 @@ mod tests { let new_session_id = SessionId::from("forked-session-1"); set_json_response( &mock, - "acp.s1.agent.session.fork", + "acp.session.s1.agent.fork", &ForkSessionResponse::new(new_session_id), ); @@ -132,8 +132,8 @@ mod tests { tokio::time::sleep(Duration::from_millis(300)).await; let published = mock.published_messages(); assert!( - published.contains(&"acp.forked-session-1.agent.ext.session.ready".to_string()), - "expected publish to acp.forked-session-1.agent.ext.session.ready, got: {:?}", + published.contains(&"acp.session.forked-session-1.agent.ext.ready".to_string()), + "expected publish to acp.session.forked-session-1.agent.ext.ready, got: {:?}", published ); } @@ -143,7 +143,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.fork", + "acp.session.s1.agent.fork", &ForkSessionResponse::new("forked-1"), ); @@ -184,7 +184,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.fork", + "acp.session.s1.agent.fork", &ForkSessionResponse::new("forked-1"), ); mock.fail_publish_count(4); diff --git a/rsworkspace/crates/acp-nats/src/agent/load_session.rs b/rsworkspace/crates/acp-nats/src/agent/load_session.rs index 0062c5b5a..16b69c353 100644 --- a/rsworkspace/crates/acp-nats/src/agent/load_session.rs +++ b/rsworkspace/crates/acp-nats/src/agent/load_session.rs @@ -1,6 +1,6 @@ use super::Bridge; use crate::error::map_nats_error; -use crate::nats::{self, FlushClient, PublishClient, RequestClient, agent}; +use crate::nats::{self, FlushClient, PublishClient, RequestClient, session}; use crate::session_id::AcpSessionId; use agent_client_protocol::{Error, ErrorCode, LoadSessionRequest, LoadSessionResponse, Result}; use tracing::{info, instrument}; @@ -29,7 +29,7 @@ pub async fn handle( nats, @@ -67,7 +67,7 @@ mod tests { async fn load_session_forwards_request_and_returns_response() { let (mock, bridge) = mock_bridge(); let expected = LoadSessionResponse::new(); - set_json_response(&mock, "acp.s1.agent.session.load", &expected); + set_json_response(&mock, "acp.session.s1.agent.load", &expected); let request = LoadSessionRequest::new("s1", "."); let result = bridge.load_session(request).await; @@ -90,7 +90,7 @@ mod tests { #[tokio::test] async fn load_session_returns_error_when_response_is_invalid_json() { let (mock, bridge) = mock_bridge(); - mock.set_response("acp.s1.agent.session.load", "not json".into()); + mock.set_response("acp.session.s1.agent.load", "not json".into()); let request = LoadSessionRequest::new("s1", "."); let err = bridge.load_session(request).await.unwrap_err(); @@ -104,7 +104,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.load", + "acp.session.s1.agent.load", &LoadSessionResponse::new(), ); @@ -153,7 +153,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.load", + "acp.session.s1.agent.load", &LoadSessionResponse::new(), ); mock.fail_publish_count(4); @@ -181,7 +181,7 @@ mod tests { let (mock, bridge) = mock_bridge(); set_json_response( &mock, - "acp.s1.agent.session.load", + "acp.session.s1.agent.load", &LoadSessionResponse::new(), ); @@ -192,8 +192,8 @@ mod tests { tokio::time::sleep(Duration::from_millis(300)).await; let published = mock.published_messages(); assert!( - published.contains(&"acp.s1.agent.ext.session.ready".to_string()), - "expected publish to acp.s1.agent.ext.session.ready, got: {:?}", + published.contains(&"acp.session.s1.agent.ext.ready".to_string()), + "expected publish to acp.session.s1.agent.ext.ready, got: {:?}", published ); } diff --git a/rsworkspace/crates/acp-nats/src/agent/new_session.rs b/rsworkspace/crates/acp-nats/src/agent/new_session.rs index 6cceffc17..ff33f9812 100644 --- a/rsworkspace/crates/acp-nats/src/agent/new_session.rs +++ b/rsworkspace/crates/acp-nats/src/agent/new_session.rs @@ -177,8 +177,8 @@ mod tests { tokio::time::sleep(Duration::from_millis(300)).await; let published = mock.published_messages(); assert!( - published.contains(&"acp.test-session-1.agent.ext.session.ready".to_string()), - "expected publish to acp.test-session-1.agent.ext.session.ready, got: {:?}", + published.contains(&"acp.session.test-session-1.agent.ext.ready".to_string()), + "expected publish to acp.session.test-session-1.agent.ext.ready, got: {:?}", published ); } diff --git a/rsworkspace/crates/acp-nats/src/agent/prompt.rs b/rsworkspace/crates/acp-nats/src/agent/prompt.rs index 0b13f8ade..73abcaa68 100644 --- a/rsworkspace/crates/acp-nats/src/agent/prompt.rs +++ b/rsworkspace/crates/acp-nats/src/agent/prompt.rs @@ -8,7 +8,7 @@ use tracing::{instrument, warn}; use trogon_std::JsonSerialize; use crate::agent::Bridge; -use crate::nats::{FlushClient, PublishClient, RequestClient, SubscribeClient, agent}; +use crate::nats::{FlushClient, PublishClient, RequestClient, SubscribeClient, session}; use crate::session_id::AcpSessionId; pub use trogon_nats::REQ_ID_HEADER; @@ -42,19 +42,19 @@ where // Subscribe BEFORE publishing — prevents losing the first event if the runner responds instantly. let mut notifications_sub = bridge .nats - .subscribe(agent::session_update(prefix, sid, &req_id)) + .subscribe(session::agent::update(prefix, sid, &req_id)) .await .map_err(|e| Error::new(ErrorCode::InternalError.into(), format!("subscribe: {e}")))?; let mut response_sub = bridge .nats - .subscribe(agent::ext_session_prompt_response(prefix, sid, &req_id)) + .subscribe(session::agent::prompt_response(prefix, sid, &req_id)) .await .map_err(|e| Error::new(ErrorCode::InternalError.into(), format!("subscribe: {e}")))?; let mut cancel_sub = bridge .nats - .subscribe(agent::session_cancelled(prefix, sid)) + .subscribe(session::agent::cancelled(prefix, sid)) .await .map_err(|e| { Error::new( @@ -70,7 +70,7 @@ where let mut headers = async_nats::HeaderMap::new(); headers.insert(REQ_ID_HEADER, req_id.as_str()); - let prompt_subject = agent::session_prompt(prefix, sid); + let prompt_subject = session::agent::prompt(prefix, sid); bridge .nats .publish_with_headers(prompt_subject, headers, Bytes::from(payload_bytes)) @@ -396,8 +396,8 @@ mod tests { let subjects = mock.published_messages(); assert!( - subjects.iter().any(|s| s == "acp.s1.agent.session.prompt"), - "expected publish to acp.s1.agent.session.prompt, got: {:?}", + subjects.iter().any(|s| s == "acp.session.s1.agent.prompt"), + "expected publish to acp.session.s1.agent.prompt, got: {:?}", subjects ); } diff --git a/rsworkspace/crates/acp-nats/src/agent/resume_session.rs b/rsworkspace/crates/acp-nats/src/agent/resume_session.rs index 97d97c5ee..c1827730f 100644 --- a/rsworkspace/crates/acp-nats/src/agent/resume_session.rs +++ b/rsworkspace/crates/acp-nats/src/agent/resume_session.rs @@ -1,6 +1,6 @@ use super::Bridge; use crate::error::map_nats_error; -use crate::nats::{self, FlushClient, PublishClient, RequestClient, agent}; +use crate::nats::{self, FlushClient, PublishClient, RequestClient, session}; use crate::session_id::AcpSessionId; use agent_client_protocol::{ Error, ErrorCode, Result, ResumeSessionRequest, ResumeSessionResponse, @@ -31,7 +31,7 @@ pub async fn handle( nats, @@ -69,7 +69,7 @@ mod tests { async fn resume_session_forwards_request_and_returns_response() { let (mock, bridge) = mock_bridge(); let expected = ResumeSessionResponse::new(); - set_json_response(&mock, "acp.s1.agent.session.resume", &expected); + set_json_response(&mock, "acp.session.s1.agent.resume", &expected); let request = ResumeSessionRequest::new("s1", "."); let result = bridge.resume_session(request).await; @@ -90,7 +90,7 @@ mod tests { #[tokio::test] async fn resume_session_returns_error_when_response_is_invalid_json() { let (mock, bridge) = mock_bridge(); - mock.set_response("acp.s1.agent.session.resume", "not json".into()); + mock.set_response("acp.session.s1.agent.resume", "not json".into()); let request = ResumeSessionRequest::new("s1", "."); let err = bridge.resume_session(request).await.unwrap_err(); @@ -113,7 +113,7 @@ mod tests { let (mock, bridge) = mock_bridge(); set_json_response( &mock, - "acp.s1.agent.session.resume", + "acp.session.s1.agent.resume", &ResumeSessionResponse::new(), ); @@ -124,8 +124,8 @@ mod tests { tokio::time::sleep(Duration::from_millis(300)).await; let published = mock.published_messages(); assert!( - published.contains(&"acp.s1.agent.ext.session.ready".to_string()), - "expected publish to acp.s1.agent.ext.session.ready, got: {:?}", + published.contains(&"acp.session.s1.agent.ext.ready".to_string()), + "expected publish to acp.session.s1.agent.ext.ready, got: {:?}", published ); } @@ -135,7 +135,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.resume", + "acp.session.s1.agent.resume", &ResumeSessionResponse::new(), ); @@ -176,7 +176,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.resume", + "acp.session.s1.agent.resume", &ResumeSessionResponse::new(), ); mock.fail_publish_count(4); diff --git a/rsworkspace/crates/acp-nats/src/agent/set_session_config_option.rs b/rsworkspace/crates/acp-nats/src/agent/set_session_config_option.rs index 94b78d421..3ce4ca67e 100644 --- a/rsworkspace/crates/acp-nats/src/agent/set_session_config_option.rs +++ b/rsworkspace/crates/acp-nats/src/agent/set_session_config_option.rs @@ -1,6 +1,6 @@ use super::Bridge; use crate::error::map_nats_error; -use crate::nats::{self, RequestClient, agent}; +use crate::nats::{self, RequestClient, session}; use crate::session_id::AcpSessionId; use agent_client_protocol::{ Error, ErrorCode, Result, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, @@ -31,7 +31,8 @@ pub async fn handle( ) })?; let nats = bridge.nats(); - let subject = agent::session_set_config_option(bridge.config.acp_prefix(), session_id.as_str()); + let subject = + session::agent::set_config_option(bridge.config.acp_prefix(), session_id.as_str()); let result = nats::request_with_timeout::< N, @@ -64,7 +65,7 @@ mod tests { async fn set_session_config_option_forwards_request_and_returns_response() { let (mock, bridge) = mock_bridge(); let expected = SetSessionConfigOptionResponse::new(vec![]); - set_json_response(&mock, "acp.s1.agent.session.set_config_option", &expected); + set_json_response(&mock, "acp.session.s1.agent.set_config_option", &expected); let request = SetSessionConfigOptionRequest::new("s1", "theme", "dark"); let result = bridge.set_session_config_option(request).await; @@ -85,7 +86,7 @@ mod tests { #[tokio::test] async fn set_session_config_option_returns_error_when_response_is_invalid_json() { let (mock, bridge) = mock_bridge(); - mock.set_response("acp.s1.agent.session.set_config_option", "not json".into()); + mock.set_response("acp.session.s1.agent.set_config_option", "not json".into()); let request = SetSessionConfigOptionRequest::new("s1", "theme", "dark"); let err = bridge.set_session_config_option(request).await.unwrap_err(); @@ -108,7 +109,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.set_config_option", + "acp.session.s1.agent.set_config_option", &SetSessionConfigOptionResponse::new(vec![]), ); diff --git a/rsworkspace/crates/acp-nats/src/agent/set_session_mode.rs b/rsworkspace/crates/acp-nats/src/agent/set_session_mode.rs index 4e511f2b0..7842008e7 100644 --- a/rsworkspace/crates/acp-nats/src/agent/set_session_mode.rs +++ b/rsworkspace/crates/acp-nats/src/agent/set_session_mode.rs @@ -1,6 +1,6 @@ use super::Bridge; use crate::error::map_nats_error; -use crate::nats::{self, RequestClient, agent}; +use crate::nats::{self, RequestClient, session}; use crate::session_id::AcpSessionId; use agent_client_protocol::{ Error, ErrorCode, Result, SetSessionModeRequest, SetSessionModeResponse, @@ -31,7 +31,7 @@ pub async fn handle( ) })?; let nats = bridge.nats(); - let subject = agent::session_set_mode(bridge.config.acp_prefix(), session_id.as_str()); + let subject = session::agent::set_mode(bridge.config.acp_prefix(), session_id.as_str()); let result = nats::request_with_timeout::( nats, @@ -65,7 +65,7 @@ mod tests { let expected = SetSessionModeResponse::new(); set_json_response( &mock, - "acp.session-mode-001.agent.session.set_mode", + "acp.session.session-mode-001.agent.set_mode", &expected, ); @@ -88,7 +88,7 @@ mod tests { #[tokio::test] async fn set_session_mode_returns_error_when_response_is_invalid_json() { let (mock, bridge) = mock_bridge(); - mock.set_response("acp.s1.agent.session.set_mode", "not json".into()); + mock.set_response("acp.session.s1.agent.set_mode", "not json".into()); let request = SetSessionModeRequest::new("s1", "mode-1"); let err = bridge.set_session_mode(request).await.unwrap_err(); @@ -111,7 +111,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.set_mode", + "acp.session.s1.agent.set_mode", &SetSessionModeResponse::new(), ); diff --git a/rsworkspace/crates/acp-nats/src/agent/set_session_model.rs b/rsworkspace/crates/acp-nats/src/agent/set_session_model.rs index 5b8bfa76c..3e273a319 100644 --- a/rsworkspace/crates/acp-nats/src/agent/set_session_model.rs +++ b/rsworkspace/crates/acp-nats/src/agent/set_session_model.rs @@ -1,6 +1,6 @@ use super::Bridge; use crate::error::map_nats_error; -use crate::nats::{self, RequestClient, agent}; +use crate::nats::{self, RequestClient, session}; use crate::session_id::AcpSessionId; use agent_client_protocol::{ Error, ErrorCode, Result, SetSessionModelRequest, SetSessionModelResponse, @@ -31,7 +31,7 @@ pub async fn handle( ) })?; let nats = bridge.nats(); - let subject = agent::session_set_model(bridge.config.acp_prefix(), session_id.as_str()); + let subject = session::agent::set_model(bridge.config.acp_prefix(), session_id.as_str()); let result = nats::request_with_timeout::( nats, @@ -65,7 +65,7 @@ mod tests { async fn set_session_model_forwards_request_and_returns_response() { let (mock, bridge) = mock_bridge(); let expected = SetSessionModelResponse::new(); - set_json_response(&mock, "acp.s1.agent.session.set_model", &expected); + set_json_response(&mock, "acp.session.s1.agent.set_model", &expected); let request = SetSessionModelRequest::new("s1", "claude-sonnet-4-6"); let result = bridge.set_session_model(request).await; @@ -86,7 +86,7 @@ mod tests { #[tokio::test] async fn set_session_model_returns_error_when_response_is_invalid_json() { let (mock, bridge) = mock_bridge(); - mock.set_response("acp.s1.agent.session.set_model", "not json".into()); + mock.set_response("acp.session.s1.agent.set_model", "not json".into()); let request = SetSessionModelRequest::new("s1", "claude-sonnet-4-6"); let err = bridge.set_session_model(request).await.unwrap_err(); @@ -109,7 +109,7 @@ mod tests { let (mock, bridge, exporter, provider) = mock_bridge_with_metrics(); set_json_response( &mock, - "acp.s1.agent.session.set_model", + "acp.session.s1.agent.set_model", &SetSessionModelResponse::new(), ); diff --git a/rsworkspace/crates/acp-nats/src/client/mod.rs b/rsworkspace/crates/acp-nats/src/client/mod.rs index 0119832ed..34ccc2330 100644 --- a/rsworkspace/crates/acp-nats/src/client/mod.rs +++ b/rsworkspace/crates/acp-nats/src/client/mod.rs @@ -68,7 +68,7 @@ pub async fn run< bridge: Rc>, serializer: S, ) { - let wildcard = crate::nats::client_subjects::wildcards::all(bridge.config.acp_prefix()); + let wildcard = crate::nats::session::wildcards::all_client(bridge.config.acp_prefix()); info!("Starting client proxy - subscribing to {}", wildcard); let mut subscriber = match nats.subscribe(wildcard).await { @@ -520,7 +520,7 @@ mod tests { SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from("hi"))), ); let payload = serde_json::to_vec(¬ification).unwrap(); - let msg = make_msg("acp.sess1.client.session.update", &payload, None); + let msg = make_msg("acp.session.sess1.client.session.update", &payload, None); let tx = nats.inject_messages(); tx.unbounded_send(msg).unwrap(); @@ -559,7 +559,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.update", + "acp.session.sess-1.client.session.update", parsed, payload, None, @@ -599,7 +599,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.read_text_file", + "acp.session.sess-1.client.fs.read_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -640,7 +640,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.write_text_file", + "acp.session.sess-1.client.fs.write_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -722,7 +722,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.create", + "acp.session.sess-1.client.terminal.create", parsed, payload, Some("_INBOX.reply".to_string()), @@ -760,7 +760,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-a.client.terminal.create", + "acp.session.sess-a.client.terminal.create", parsed, payload, Some("_INBOX.reply".to_string()), @@ -800,7 +800,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -843,7 +843,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.output", + "acp.session.sess-1.client.terminal.output", parsed, payload, Some("_INBOX.reply".to_string()), @@ -896,7 +896,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.release", + "acp.session.sess-1.client.terminal.release", parsed, payload, Some("_INBOX.reply".to_string()), @@ -944,7 +944,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, None, @@ -982,7 +982,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1026,7 +1026,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1078,7 +1078,7 @@ mod tests { serializer: &serializer, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1124,7 +1124,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1171,7 +1171,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1210,7 +1210,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1249,7 +1249,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1288,7 +1288,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1322,7 +1322,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.wait_for_exit", + "acp.session.sess-1.client.terminal.wait_for_exit", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1354,7 +1354,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.update", + "acp.session.sess-1.client.session.update", parsed, payload, None, @@ -1385,7 +1385,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.update", + "acp.session.sess-1.client.session.update", parsed, payload, None, @@ -1419,7 +1419,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.request_permission", + "acp.session.sess-1.client.session.request_permission", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1454,7 +1454,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.request_permission", + "acp.session.sess-1.client.session.request_permission", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1487,7 +1487,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.read_text_file", + "acp.session.sess-1.client.fs.read_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1520,7 +1520,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.create", + "acp.session.sess-1.client.terminal.create", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1556,7 +1556,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1589,7 +1589,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.output", + "acp.session.sess-1.client.terminal.output", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1622,7 +1622,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.release", + "acp.session.sess-1.client.terminal.release", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1659,7 +1659,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.write_text_file", + "acp.session.sess-1.client.fs.write_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1692,7 +1692,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.read_text_file", + "acp.session.sess-1.client.fs.read_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1725,7 +1725,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.create", + "acp.session.sess-1.client.terminal.create", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1761,7 +1761,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1794,7 +1794,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.output", + "acp.session.sess-1.client.terminal.output", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1827,7 +1827,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.release", + "acp.session.sess-1.client.terminal.release", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1864,7 +1864,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.write_text_file", + "acp.session.sess-1.client.fs.write_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1900,7 +1900,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.write_text_file", + "acp.session.sess-1.client.fs.write_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1936,7 +1936,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.write_text_file", + "acp.session.sess-1.client.fs.write_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -1973,7 +1973,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-a.client.terminal.release", + "acp.session.sess-a.client.terminal.release", parsed, payload, Some("_INBOX.err".to_string()), @@ -2015,7 +2015,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.release", + "acp.session.sess-1.client.terminal.release", parsed, payload, Some("_INBOX.err".to_string()), @@ -2059,7 +2059,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.release", + "acp.session.sess-1.client.terminal.release", parsed, payload, None, @@ -2097,7 +2097,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.update", + "acp.session.sess-1.client.session.update", parsed, payload, None, @@ -2136,7 +2136,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.read_text_file", + "acp.session.sess-1.client.fs.read_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2174,7 +2174,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.create", + "acp.session.sess-1.client.terminal.create", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2212,7 +2212,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2250,7 +2250,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.output", + "acp.session.sess-1.client.terminal.output", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2288,7 +2288,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.release", + "acp.session.sess-1.client.terminal.release", parsed, payload, Some("_INBOX.err".to_string()), @@ -2331,7 +2331,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.request_permission", + "acp.session.sess-1.client.session.request_permission", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2369,7 +2369,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.output", + "acp.session.sess-1.client.terminal.output", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2413,7 +2413,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.output", + "acp.session.sess-1.client.terminal.output", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2451,7 +2451,7 @@ mod tests { serializer: &serializer, }; dispatch_client_method( - "acp.sess-1.client.terminal.output", + "acp.session.sess-1.client.terminal.output", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2491,7 +2491,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, None, @@ -2533,7 +2533,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-a.client.terminal.kill", + "acp.session.sess-a.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2581,7 +2581,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2938,7 +2938,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -2991,7 +2991,7 @@ mod tests { serializer: &serializer, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3040,7 +3040,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3081,7 +3081,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3123,7 +3123,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3164,7 +3164,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.kill", + "acp.session.sess-1.client.terminal.kill", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3200,7 +3200,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.update", + "acp.session.sess-1.client.session.update", parsed, payload, None, @@ -3239,7 +3239,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.read_text_file", + "acp.session.sess-1.client.fs.read_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3276,7 +3276,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.terminal.create", + "acp.session.sess-1.client.terminal.create", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3316,7 +3316,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.request_permission", + "acp.session.sess-1.client.session.request_permission", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3356,7 +3356,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.read_text_file", + "acp.session.sess-1.client.fs.read_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3397,7 +3397,7 @@ mod tests { serializer: &serializer, }; dispatch_client_method( - "acp.sess-1.client.fs.read_text_file", + "acp.session.sess-1.client.fs.read_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3479,7 +3479,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.update", + "acp.session.sess-1.client.session.update", parsed, payload, None, @@ -3517,7 +3517,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.read_text_file", + "acp.session.sess-1.client.fs.read_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3562,7 +3562,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.request_permission", + "acp.session.sess-1.client.session.request_permission", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3599,7 +3599,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.fs.write_text_file", + "acp.session.sess-1.client.fs.write_text_file", parsed, payload, Some("_INBOX.reply".to_string()), @@ -3644,7 +3644,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.request_permission", + "acp.session.sess-1.client.session.request_permission", parsed, payload, Some("_INBOX.err".to_string()), @@ -3689,7 +3689,7 @@ mod tests { serializer: &StdJsonSerialize, }; dispatch_client_method( - "acp.sess-1.client.session.request_permission", + "acp.session.sess-1.client.session.request_permission", parsed, payload, Some("_INBOX.err".to_string()), @@ -3736,7 +3736,7 @@ mod tests { serializer: &serializer, }; dispatch_client_method( - "acp.sess-1.client.session.request_permission", + "acp.session.sess-1.client.session.request_permission", parsed, payload, Some("_INBOX.err".to_string()), @@ -3798,7 +3798,7 @@ mod tests { let client = Rc::new(MockClient::new()); let in_flight = Rc::new(Cell::new(1usize)); - let msg = make_msg("acp.sess1.client.session.update", b"{}", None); + let msg = make_msg("acp.session.sess1.client.session.update", b"{}", None); process_message(msg, &nats, client, bridge, &in_flight, 1, &StdJsonSerialize).await; assert!(nats.published_messages().is_empty()); @@ -3821,7 +3821,7 @@ mod tests { }; let payload = serde_json::to_vec(&envelope).unwrap(); let msg = make_msg( - "acp.sess1.client.fs.read_text_file", + "acp.session.sess1.client.fs.read_text_file", &payload, Some("_INBOX.reply"), ); @@ -3848,7 +3848,7 @@ mod tests { }; let payload = serde_json::to_vec(&envelope).unwrap(); let msg = make_msg( - "acp.sess1.client.fs.read_text_file", + "acp.session.sess1.client.fs.read_text_file", &payload, Some("_INBOX.reply"), ); @@ -3875,7 +3875,7 @@ mod tests { }; let payload = serde_json::to_vec(&envelope).unwrap(); let msg = make_msg( - "acp.sess1.client.fs.read_text_file", + "acp.session.sess1.client.fs.read_text_file", &payload, Some("_INBOX.reply"), ); @@ -3902,7 +3902,7 @@ mod tests { }; let payload = serde_json::to_vec(&envelope).unwrap(); let msg = make_msg( - "acp.sess1.client.fs.read_text_file", + "acp.session.sess1.client.fs.read_text_file", &payload, Some("_INBOX.reply"), ); @@ -3929,7 +3929,7 @@ mod tests { }; let payload = serde_json::to_vec(&envelope).unwrap(); let msg = make_msg( - "acp.sess1.client.fs.read_text_file", + "acp.session.sess1.client.fs.read_text_file", &payload, Some("_INBOX.reply"), ); @@ -3954,7 +3954,7 @@ mod tests { ); let payload = serde_json::to_vec(¬ification).unwrap(); - let msg = make_msg("acp.sess1.client.session.update", &payload, None); + let msg = make_msg("acp.session.sess1.client.session.update", &payload, None); process_message( msg, &nats, diff --git a/rsworkspace/crates/acp-nats/src/client_proxy.rs b/rsworkspace/crates/acp-nats/src/client_proxy.rs index 336f6d1ef..c827d0b7f 100644 --- a/rsworkspace/crates/acp-nats/src/client_proxy.rs +++ b/rsworkspace/crates/acp-nats/src/client_proxy.rs @@ -1,5 +1,5 @@ use crate::acp_prefix::AcpPrefix; -use crate::nats::client_subjects; +use crate::nats::session; use crate::session_id::AcpSessionId; use agent_client_protocol::{ Client, CreateTerminalRequest, CreateTerminalResponse, Error, ErrorCode, KillTerminalRequest, @@ -70,32 +70,32 @@ impl Client for NatsClientProxy< &self, args: RequestPermissionRequest, ) -> Result { - let s = client_subjects::session_request_permission(self.prefix(), self.session_id()); + let s = session::client::session_request_permission(self.prefix(), self.session_id()); self.request(&s, &args).await } async fn session_notification(&self, args: SessionNotification) -> Result<()> { - let s = client_subjects::session_update(self.prefix(), self.session_id()); + let s = session::client::session_update(self.prefix(), self.session_id()); self.notify(&s, &args).await } async fn read_text_file(&self, args: ReadTextFileRequest) -> Result { - let s = client_subjects::fs_read_text_file(self.prefix(), self.session_id()); + let s = session::client::fs_read_text_file(self.prefix(), self.session_id()); self.request(&s, &args).await } async fn write_text_file(&self, args: WriteTextFileRequest) -> Result { - let s = client_subjects::fs_write_text_file(self.prefix(), self.session_id()); + let s = session::client::fs_write_text_file(self.prefix(), self.session_id()); self.request(&s, &args).await } async fn create_terminal(&self, args: CreateTerminalRequest) -> Result { - let s = client_subjects::terminal_create(self.prefix(), self.session_id()); + let s = session::client::terminal_create(self.prefix(), self.session_id()); self.request(&s, &args).await } async fn terminal_output(&self, args: TerminalOutputRequest) -> Result { - let s = client_subjects::terminal_output(self.prefix(), self.session_id()); + let s = session::client::terminal_output(self.prefix(), self.session_id()); self.request(&s, &args).await } @@ -103,7 +103,7 @@ impl Client for NatsClientProxy< &self, args: ReleaseTerminalRequest, ) -> Result { - let s = client_subjects::terminal_release(self.prefix(), self.session_id()); + let s = session::client::terminal_release(self.prefix(), self.session_id()); self.request(&s, &args).await } @@ -111,12 +111,12 @@ impl Client for NatsClientProxy< &self, args: WaitForTerminalExitRequest, ) -> Result { - let s = client_subjects::terminal_wait_for_exit(self.prefix(), self.session_id()); + let s = session::client::terminal_wait_for_exit(self.prefix(), self.session_id()); self.request(&s, &args).await } async fn kill_terminal(&self, args: KillTerminalRequest) -> Result { - let s = client_subjects::terminal_kill(self.prefix(), self.session_id()); + let s = session::client::terminal_kill(self.prefix(), self.session_id()); self.request(&s, &args).await } } @@ -145,7 +145,7 @@ mod tests { let nats = AdvancedMockNatsClient::new(); let response = RequestPermissionResponse::new(RequestPermissionOutcome::Cancelled); nats.set_response( - "acp.s1.client.session.request_permission", + "acp.session.s1.client.session.request_permission", serde_json::to_vec(&response).unwrap().into(), ); @@ -173,7 +173,7 @@ mod tests { assert!(result.is_ok()); assert_eq!( nats.published_messages(), - vec!["acp.s1.client.session.update"] + vec!["acp.session.s1.client.session.update"] ); } @@ -182,7 +182,7 @@ mod tests { let nats = AdvancedMockNatsClient::new(); let response = ReadTextFileResponse::new("file contents"); nats.set_response( - "acp.s1.client.fs.read_text_file", + "acp.session.s1.client.fs.read_text_file", serde_json::to_vec(&response).unwrap().into(), ); @@ -214,7 +214,7 @@ mod tests { let nats = AdvancedMockNatsClient::new(); let response = agent_client_protocol::WriteTextFileResponse::default(); nats.set_response( - "acp.s1.client.fs.write_text_file", + "acp.session.s1.client.fs.write_text_file", serde_json::to_vec(&response).unwrap().into(), ); @@ -231,7 +231,7 @@ mod tests { let nats = AdvancedMockNatsClient::new(); let response = CreateTerminalResponse::new("t1"); nats.set_response( - "acp.s1.client.terminal.create", + "acp.session.s1.client.terminal.create", serde_json::to_vec(&response).unwrap().into(), ); @@ -248,7 +248,7 @@ mod tests { let nats = AdvancedMockNatsClient::new(); let response = TerminalOutputResponse::new("output", false); nats.set_response( - "acp.s1.client.terminal.output", + "acp.session.s1.client.terminal.output", serde_json::to_vec(&response).unwrap().into(), ); @@ -265,7 +265,7 @@ mod tests { let nats = AdvancedMockNatsClient::new(); let response = ReleaseTerminalResponse::default(); nats.set_response( - "acp.s1.client.terminal.release", + "acp.session.s1.client.terminal.release", serde_json::to_vec(&response).unwrap().into(), ); @@ -282,7 +282,7 @@ mod tests { let nats = AdvancedMockNatsClient::new(); let response = KillTerminalResponse::default(); nats.set_response( - "acp.s1.client.terminal.kill", + "acp.session.s1.client.terminal.kill", serde_json::to_vec(&response).unwrap().into(), ); @@ -298,7 +298,7 @@ mod tests { let response = WaitForTerminalExitResponse::new(agent_client_protocol::TerminalExitStatus::new()); nats.set_response( - "acp.s1.client.terminal.wait_for_exit", + "acp.session.s1.client.terminal.wait_for_exit", serde_json::to_vec(&response).unwrap().into(), ); diff --git a/rsworkspace/crates/acp-nats/src/constants.rs b/rsworkspace/crates/acp-nats/src/constants.rs index 714b894d1..eb0b38a05 100644 --- a/rsworkspace/crates/acp-nats/src/constants.rs +++ b/rsworkspace/crates/acp-nats/src/constants.rs @@ -22,9 +22,10 @@ pub const MAX_METHOD_NAME_LENGTH: usize = 128; pub const AGENT_UNAVAILABLE: i32 = -32001; -pub const AGENT_MARKER: &str = ".agent."; -pub const AGENT_EXT_PREFIX: &str = "agent.ext."; -pub const EXT_SUBJECT_PREFIX: &str = "client.ext."; +pub const SESSION_PREFIX: &str = ".session."; +pub const SESSION_AGENT_MARKER: &str = ".agent."; +pub const SESSION_CLIENT_MARKER: &str = ".client."; +pub const EXT_SUBJECT_PREFIX: &str = "ext."; pub const CONTENT_TYPE_JSON: &str = "application/json"; pub const CONTENT_TYPE_PLAIN: &str = "text/plain"; diff --git a/rsworkspace/crates/acp-nats/src/nats/mod.rs b/rsworkspace/crates/acp-nats/src/nats/mod.rs index b92d07954..765edd33d 100644 --- a/rsworkspace/crates/acp-nats/src/nats/mod.rs +++ b/rsworkspace/crates/acp-nats/src/nats/mod.rs @@ -8,7 +8,7 @@ pub use parsing::{ AgentMethod, ClientMethod, ParsedAgentSubject, ParsedClientSubject, parse_agent_subject, parse_client_subject, }; -pub use subjects::{agent, client as client_subjects, wildcards}; +pub use subjects::{agent, session}; pub use trogon_nats::{ FlushClient, FlushPolicy, PublishClient, PublishOptions, RequestClient, RetryPolicy, SubscribeClient, client, connect, headers_with_trace_context, inject_trace_context, publish, diff --git a/rsworkspace/crates/acp-nats/src/nats/parsing.rs b/rsworkspace/crates/acp-nats/src/nats/parsing.rs index 5234e8fb8..f0f2420f2 100644 --- a/rsworkspace/crates/acp-nats/src/nats/parsing.rs +++ b/rsworkspace/crates/acp-nats/src/nats/parsing.rs @@ -1,4 +1,6 @@ -use crate::constants::{AGENT_EXT_PREFIX, AGENT_MARKER, EXT_SUBJECT_PREFIX}; +use crate::constants::{ + EXT_SUBJECT_PREFIX, SESSION_AGENT_MARKER, SESSION_CLIENT_MARKER, SESSION_PREFIX, +}; use crate::ext_method_name::ExtMethodName; use crate::session_id::AcpSessionId; @@ -21,38 +23,33 @@ pub enum AgentMethod { } impl AgentMethod { - pub fn is_session_scoped(&self) -> bool { - !matches!( - self, - Self::Initialize - | Self::Authenticate - | Self::SessionNew - | Self::SessionList - | Self::Ext(_) - ) - } - - fn from_suffix(suffix: &str) -> Option { + fn from_global_suffix(suffix: &str) -> Option { match suffix { - "agent.initialize" => Some(Self::Initialize), - "agent.authenticate" => Some(Self::Authenticate), - "agent.session.new" => Some(Self::SessionNew), - "agent.session.list" => Some(Self::SessionList), - "agent.session.load" => Some(Self::SessionLoad), - "agent.session.prompt" => Some(Self::SessionPrompt), - "agent.session.cancel" => Some(Self::SessionCancel), - "agent.session.set_mode" => Some(Self::SessionSetMode), - "agent.session.set_config_option" => Some(Self::SessionSetConfigOption), - "agent.session.set_model" => Some(Self::SessionSetModel), - "agent.session.fork" => Some(Self::SessionFork), - "agent.session.resume" => Some(Self::SessionResume), - "agent.session.close" => Some(Self::SessionClose), + "initialize" => Some(Self::Initialize), + "authenticate" => Some(Self::Authenticate), + "session.new" => Some(Self::SessionNew), + "session.list" => Some(Self::SessionList), other => { - let ext_name = other.strip_prefix(AGENT_EXT_PREFIX)?; + let ext_name = other.strip_prefix("ext.")?; Some(Self::Ext(ExtMethodName::new(ext_name).ok()?)) } } } + + fn from_session_suffix(suffix: &str) -> Option { + match suffix { + "load" => Some(Self::SessionLoad), + "prompt" => Some(Self::SessionPrompt), + "cancel" => Some(Self::SessionCancel), + "set_mode" => Some(Self::SessionSetMode), + "set_config_option" => Some(Self::SessionSetConfigOption), + "set_model" => Some(Self::SessionSetModel), + "fork" => Some(Self::SessionFork), + "resume" => Some(Self::SessionResume), + "close" => Some(Self::SessionClose), + _ => None, + } + } } #[derive(Debug)] @@ -62,25 +59,35 @@ pub struct ParsedAgentSubject { } pub fn parse_agent_subject(subject: &str) -> Option { - for (agent_byte_pos, _) in subject.match_indices(AGENT_MARKER) { - let suffix = &subject[agent_byte_pos + 1..]; - let method = match AgentMethod::from_suffix(suffix) { - Some(m) => m, - None => continue, - }; + if let Some(parsed) = try_parse_session_agent(subject) { + return Some(parsed); + } - let session_id = if method.is_session_scoped() { - let before_agent = &subject[..agent_byte_pos]; - let session_dot = before_agent.rfind('.')?; - let raw = &before_agent[session_dot + 1..]; - Some(AcpSessionId::new(raw).ok()?) - } else { - None - }; + let agent_pos = subject.rfind(".agent.")?; + let suffix = &subject[agent_pos + ".agent.".len()..]; + let method = AgentMethod::from_global_suffix(suffix)?; + Some(ParsedAgentSubject { + session_id: None, + method, + }) +} - return Some(ParsedAgentSubject { session_id, method }); +fn try_parse_session_agent(subject: &str) -> Option { + for (session_pos, _) in subject.match_indices(SESSION_PREFIX) { + let after_session = &subject[session_pos + SESSION_PREFIX.len()..]; + if let Some(agent_pos) = after_session.find(SESSION_AGENT_MARKER) { + let sid = &after_session[..agent_pos]; + if let Ok(session_id) = AcpSessionId::new(sid) { + let method_suffix = &after_session[agent_pos + SESSION_AGENT_MARKER.len()..]; + if let Some(method) = AgentMethod::from_session_suffix(method_suffix) { + return Some(ParsedAgentSubject { + session_id: Some(session_id), + method, + }); + } + } + } } - None } @@ -102,16 +109,16 @@ pub enum ClientMethod { impl ClientMethod { pub fn from_subject_suffix(suffix: &str) -> Option { match suffix { - "client.fs.read_text_file" => Some(Self::FsReadTextFile), - "client.fs.write_text_file" => Some(Self::FsWriteTextFile), - "client.session.request_permission" => Some(Self::SessionRequestPermission), - "client.session.update" => Some(Self::SessionUpdate), - "client.terminal.create" => Some(Self::TerminalCreate), - "client.terminal.kill" => Some(Self::TerminalKill), - "client.terminal.output" => Some(Self::TerminalOutput), - "client.terminal.release" => Some(Self::TerminalRelease), - "client.terminal.wait_for_exit" => Some(Self::TerminalWaitForExit), - "client.ext.session.prompt_response" => Some(Self::ExtSessionPromptResponse), + "fs.read_text_file" => Some(Self::FsReadTextFile), + "fs.write_text_file" => Some(Self::FsWriteTextFile), + "session.request_permission" => Some(Self::SessionRequestPermission), + "session.update" => Some(Self::SessionUpdate), + "terminal.create" => Some(Self::TerminalCreate), + "terminal.kill" => Some(Self::TerminalKill), + "terminal.output" => Some(Self::TerminalOutput), + "terminal.release" => Some(Self::TerminalRelease), + "terminal.wait_for_exit" => Some(Self::TerminalWaitForExit), + "ext.session.prompt_response" => Some(Self::ExtSessionPromptResponse), other => { let ext_name = other.strip_prefix(EXT_SUBJECT_PREFIX)?; ExtMethodName::new(ext_name).ok()?; @@ -128,489 +135,320 @@ pub struct ParsedClientSubject { } pub fn parse_client_subject(subject: &str) -> Option { - let client_byte_pos = subject.rmatch_indices(".client.").next()?.0; - - let before_client = &subject[..client_byte_pos]; - let session_dot = before_client.rfind('.')?; - let session_id = &before_client[session_dot + 1..]; - let session_id = AcpSessionId::new(session_id).ok()?; - - let suffix = &subject[client_byte_pos + 1..]; - - let method = ClientMethod::from_subject_suffix(suffix)?; - - Some(ParsedClientSubject { session_id, method }) + for (session_pos, _) in subject.match_indices(SESSION_PREFIX) { + let after_session = &subject[session_pos + SESSION_PREFIX.len()..]; + if let Some(client_pos) = after_session.find(SESSION_CLIENT_MARKER) { + let sid = &after_session[..client_pos]; + if let Ok(session_id) = AcpSessionId::new(sid) { + let method_suffix = &after_session[client_pos + SESSION_CLIENT_MARKER.len()..]; + if let Some(method) = ClientMethod::from_subject_suffix(method_suffix) { + return Some(ParsedClientSubject { session_id, method }); + } + } + } + } + None } #[cfg(test)] mod tests { use super::*; - #[test] - fn test_parse_fs_read_text_file() { - let subject = "acp.sess123.client.fs.read_text_file"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::FsReadTextFile); - } - - #[test] - fn test_parse_fs_write_text_file() { - let subject = "acp.sess456.client.fs.write_text_file"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess456"); - assert_eq!(parsed.method, ClientMethod::FsWriteTextFile); - } + // Agent global methods #[test] - fn test_parse_session_request_permission() { - let subject = "acp.sess123.client.session.request_permission"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::SessionRequestPermission); + fn parse_agent_initialize() { + let parsed = parse_agent_subject("acp.agent.initialize").unwrap(); + assert!(parsed.session_id.is_none()); + assert_eq!(parsed.method, AgentMethod::Initialize); } #[test] - fn test_parse_session_update() { - let subject = "acp.sess123.client.session.update"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::SessionUpdate); + fn parse_agent_authenticate() { + let parsed = parse_agent_subject("acp.agent.authenticate").unwrap(); + assert!(parsed.session_id.is_none()); + assert_eq!(parsed.method, AgentMethod::Authenticate); } #[test] - fn test_parse_terminal_create() { - let subject = "acp.sess123.client.terminal.create"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::TerminalCreate); + fn parse_agent_session_new() { + let parsed = parse_agent_subject("acp.agent.session.new").unwrap(); + assert!(parsed.session_id.is_none()); + assert_eq!(parsed.method, AgentMethod::SessionNew); } #[test] - fn test_parse_terminal_kill() { - let subject = "acp.sess123.client.terminal.kill"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::TerminalKill); + fn parse_agent_session_list() { + let parsed = parse_agent_subject("acp.agent.session.list").unwrap(); + assert!(parsed.session_id.is_none()); + assert_eq!(parsed.method, AgentMethod::SessionList); } #[test] - fn test_parse_terminal_output() { - let subject = "acp.sess123.client.terminal.output"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::TerminalOutput); + fn parse_agent_ext() { + let parsed = parse_agent_subject("acp.agent.ext.my_tool").unwrap(); + assert!(parsed.session_id.is_none()); + assert_eq!( + parsed.method, + AgentMethod::Ext(ExtMethodName::new("my_tool").unwrap()) + ); } #[test] - fn test_parse_terminal_release() { - let subject = "acp.sess123.client.terminal.release"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::TerminalRelease); + fn parse_agent_ext_dotted() { + let parsed = parse_agent_subject("acp.agent.ext.vendor.op").unwrap(); + assert!(parsed.session_id.is_none()); + assert_eq!( + parsed.method, + AgentMethod::Ext(ExtMethodName::new("vendor.op").unwrap()) + ); } - #[test] - fn test_parse_terminal_wait_for_exit() { - let subject = "acp.sess123.client.terminal.wait_for_exit"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::TerminalWaitForExit); - } + // Session-scoped agent methods #[test] - fn test_parse_ext_session_prompt_response() { - let subject = "acp.sess999.client.ext.session.prompt_response"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess999"); - assert_eq!(parsed.method, ClientMethod::ExtSessionPromptResponse); + fn parse_session_agent_load() { + let parsed = parse_agent_subject("acp.session.s1.agent.load").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionLoad); } #[test] - fn test_parse_with_custom_prefix() { - let subject = "myapp.sess123.client.session.update"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::SessionUpdate); + fn parse_session_agent_prompt() { + let parsed = parse_agent_subject("acp.session.s1.agent.prompt").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionPrompt); } #[test] - fn test_parse_with_long_prefix() { - let subject = "my.multi.part.prefix.sess123.client.session.update"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::SessionUpdate); + fn parse_session_agent_cancel() { + let parsed = parse_agent_subject("acp.session.s1.agent.cancel").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionCancel); } #[test] - fn test_parse_empty_subject() { - assert!(parse_client_subject("").is_none()); + fn parse_session_agent_set_mode() { + let parsed = parse_agent_subject("acp.session.s1.agent.set_mode").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionSetMode); } #[test] - fn test_parse_no_method_suffix() { - assert!(parse_client_subject("acp.client").is_none()); - assert!(parse_client_subject("acp.sess.client").is_none()); + fn parse_session_agent_set_config_option() { + let parsed = parse_agent_subject("acp.session.s1.agent.set_config_option").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionSetConfigOption); } #[test] - fn test_parse_no_client_marker() { - assert!(parse_client_subject("acp.sess123.agent.initialize").is_none()); - assert!(parse_client_subject("acp.sess123.other.method").is_none()); + fn parse_session_agent_set_model() { + let parsed = parse_agent_subject("acp.session.s1.agent.set_model").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionSetModel); } #[test] - fn test_parse_unknown_method() { - assert!(parse_client_subject("acp.sess123.client.unknown.method").is_none()); + fn parse_session_agent_fork() { + let parsed = parse_agent_subject("acp.session.s1.agent.fork").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionFork); } #[test] - fn test_parse_rejects_invalid_session_tokens() { - assert!(parse_client_subject("acp.session*id.client.session.update").is_none()); - assert!(parse_client_subject("acp.session id.client.fs.read_text_file").is_none()); - assert!(parse_client_subject("acp.session>id.client.fs.read_text_file").is_none()); + fn parse_session_agent_resume() { + let parsed = parse_agent_subject("acp.session.s1.agent.resume").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionResume); } #[test] - fn test_parse_with_client_in_prefix() { - let subject = "org.client.app.sess123.client.session.update"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::SessionUpdate); + fn parse_session_agent_close() { + let parsed = parse_agent_subject("acp.session.s1.agent.close").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionClose); } #[test] - fn test_parse_malformed_structure() { - assert!(parse_client_subject("...").is_none()); - assert!(parse_client_subject("acp..client.method").is_none()); - } - - #[test] - fn test_parse_empty_session_id() { - assert!(parse_client_subject("acp..client.fs.read_text_file").is_none()); - } - - #[test] - fn test_parse_client_not_in_correct_position() { - assert!(parse_client_subject("client.acp.sess123.method").is_none()); - } - - #[test] - fn test_client_method_from_suffix_all_variants() { - let test_cases = vec![ - ( - "client.fs.read_text_file", - Some(ClientMethod::FsReadTextFile), - ), - ( - "client.fs.write_text_file", - Some(ClientMethod::FsWriteTextFile), - ), - ( - "client.session.request_permission", - Some(ClientMethod::SessionRequestPermission), - ), - ("client.session.update", Some(ClientMethod::SessionUpdate)), - ("client.terminal.create", Some(ClientMethod::TerminalCreate)), - ("client.terminal.kill", Some(ClientMethod::TerminalKill)), - ("client.terminal.output", Some(ClientMethod::TerminalOutput)), - ( - "client.terminal.release", - Some(ClientMethod::TerminalRelease), - ), - ( - "client.terminal.wait_for_exit", - Some(ClientMethod::TerminalWaitForExit), - ), - ( - "client.ext.session.prompt_response", - Some(ClientMethod::ExtSessionPromptResponse), - ), - ( - "client.ext.my_method", - Some(ClientMethod::Ext("my_method".to_string())), - ), - ( - "client.ext.vendor.operation", - Some(ClientMethod::Ext("vendor.operation".to_string())), - ), - ("client.unknown", None), - ("client.ext.", None), - ("client.ext.method.*", None), - ("", None), - ]; - - for (suffix, expected) in test_cases { - assert_eq!( - ClientMethod::from_subject_suffix(suffix), - expected, - "Failed for suffix: {}", - suffix - ); - } + fn parse_agent_custom_prefix() { + let parsed = parse_agent_subject("myapp.agent.initialize").unwrap(); + assert!(parsed.session_id.is_none()); + assert_eq!(parsed.method, AgentMethod::Initialize); } #[test] - fn test_parse_session_id_extraction() { - let test_cases = vec![ - ("acp.sess1.client.fs.read_text_file", "sess1"), - ( - "myapp.my-session-123.client.terminal.create", - "my-session-123", - ), - ( - "prefix.session_with_underscores.client.session.update", - "session_with_underscores", - ), - ]; - - for (subject, expected_session_id) in test_cases { - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), expected_session_id); - } + fn parse_session_agent_custom_prefix() { + let parsed = parse_agent_subject("myapp.session.s1.agent.prompt").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); + assert_eq!(parsed.method, AgentMethod::SessionPrompt); } #[test] - fn test_parse_ext_method() { - let subject = "acp.sess123.client.ext.my_custom_method"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!( - parsed.method, - ClientMethod::Ext("my_custom_method".to_string()) - ); + fn parse_agent_empty_returns_none() { + assert!(parse_agent_subject("").is_none()); } #[test] - fn test_parse_ext_method_dotted_namespace() { - let subject = "acp.sess123.client.ext.vendor.operation"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!( - parsed.method, - ClientMethod::Ext("vendor.operation".to_string()) - ); + fn parse_agent_unknown_method_returns_none() { + assert!(parse_agent_subject("acp.agent.unknown").is_none()); } #[test] - fn test_parse_ext_empty_name_returns_none() { - assert!(parse_client_subject("acp.sess123.client.ext.").is_none()); + fn parse_agent_invalid_session_id_returns_none() { + assert!(parse_agent_subject("acp.session.s*1.agent.load").is_none()); } #[test] - fn test_parse_ext_wildcard_name_returns_none() { - assert!(parse_client_subject("acp.sess123.client.ext.method.*").is_none()); + fn parse_agent_client_subject_returns_none() { + assert!(parse_agent_subject("acp.session.s1.client.session.update").is_none()); } #[test] - fn test_parse_ext_rejects_ambiguous_client_in_name() { - assert!(parse_client_subject("acp.sess123.client.ext.foo.client.bar").is_none()); + fn parse_agent_no_overlap_with_session_id_agent() { + let parsed = parse_agent_subject("acp.session.agent.agent.load").unwrap(); + assert_eq!(parsed.session_id.unwrap().as_str(), "agent"); + assert_eq!(parsed.method, AgentMethod::SessionLoad); } - #[test] - fn test_parse_ext_with_client_in_prefix() { - let subject = "org.client.app.sess123.client.ext.my_tool"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.session_id.as_str(), "sess123"); - assert_eq!(parsed.method, ClientMethod::Ext("my_tool".to_string())); - } + // Client methods #[test] - fn test_parse_ext_does_not_shadow_prompt_response() { - let subject = "acp.sess123.client.ext.session.prompt_response"; - let parsed = parse_client_subject(subject).unwrap(); - assert_eq!(parsed.method, ClientMethod::ExtSessionPromptResponse); + fn parse_client_fs_read() { + let parsed = parse_client_subject("acp.session.s1.client.fs.read_text_file").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::FsReadTextFile); } #[test] - fn test_client_method_equality() { - assert_eq!(ClientMethod::FsReadTextFile, ClientMethod::FsReadTextFile); - assert_ne!(ClientMethod::FsReadTextFile, ClientMethod::FsWriteTextFile); + fn parse_client_fs_write() { + let parsed = parse_client_subject("acp.session.s1.client.fs.write_text_file").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::FsWriteTextFile); } #[test] - fn test_agent_parse_initialize() { - let parsed = parse_agent_subject("acp.agent.initialize").unwrap(); - assert!(parsed.session_id.is_none()); - assert_eq!(parsed.method, AgentMethod::Initialize); + fn parse_client_request_permission() { + let parsed = + parse_client_subject("acp.session.s1.client.session.request_permission").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::SessionRequestPermission); } #[test] - fn test_agent_parse_authenticate() { - let parsed = parse_agent_subject("acp.agent.authenticate").unwrap(); - assert!(parsed.session_id.is_none()); - assert_eq!(parsed.method, AgentMethod::Authenticate); + fn parse_client_session_update() { + let parsed = parse_client_subject("acp.session.s1.client.session.update").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::SessionUpdate); } #[test] - fn test_agent_parse_session_new() { - let parsed = parse_agent_subject("acp.agent.session.new").unwrap(); - assert!(parsed.session_id.is_none()); - assert_eq!(parsed.method, AgentMethod::SessionNew); + fn parse_client_terminal_create() { + let parsed = parse_client_subject("acp.session.s1.client.terminal.create").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::TerminalCreate); } #[test] - fn test_agent_parse_session_list() { - let parsed = parse_agent_subject("acp.agent.session.list").unwrap(); - assert!(parsed.session_id.is_none()); - assert_eq!(parsed.method, AgentMethod::SessionList); + fn parse_client_terminal_kill() { + let parsed = parse_client_subject("acp.session.s1.client.terminal.kill").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::TerminalKill); } #[test] - fn test_agent_parse_session_load() { - let parsed = parse_agent_subject("acp.s1.agent.session.load").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionLoad); + fn parse_client_terminal_output() { + let parsed = parse_client_subject("acp.session.s1.client.terminal.output").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::TerminalOutput); } #[test] - fn test_agent_parse_session_prompt() { - let parsed = parse_agent_subject("acp.s1.agent.session.prompt").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionPrompt); + fn parse_client_terminal_release() { + let parsed = parse_client_subject("acp.session.s1.client.terminal.release").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::TerminalRelease); } #[test] - fn test_agent_parse_session_cancel() { - let parsed = parse_agent_subject("acp.s1.agent.session.cancel").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionCancel); + fn parse_client_terminal_wait() { + let parsed = parse_client_subject("acp.session.s1.client.terminal.wait_for_exit").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::TerminalWaitForExit); } #[test] - fn test_agent_parse_session_set_mode() { - let parsed = parse_agent_subject("acp.s1.agent.session.set_mode").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionSetMode); + fn parse_client_ext_prompt_response() { + let parsed = + parse_client_subject("acp.session.s1.client.ext.session.prompt_response").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::ExtSessionPromptResponse); } #[test] - fn test_agent_parse_session_set_config_option() { - let parsed = parse_agent_subject("acp.s1.agent.session.set_config_option").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionSetConfigOption); + fn parse_client_custom_prefix() { + let parsed = parse_client_subject("myapp.session.s1.client.fs.read_text_file").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::FsReadTextFile); } #[test] - fn test_agent_parse_session_set_model() { - let parsed = parse_agent_subject("acp.s1.agent.session.set_model").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionSetModel); + fn parse_client_empty_returns_none() { + assert!(parse_client_subject("").is_none()); } #[test] - fn test_agent_parse_session_fork() { - let parsed = parse_agent_subject("acp.s1.agent.session.fork").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionFork); + fn parse_client_no_session_returns_none() { + assert!(parse_client_subject("acp.client.fs.read_text_file").is_none()); } #[test] - fn test_agent_parse_session_resume() { - let parsed = parse_agent_subject("acp.s1.agent.session.resume").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionResume); + fn parse_client_unknown_method_returns_none() { + assert!(parse_client_subject("acp.session.s1.client.unknown").is_none()); } #[test] - fn test_agent_parse_session_close() { - let parsed = parse_agent_subject("acp.s1.agent.session.close").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionClose); + fn parse_client_session_but_no_valid_method_returns_none() { + assert!(parse_client_subject("acp.session.s1.client.nope").is_none()); } #[test] - fn test_agent_parse_ext_method() { - let parsed = parse_agent_subject("acp.agent.ext.my_tool").unwrap(); - assert!(parsed.session_id.is_none()); - assert_eq!( - parsed.method, - AgentMethod::Ext(ExtMethodName::new("my_tool").unwrap()) - ); + fn parse_client_prefix_containing_session_word() { + let parsed = + parse_client_subject("my.session.app.session.s1.client.fs.read_text_file").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::FsReadTextFile); } #[test] - fn test_agent_parse_ext_dotted_namespace() { - let parsed = parse_agent_subject("acp.agent.ext.vendor.operation").unwrap(); - assert!(parsed.session_id.is_none()); - assert_eq!( - parsed.method, - AgentMethod::Ext(ExtMethodName::new("vendor.operation").unwrap()) - ); + fn parse_session_agent_unknown_method_returns_none() { + assert!(parse_agent_subject("acp.session.s1.agent.unknown").is_none()); } #[test] - fn test_agent_parse_custom_prefix() { - let parsed = parse_agent_subject("myapp.agent.initialize").unwrap(); + fn parse_agent_prefix_containing_agent_word() { + let parsed = parse_agent_subject("org.agent.app.agent.initialize").unwrap(); assert!(parsed.session_id.is_none()); assert_eq!(parsed.method, AgentMethod::Initialize); } #[test] - fn test_agent_parse_multi_part_prefix() { - let parsed = parse_agent_subject("my.multi.prefix.s1.agent.session.load").unwrap(); + fn parse_session_agent_prefix_containing_session_word() { + let parsed = parse_agent_subject("my.session.app.session.s1.agent.prompt").unwrap(); assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionLoad); - } - - #[test] - fn test_agent_parse_empty_returns_none() { - assert!(parse_agent_subject("").is_none()); - } - - #[test] - fn test_agent_parse_no_agent_marker_returns_none() { - assert!(parse_agent_subject("acp.client.session.update").is_none()); - } - - #[test] - fn test_agent_parse_unknown_method_returns_none() { - assert!(parse_agent_subject("acp.agent.unknown.method").is_none()); - } - - #[test] - fn test_agent_parse_invalid_session_id_returns_none() { - assert!(parse_agent_subject("acp.sess*ion.agent.session.load").is_none()); - } - - #[test] - fn test_agent_parse_ext_empty_name_returns_none() { - assert!(parse_agent_subject("acp.agent.ext.").is_none()); - } - - #[test] - fn test_agent_parse_ext_wildcard_returns_none() { - assert!(parse_agent_subject("acp.agent.ext.*").is_none()); + assert_eq!(parsed.method, AgentMethod::SessionPrompt); } #[test] - fn test_agent_parse_multi_dot_prefix_global_method_has_no_session() { - let parsed = parse_agent_subject("my.multi.agent.initialize").unwrap(); + fn parse_agent_prefix_containing_session_falls_through_to_global() { + let parsed = parse_agent_subject("my.session.handler.agent.initialize").unwrap(); assert!(parsed.session_id.is_none()); assert_eq!(parsed.method, AgentMethod::Initialize); } #[test] - fn test_agent_parse_prefix_containing_agent_word() { - let parsed = parse_agent_subject("org.agent.app.agent.initialize").unwrap(); - assert!(parsed.session_id.is_none()); - assert_eq!(parsed.method, AgentMethod::Initialize); - } - - #[test] - fn test_agent_parse_ext_method_containing_agent_segment() { - let parsed = parse_agent_subject("acp.agent.ext.agent.foo").unwrap(); - assert!(parsed.session_id.is_none()); - assert_eq!( - parsed.method, - AgentMethod::Ext(ExtMethodName::new("agent.foo").unwrap()) - ); - } - - #[test] - fn test_agent_parse_multi_dot_prefix_session_scoped() { - let parsed = parse_agent_subject("my.multi.s1.agent.session.prompt").unwrap(); - assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); - assert_eq!(parsed.method, AgentMethod::SessionPrompt); + fn parse_client_ext_method() { + let parsed = parse_client_subject("acp.session.s1.client.ext.my_tool").unwrap(); + assert_eq!(parsed.session_id.as_str(), "s1"); + assert_eq!(parsed.method, ClientMethod::Ext("my_tool".to_string())); } } diff --git a/rsworkspace/crates/acp-nats/src/nats/subjects.rs b/rsworkspace/crates/acp-nats/src/nats/subjects.rs index 5c82aaf6b..f05fbde2e 100644 --- a/rsworkspace/crates/acp-nats/src/nats/subjects.rs +++ b/rsworkspace/crates/acp-nats/src/nats/subjects.rs @@ -11,507 +11,480 @@ pub mod agent { format!("{}.agent.session.new", prefix) } - pub fn session_load(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.load", prefix, session_id) - } - - pub fn session_cancel(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.cancel", prefix, session_id) + pub fn session_list(prefix: &str) -> String { + format!("{}.agent.session.list", prefix) } - pub fn session_cancelled(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.cancelled", prefix, session_id) + pub fn ext(prefix: &str, method: &str) -> String { + format!("{}.agent.ext.{}", prefix, method) } - pub fn session_set_mode(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.set_mode", prefix, session_id) + pub mod wildcards { + pub fn all(prefix: &str) -> String { + format!("{}.agent.>", prefix) + } } +} - pub fn ext_session_ready(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.ext.session.ready", prefix, session_id) - } +pub mod session { + pub mod agent { + pub fn load(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.load", prefix, session_id) + } - pub fn session_prompt(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.prompt", prefix, session_id) - } + pub fn prompt(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.prompt", prefix, session_id) + } - pub fn session_prompt_wildcard(prefix: &str) -> String { - format!("{}.*.agent.session.prompt", prefix) - } + pub fn prompt_wildcard(prefix: &str) -> String { + format!("{}.session.*.agent.prompt", prefix) + } - pub fn session_update(prefix: &str, session_id: &str, req_id: &str) -> String { - format!("{}.{}.agent.session.update.{}", prefix, session_id, req_id) - } + pub fn cancel(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.cancel", prefix, session_id) + } - pub fn ext_session_prompt_response(prefix: &str, session_id: &str, req_id: &str) -> String { - format!( - "{}.{}.agent.ext.session.prompt.response.{}", - prefix, session_id, req_id - ) - } + pub fn cancelled(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.cancelled", prefix, session_id) + } - pub fn session_list(prefix: &str) -> String { - format!("{}.agent.session.list", prefix) - } + pub fn set_mode(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.set_mode", prefix, session_id) + } - pub fn session_set_config_option(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.set_config_option", prefix, session_id) - } + pub fn set_config_option(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.set_config_option", prefix, session_id) + } - pub fn session_set_model(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.set_model", prefix, session_id) - } + pub fn set_model(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.set_model", prefix, session_id) + } - pub fn session_fork(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.fork", prefix, session_id) - } + pub fn fork(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.fork", prefix, session_id) + } - pub fn session_resume(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.resume", prefix, session_id) - } + pub fn resume(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.resume", prefix, session_id) + } - pub fn session_close(prefix: &str, session_id: &str) -> String { - format!("{}.{}.agent.session.close", prefix, session_id) - } + pub fn close(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.close", prefix, session_id) + } - pub fn ext(prefix: &str, method: &str) -> String { - format!("{}.agent.ext.{}", prefix, method) - } + pub fn ext_ready(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.ext.ready", prefix, session_id) + } - pub mod wildcards { - pub fn all(prefix: &str) -> String { - format!("{}.agent.>", prefix) + pub fn update(prefix: &str, session_id: &str, req_id: &str) -> String { + format!("{}.session.{}.agent.update.{}", prefix, session_id, req_id) } - pub fn all_sessions(prefix: &str) -> String { - format!("{}.*.agent.>", prefix) + pub fn prompt_response(prefix: &str, session_id: &str, req_id: &str) -> String { + format!( + "{}.session.{}.agent.prompt.response.{}", + prefix, session_id, req_id + ) } } -} -pub mod client { - pub fn fs_read_text_file(prefix: &str, session_id: &str) -> String { - format!("{}.{}.client.fs.read_text_file", prefix, session_id) - } + pub mod client { + pub fn fs_read_text_file(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.client.fs.read_text_file", prefix, session_id) + } - pub fn fs_write_text_file(prefix: &str, session_id: &str) -> String { - format!("{}.{}.client.fs.write_text_file", prefix, session_id) - } + pub fn fs_write_text_file(prefix: &str, session_id: &str) -> String { + format!( + "{}.session.{}.client.fs.write_text_file", + prefix, session_id + ) + } - pub fn session_request_permission(prefix: &str, session_id: &str) -> String { - format!( - "{}.{}.client.session.request_permission", - prefix, session_id - ) - } + pub fn session_request_permission(prefix: &str, session_id: &str) -> String { + format!( + "{}.session.{}.client.session.request_permission", + prefix, session_id + ) + } - pub fn session_update(prefix: &str, session_id: &str) -> String { - format!("{}.{}.client.session.update", prefix, session_id) - } + pub fn session_update(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.client.session.update", prefix, session_id) + } - pub fn terminal_create(prefix: &str, session_id: &str) -> String { - format!("{}.{}.client.terminal.create", prefix, session_id) - } + pub fn terminal_create(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.client.terminal.create", prefix, session_id) + } - pub fn terminal_kill(prefix: &str, session_id: &str) -> String { - format!("{}.{}.client.terminal.kill", prefix, session_id) - } + pub fn terminal_kill(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.client.terminal.kill", prefix, session_id) + } - pub fn terminal_output(prefix: &str, session_id: &str) -> String { - format!("{}.{}.client.terminal.output", prefix, session_id) - } + pub fn terminal_output(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.client.terminal.output", prefix, session_id) + } - pub fn terminal_release(prefix: &str, session_id: &str) -> String { - format!("{}.{}.client.terminal.release", prefix, session_id) - } + pub fn terminal_release(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.client.terminal.release", prefix, session_id) + } - pub fn terminal_wait_for_exit(prefix: &str, session_id: &str) -> String { - format!("{}.{}.client.terminal.wait_for_exit", prefix, session_id) + pub fn terminal_wait_for_exit(prefix: &str, session_id: &str) -> String { + format!( + "{}.session.{}.client.terminal.wait_for_exit", + prefix, session_id + ) + } } pub mod wildcards { pub fn all(prefix: &str) -> String { - format!("{}.*.client.>", prefix) + format!("{}.session.>", prefix) + } + + pub fn one(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.>", prefix, session_id) + } + + pub fn all_agent(prefix: &str) -> String { + format!("{}.session.*.agent.>", prefix) + } + + pub fn all_client(prefix: &str) -> String { + format!("{}.session.*.client.>", prefix) + } + + pub fn one_agent(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.agent.>", prefix, session_id) } - } -} -pub mod wildcards { - pub fn all(prefix: &str) -> String { - format!("{}.>", prefix) + pub fn one_client(prefix: &str, session_id: &str) -> String { + format!("{}.session.{}.client.>", prefix, session_id) + } } } #[cfg(test)] mod tests { - use super::{agent, client, wildcards}; + use super::{agent, session}; #[test] - fn initialize_subject() { + fn agent_initialize() { assert_eq!(agent::initialize("acp"), "acp.agent.initialize"); } #[test] - fn authenticate_subject() { + fn agent_authenticate() { assert_eq!(agent::authenticate("acp"), "acp.agent.authenticate"); } #[test] - fn session_new_subject() { + fn agent_session_new() { assert_eq!(agent::session_new("acp"), "acp.agent.session.new"); } #[test] - fn session_load_subject() { - assert_eq!( - agent::session_load("acp", "s1"), - "acp.s1.agent.session.load" - ); + fn agent_session_list() { + assert_eq!(agent::session_list("acp"), "acp.agent.session.list"); } #[test] - fn session_cancel_subject() { - assert_eq!( - agent::session_cancel("acp", "s1"), - "acp.s1.agent.session.cancel" - ); + fn agent_ext() { + assert_eq!(agent::ext("acp", "my_tool"), "acp.agent.ext.my_tool"); } #[test] - fn session_set_mode_subject() { - assert_eq!( - agent::session_set_mode("acp", "s1"), - "acp.s1.agent.session.set_mode" - ); + fn agent_ext_dotted() { + assert_eq!(agent::ext("acp", "vendor.op"), "acp.agent.ext.vendor.op"); } #[test] - fn ext_session_ready_subject() { - assert_eq!( - agent::ext_session_ready("acp", "s1"), - "acp.s1.agent.ext.session.ready" - ); + fn agent_wildcard_all() { + assert_eq!(agent::wildcards::all("acp"), "acp.agent.>"); } #[test] - fn custom_prefix_places_prefix_at_root() { - assert_eq!(agent::initialize("myapp"), "myapp.agent.initialize"); + fn session_agent_load() { assert_eq!( - agent::session_load("myapp", "sess-42"), - "myapp.sess-42.agent.session.load" - ); - assert_eq!( - agent::ext_session_ready("myapp", "sess-42"), - "myapp.sess-42.agent.ext.session.ready" + session::agent::load("acp", "s1"), + "acp.session.s1.agent.load" ); } #[test] - fn session_prompt_subject() { + fn session_agent_prompt() { assert_eq!( - agent::session_prompt("acp", "s1"), - "acp.s1.agent.session.prompt" + session::agent::prompt("acp", "s1"), + "acp.session.s1.agent.prompt" ); } #[test] - fn session_prompt_wildcard_subject() { + fn session_agent_prompt_wildcard() { assert_eq!( - agent::session_prompt_wildcard("acp"), - "acp.*.agent.session.prompt" + session::agent::prompt_wildcard("acp"), + "acp.session.*.agent.prompt" ); } #[test] - fn session_update_subject() { + fn session_agent_cancel() { assert_eq!( - agent::session_update("acp", "s1", "req-abc"), - "acp.s1.agent.session.update.req-abc" + session::agent::cancel("acp", "s1"), + "acp.session.s1.agent.cancel" ); } #[test] - fn ext_session_prompt_response_subject() { + fn session_agent_cancelled() { assert_eq!( - agent::ext_session_prompt_response("acp", "s1", "req-abc"), - "acp.s1.agent.ext.session.prompt.response.req-abc" + session::agent::cancelled("acp", "s1"), + "acp.session.s1.agent.cancelled" ); } #[test] - fn session_cancelled_subject() { + fn session_agent_set_mode() { assert_eq!( - agent::session_cancelled("acp", "s1"), - "acp.s1.agent.session.cancelled" + session::agent::set_mode("acp", "s1"), + "acp.session.s1.agent.set_mode" ); } #[test] - fn session_list_subject() { - assert_eq!(agent::session_list("acp"), "acp.agent.session.list"); - } - - #[test] - fn session_set_config_option_subject() { + fn session_agent_set_config_option() { assert_eq!( - agent::session_set_config_option("acp", "s1"), - "acp.s1.agent.session.set_config_option" + session::agent::set_config_option("acp", "s1"), + "acp.session.s1.agent.set_config_option" ); } #[test] - fn session_set_model_subject() { + fn session_agent_set_model() { assert_eq!( - agent::session_set_model("acp", "s1"), - "acp.s1.agent.session.set_model" + session::agent::set_model("acp", "s1"), + "acp.session.s1.agent.set_model" ); } #[test] - fn session_fork_subject() { + fn session_agent_fork() { assert_eq!( - agent::session_fork("acp", "s1"), - "acp.s1.agent.session.fork" + session::agent::fork("acp", "s1"), + "acp.session.s1.agent.fork" ); } #[test] - fn session_resume_subject() { + fn session_agent_resume() { assert_eq!( - agent::session_resume("acp", "s1"), - "acp.s1.agent.session.resume" + session::agent::resume("acp", "s1"), + "acp.session.s1.agent.resume" ); } #[test] - fn session_close_subject() { + fn session_agent_close() { assert_eq!( - agent::session_close("acp", "s1"), - "acp.s1.agent.session.close" + session::agent::close("acp", "s1"), + "acp.session.s1.agent.close" ); } #[test] - fn session_scoped_subjects_share_token_layout() { - let prefix = "acp"; - let sid = "abc"; - let expected_prefix = format!("{}.{}.agent.", prefix, sid); - - assert!(agent::session_load(prefix, sid).starts_with(&expected_prefix)); - assert!(agent::session_cancel(prefix, sid).starts_with(&expected_prefix)); - assert!(agent::session_set_mode(prefix, sid).starts_with(&expected_prefix)); - assert!(agent::session_set_config_option(prefix, sid).starts_with(&expected_prefix)); - assert!(agent::session_set_model(prefix, sid).starts_with(&expected_prefix)); - assert!(agent::session_fork(prefix, sid).starts_with(&expected_prefix)); - assert!(agent::session_resume(prefix, sid).starts_with(&expected_prefix)); - assert!(agent::session_close(prefix, sid).starts_with(&expected_prefix)); - assert!(agent::ext_session_ready(prefix, sid).starts_with(&expected_prefix)); - } - - #[test] - fn ext_subject() { - assert_eq!(agent::ext("acp", "my_tool"), "acp.agent.ext.my_tool"); - } - - #[test] - fn agent_wildcard_all_subject() { - assert_eq!(agent::wildcards::all("acp"), "acp.agent.>"); + fn session_agent_ext_ready() { + assert_eq!( + session::agent::ext_ready("acp", "s1"), + "acp.session.s1.agent.ext.ready" + ); } #[test] - fn agent_wildcard_all_sessions_subject() { - assert_eq!(agent::wildcards::all_sessions("acp"), "acp.*.agent.>"); + fn session_agent_update() { + assert_eq!( + session::agent::update("acp", "s1", "req-abc"), + "acp.session.s1.agent.update.req-abc" + ); } #[test] - fn agent_wildcards_overlap_when_session_id_is_agent() { - let subject = "acp.agent.agent.session.load"; - let global = agent::wildcards::all("acp"); - let session = agent::wildcards::all_sessions("acp"); - - // Both wildcards match this subject in NATS: - // - "acp.agent.>" matches because "agent.session.load" falls under "acp.agent." - // - "acp.*.agent.>" matches because * = "agent", rest = "session.load" - // This is a known trade-off: using two subscriptions avoids consuming - // client messages, but causes duplicate delivery for session ID "agent". - assert!(nats_wildcard_matches(&global, subject)); - assert!(nats_wildcard_matches(&session, subject)); - } - - fn nats_wildcard_matches(pattern: &str, subject: &str) -> bool { - let pattern_parts: Vec<&str> = pattern.split('.').collect(); - let subject_parts: Vec<&str> = subject.split('.').collect(); - nats_match(&pattern_parts, &subject_parts) - } - - fn nats_match(pattern: &[&str], subject: &[&str]) -> bool { - match (pattern.first(), subject.first()) { - (Some(&">"), _) => true, - (Some(&"*"), Some(_)) => nats_match(&pattern[1..], &subject[1..]), - (Some(p), Some(s)) if p == s => nats_match(&pattern[1..], &subject[1..]), - (None, None) => true, - _ => false, - } + fn session_agent_prompt_response() { + assert_eq!( + session::agent::prompt_response("acp", "s1", "req-abc"), + "acp.session.s1.agent.prompt.response.req-abc" + ); } #[test] - fn nats_match_exact_match() { - assert!(nats_wildcard_matches( - "acp.agent.initialize", - "acp.agent.initialize" - )); + fn session_client_fs_read() { + assert_eq!( + session::client::fs_read_text_file("acp", "s1"), + "acp.session.s1.client.fs.read_text_file" + ); } #[test] - fn nats_match_no_match() { - assert!(!nats_wildcard_matches( - "acp.agent.initialize", - "acp.agent.authenticate" - )); + fn session_client_fs_write() { + assert_eq!( + session::client::fs_write_text_file("acp", "s1"), + "acp.session.s1.client.fs.write_text_file" + ); } #[test] - fn nats_match_length_mismatch() { - assert!(!nats_wildcard_matches("acp.agent", "acp.agent.initialize")); + fn session_client_request_permission() { + assert_eq!( + session::client::session_request_permission("acp", "s1"), + "acp.session.s1.client.session.request_permission" + ); } #[test] - fn prefix_wildcard_all() { - assert_eq!(wildcards::all("acp"), "acp.>"); + fn session_client_session_update() { + assert_eq!( + session::client::session_update("acp", "s1"), + "acp.session.s1.client.session.update" + ); } #[test] - fn prefix_wildcard_custom_prefix() { - assert_eq!(wildcards::all("myapp"), "myapp.>"); + fn session_client_terminal_create() { + assert_eq!( + session::client::terminal_create("acp", "s1"), + "acp.session.s1.client.terminal.create" + ); } #[test] - fn agent_wildcard_custom_prefix() { - assert_eq!(agent::wildcards::all("myapp"), "myapp.agent.>"); - assert_eq!(agent::wildcards::all_sessions("myapp"), "myapp.*.agent.>"); + fn session_client_terminal_kill() { + assert_eq!( + session::client::terminal_kill("acp", "s1"), + "acp.session.s1.client.terminal.kill" + ); } #[test] - fn ext_subject_dotted_method() { + fn session_client_terminal_output() { assert_eq!( - agent::ext("acp", "vendor.operation"), - "acp.agent.ext.vendor.operation" + session::client::terminal_output("acp", "s1"), + "acp.session.s1.client.terminal.output" ); } #[test] - fn ext_subject_custom_prefix() { - assert_eq!(agent::ext("myapp", "my_tool"), "myapp.agent.ext.my_tool"); + fn session_client_terminal_release() { + assert_eq!( + session::client::terminal_release("acp", "s1"), + "acp.session.s1.client.terminal.release" + ); } #[test] - fn client_fs_read_text_file_subject() { + fn session_client_terminal_wait_for_exit() { assert_eq!( - client::fs_read_text_file("acp", "s1"), - "acp.s1.client.fs.read_text_file" + session::client::terminal_wait_for_exit("acp", "s1"), + "acp.session.s1.client.terminal.wait_for_exit" ); } #[test] - fn client_fs_write_text_file_subject() { - assert_eq!( - client::fs_write_text_file("acp", "s1"), - "acp.s1.client.fs.write_text_file" - ); + fn session_wildcard_all() { + assert_eq!(session::wildcards::all("acp"), "acp.session.>"); } #[test] - fn client_session_request_permission_subject() { - assert_eq!( - client::session_request_permission("acp", "s1"), - "acp.s1.client.session.request_permission" - ); + fn session_wildcard_one() { + assert_eq!(session::wildcards::one("acp", "s1"), "acp.session.s1.>"); } #[test] - fn client_session_update_subject() { + fn session_wildcard_all_agent() { assert_eq!( - client::session_update("acp", "s1"), - "acp.s1.client.session.update" + session::wildcards::all_agent("acp"), + "acp.session.*.agent.>" ); } #[test] - fn client_terminal_create_subject() { + fn session_wildcard_all_client() { assert_eq!( - client::terminal_create("acp", "s1"), - "acp.s1.client.terminal.create" + session::wildcards::all_client("acp"), + "acp.session.*.client.>" ); } #[test] - fn client_terminal_kill_subject() { + fn session_wildcard_one_agent() { assert_eq!( - client::terminal_kill("acp", "s1"), - "acp.s1.client.terminal.kill" + session::wildcards::one_agent("acp", "s1"), + "acp.session.s1.agent.>" ); } #[test] - fn client_terminal_output_subject() { + fn session_wildcard_one_client() { assert_eq!( - client::terminal_output("acp", "s1"), - "acp.s1.client.terminal.output" + session::wildcards::one_client("acp", "s1"), + "acp.session.s1.client.>" ); } #[test] - fn client_terminal_release_subject() { - assert_eq!( - client::terminal_release("acp", "s1"), - "acp.s1.client.terminal.release" - ); + fn custom_prefix_global() { + assert_eq!(agent::initialize("myapp"), "myapp.agent.initialize"); + assert_eq!(agent::session_new("myapp"), "myapp.agent.session.new"); } #[test] - fn client_terminal_wait_for_exit_subject() { + fn custom_prefix_session() { + assert_eq!( + session::agent::prompt("myapp", "s1"), + "myapp.session.s1.agent.prompt" + ); assert_eq!( - client::terminal_wait_for_exit("acp", "s1"), - "acp.s1.client.terminal.wait_for_exit" + session::client::fs_read_text_file("myapp", "s1"), + "myapp.session.s1.client.fs.read_text_file" ); } #[test] - fn client_wildcard_all_subject() { - assert_eq!(client::wildcards::all("acp"), "acp.*.client.>"); - } + fn no_overlap_agent_and_session_are_distinct() { + let global = agent::wildcards::all("acp"); + let session_agent = session::wildcards::all_agent("acp"); - #[test] - fn client_wildcard_custom_prefix() { - assert_eq!(client::wildcards::all("myapp"), "myapp.*.client.>"); + assert!(global.starts_with("acp.agent.")); + assert!(session_agent.starts_with("acp.session.")); } #[test] - fn client_subjects_share_token_layout() { + fn session_scoped_agent_subjects_share_layout() { let prefix = "acp"; let sid = "abc"; - let expected_prefix = format!("{}.{}.client.", prefix, sid); + let expected = format!("{}.session.{}.agent.", prefix, sid); - assert!(client::fs_read_text_file(prefix, sid).starts_with(&expected_prefix)); - assert!(client::fs_write_text_file(prefix, sid).starts_with(&expected_prefix)); - assert!(client::session_request_permission(prefix, sid).starts_with(&expected_prefix)); - assert!(client::session_update(prefix, sid).starts_with(&expected_prefix)); - assert!(client::terminal_create(prefix, sid).starts_with(&expected_prefix)); - assert!(client::terminal_kill(prefix, sid).starts_with(&expected_prefix)); - assert!(client::terminal_output(prefix, sid).starts_with(&expected_prefix)); - assert!(client::terminal_release(prefix, sid).starts_with(&expected_prefix)); - assert!(client::terminal_wait_for_exit(prefix, sid).starts_with(&expected_prefix)); + assert!(session::agent::load(prefix, sid).starts_with(&expected)); + assert!(session::agent::prompt(prefix, sid).starts_with(&expected)); + assert!(session::agent::cancel(prefix, sid).starts_with(&expected)); + assert!(session::agent::set_mode(prefix, sid).starts_with(&expected)); + assert!(session::agent::set_config_option(prefix, sid).starts_with(&expected)); + assert!(session::agent::set_model(prefix, sid).starts_with(&expected)); + assert!(session::agent::fork(prefix, sid).starts_with(&expected)); + assert!(session::agent::resume(prefix, sid).starts_with(&expected)); + assert!(session::agent::close(prefix, sid).starts_with(&expected)); } #[test] - fn client_subjects_custom_prefix() { - assert_eq!( - client::fs_read_text_file("myapp", "sess-42"), - "myapp.sess-42.client.fs.read_text_file" - ); - assert_eq!( - client::terminal_create("myapp", "sess-42"), - "myapp.sess-42.client.terminal.create" - ); + fn session_scoped_client_subjects_share_layout() { + let prefix = "acp"; + let sid = "abc"; + let expected = format!("{}.session.{}.client.", prefix, sid); + + assert!(session::client::fs_read_text_file(prefix, sid).starts_with(&expected)); + assert!(session::client::fs_write_text_file(prefix, sid).starts_with(&expected)); + assert!(session::client::session_request_permission(prefix, sid).starts_with(&expected)); + assert!(session::client::session_update(prefix, sid).starts_with(&expected)); + assert!(session::client::terminal_create(prefix, sid).starts_with(&expected)); + assert!(session::client::terminal_kill(prefix, sid).starts_with(&expected)); + assert!(session::client::terminal_output(prefix, sid).starts_with(&expected)); + assert!(session::client::terminal_release(prefix, sid).starts_with(&expected)); + assert!(session::client::terminal_wait_for_exit(prefix, sid).starts_with(&expected)); } }